Compare commits
318 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a26397131d | |||
| 7659e8f0f7 | |||
| 90b608bdc6 | |||
| fffe2a1ccb | |||
| fd7706de6d | |||
| d0a5c93e49 | |||
| 263bf08f34 | |||
| e050f30efa | |||
| c2b1adf588 | |||
| 647cd3c33f | |||
| 8fdbb39ee4 | |||
| ee029294f1 | |||
| 563e328ce3 | |||
| 8f543d7a84 | |||
| 62e09190f3 | |||
| 50c774fd78 | |||
| 10e4bcc723 | |||
| 964ad6d046 | |||
| 56fb76017d | |||
| 5a9141e10c | |||
| db94282207 | |||
| 9f6446c30c | |||
| d570ce361d | |||
| 868fcf2c5a | |||
| dd35a85316 | |||
| 5003f8b3a2 | |||
| d044f938e3 | |||
| e549779164 | |||
| e2f2b325a6 | |||
| 9860c1db54 | |||
| 7d820f5b88 | |||
| 61d6e5643c | |||
| b444de591a | |||
| 21c86c9dfa | |||
| 8e70754533 | |||
| 4270d74338 | |||
| db506fa992 | |||
| c1b06eaa6f | |||
| 6bd1d09fa8 | |||
| 70da228dcc | |||
| 9888efe437 | |||
| 65756b62a5 | |||
| 59a0d593d4 | |||
| d519b80b61 | |||
| e92725f38b | |||
| ec0cb0bbb7 | |||
| a4b26374f4 | |||
| dcac6a4bb0 | |||
| dd6eecb0c2 | |||
| fec100a273 | |||
| 8f944b1b46 | |||
| 69498003d8 | |||
| e019f557ff | |||
| 4b5611ef6c | |||
| ca44b2cc2c | |||
| 10e0972d79 | |||
| 28908d81a3 | |||
| 0503a50754 | |||
| 65a92042d6 | |||
| f554fdefd3 | |||
| bdbd4d5302 | |||
| 3ee1683349 | |||
| 3a7ad429c2 | |||
| 89bd055f02 | |||
| 835b3b7b8b | |||
| 934f90cdff | |||
| 92cc683b8e | |||
| 80d548e8bd | |||
| 7ec1efb85d | |||
| f5945a788f | |||
| 2d0e2e0cca | |||
| bff6ca7e9d | |||
| 06b4960984 | |||
| 2fe393204b | |||
| 876950a84e | |||
| 6292ef9dfb | |||
| 798fb8f937 | |||
| f6dd4c03c3 | |||
| f87fbddef7 | |||
| aa2e10440d | |||
| 34b0b793ba | |||
| 1f159bf826 | |||
| b8253b6dcc | |||
| 79fd9070e4 | |||
| 7b96cd0447 | |||
| 01bc9becc0 | |||
| 9a009b73dc | |||
| fe35cbae49 | |||
| c3a880e5f5 | |||
| 1c906113ab | |||
| 6f3dcd958d | |||
| 7a9f4cd64f | |||
| 9a67af7c55 | |||
| 501de6ffef | |||
| 210d978279 | |||
| a35771acc4 | |||
| 637faef690 | |||
| c800eb5d4d | |||
| 0e062ed065 | |||
| f2e89da724 | |||
| ac29f0bf98 | |||
| d174e99c80 | |||
| 5006a96181 | |||
| ce8c020477 | |||
| 98c96b8217 | |||
| 43404adf49 | |||
| 90ea462206 | |||
| 92a78f6f12 | |||
| be7fbd405e | |||
| 98e3c6ebfd | |||
| fbca205cca | |||
| d3c25a1aff | |||
| 84f2778bc0 | |||
| 688185c367 | |||
| bf48bfdd7c | |||
| bde0b01d06 | |||
| a1b7c8ad1d | |||
| 37ff0d1fab | |||
| 0c218df3ad | |||
| 259f27bf1b | |||
| f2bc8e44fc | |||
| c44bf73b42 | |||
| fbd19f9da4 | |||
| 50fc0783d4 | |||
| c372272394 | |||
| 22d653cc76 | |||
| 46dbfcbe77 | |||
| 91d51e660b | |||
| 76f5f12563 | |||
| 08bc0eff8c | |||
| be1d219fea | |||
| 52034ef55c | |||
| 47ab41088e | |||
| fb5484f44d | |||
| cfbab0432c | |||
| 34bf74da84 | |||
| 889f90015a | |||
| 1d0817b1b3 | |||
| 54150a9157 | |||
| a8a89ca089 | |||
| bb4eca1b0c | |||
| 6ce6fd3aa8 | |||
| 35ec18cfac | |||
| 3795e788bb | |||
| 4b239030c5 | |||
| 7162ce4a77 | |||
| 03f0e4a477 | |||
| a23a194660 | |||
| 45faa269a4 | |||
| 981a1aac4f | |||
| 70ccf7b691 | |||
| 8bc763be9b | |||
| 815bb08fa9 | |||
| 34773537c2 | |||
| 6c285a0856 | |||
| a062592043 | |||
| 8978e340c7 | |||
| 07c1bba829 | |||
| 4f836f5e3a | |||
| 2cfc24a808 | |||
| 07743368f4 | |||
| 592c04c5ab | |||
| bb8a72876b | |||
| b9b501edfa | |||
| 44fe7778b6 | |||
| 6ea5ad1619 | |||
| d9b819d1a1 | |||
| 5ac9eb5d5c | |||
| 1345603e09 | |||
| bd66408c3d | |||
| f75e078fed | |||
| 09fc82f7b7 | |||
| 7bc9a0357e | |||
| dc6420ccb0 | |||
| fadf72c245 | |||
| c8ff60d986 | |||
| e5cd8ffa61 | |||
| c36f58e491 | |||
| 45d348c0ef | |||
| ae26f00a36 | |||
| a6e765f51c | |||
| 3c428ade52 | |||
| 011020a945 | |||
| 368322f906 | |||
| a3ff181b6e | |||
| 720a5f8897 | |||
| 633cb44db6 | |||
| 73f234d8f5 | |||
| a49490baa7 | |||
| a90f08a85f | |||
| 17ee037525 | |||
| 75aa55d340 | |||
| d32cd793d0 | |||
| 29781bbac4 | |||
| 21ea36a4f7 | |||
| 5e99b9d555 | |||
| 3190608d36 | |||
| 4d0aecb8c2 | |||
| e1f420c3ae | |||
| cbf3dd9776 | |||
| 0ff97ac4e0 | |||
| 52b37c2a13 | |||
| 732fa3b9de | |||
| 4047aaf48a | |||
| bc3e7ae29b | |||
| ed87e56a33 | |||
| 28ce1e856c | |||
| 4c13b7ad02 | |||
| 49df497f35 | |||
| 5221ab481e | |||
| 6655d725ae | |||
| 220f9f15e5 | |||
| 1e8a27612f | |||
| 7ecec2bb64 | |||
| fceb92eb6f | |||
| 8b92051900 | |||
| 03f3e039e0 | |||
| 18ebf7baaf | |||
| 20b28f2a68 | |||
| 6d0fdc6860 | |||
| 9f0e82446e | |||
| cb69991f7f | |||
| 327fdd66e4 | |||
| 7d01b4bd5a | |||
| d15a862e5b | |||
| 5a31118c96 | |||
| 8eaeb1953b | |||
| 25674c04c8 | |||
| cd6e7c81e5 | |||
| d915de8ff9 | |||
| 1307d49762 | |||
| 2cffd4fbbb | |||
| 031209490f | |||
| 5d75629a73 | |||
| 27c4afd41b | |||
| 9db4a2430a | |||
| e1ac3732bd | |||
| 56ad572387 | |||
| 70beb45c4e | |||
| 698c0a62a2 | |||
| 8421649bcc | |||
| e8883781e5 | |||
| 77e9ae94cf | |||
| 30344ef5cb | |||
| fa0460abd0 | |||
| ee52db3f7c | |||
| 00c8259bd0 | |||
| 470a74f420 | |||
| 3d5a03a629 | |||
| cc8646cf1b | |||
| 308c89aa0b | |||
| de37c3da5a | |||
| 593b924f32 | |||
| bc3cb79f91 | |||
| 9622d5de06 | |||
| 2dddb77ca4 | |||
| 1d43eda9b7 | |||
| dbb1843285 | |||
| dfe1b853d1 | |||
| c7e4d4eaae | |||
| 7c59e8386e | |||
| 366311edbb | |||
| 2fc6a6ca77 | |||
| 9945cb7a94 | |||
| 50918756d7 | |||
| 43c37763d8 | |||
| 4e365f54af | |||
| 09ddf53b01 | |||
| 7fbfa71434 | |||
| ae46cd2332 | |||
| 772a22a182 | |||
| 636ac974b8 | |||
| 216c8211ac | |||
| 805d3e65e3 | |||
| 73c69c3761 | |||
| fe442f27f2 | |||
| 8b51f6ebaa | |||
| ab745ad56b | |||
| 62d3dc63d1 | |||
| fcfd9894a3 | |||
| df076b563a | |||
| 366fbff012 | |||
| d8f7175da9 | |||
| 2bd3845d22 | |||
| 720f83bd0b | |||
| c2fbd918dd | |||
| 902361e5d6 | |||
| 5a2576b368 | |||
| d71014a797 | |||
| d2eaf5c6da | |||
| 17d4fec256 | |||
| 4a96bac457 | |||
| 4977979b08 | |||
| 8fa19df113 | |||
| 217d37e3d3 | |||
| e86d4e05ce | |||
| 6fcb0a2b3c | |||
| 560edf9fbf | |||
| e532f372b5 | |||
| 1101796641 | |||
| c2757f68a6 | |||
| d648226d13 | |||
| 4987819227 | |||
| 8d27997e2e | |||
| b9ee94a47d | |||
| 8b531cc726 | |||
| 753fc762a0 | |||
| 80396a444e | |||
| 52dfee9ca6 | |||
| 80b8b9afdd | |||
| 20dc72022d | |||
| 9116f404db | |||
| 029e5f6d02 | |||
| 6ccaf89d86 | |||
| 0d706abbd3 | |||
| f4a27e59a3 | |||
| 5ab7d0e9b3 | |||
| d198634326 |
@@ -1,13 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
'root': true,
|
|
||||||
'env': {
|
|
||||||
'node': true
|
|
||||||
},
|
|
||||||
'extends': [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:vue/vue3-essential'
|
|
||||||
],
|
|
||||||
'rules': {
|
|
||||||
'vue/no-use-v-if-with-v-for': 'off'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -50,5 +50,7 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
platforms: ${{ vars.BUILD_SNAPSHOT_PLATFORMS }}
|
platforms: ${{ vars.BUILD_SNAPSHOT_PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
|
build-args: |
|
||||||
|
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
@@ -45,5 +45,7 @@ jobs:
|
|||||||
linux/arm/v7
|
linux/arm/v7
|
||||||
linux/arm/v6
|
linux/arm/v6
|
||||||
push: true
|
push: true
|
||||||
|
build-args: |
|
||||||
|
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -26,3 +26,5 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: false
|
push: false
|
||||||
|
build-args: |
|
||||||
|
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||||
|
|||||||
+5
-3
@@ -1,7 +1,9 @@
|
|||||||
# Build backend binary file
|
# Build backend binary file
|
||||||
FROM golang:1.21.12-alpine3.20 AS be-builder
|
FROM golang:1.23.4-alpine3.21 AS be-builder
|
||||||
ARG RELEASE_BUILD
|
ARG RELEASE_BUILD
|
||||||
|
ARG SKIP_TESTS
|
||||||
ENV RELEASE_BUILD=$RELEASE_BUILD
|
ENV RELEASE_BUILD=$RELEASE_BUILD
|
||||||
|
ENV SKIP_TESTS=$SKIP_TESTS
|
||||||
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN docker/backend-build-pre-setup.sh
|
RUN docker/backend-build-pre-setup.sh
|
||||||
@@ -9,7 +11,7 @@ RUN apk add git gcc g++ libc-dev
|
|||||||
RUN ./build.sh backend
|
RUN ./build.sh backend
|
||||||
|
|
||||||
# Build frontend files
|
# Build frontend files
|
||||||
FROM --platform=$BUILDPLATFORM node:18.20.3-alpine3.20 AS fe-builder
|
FROM --platform=$BUILDPLATFORM node:20.18.1-alpine3.21 AS fe-builder
|
||||||
ARG RELEASE_BUILD
|
ARG RELEASE_BUILD
|
||||||
ENV RELEASE_BUILD=$RELEASE_BUILD
|
ENV RELEASE_BUILD=$RELEASE_BUILD
|
||||||
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
||||||
@@ -19,7 +21,7 @@ RUN apk add git
|
|||||||
RUN ./build.sh frontend
|
RUN ./build.sh frontend
|
||||||
|
|
||||||
# Package docker image
|
# Package docker image
|
||||||
FROM alpine:3.20.1
|
FROM alpine:3.21.0
|
||||||
LABEL maintainer="MaysWind <i@mayswind.net>"
|
LABEL maintainer="MaysWind <i@mayswind.net>"
|
||||||
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
|
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
|
||||||
RUN apk --no-cache add tzdata
|
RUN apk --no-cache add tzdata
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Online Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-dem
|
|||||||
7. Multi-language support
|
7. Multi-language support
|
||||||
8. Two-factor authentication
|
8. Two-factor authentication
|
||||||
9. Application lock (PIN code / WebAuthn)
|
9. Application lock (PIN code / WebAuthn)
|
||||||
10. Data export
|
10. Data import & export
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
### Desktop Version
|
### Desktop Version
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
set "TYPE="
|
set "TYPE="
|
||||||
set "NO_LINT=0"
|
set "NO_LINT=0"
|
||||||
set "NO_TEST=0"
|
set "NO_TEST=0"
|
||||||
|
set "SKIP_TESTS=%SKIP_TESTS%"
|
||||||
set "RELEASE=%RELEASE_BUILD%"
|
set "RELEASE=%RELEASE_BUILD%"
|
||||||
set "RELEASE_TYPE=unknown"
|
set "RELEASE_TYPE=unknown"
|
||||||
set "VERSION="
|
set "VERSION="
|
||||||
@@ -56,7 +57,7 @@ goto :pre_parse_args
|
|||||||
echo /r, --release Build release (The script will use environment variable "RELEASE_BUILD" to detect whether this is release building by default)
|
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 /o, --output ^<filename^> Package file name (For "package" type only)
|
||||||
echo --no-lint Do not execute lint check before building
|
echo --no-lint Do not execute lint check before building
|
||||||
echo --no-test Do not execute unit testing 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
|
echo /h, --help Show help
|
||||||
goto :eof
|
goto :eof
|
||||||
|
|
||||||
@@ -139,7 +140,13 @@ goto :pre_parse_args
|
|||||||
if "%NO_TEST%"=="0" (
|
if "%NO_TEST%"=="0" (
|
||||||
echo Executing backend unit testing...
|
echo Executing backend unit testing...
|
||||||
call go clean -cache
|
call go clean -cache
|
||||||
|
|
||||||
|
if "%SKIP_TESTS%"=="" (
|
||||||
call go test .\... -v
|
call go test .\... -v
|
||||||
|
) else (
|
||||||
|
echo (Skip unit test "%SKIP_TESTS%")
|
||||||
|
call go test .\... -v -skip "%SKIP_TESTS%"
|
||||||
|
)
|
||||||
|
|
||||||
if !errorlevel! neq 0 (
|
if !errorlevel! neq 0 (
|
||||||
call :echo_red "Error: Failed to pass unit testing"
|
call :echo_red "Error: Failed to pass unit testing"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
TYPE=""
|
TYPE=""
|
||||||
NO_LINT="0"
|
NO_LINT="0"
|
||||||
NO_TEST="0"
|
NO_TEST="0"
|
||||||
|
SKIP_TESTS="${SKIP_TESTS}"
|
||||||
RELEASE=${RELEASE_BUILD:-"0"}
|
RELEASE=${RELEASE_BUILD:-"0"}
|
||||||
RELEASE_TYPE="unknown"
|
RELEASE_TYPE="unknown"
|
||||||
VERSION=""
|
VERSION=""
|
||||||
@@ -43,7 +44,7 @@ Options:
|
|||||||
-o, --output <filename> Package file name (For "package" type only)
|
-o, --output <filename> Package file name (For "package" type only)
|
||||||
-t, --tag Docker tag (For "docker" type only)
|
-t, --tag Docker tag (For "docker" type only)
|
||||||
--no-lint Do not execute lint check before building
|
--no-lint Do not execute lint check before building
|
||||||
--no-test Do not execute unit testing before building
|
--no-test Do not execute unit testing before building (You can use environment variable "SKIP_TESTS" to skip specified tests)
|
||||||
-h, --help Show help
|
-h, --help Show help
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -137,7 +138,13 @@ build_backend() {
|
|||||||
if [ "$NO_TEST" = "0" ]; then
|
if [ "$NO_TEST" = "0" ]; then
|
||||||
echo "Executing backend unit testing..."
|
echo "Executing backend unit testing..."
|
||||||
go clean -cache
|
go clean -cache
|
||||||
|
|
||||||
|
if [ -z "$SKIP_TESTS" ]; then
|
||||||
go test ./... -v
|
go test ./... -v
|
||||||
|
else
|
||||||
|
echo "(Skip unit test \"$SKIP_TESTS\")"
|
||||||
|
go test ./... -v -skip "$SKIP_TESTS"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$?" != "0" ]; then
|
if [ "$?" != "0" ]; then
|
||||||
echo_red "Error: Failed to pass unit testing"
|
echo_red "Error: Failed to pass unit testing"
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
+26
-17
@@ -3,6 +3,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
@@ -16,32 +17,32 @@ var Database = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "update",
|
Name: "update",
|
||||||
Usage: "Update database structure",
|
Usage: "Update database structure",
|
||||||
Action: updateDatabaseStructure,
|
Action: bindAction(updateDatabaseStructure),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateDatabaseStructure(c *cli.Context) error {
|
func updateDatabaseStructure(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[database.updateDatabaseStructure] starting maintaining")
|
log.CliInfof(c, "[database.updateDatabaseStructure] starting maintaining")
|
||||||
|
|
||||||
err = updateAllDatabaseTablesStructure()
|
err = updateAllDatabaseTablesStructure(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[database.updateDatabaseStructure] update database table structure failed, because %s", err.Error())
|
log.CliErrorf(c, "[database.updateDatabaseStructure] update database table structure failed, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[database.updateDatabaseStructure] all tables maintained successfully")
|
log.CliInfof(c, "[database.updateDatabaseStructure] all tables maintained successfully")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAllDatabaseTablesStructure() error {
|
func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
err = datastore.Container.UserStore.SyncStructs(new(models.User))
|
err = datastore.Container.UserStore.SyncStructs(new(models.User))
|
||||||
@@ -50,7 +51,7 @@ func updateAllDatabaseTablesStructure() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] user table maintained successfully")
|
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] user table maintained successfully")
|
||||||
|
|
||||||
err = datastore.Container.UserStore.SyncStructs(new(models.TwoFactor))
|
err = datastore.Container.UserStore.SyncStructs(new(models.TwoFactor))
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ func updateAllDatabaseTablesStructure() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] two-factor table maintained successfully")
|
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] two-factor table maintained successfully")
|
||||||
|
|
||||||
err = datastore.Container.UserStore.SyncStructs(new(models.TwoFactorRecoveryCode))
|
err = datastore.Container.UserStore.SyncStructs(new(models.TwoFactorRecoveryCode))
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ func updateAllDatabaseTablesStructure() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] two-factor recovery code table maintained successfully")
|
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] two-factor recovery code table maintained successfully")
|
||||||
|
|
||||||
err = datastore.Container.TokenStore.SyncStructs(new(models.TokenRecord))
|
err = datastore.Container.TokenStore.SyncStructs(new(models.TokenRecord))
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ func updateAllDatabaseTablesStructure() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] token record table maintained successfully")
|
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] token record table maintained successfully")
|
||||||
|
|
||||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.Account))
|
err = datastore.Container.UserDataStore.SyncStructs(new(models.Account))
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ func updateAllDatabaseTablesStructure() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] account table maintained successfully")
|
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] account table maintained successfully")
|
||||||
|
|
||||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.Transaction))
|
err = datastore.Container.UserDataStore.SyncStructs(new(models.Transaction))
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ func updateAllDatabaseTablesStructure() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction table maintained successfully")
|
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction table maintained successfully")
|
||||||
|
|
||||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionCategory))
|
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionCategory))
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ func updateAllDatabaseTablesStructure() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction category table maintained successfully")
|
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction category table maintained successfully")
|
||||||
|
|
||||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTag))
|
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTag))
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ func updateAllDatabaseTablesStructure() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction tag table maintained successfully")
|
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction tag table maintained successfully")
|
||||||
|
|
||||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTagIndex))
|
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTagIndex))
|
||||||
|
|
||||||
@@ -114,7 +115,7 @@ func updateAllDatabaseTablesStructure() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction tag index table maintained successfully")
|
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction tag index table maintained successfully")
|
||||||
|
|
||||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTemplate))
|
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTemplate))
|
||||||
|
|
||||||
@@ -122,7 +123,15 @@ func updateAllDatabaseTablesStructure() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction template table maintained successfully")
|
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")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-17
@@ -4,8 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
|
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
func initializeSystem(c *core.CliContext) (*settings.Config, error) {
|
||||||
var err error
|
var err error
|
||||||
configFilePath := c.String("conf-path")
|
configFilePath := c.String("conf-path")
|
||||||
isDisableBootLog := c.Bool("no-boot-log")
|
isDisableBootLog := c.Bool("no-boot-log")
|
||||||
@@ -25,26 +25,26 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
|||||||
if configFilePath != "" {
|
if configFilePath != "" {
|
||||||
if _, err = os.Stat(configFilePath); err != nil {
|
if _, err = os.Stat(configFilePath); err != nil {
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootErrorf("[initializer.initializeSystem] cannot load configuration from custom config path %s, because file not exists", configFilePath)
|
log.BootErrorf(c, "[initializer.initializeSystem] cannot load configuration from custom config path %s, because file not exists", configFilePath)
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootInfof("[initializer.initializeSystem] will loading configuration from custom config path %s", configFilePath)
|
log.BootInfof(c, "[initializer.initializeSystem] will loading configuration from custom config path %s", configFilePath)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
configFilePath, err = settings.GetDefaultConfigFilePath()
|
configFilePath, err = settings.GetDefaultConfigFilePath()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootErrorf("[initializer.initializeSystem] cannot get default configuration path, because %s", err.Error())
|
log.BootErrorf(c, "[initializer.initializeSystem] cannot get default configuration path, because %s", err.Error())
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootInfof("[initializer.initializeSystem] will load configuration from default config path %s", configFilePath)
|
log.BootInfof(c, "[initializer.initializeSystem] will load configuration from default config path %s", configFilePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,13 +52,13 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootErrorf("[initializer.initializeSystem] cannot load configuration, because %s", err.Error())
|
log.BootErrorf(c, "[initializer.initializeSystem] cannot load configuration, because %s", err.Error())
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.SecretKeyNoSet {
|
if config.SecretKeyNoSet {
|
||||||
log.BootWarnf("[initializer.initializeSystem] \"secret_key\" in config file is not set, please change it to keep your user data safe")
|
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)
|
settings.SetCurrentConfig(config)
|
||||||
@@ -67,7 +67,7 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootErrorf("[initializer.initializeSystem] initializes data store failed, because %s", err.Error())
|
log.BootErrorf(c, "[initializer.initializeSystem] initializes data store failed, because %s", err.Error())
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootErrorf("[initializer.initializeSystem] sets logger configuration failed, because %s", err.Error())
|
log.BootErrorf(c, "[initializer.initializeSystem] sets logger configuration failed, because %s", err.Error())
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootErrorf("[initializer.initializeSystem] initializes object storage failed, because %s", err.Error())
|
log.BootErrorf(c, "[initializer.initializeSystem] initializes object storage failed, because %s", err.Error())
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootErrorf("[initializer.initializeSystem] initializes uuid generator failed, because %s", err.Error())
|
log.BootErrorf(c, "[initializer.initializeSystem] initializes uuid generator failed, because %s", err.Error())
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,16 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootErrorf("[initializer.initializeSystem] initializes duplicate checker failed, because %s", err.Error())
|
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())
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -112,7 +121,7 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootErrorf("[initializer.initializeSystem] initializes mailer failed, because %s", err.Error())
|
log.BootErrorf(c, "[initializer.initializeSystem] initializes mailer failed, because %s", err.Error())
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -121,7 +130,7 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootErrorf("[initializer.initializeSystem] initializes exchange rates data source failed, because %s", err.Error())
|
log.BootErrorf(c, "[initializer.initializeSystem] initializes exchange rates data source failed, because %s", err.Error())
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -129,7 +138,7 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
|||||||
cfgJson, _ := json.Marshal(getConfigWithoutSensitiveData(config))
|
cfgJson, _ := json.Marshal(getConfigWithoutSensitiveData(config))
|
||||||
|
|
||||||
if !isDisableBootLog {
|
if !isDisableBootLog {
|
||||||
log.BootInfof("[initializer.initializeSystem] has loaded configuration %s", cfgJson)
|
log.BootInfof(c, "[initializer.initializeSystem] has loaded configuration %s", cfgJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
|
|||||||
+3
-2
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ var SecurityUtils = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "gen-secret-key",
|
Name: "gen-secret-key",
|
||||||
Usage: "Generate a random secret key",
|
Usage: "Generate a random secret key",
|
||||||
Action: genSecretKey,
|
Action: bindAction(genSecretKey),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.IntFlag{
|
&cli.IntFlag{
|
||||||
Name: "length",
|
Name: "length",
|
||||||
@@ -30,7 +31,7 @@ var SecurityUtils = &cli.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func genSecretKey(c *cli.Context) error {
|
func genSecretKey(c *core.CliContext) error {
|
||||||
length := c.Int("length")
|
length := c.Int("length")
|
||||||
|
|
||||||
if length <= 0 {
|
if length <= 0 {
|
||||||
|
|||||||
+310
-68
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
clis "github.com/mayswind/ezbookkeeping/pkg/cli"
|
clis "github.com/mayswind/ezbookkeeping/pkg/cli"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
@@ -21,7 +22,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "user-add",
|
Name: "user-add",
|
||||||
Usage: "Add new user",
|
Usage: "Add new user",
|
||||||
Action: addNewUser,
|
Action: bindAction(addNewUser),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -58,7 +59,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "user-get",
|
Name: "user-get",
|
||||||
Usage: "Get specified user info",
|
Usage: "Get specified user info",
|
||||||
Action: getUserInfo,
|
Action: bindAction(getUserInfo),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -71,7 +72,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "user-modify-password",
|
Name: "user-modify-password",
|
||||||
Usage: "Modify user password",
|
Usage: "Modify user password",
|
||||||
Action: modifyUserPassword,
|
Action: bindAction(modifyUserPassword),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -90,7 +91,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "user-enable",
|
Name: "user-enable",
|
||||||
Usage: "Enable specified user",
|
Usage: "Enable specified user",
|
||||||
Action: enableUser,
|
Action: bindAction(enableUser),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -103,7 +104,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "user-disable",
|
Name: "user-disable",
|
||||||
Usage: "Disable specified user",
|
Usage: "Disable specified user",
|
||||||
Action: disableUser,
|
Action: bindAction(disableUser),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -113,10 +114,67 @@ 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",
|
Name: "user-resend-verify-email",
|
||||||
Usage: "Resend user verify email",
|
Usage: "Resend user verify email",
|
||||||
Action: resendUserVerifyEmail,
|
Action: bindAction(resendUserVerifyEmail),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -129,7 +187,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "user-set-email-verified",
|
Name: "user-set-email-verified",
|
||||||
Usage: "Set user email address verified",
|
Usage: "Set user email address verified",
|
||||||
Action: setUserEmailVerified,
|
Action: bindAction(setUserEmailVerified),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -142,7 +200,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "user-set-email-unverified",
|
Name: "user-set-email-unverified",
|
||||||
Usage: "Set user email address unverified",
|
Usage: "Set user email address unverified",
|
||||||
Action: setUserEmailUnverified,
|
Action: bindAction(setUserEmailUnverified),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -155,7 +213,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "user-delete",
|
Name: "user-delete",
|
||||||
Usage: "Delete specified user",
|
Usage: "Delete specified user",
|
||||||
Action: deleteUser,
|
Action: bindAction(deleteUser),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -168,7 +226,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "user-2fa-disable",
|
Name: "user-2fa-disable",
|
||||||
Usage: "Disable user 2fa setting",
|
Usage: "Disable user 2fa setting",
|
||||||
Action: disableUser2FA,
|
Action: bindAction(disableUser2FA),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -181,7 +239,20 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "user-session-list",
|
Name: "user-session-list",
|
||||||
Usage: "List all user sessions",
|
Usage: "List all user sessions",
|
||||||
Action: listUserTokens,
|
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),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -194,7 +265,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "user-session-clear",
|
Name: "user-session-clear",
|
||||||
Usage: "Clear user all sessions",
|
Usage: "Clear user all sessions",
|
||||||
Action: clearUserTokens,
|
Action: bindAction(clearUserTokens),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -207,7 +278,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "send-password-reset-mail",
|
Name: "send-password-reset-mail",
|
||||||
Usage: "Send password reset mail",
|
Usage: "Send password reset mail",
|
||||||
Action: sendPasswordResetMail,
|
Action: bindAction(sendPasswordResetMail),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -220,7 +291,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "transaction-check",
|
Name: "transaction-check",
|
||||||
Usage: "Check whether user all transactions and accounts are correct",
|
Usage: "Check whether user all transactions and accounts are correct",
|
||||||
Action: checkUserTransactionAndAccount,
|
Action: bindAction(checkUserTransactionAndAccount),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -233,7 +304,7 @@ var UserData = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "transaction-tag-index-fix-transaction-time",
|
Name: "transaction-tag-index-fix-transaction-time",
|
||||||
Usage: "Fix the transaction tag index data which does not have transaction time",
|
Usage: "Fix the transaction tag index data which does not have transaction time",
|
||||||
Action: fixTransactionTagIndexNotHaveTransactionTime,
|
Action: bindAction(fixTransactionTagIndexNotHaveTransactionTime),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -243,10 +314,35 @@ var UserData = &cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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",
|
Name: "transaction-export",
|
||||||
Usage: "Export user all transactions to file",
|
Usage: "Export user all transactions to file",
|
||||||
Action: exportUserTransaction,
|
Action: bindAction(exportUserTransaction),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
@@ -271,7 +367,7 @@ var UserData = &cli.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func addNewUser(c *cli.Context) error {
|
func addNewUser(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -287,7 +383,7 @@ func addNewUser(c *cli.Context) error {
|
|||||||
user, err := clis.UserData.AddNewUser(c, username, email, nickname, password, defaultCurrency)
|
user, err := clis.UserData.AddNewUser(c, username, email, nickname, password, defaultCurrency)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.addNewUser] error occurs when adding new user")
|
log.CliErrorf(c, "[user_data.addNewUser] error occurs when adding new user")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +392,7 @@ func addNewUser(c *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserInfo(c *cli.Context) error {
|
func getUserInfo(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -307,7 +403,7 @@ func getUserInfo(c *cli.Context) error {
|
|||||||
user, err := clis.UserData.GetUserByUsername(c, username)
|
user, err := clis.UserData.GetUserByUsername(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.getUserInfo] error occurs when getting user data")
|
log.CliErrorf(c, "[user_data.getUserInfo] error occurs when getting user data")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +412,7 @@ func getUserInfo(c *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func modifyUserPassword(c *cli.Context) error {
|
func modifyUserPassword(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -328,16 +424,16 @@ func modifyUserPassword(c *cli.Context) error {
|
|||||||
err = clis.UserData.ModifyUserPassword(c, username, password)
|
err = clis.UserData.ModifyUserPassword(c, username, password)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.modifyUserPassword] error occurs when modifying user password")
|
log.CliErrorf(c, "[user_data.modifyUserPassword] error occurs when modifying user password")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.modifyUserPassword] password of user \"%s\" has been changed", username)
|
log.CliInfof(c, "[user_data.modifyUserPassword] password of user \"%s\" has been changed", username)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendPasswordResetMail(c *cli.Context) error {
|
func sendPasswordResetMail(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -348,16 +444,16 @@ func sendPasswordResetMail(c *cli.Context) error {
|
|||||||
err = clis.UserData.SendPasswordResetMail(c, username)
|
err = clis.UserData.SendPasswordResetMail(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.sendPasswordResetMail] error occurs when sending password reset email")
|
log.CliErrorf(c, "[user_data.sendPasswordResetMail] error occurs when sending password reset email")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.sendPasswordResetMail] a password reset email for user \"%s\" has been sent", username)
|
log.CliInfof(c, "[user_data.sendPasswordResetMail] a password reset email for user \"%s\" has been sent", username)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableUser(c *cli.Context) error {
|
func enableUser(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -368,16 +464,16 @@ func enableUser(c *cli.Context) error {
|
|||||||
err = clis.UserData.EnableUser(c, username)
|
err = clis.UserData.EnableUser(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.enableUser] error occurs when setting user enabled")
|
log.CliErrorf(c, "[user_data.enableUser] error occurs when setting user enabled")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.enableUser] user \"%s\" has been set enabled", username)
|
log.CliInfof(c, "[user_data.enableUser] user \"%s\" has been set enabled", username)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func disableUser(c *cli.Context) error {
|
func disableUser(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -388,16 +484,91 @@ func disableUser(c *cli.Context) error {
|
|||||||
err = clis.UserData.DisableUser(c, username)
|
err = clis.UserData.DisableUser(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.disableUser] error occurs when setting user disabled")
|
log.CliErrorf(c, "[user_data.disableUser] error occurs when setting user disabled")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.disableUser] user \"%s\" has been set disabled", username)
|
log.CliInfof(c, "[user_data.disableUser] user \"%s\" has been set disabled", username)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resendUserVerifyEmail(c *cli.Context) error {
|
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 {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -408,16 +579,16 @@ func resendUserVerifyEmail(c *cli.Context) error {
|
|||||||
err = clis.UserData.ResendVerifyEmail(c, username)
|
err = clis.UserData.ResendVerifyEmail(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.resendUserVerifyEmail] error occurs when resending user verify email")
|
log.CliErrorf(c, "[user_data.resendUserVerifyEmail] error occurs when resending user verify email")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.resendUserVerifyEmail] verify email for user \"%s\" has been resent", username)
|
log.CliInfof(c, "[user_data.resendUserVerifyEmail] verify email for user \"%s\" has been resent", username)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setUserEmailVerified(c *cli.Context) error {
|
func setUserEmailVerified(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -428,16 +599,16 @@ func setUserEmailVerified(c *cli.Context) error {
|
|||||||
err = clis.UserData.SetUserEmailVerified(c, username)
|
err = clis.UserData.SetUserEmailVerified(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.setUserEmailVerified] error occurs when setting user email address verified")
|
log.CliErrorf(c, "[user_data.setUserEmailVerified] error occurs when setting user email address verified")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.setUserEmailVerified] user \"%s\" email address has been set verified", username)
|
log.CliInfof(c, "[user_data.setUserEmailVerified] user \"%s\" email address has been set verified", username)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setUserEmailUnverified(c *cli.Context) error {
|
func setUserEmailUnverified(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -448,16 +619,16 @@ func setUserEmailUnverified(c *cli.Context) error {
|
|||||||
err = clis.UserData.SetUserEmailUnverified(c, username)
|
err = clis.UserData.SetUserEmailUnverified(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.setUserEmailUnverified] error occurs when setting user email address unverified")
|
log.CliErrorf(c, "[user_data.setUserEmailUnverified] error occurs when setting user email address unverified")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.setUserEmailUnverified] user \"%s\" email address has been set unverified", username)
|
log.CliInfof(c, "[user_data.setUserEmailUnverified] user \"%s\" email address has been set unverified", username)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteUser(c *cli.Context) error {
|
func deleteUser(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -468,16 +639,16 @@ func deleteUser(c *cli.Context) error {
|
|||||||
err = clis.UserData.DeleteUser(c, username)
|
err = clis.UserData.DeleteUser(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.deleteUser] error occurs when deleting user")
|
log.CliErrorf(c, "[user_data.deleteUser] error occurs when deleting user")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.deleteUser] user \"%s\" has been deleted", username)
|
log.CliInfof(c, "[user_data.deleteUser] user \"%s\" has been deleted", username)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func disableUser2FA(c *cli.Context) error {
|
func disableUser2FA(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -488,16 +659,16 @@ func disableUser2FA(c *cli.Context) error {
|
|||||||
err = clis.UserData.DisableUserTwoFactorAuthorization(c, username)
|
err = clis.UserData.DisableUserTwoFactorAuthorization(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.disableUser2FA] error occurs when disabling user two-factor authorization")
|
log.CliErrorf(c, "[user_data.disableUser2FA] error occurs when disabling user two-factor authorization")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.disableUser2FA] two-factor authorization of user \"%s\" has been disabled", username)
|
log.CliInfof(c, "[user_data.disableUser2FA] two-factor authorization of user \"%s\" has been disabled", username)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func listUserTokens(c *cli.Context) error {
|
func listUserTokens(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -508,7 +679,7 @@ func listUserTokens(c *cli.Context) error {
|
|||||||
tokens, err := clis.UserData.ListUserTokens(c, username)
|
tokens, err := clis.UserData.ListUserTokens(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.listUserTokens] error occurs when getting user tokens")
|
log.CliErrorf(c, "[user_data.listUserTokens] error occurs when getting user tokens")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,7 +694,28 @@ func listUserTokens(c *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearUserTokens(c *cli.Context) error {
|
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 {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -534,16 +726,16 @@ func clearUserTokens(c *cli.Context) error {
|
|||||||
err = clis.UserData.ClearUserTokens(c, username)
|
err = clis.UserData.ClearUserTokens(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.clearUserTokens] error occurs when clearing user tokens")
|
log.CliErrorf(c, "[user_data.clearUserTokens] error occurs when clearing user tokens")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.clearUserTokens] all tokens of user \"%s\" has been cleared", username)
|
log.CliInfof(c, "[user_data.clearUserTokens] all tokens of user \"%s\" has been cleared", username)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkUserTransactionAndAccount(c *cli.Context) error {
|
func checkUserTransactionAndAccount(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -552,21 +744,21 @@ func checkUserTransactionAndAccount(c *cli.Context) error {
|
|||||||
|
|
||||||
username := c.String("username")
|
username := c.String("username")
|
||||||
|
|
||||||
log.BootInfof("[user_data.checkUserTransactionAndAccount] starting checking user \"%s\" data", username)
|
log.CliInfof(c, "[user_data.checkUserTransactionAndAccount] starting checking user \"%s\" data", username)
|
||||||
|
|
||||||
_, err = clis.UserData.CheckTransactionAndAccount(c, username)
|
_, err = clis.UserData.CheckTransactionAndAccount(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.checkUserTransactionAndAccount] error occurs when checking user data")
|
log.CliErrorf(c, "[user_data.checkUserTransactionAndAccount] error occurs when checking user data")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.checkUserTransactionAndAccount] user transactions and accounts data has been checked successfully, there is no problem with user data")
|
log.CliInfof(c, "[user_data.checkUserTransactionAndAccount] user transactions and accounts data has been checked successfully, there is no problem with user data")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fixTransactionTagIndexNotHaveTransactionTime(c *cli.Context) error {
|
func fixTransactionTagIndexNotHaveTransactionTime(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -575,21 +767,21 @@ func fixTransactionTagIndexNotHaveTransactionTime(c *cli.Context) error {
|
|||||||
|
|
||||||
username := c.String("username")
|
username := c.String("username")
|
||||||
|
|
||||||
log.BootInfof("[user_data.fixTransactionTagIndexNotHaveTransactionTime] starting fixing user \"%s\" transaction tag index data", username)
|
log.CliInfof(c, "[user_data.fixTransactionTagIndexNotHaveTransactionTime] starting fixing user \"%s\" transaction tag index data", username)
|
||||||
|
|
||||||
_, err = clis.UserData.FixTransactionTagIndexWithTransactionTime(c, username)
|
_, err = clis.UserData.FixTransactionTagIndexWithTransactionTime(c, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.fixTransactionTagIndexNotHaveTransactionTime] error occurs when fixing user data")
|
log.CliErrorf(c, "[user_data.fixTransactionTagIndexNotHaveTransactionTime] error occurs when fixing user data")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.fixTransactionTagIndexNotHaveTransactionTime] user transaction tag index data has been fixed successfully")
|
log.CliInfof(c, "[user_data.fixTransactionTagIndexNotHaveTransactionTime] user transaction tag index data has been fixed successfully")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func exportUserTransaction(c *cli.Context) error {
|
func exportUserTransaction(c *core.CliContext) error {
|
||||||
_, err := initializeSystem(c)
|
_, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -601,39 +793,88 @@ func exportUserTransaction(c *cli.Context) error {
|
|||||||
fileType := c.String("type")
|
fileType := c.String("type")
|
||||||
|
|
||||||
if fileType != "" && fileType != "csv" && fileType != "tsv" {
|
if fileType != "" && fileType != "csv" && fileType != "tsv" {
|
||||||
log.BootErrorf("[user_data.exportUserTransaction] export file type is not supported")
|
log.CliErrorf(c, "[user_data.exportUserTransaction] export file type is not supported")
|
||||||
return errs.ErrNotSupported
|
return errs.ErrNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
if filePath == "" {
|
if filePath == "" {
|
||||||
log.BootErrorf("[user_data.exportUserTransaction] export file path is unspecified")
|
log.CliErrorf(c, "[user_data.exportUserTransaction] export file path is unspecified")
|
||||||
return os.ErrNotExist
|
return os.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
fileExists, err := utils.IsExists(filePath)
|
fileExists, err := utils.IsExists(filePath)
|
||||||
|
|
||||||
if fileExists {
|
if fileExists {
|
||||||
log.BootErrorf("[user_data.exportUserTransaction] specified file path already exists")
|
log.CliErrorf(c, "[user_data.exportUserTransaction] specified file path already exists")
|
||||||
return os.ErrExist
|
return os.ErrExist
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.exportUserTransaction] starting exporting user \"%s\" data", username)
|
log.CliInfof(c, "[user_data.exportUserTransaction] starting exporting user \"%s\" data", username)
|
||||||
|
|
||||||
content, err := clis.UserData.ExportTransaction(c, username, fileType)
|
content, err := clis.UserData.ExportTransaction(c, username, fileType)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.exportUserTransaction] error occurs when exporting user data")
|
log.CliErrorf(c, "[user_data.exportUserTransaction] error occurs when exporting user data")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = utils.WriteFile(filePath, content)
|
err = utils.WriteFile(filePath, content)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[user_data.exportUserTransaction] failed to write to %s", filePath)
|
log.CliErrorf(c, "[user_data.exportUserTransaction] failed to write to %s", filePath)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[user_data.exportUserTransaction] user transactions have been exported to %s", filePath)
|
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)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -660,6 +901,7 @@ func printUserInfo(user *models.User) {
|
|||||||
fmt.Printf("[CurrencyDisplayType] %s (%d)\n", user.CurrencyDisplayType, user.CurrencyDisplayType)
|
fmt.Printf("[CurrencyDisplayType] %s (%d)\n", user.CurrencyDisplayType, user.CurrencyDisplayType)
|
||||||
fmt.Printf("[ExpenseAmountColor] %s (%d)\n", user.ExpenseAmountColor, user.ExpenseAmountColor)
|
fmt.Printf("[ExpenseAmountColor] %s (%d)\n", user.ExpenseAmountColor, user.ExpenseAmountColor)
|
||||||
fmt.Printf("[IncomeAmountColor] %s (%d)\n", user.IncomeAmountColor, user.IncomeAmountColor)
|
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("[Deleted] %t\n", user.Deleted)
|
||||||
fmt.Printf("[EmailVerified] %t\n", user.EmailVerified)
|
fmt.Printf("[EmailVerified] %t\n", user.EmailVerified)
|
||||||
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(user.CreatedUnixTime), user.CreatedUnixTime)
|
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(user.CreatedUnixTime), user.CreatedUnixTime)
|
||||||
|
|||||||
+7
-6
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/requestid"
|
"github.com/mayswind/ezbookkeeping/pkg/requestid"
|
||||||
@@ -21,7 +22,7 @@ var Utilities = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "parse-default-request-id",
|
Name: "parse-default-request-id",
|
||||||
Usage: "Parse a request id which is generated by default request generator and show the details",
|
Usage: "Parse a request id which is generated by default request generator and show the details",
|
||||||
Action: parseRequestId,
|
Action: bindAction(parseRequestId),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "id",
|
Name: "id",
|
||||||
@@ -33,7 +34,7 @@ var Utilities = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "send-test-mail",
|
Name: "send-test-mail",
|
||||||
Usage: "Send an email to specified e-mail address",
|
Usage: "Send an email to specified e-mail address",
|
||||||
Action: sendTestMail,
|
Action: bindAction(sendTestMail),
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "to",
|
Name: "to",
|
||||||
@@ -45,15 +46,15 @@ var Utilities = &cli.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseRequestId(c *cli.Context) error {
|
func parseRequestId(c *core.CliContext) error {
|
||||||
config, err := initializeSystem(c)
|
config, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = requestid.InitializeRequestIdGenerator(config)
|
err = requestid.InitializeRequestIdGenerator(c, config)
|
||||||
defaultGenerator, err := requestid.NewDefaultRequestIdGenerator(config)
|
defaultGenerator, err := requestid.NewDefaultRequestIdGenerator(c, config)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -73,7 +74,7 @@ func parseRequestId(c *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendTestMail(c *cli.Context) error {
|
func sendTestMail(c *core.CliContext) error {
|
||||||
config, err := initializeSystem(c)
|
config, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+69
-37
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/api"
|
"github.com/mayswind/ezbookkeeping/pkg/api"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/cron"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/middlewares"
|
"github.com/mayswind/ezbookkeeping/pkg/middlewares"
|
||||||
@@ -32,33 +33,40 @@ var WebServer = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "run",
|
Name: "run",
|
||||||
Usage: "Run ezBookkeeping web server",
|
Usage: "Run ezBookkeeping web server",
|
||||||
Action: startWebServer,
|
Action: bindAction(startWebServer),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func startWebServer(c *cli.Context) error {
|
func startWebServer(c *core.CliContext) error {
|
||||||
config, err := initializeSystem(c)
|
config, err := initializeSystem(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[webserver.startWebServer] static root path is %s", config.StaticRootPath)
|
log.BootInfof(c, "[webserver.startWebServer] static root path is %s", config.StaticRootPath)
|
||||||
|
|
||||||
if config.AutoUpdateDatabase {
|
if config.AutoUpdateDatabase {
|
||||||
err = updateAllDatabaseTablesStructure()
|
err = updateAllDatabaseTablesStructure(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[webserver.startWebServer] update database table structure failed, because %s", err.Error())
|
log.BootErrorf(c, "[webserver.startWebServer] update database table structure failed, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = requestid.InitializeRequestIdGenerator(config)
|
err = requestid.InitializeRequestIdGenerator(c, config)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[webserver.startWebServer] initializes requestid generator failed, because %s", err.Error())
|
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())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +76,7 @@ func startWebServer(c *cli.Context) error {
|
|||||||
uuidServerInfo = fmt.Sprintf(", current uuid server id is %d", config.UuidServerId)
|
uuidServerInfo = fmt.Sprintf(", current uuid server id is %d", config.UuidServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.BootInfof("[webserver.startWebServer] %s%s", serverInfo, uuidServerInfo)
|
log.BootInfof(c, "[webserver.startWebServer] %s%s", serverInfo, uuidServerInfo)
|
||||||
|
|
||||||
if config.Mode == settings.MODE_PRODUCTION {
|
if config.Mode == settings.MODE_PRODUCTION {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
@@ -95,6 +103,8 @@ func startWebServer(c *cli.Context) error {
|
|||||||
router.NoRoute(bindApi(api.Default.ApiNotFound))
|
router.NoRoute(bindApi(api.Default.ApiNotFound))
|
||||||
router.NoMethod(bindApi(api.Default.MethodNotAllowed))
|
router.NoMethod(bindApi(api.Default.MethodNotAllowed))
|
||||||
|
|
||||||
|
serverSettingsCacheStore := persistence.NewInMemoryStore(time.Minute)
|
||||||
|
|
||||||
router.StaticFile("/", filepath.Join(config.StaticRootPath, "index.html"))
|
router.StaticFile("/", filepath.Join(config.StaticRootPath, "index.html"))
|
||||||
router.Static("/js", filepath.Join(config.StaticRootPath, "js"))
|
router.Static("/js", filepath.Join(config.StaticRootPath, "js"))
|
||||||
router.Static("/css", filepath.Join(config.StaticRootPath, "css"))
|
router.Static("/css", filepath.Join(config.StaticRootPath, "css"))
|
||||||
@@ -106,12 +116,9 @@ func startWebServer(c *cli.Context) error {
|
|||||||
router.StaticFile("favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
|
router.StaticFile("favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
|
||||||
router.StaticFile("touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
|
router.StaticFile("touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
|
||||||
router.StaticFile("manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
|
router.StaticFile("manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
|
||||||
|
router.GET("/server_settings.js", bindCachedJs(api.ServerSettings.ServerSettingsJavascriptHandler, serverSettingsCacheStore))
|
||||||
|
|
||||||
mobileEntryRoute := router.Group("/mobile")
|
router.StaticFile("/mobile", filepath.Join(config.StaticRootPath, "mobile.html"))
|
||||||
mobileEntryRoute.Use(bindMiddleware(middlewares.ServerSettingsCookie(config)))
|
|
||||||
{
|
|
||||||
mobileEntryRoute.StaticFile("/", filepath.Join(config.StaticRootPath, "mobile.html"))
|
|
||||||
}
|
|
||||||
router.Static("/mobile/js", filepath.Join(config.StaticRootPath, "js"))
|
router.Static("/mobile/js", filepath.Join(config.StaticRootPath, "js"))
|
||||||
router.Static("/mobile/css", filepath.Join(config.StaticRootPath, "css"))
|
router.Static("/mobile/css", filepath.Join(config.StaticRootPath, "css"))
|
||||||
router.Static("/mobile/img", filepath.Join(config.StaticRootPath, "img"))
|
router.Static("/mobile/img", filepath.Join(config.StaticRootPath, "img"))
|
||||||
@@ -121,16 +128,13 @@ func startWebServer(c *cli.Context) error {
|
|||||||
router.StaticFile("/mobile/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
|
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/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
|
||||||
router.StaticFile("/mobile/sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
|
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++ {
|
for i := 0; i < len(workboxFileNames); i++ {
|
||||||
router.StaticFile("/mobile/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
|
router.StaticFile("/mobile/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
|
||||||
}
|
}
|
||||||
|
|
||||||
desktopEntryRoute := router.Group("/desktop")
|
router.StaticFile("/desktop", filepath.Join(config.StaticRootPath, "desktop.html"))
|
||||||
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/js", filepath.Join(config.StaticRootPath, "js"))
|
||||||
router.Static("/desktop/css", filepath.Join(config.StaticRootPath, "css"))
|
router.Static("/desktop/css", filepath.Join(config.StaticRootPath, "css"))
|
||||||
router.Static("/desktop/img", filepath.Join(config.StaticRootPath, "img"))
|
router.Static("/desktop/img", filepath.Join(config.StaticRootPath, "img"))
|
||||||
@@ -140,12 +144,13 @@ func startWebServer(c *cli.Context) error {
|
|||||||
router.StaticFile("/desktop/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
|
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/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
|
||||||
router.StaticFile("/desktop/sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
|
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++ {
|
for i := 0; i < len(workboxFileNames); i++ {
|
||||||
router.StaticFile("/desktop/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
|
router.StaticFile("/desktop/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.AvatarProvider == settings.InternalAvatarProvider {
|
if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL {
|
||||||
avatarRoute := router.Group("/avatar")
|
avatarRoute := router.Group("/avatar")
|
||||||
avatarRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
avatarRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
||||||
{
|
{
|
||||||
@@ -153,12 +158,15 @@ func startWebServer(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
|
if config.EnableTransactionPictures {
|
||||||
|
pictureRoute := router.Group("/pictures")
|
||||||
if config.Mode == settings.MODE_DEVELOPMENT {
|
pictureRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
||||||
devRoute := router.Group("/dev")
|
{
|
||||||
devRoute.GET("/cookies", bindMiddleware(middlewares.ServerSettingsCookie(config)))
|
pictureRoute.GET("/:fileName", bindImage(api.TransactionPictures.TransactionPictureGetHandler))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
|
||||||
|
|
||||||
proxyRoute := router.Group("/proxy")
|
proxyRoute := router.Group("/proxy")
|
||||||
proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
||||||
@@ -253,7 +261,7 @@ func startWebServer(c *cli.Context) error {
|
|||||||
apiV1Route.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler))
|
apiV1Route.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler))
|
||||||
apiV1Route.POST("/users/profile/update.json", bindApiWithTokenUpdate(api.Users.UserUpdateProfileHandler, config))
|
apiV1Route.POST("/users/profile/update.json", bindApiWithTokenUpdate(api.Users.UserUpdateProfileHandler, config))
|
||||||
|
|
||||||
if config.AvatarProvider == settings.InternalAvatarProvider {
|
if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL {
|
||||||
apiV1Route.POST("/users/avatar/update.json", bindApi(api.Users.UserUpdateAvatarHandler))
|
apiV1Route.POST("/users/avatar/update.json", bindApi(api.Users.UserUpdateAvatarHandler))
|
||||||
apiV1Route.POST("/users/avatar/remove.json", bindApi(api.Users.UserRemoveAvatarHandler))
|
apiV1Route.POST("/users/avatar/remove.json", bindApi(api.Users.UserRemoveAvatarHandler))
|
||||||
}
|
}
|
||||||
@@ -301,6 +309,17 @@ func startWebServer(c *cli.Context) error {
|
|||||||
apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler))
|
apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler))
|
||||||
apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
|
apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
|
||||||
|
|
||||||
|
if config.EnableDataImport {
|
||||||
|
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
|
// Transaction Categories
|
||||||
apiV1Route.GET("/transaction/categories/list.json", bindApi(api.TransactionCategories.CategoryListHandler))
|
apiV1Route.GET("/transaction/categories/list.json", bindApi(api.TransactionCategories.CategoryListHandler))
|
||||||
apiV1Route.GET("/transaction/categories/get.json", bindApi(api.TransactionCategories.CategoryGetHandler))
|
apiV1Route.GET("/transaction/categories/get.json", bindApi(api.TransactionCategories.CategoryGetHandler))
|
||||||
@@ -337,20 +356,20 @@ func startWebServer(c *cli.Context) error {
|
|||||||
listenAddr := fmt.Sprintf("%s:%d", config.HttpAddr, config.HttpPort)
|
listenAddr := fmt.Sprintf("%s:%d", config.HttpAddr, config.HttpPort)
|
||||||
|
|
||||||
if config.Protocol == settings.SCHEME_SOCKET {
|
if config.Protocol == settings.SCHEME_SOCKET {
|
||||||
log.BootInfof("[webserver.startWebServer] will run at socks:%s", config.UnixSocketPath)
|
log.BootInfof(c, "[webserver.startWebServer] will run at socks:%s", config.UnixSocketPath)
|
||||||
err = router.RunUnix(config.UnixSocketPath)
|
err = router.RunUnix(config.UnixSocketPath)
|
||||||
} else if config.Protocol == settings.SCHEME_HTTP {
|
} else if config.Protocol == settings.SCHEME_HTTP {
|
||||||
log.BootInfof("[webserver.startWebServer] will run at http://%s", listenAddr)
|
log.BootInfof(c, "[webserver.startWebServer] will run at http://%s", listenAddr)
|
||||||
err = router.Run(listenAddr)
|
err = router.Run(listenAddr)
|
||||||
} else if config.Protocol == settings.SCHEME_HTTPS {
|
} else if config.Protocol == settings.SCHEME_HTTPS {
|
||||||
log.BootInfof("[webserver.startWebServer] will run at https://%s", listenAddr)
|
log.BootInfof(c, "[webserver.startWebServer] will run at https://%s", listenAddr)
|
||||||
err = router.RunTLS(listenAddr, config.CertFile, config.CertKeyFile)
|
err = router.RunTLS(listenAddr, config.CertFile, config.CertKeyFile)
|
||||||
} else {
|
} else {
|
||||||
err = errs.ErrInvalidProtocol
|
err = errs.ErrInvalidProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.BootErrorf("[webserver.startWebServer] cannot start, because %s", err)
|
log.BootErrorf(c, "[webserver.startWebServer] cannot start, because %s", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,13 +378,13 @@ func startWebServer(c *cli.Context) error {
|
|||||||
|
|
||||||
func bindMiddleware(fn core.MiddlewareHandlerFunc) gin.HandlerFunc {
|
func bindMiddleware(fn core.MiddlewareHandlerFunc) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
fn(core.WrapContext(c))
|
fn(core.WrapWebContext(c))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc {
|
func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc {
|
||||||
return func(ginCtx *gin.Context) {
|
return func(ginCtx *gin.Context) {
|
||||||
c := core.WrapContext(ginCtx)
|
c := core.WrapWebContext(ginCtx)
|
||||||
result, err := fn(c)
|
result, err := fn(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -378,7 +397,7 @@ func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc {
|
|||||||
|
|
||||||
func bindApiWithTokenUpdate(fn core.ApiHandlerFunc, config *settings.Config) gin.HandlerFunc {
|
func bindApiWithTokenUpdate(fn core.ApiHandlerFunc, config *settings.Config) gin.HandlerFunc {
|
||||||
return func(ginCtx *gin.Context) {
|
return func(ginCtx *gin.Context) {
|
||||||
c := core.WrapContext(ginCtx)
|
c := core.WrapWebContext(ginCtx)
|
||||||
result, err := fn(c)
|
result, err := fn(c)
|
||||||
|
|
||||||
if err == nil && config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationInternalProxyMethod {
|
if err == nil && config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationInternalProxyMethod {
|
||||||
@@ -393,9 +412,22 @@ 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 {
|
func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
|
||||||
return func(ginCtx *gin.Context) {
|
return func(ginCtx *gin.Context) {
|
||||||
c := core.WrapContext(ginCtx)
|
c := core.WrapWebContext(ginCtx)
|
||||||
result, fileName, err := fn(c)
|
result, fileName, err := fn(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -408,7 +440,7 @@ func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
|
|||||||
|
|
||||||
func bindTsv(fn core.DataHandlerFunc) gin.HandlerFunc {
|
func bindTsv(fn core.DataHandlerFunc) gin.HandlerFunc {
|
||||||
return func(ginCtx *gin.Context) {
|
return func(ginCtx *gin.Context) {
|
||||||
c := core.WrapContext(ginCtx)
|
c := core.WrapWebContext(ginCtx)
|
||||||
result, fileName, err := fn(c)
|
result, fileName, err := fn(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -421,7 +453,7 @@ func bindTsv(fn core.DataHandlerFunc) gin.HandlerFunc {
|
|||||||
|
|
||||||
func bindImage(fn core.ImageHandlerFunc) gin.HandlerFunc {
|
func bindImage(fn core.ImageHandlerFunc) gin.HandlerFunc {
|
||||||
return func(ginCtx *gin.Context) {
|
return func(ginCtx *gin.Context) {
|
||||||
c := core.WrapContext(ginCtx)
|
c := core.WrapWebContext(ginCtx)
|
||||||
result, contentType, err := fn(c)
|
result, contentType, err := fn(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -434,7 +466,7 @@ func bindImage(fn core.ImageHandlerFunc) gin.HandlerFunc {
|
|||||||
|
|
||||||
func bindCachedImage(fn core.ImageHandlerFunc, store persistence.CacheStore) gin.HandlerFunc {
|
func bindCachedImage(fn core.ImageHandlerFunc, store persistence.CacheStore) gin.HandlerFunc {
|
||||||
return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) {
|
return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) {
|
||||||
c := core.WrapContext(ginCtx)
|
c := core.WrapWebContext(ginCtx)
|
||||||
result, contentType, err := fn(c)
|
result, contentType, err := fn(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -447,7 +479,7 @@ func bindCachedImage(fn core.ImageHandlerFunc, store persistence.CacheStore) gin
|
|||||||
|
|
||||||
func bindProxy(fn core.ProxyHandlerFunc) gin.HandlerFunc {
|
func bindProxy(fn core.ProxyHandlerFunc) gin.HandlerFunc {
|
||||||
return func(ginCtx *gin.Context) {
|
return func(ginCtx *gin.Context) {
|
||||||
c := core.WrapContext(ginCtx)
|
c := core.WrapWebContext(ginCtx)
|
||||||
proxy, err := fn(c)
|
proxy, err := fn(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+66
-7
@@ -146,10 +146,17 @@ checker_type = in_memory
|
|||||||
# For "in_memory" duplicate checker only, cleanup expired data interval seconds (1 - 4294967295), default is 60 (1 minutes)
|
# For "in_memory" duplicate checker only, cleanup expired data interval seconds (1 - 4294967295), default is 60 (1 minutes)
|
||||||
cleanup_interval = 60
|
cleanup_interval = 60
|
||||||
|
|
||||||
# The minimum interval seconds (0 - 4294967295) between duplicate submissions on the same page (exiting and re-entering the page is considered as a new session)
|
# 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)
|
# Set to 0 to disable duplicate checker for new data submissions, default is 300 (5 minutes)
|
||||||
duplicate_submissions_interval = 300
|
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]
|
[security]
|
||||||
# Used for signing, you must change it to keep your user data safe before you first run ezBookkeeping
|
# Used for signing, you must change it to keep your user data safe before you first run ezBookkeeping
|
||||||
secret_key =
|
secret_key =
|
||||||
@@ -192,16 +199,58 @@ enable_forget_password = true
|
|||||||
# Set to true to require email must be verified when use forget password
|
# Set to true to require email must be verified when use forget password
|
||||||
forget_password_require_email_verify = false
|
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:
|
# 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
|
# "internal": Use the internal object storage to store user avatar (refer to "storage" settings), supports updating avatar by user self
|
||||||
# "gravatar": https://gravatar.com
|
# "gravatar": https://gravatar.com
|
||||||
# Leave blank if you want to disable user avatar
|
# Leave blank if you want to disable user avatar
|
||||||
avatar_provider = internal
|
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 =
|
||||||
|
|
||||||
[data]
|
[data]
|
||||||
# Set to true to allow users to export their data
|
# Set to true to allow users to export their data
|
||||||
enable_export = true
|
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 Simplified Chinese
|
||||||
|
login_page_tips_content =
|
||||||
|
|
||||||
[notification]
|
[notification]
|
||||||
# Set to true to display custom notification in home page every time users register
|
# Set to true to display custom notification in home page every time users register
|
||||||
enable_notification_after_register = false
|
enable_notification_after_register = false
|
||||||
@@ -291,12 +340,22 @@ custom_map_tile_server_default_zoom_level = 14
|
|||||||
|
|
||||||
[exchange_rates]
|
[exchange_rates]
|
||||||
# Exchange rates data source, supports the following types:
|
# Exchange rates data source, supports the following types:
|
||||||
# "euro_central_bank"
|
# "reserve_bank_of_australia": https://www.rba.gov.au/statistics/frequency/exchange-rates.html
|
||||||
# "bank_of_canada"
|
# "bank_of_canada": https://www.bankofcanada.ca/rates/exchange/daily-exchange-rates/
|
||||||
# "reserve_bank_of_australia",
|
# "czech_national_bank": https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/
|
||||||
# "czech_national_bank"
|
# "danmarks_national_bank": https://www.nationalbanken.dk/en/what-we-do/stable-prices-monetary-policy-and-the-danish-economy/exchange-rates
|
||||||
# "national_bank_of_poland"
|
# "euro_central_bank": https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html
|
||||||
# "monetary_authority_of_singapore"
|
# "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
|
||||||
data_source = euro_central_bank
|
data_source = euro_central_bank
|
||||||
|
|
||||||
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds)
|
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds)
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
|
import { includeIgnoreFile } from '@eslint/compat';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const gitignorePath = path.resolve(__dirname, '.gitignore');
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [...compat.extends('eslint:recommended', 'plugin:vue/vue3-essential'),
|
||||||
|
includeIgnoreFile(gitignorePath), {
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
files: [
|
||||||
|
"**/*.{vue,js,jsx,cjs,mjs}"
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'vue/no-use-v-if-with-v-for': 'off',
|
||||||
|
'vue/valid-v-slot': ['error', {
|
||||||
|
allowModifiers: true,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}];
|
||||||
@@ -36,6 +36,7 @@ func main() {
|
|||||||
cmd.WebServer,
|
cmd.WebServer,
|
||||||
cmd.Database,
|
cmd.Database,
|
||||||
cmd.UserData,
|
cmd.UserData,
|
||||||
|
cmd.CronJobs,
|
||||||
cmd.SecurityUtils,
|
cmd.SecurityUtils,
|
||||||
cmd.Utilities,
|
cmd.Utilities,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
module github.com/mayswind/ezbookkeeping
|
module github.com/mayswind/ezbookkeeping
|
||||||
|
|
||||||
go 1.21
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/boombuler/barcode v1.0.2
|
github.com/boombuler/barcode v1.0.2
|
||||||
|
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b
|
||||||
github.com/gin-contrib/cache v1.3.0
|
github.com/gin-contrib/cache v1.3.0
|
||||||
github.com/gin-contrib/gzip v1.0.1
|
github.com/gin-contrib/gzip v1.0.1
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/go-playground/validator/v10 v10.22.0
|
github.com/go-co-op/gocron/v2 v2.12.3
|
||||||
|
github.com/go-playground/validator/v10 v10.22.1
|
||||||
github.com/go-sql-driver/mysql v1.8.1
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/mattn/go-sqlite3 v1.14.22
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
github.com/minio/minio-go/v7 v7.0.74
|
github.com/minio/minio-go/v7 v7.0.80
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/pquerna/otp v1.4.0
|
github.com/pquerna/otp v1.4.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/urfave/cli/v2 v2.27.1
|
github.com/urfave/cli/v2 v2.27.4
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8
|
github.com/wk8/go-ordered-map/v2 v2.1.8
|
||||||
golang.org/x/crypto v0.25.0
|
golang.org/x/crypto v0.28.0
|
||||||
|
golang.org/x/net v0.30.0
|
||||||
|
golang.org/x/text v0.19.0
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
gopkg.in/mail.v2 v2.3.1
|
gopkg.in/mail.v2 v2.3.1
|
||||||
xorm.io/builder v0.3.13
|
xorm.io/builder v0.3.13
|
||||||
@@ -37,9 +41,11 @@ require (
|
|||||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect
|
||||||
|
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
@@ -49,8 +55,9 @@ require (
|
|||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/gomodule/redigo v1.8.9 // indirect
|
github.com/gomodule/redigo v1.8.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
@@ -61,17 +68,18 @@ require (
|
|||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // 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/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
|
||||||
github.com/rs/xid v1.5.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||||
|
github.com/tealeg/xlsx v1.0.5 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
golang.org/x/net v0.26.0 // indirect
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
golang.org/x/text v0.16.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -25,13 +25,19 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
|
|||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
@@ -43,14 +49,16 @@ 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-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
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/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.12.3 h1:3JkKjkFoAPp/i0YE+sonlF5gi+xnBChwYh75nX16MaE=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.12.3/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
|
||||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
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/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
|
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||||
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
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/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
@@ -67,16 +75,21 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
|
|||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
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/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
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/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
@@ -85,14 +98,14 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
|
|||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
|
github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
|
||||||
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
|
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
|
||||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
github.com/minio/minio-go/v7 v7.0.74 h1:fTo/XlPBTSpo3BAMshlwKL5RspXRv9us5UeHEGYCFe0=
|
github.com/minio/minio-go/v7 v7.0.80 h1:2mdUHXEykRdY/BigLt3Iuu1otL0JTogT0Nmltg0wujk=
|
||||||
github.com/minio/minio-go/v7 v7.0.74/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8=
|
github.com/minio/minio-go/v7 v7.0.80/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -109,10 +122,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
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/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/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
|
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
|
||||||
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
|
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
|
||||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
@@ -131,39 +146,44 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.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 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||||
|
github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
|
||||||
|
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
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/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
|
||||||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||||
|
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/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
|||||||
Generated
+3169
-2333
File diff suppressed because it is too large
Load Diff
+26
-23
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ezbookkeeping",
|
"name": "ezbookkeeping",
|
||||||
"version": "0.5.0",
|
"version": "0.7.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -15,49 +15,52 @@
|
|||||||
"serve": "cross-env NODE_ENV=development vite",
|
"serve": "cross-env NODE_ENV=development vite",
|
||||||
"build": "cross-env NODE_ENV=production vite build",
|
"build": "cross-env NODE_ENV=production vite build",
|
||||||
"serve:dist": "vite preview",
|
"serve:dist": "vite preview",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
"lint": "eslint . --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@vuepic/vue-datepicker": "^8.8.1",
|
"@vuepic/vue-datepicker": "^10.0.0",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.7",
|
||||||
"cbor-js": "^0.1.0",
|
"cbor-js": "^0.1.0",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dom7": "^4.0.6",
|
"dom7": "^4.0.6",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"framework7": "^8.3.3",
|
"framework7": "^8.3.4",
|
||||||
"framework7-icons": "^5.0.5",
|
"framework7-icons": "^5.0.5",
|
||||||
"framework7-vue": "^8.3.3",
|
"framework7-vue": "^8.3.4",
|
||||||
"js-cookie": "^3.0.5",
|
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"line-awesome": "^1.3.0",
|
"line-awesome": "^1.3.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.45",
|
"moment-timezone": "^0.5.46",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.2.5",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"skeleton-elements": "^4.0.1",
|
"skeleton-elements": "^4.0.1",
|
||||||
"swiper": "^10.2.0",
|
"swiper": "^10.2.0",
|
||||||
"ua-parser-js": "^1.0.38",
|
"ua-parser-js": "^1.0.39",
|
||||||
"vue": "^3.4.31",
|
"vue": "^3.5.12",
|
||||||
"vue-echarts": "^6.7.3",
|
"vue-echarts": "^7.0.3",
|
||||||
"vue-i18n": "^9.13.1",
|
"vue-i18n": "^10.0.4",
|
||||||
"vue-router": "^4.4.0",
|
"vue-router": "^4.4.5",
|
||||||
"vue3-perfect-scrollbar": "^2.0.0",
|
"vue3-perfect-scrollbar": "^2.0.0",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"vuetify": "^3.6.11"
|
"vuetify": "^3.7.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@eslint/compat": "^1.2.2",
|
||||||
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
|
"@eslint/js": "^9.14.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
"eslint-plugin-vue": "^9.30.0",
|
||||||
"git-rev-sync": "^3.0.2",
|
"git-rev-sync": "^3.0.2",
|
||||||
"postcss-preset-env": "^9.5.16",
|
"globals": "^15.11.0",
|
||||||
"sass": "^1.77.6",
|
"postcss-preset-env": "^10.0.9",
|
||||||
"vite": "^5.3.3",
|
"sass": "^1.80.6",
|
||||||
"vite-plugin-pwa": "^0.20.0",
|
"vite": "^5.4.10",
|
||||||
"vite-plugin-vuetify": "^2.0.3"
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
|
"vite-plugin-vuetify": "^2.0.4"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|||||||
+131
-59
@@ -16,23 +16,31 @@ import (
|
|||||||
|
|
||||||
// AccountsApi represents account api
|
// AccountsApi represents account api
|
||||||
type AccountsApi struct {
|
type AccountsApi struct {
|
||||||
|
ApiUsingConfig
|
||||||
|
ApiUsingDuplicateChecker
|
||||||
accounts *services.AccountService
|
accounts *services.AccountService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize an account api singleton instance
|
// Initialize an account api singleton instance
|
||||||
var (
|
var (
|
||||||
Accounts = &AccountsApi{
|
Accounts = &AccountsApi{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||||
|
container: duplicatechecker.Container,
|
||||||
|
},
|
||||||
accounts: services.Accounts,
|
accounts: services.Accounts,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// AccountListHandler returns accounts list of current user
|
// AccountListHandler returns accounts list of current user
|
||||||
func (a *AccountsApi) AccountListHandler(c *core.Context) (any, *errs.Error) {
|
func (a *AccountsApi) AccountListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var accountListReq models.AccountListRequest
|
var accountListReq models.AccountListRequest
|
||||||
err := c.ShouldBindQuery(&accountListReq)
|
err := c.ShouldBindQuery(&accountListReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountListHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[accounts.AccountListHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +48,7 @@ func (a *AccountsApi) AccountListHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
|
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountListHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[accounts.AccountListHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,12 +95,12 @@ func (a *AccountsApi) AccountListHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AccountGetHandler returns one specific account of current user
|
// AccountGetHandler returns one specific account of current user
|
||||||
func (a *AccountsApi) AccountGetHandler(c *core.Context) (any, *errs.Error) {
|
func (a *AccountsApi) AccountGetHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var accountGetReq models.AccountGetRequest
|
var accountGetReq models.AccountGetRequest
|
||||||
err := c.ShouldBindQuery(&accountGetReq)
|
err := c.ShouldBindQuery(&accountGetReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountGetHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[accounts.AccountGetHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +108,7 @@ func (a *AccountsApi) AccountGetHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountGetReq.Id)
|
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountGetReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountGetHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", accountGetReq.Id, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,50 +138,60 @@ func (a *AccountsApi) AccountGetHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AccountCreateHandler saves a new account by request parameters for current user
|
// AccountCreateHandler saves a new account by request parameters for current user
|
||||||
func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
|
func (a *AccountsApi) AccountCreateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var accountCreateReq models.AccountCreateRequest
|
var accountCreateReq models.AccountCreateRequest
|
||||||
err := c.ShouldBindJSON(&accountCreateReq)
|
err := c.ShouldBindJSON(&accountCreateReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[accounts.AccountCreateHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
utcOffset, err := c.GetClientTimezoneOffset()
|
utcOffset, err := c.GetClientTimezoneOffset()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] cannot get client timezone offset, because %s", err.Error())
|
log.Warnf(c, "[accounts.AccountCreateHandler] cannot get client timezone offset, because %s", err.Error())
|
||||||
return nil, errs.ErrClientTimezoneOffsetInvalid
|
return nil, errs.ErrClientTimezoneOffsetInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
if accountCreateReq.Category < models.ACCOUNT_CATEGORY_CASH || accountCreateReq.Category > models.ACCOUNT_CATEGORY_INVESTMENT {
|
if accountCreateReq.Category < models.ACCOUNT_CATEGORY_CASH || accountCreateReq.Category > models.ACCOUNT_CATEGORY_CERTIFICATE_OF_DEPOSIT {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account category invalid, category is %d", accountCreateReq.Category)
|
log.Warnf(c, "[accounts.AccountCreateHandler] account category invalid, category is %d", accountCreateReq.Category)
|
||||||
return nil, errs.ErrAccountCategoryInvalid
|
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 accountCreateReq.Type == models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
|
||||||
if len(accountCreateReq.SubAccounts) > 0 {
|
if len(accountCreateReq.SubAccounts) > 0 {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account cannot have any sub-accounts")
|
log.Warnf(c, "[accounts.AccountCreateHandler] account cannot have any sub-accounts")
|
||||||
return nil, errs.ErrAccountCannotHaveSubAccounts
|
return nil, errs.ErrAccountCannotHaveSubAccounts
|
||||||
}
|
}
|
||||||
|
|
||||||
if accountCreateReq.Currency == validators.ParentAccountCurrencyPlaceholder {
|
if accountCreateReq.Currency == validators.ParentAccountCurrencyPlaceholder {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account cannot set currency placeholder")
|
log.Warnf(c, "[accounts.AccountCreateHandler] account cannot set currency placeholder")
|
||||||
return nil, errs.ErrAccountCurrencyInvalid
|
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 {
|
} else if accountCreateReq.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
|
||||||
if len(accountCreateReq.SubAccounts) < 1 {
|
if len(accountCreateReq.SubAccounts) < 1 {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account does not have any sub-accounts")
|
log.Warnf(c, "[accounts.AccountCreateHandler] account does not have any sub-accounts")
|
||||||
return nil, errs.ErrAccountHaveNoSubAccount
|
return nil, errs.ErrAccountHaveNoSubAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
if accountCreateReq.Currency != validators.ParentAccountCurrencyPlaceholder {
|
if accountCreateReq.Currency != validators.ParentAccountCurrencyPlaceholder {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] parent account cannot set currency")
|
log.Warnf(c, "[accounts.AccountCreateHandler] parent account cannot set currency")
|
||||||
return nil, errs.ErrParentAccountCannotSetCurrency
|
return nil, errs.ErrParentAccountCannotSetCurrency
|
||||||
}
|
}
|
||||||
|
|
||||||
if accountCreateReq.Balance != 0 {
|
if accountCreateReq.Balance != 0 {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] parent account cannot set balance")
|
log.Warnf(c, "[accounts.AccountCreateHandler] parent account cannot set balance")
|
||||||
return nil, errs.ErrParentAccountCannotSetBalance
|
return nil, errs.ErrParentAccountCannotSetBalance
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,22 +199,32 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
subAccount := accountCreateReq.SubAccounts[i]
|
subAccount := accountCreateReq.SubAccounts[i]
|
||||||
|
|
||||||
if subAccount.Category != accountCreateReq.Category {
|
if subAccount.Category != accountCreateReq.Category {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] category of sub-account not equals to parent")
|
log.Warnf(c, "[accounts.AccountCreateHandler] category of sub-account#%d not equals to parent", i)
|
||||||
return nil, errs.ErrSubAccountCategoryNotEqualsToParent
|
return nil, errs.ErrSubAccountCategoryNotEqualsToParent
|
||||||
}
|
}
|
||||||
|
|
||||||
if subAccount.Type != models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
|
if subAccount.Type != models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] sub-account type invalid")
|
log.Warnf(c, "[accounts.AccountCreateHandler] sub-account#%d type invalid", i)
|
||||||
return nil, errs.ErrSubAccountTypeInvalid
|
return nil, errs.ErrSubAccountTypeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
if subAccount.Currency == validators.ParentAccountCurrencyPlaceholder {
|
if subAccount.Currency == validators.ParentAccountCurrencyPlaceholder {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] sub-account cannot set currency placeholder")
|
log.Warnf(c, "[accounts.AccountCreateHandler] sub-account#%d cannot set currency placeholder", i)
|
||||||
return nil, errs.ErrAccountCurrencyInvalid
|
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 {
|
} else {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account type invalid, type is %d", accountCreateReq.Type)
|
log.Warnf(c, "[accounts.AccountCreateHandler] account type invalid, type is %d", accountCreateReq.Type)
|
||||||
return nil, errs.ErrAccountTypeInvalid
|
return nil, errs.ErrAccountTypeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,25 +232,25 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
maxOrderId, err := a.accounts.GetMaxDisplayOrder(c, uid, accountCreateReq.Category)
|
maxOrderId, err := a.accounts.GetMaxDisplayOrder(c, uid, accountCreateReq.Category)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
mainAccount := a.createNewAccountModel(uid, &accountCreateReq, maxOrderId+1)
|
mainAccount := a.createNewAccountModel(uid, &accountCreateReq, false, maxOrderId+1)
|
||||||
childrenAccounts := a.createSubAccountModels(uid, &accountCreateReq)
|
childrenAccounts, childrenAccountBalanceTimes := a.createSubAccountModels(uid, &accountCreateReq)
|
||||||
|
|
||||||
if settings.Container.Current.EnableDuplicateSubmissionsCheck && accountCreateReq.ClientSessionId != "" {
|
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && accountCreateReq.ClientSessionId != "" {
|
||||||
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId)
|
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId)
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] another account \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
log.Infof(c, "[accounts.AccountCreateHandler] another account \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
||||||
accountId, err := utils.StringToInt64(remark)
|
accountId, err := utils.StringToInt64(remark)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountId)
|
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to get existed account \"id:%d\" for user \"uid:%d\", because %s", accountId, uid, err.Error())
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,16 +275,16 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.accounts.CreateAccounts(c, mainAccount, childrenAccounts, utcOffset)
|
err = a.accounts.CreateAccounts(c, mainAccount, accountCreateReq.BalanceTime, childrenAccounts, childrenAccountBalanceTimes, utcOffset)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to create account \"id:%d\" for user \"uid:%d\", because %s", mainAccount.AccountId, uid, err.Error())
|
log.Errorf(c, "[accounts.AccountCreateHandler] failed to create account \"id:%d\" for user \"uid:%d\", because %s", mainAccount.AccountId, uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] user \"uid:%d\" has created a new account \"id:%d\" successfully", uid, mainAccount.AccountId)
|
log.Infof(c, "[accounts.AccountCreateHandler] user \"uid:%d\" has created a new account \"id:%d\" successfully", uid, mainAccount.AccountId)
|
||||||
|
|
||||||
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId, utils.Int64ToString(mainAccount.AccountId))
|
a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId, utils.Int64ToString(mainAccount.AccountId))
|
||||||
accountInfoResp := mainAccount.ToAccountInfoResponse()
|
accountInfoResp := mainAccount.ToAccountInfoResponse()
|
||||||
|
|
||||||
if len(childrenAccounts) > 0 {
|
if len(childrenAccounts) > 0 {
|
||||||
@@ -271,17 +299,17 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AccountModifyHandler saves an existed account by request parameters for current user
|
// AccountModifyHandler saves an existed account by request parameters for current user
|
||||||
func (a *AccountsApi) AccountModifyHandler(c *core.Context) (any, *errs.Error) {
|
func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var accountModifyReq models.AccountModifyRequest
|
var accountModifyReq models.AccountModifyRequest
|
||||||
err := c.ShouldBindJSON(&accountModifyReq)
|
err := c.ShouldBindJSON(&accountModifyReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountModifyHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[accounts.AccountModifyHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if accountModifyReq.Category < models.ACCOUNT_CATEGORY_CASH || accountModifyReq.Category > models.ACCOUNT_CATEGORY_INVESTMENT {
|
if accountModifyReq.Category < models.ACCOUNT_CATEGORY_CASH || accountModifyReq.Category > models.ACCOUNT_CATEGORY_CERTIFICATE_OF_DEPOSIT {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountModifyHandler] account category invalid, category is %d", accountModifyReq.Category)
|
log.Warnf(c, "[accounts.AccountModifyHandler] account category invalid, category is %d", accountModifyReq.Category)
|
||||||
return nil, errs.ErrAccountCategoryInvalid
|
return nil, errs.ErrAccountCategoryInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,13 +317,14 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountModifyReq.Id)
|
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountModifyReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountModifyHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", accountModifyReq.Id, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
accountMap := a.accounts.GetAccountMapByList(accountAndSubAccounts)
|
accountMap := a.accounts.GetAccountMapByList(accountAndSubAccounts)
|
||||||
|
mainAccount, exists := accountMap[accountModifyReq.Id]
|
||||||
|
|
||||||
if _, exists := accountMap[accountModifyReq.Id]; !exists {
|
if !exists {
|
||||||
return nil, errs.ErrAccountNotFound
|
return nil, errs.ErrAccountNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,10 +332,26 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
return nil, errs.ErrCannotAddOrDeleteSubAccountsWhenModify
|
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
|
anythingUpdate := false
|
||||||
var toUpdateAccounts []*models.Account
|
var toUpdateAccounts []*models.Account
|
||||||
|
|
||||||
toUpdateAccount := a.getToUpdateAccount(uid, &accountModifyReq, accountMap[accountModifyReq.Id])
|
toUpdateAccount := a.getToUpdateAccount(uid, &accountModifyReq, mainAccount, false)
|
||||||
|
|
||||||
if toUpdateAccount != nil {
|
if toUpdateAccount != nil {
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
@@ -320,7 +365,7 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
return nil, errs.ErrAccountNotFound
|
return nil, errs.ErrAccountNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
toUpdateSubAccount := a.getToUpdateAccount(uid, subAccountReq, accountMap[subAccountReq.Id])
|
toUpdateSubAccount := a.getToUpdateAccount(uid, subAccountReq, accountMap[subAccountReq.Id], true)
|
||||||
|
|
||||||
if toUpdateSubAccount != nil {
|
if toUpdateSubAccount != nil {
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
@@ -335,11 +380,11 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
err = a.accounts.ModifyAccounts(c, uid, toUpdateAccounts)
|
err = a.accounts.ModifyAccounts(c, uid, toUpdateAccounts)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountModifyHandler] failed to update account \"id:%d\" for user \"uid:%d\", because %s", accountModifyReq.Id, uid, err.Error())
|
log.Errorf(c, "[accounts.AccountModifyHandler] failed to update account \"id:%d\" for user \"uid:%d\", because %s", accountModifyReq.Id, uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[accounts.AccountModifyHandler] user \"uid:%d\" has updated account \"id:%d\" successfully", uid, accountModifyReq.Id)
|
log.Infof(c, "[accounts.AccountModifyHandler] user \"uid:%d\" has updated account \"id:%d\" successfully", uid, accountModifyReq.Id)
|
||||||
|
|
||||||
accountRespMap := make(map[int64]*models.AccountInfoResponse)
|
accountRespMap := make(map[int64]*models.AccountInfoResponse)
|
||||||
|
|
||||||
@@ -382,12 +427,12 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AccountHideHandler hides an existed account by request parameters for current user
|
// AccountHideHandler hides an existed account by request parameters for current user
|
||||||
func (a *AccountsApi) AccountHideHandler(c *core.Context) (any, *errs.Error) {
|
func (a *AccountsApi) AccountHideHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var accountHideReq models.AccountHideRequest
|
var accountHideReq models.AccountHideRequest
|
||||||
err := c.ShouldBindJSON(&accountHideReq)
|
err := c.ShouldBindJSON(&accountHideReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountHideHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[accounts.AccountHideHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,21 +440,21 @@ func (a *AccountsApi) AccountHideHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
err = a.accounts.HideAccount(c, uid, []int64{accountHideReq.Id}, accountHideReq.Hidden)
|
err = a.accounts.HideAccount(c, uid, []int64{accountHideReq.Id}, accountHideReq.Hidden)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountHideHandler] failed to hide account \"id:%d\" for user \"uid:%d\", because %s", accountHideReq.Id, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[accounts.AccountHideHandler] user \"uid:%d\" has hidden account \"id:%d\"", uid, accountHideReq.Id)
|
log.Infof(c, "[accounts.AccountHideHandler] user \"uid:%d\" has hidden account \"id:%d\"", uid, accountHideReq.Id)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountMoveHandler moves display order of existed accounts by request parameters for current user
|
// AccountMoveHandler moves display order of existed accounts by request parameters for current user
|
||||||
func (a *AccountsApi) AccountMoveHandler(c *core.Context) (any, *errs.Error) {
|
func (a *AccountsApi) AccountMoveHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var accountMoveReq models.AccountMoveRequest
|
var accountMoveReq models.AccountMoveRequest
|
||||||
err := c.ShouldBindJSON(&accountMoveReq)
|
err := c.ShouldBindJSON(&accountMoveReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountMoveHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[accounts.AccountMoveHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,21 +475,21 @@ func (a *AccountsApi) AccountMoveHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
err = a.accounts.ModifyAccountDisplayOrders(c, uid, accounts)
|
err = a.accounts.ModifyAccountDisplayOrders(c, uid, accounts)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountMoveHandler] failed to move accounts for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[accounts.AccountMoveHandler] failed to move accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[accounts.AccountMoveHandler] user \"uid:%d\" has moved accounts", uid)
|
log.Infof(c, "[accounts.AccountMoveHandler] user \"uid:%d\" has moved accounts", uid)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountDeleteHandler deletes an existed account by request parameters for current user
|
// AccountDeleteHandler deletes an existed account by request parameters for current user
|
||||||
func (a *AccountsApi) AccountDeleteHandler(c *core.Context) (any, *errs.Error) {
|
func (a *AccountsApi) AccountDeleteHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var accountDeleteReq models.AccountDeleteRequest
|
var accountDeleteReq models.AccountDeleteRequest
|
||||||
err := c.ShouldBindJSON(&accountDeleteReq)
|
err := c.ShouldBindJSON(&accountDeleteReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[accounts.AccountDeleteHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[accounts.AccountDeleteHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,15 +497,21 @@ func (a *AccountsApi) AccountDeleteHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
err = a.accounts.DeleteAccount(c, uid, accountDeleteReq.Id)
|
err = a.accounts.DeleteAccount(c, uid, accountDeleteReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[accounts.AccountDeleteHandler] failed to delete account \"id:%d\" for user \"uid:%d\", because %s", accountDeleteReq.Id, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[accounts.AccountDeleteHandler] user \"uid:%d\" has deleted account \"id:%d\"", uid, accountDeleteReq.Id)
|
log.Infof(c, "[accounts.AccountDeleteHandler] user \"uid:%d\" has deleted account \"id:%d\"", uid, accountDeleteReq.Id)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.AccountCreateRequest, order int32) *models.Account {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
return &models.Account{
|
return &models.Account{
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
Name: accountCreateReq.Name,
|
Name: accountCreateReq.Name,
|
||||||
@@ -472,24 +523,33 @@ func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.
|
|||||||
Currency: accountCreateReq.Currency,
|
Currency: accountCreateReq.Currency,
|
||||||
Balance: accountCreateReq.Balance,
|
Balance: accountCreateReq.Balance,
|
||||||
Comment: accountCreateReq.Comment,
|
Comment: accountCreateReq.Comment,
|
||||||
|
Extend: accountExtend,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AccountsApi) createSubAccountModels(uid int64, accountCreateReq *models.AccountCreateRequest) []*models.Account {
|
func (a *AccountsApi) createSubAccountModels(uid int64, accountCreateReq *models.AccountCreateRequest) ([]*models.Account, []int64) {
|
||||||
if len(accountCreateReq.SubAccounts) <= 0 {
|
if len(accountCreateReq.SubAccounts) <= 0 {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
childrenAccounts := make([]*models.Account, len(accountCreateReq.SubAccounts))
|
childrenAccounts := make([]*models.Account, len(accountCreateReq.SubAccounts))
|
||||||
|
childrenAccountBalanceTimes := make([]int64, len(accountCreateReq.SubAccounts))
|
||||||
|
|
||||||
for i := int32(0); i < int32(len(accountCreateReq.SubAccounts)); i++ {
|
for i := int32(0); i < int32(len(accountCreateReq.SubAccounts)); i++ {
|
||||||
childrenAccounts[i] = a.createNewAccountModel(uid, accountCreateReq.SubAccounts[i], i+1)
|
childrenAccounts[i] = a.createNewAccountModel(uid, accountCreateReq.SubAccounts[i], true, i+1)
|
||||||
|
childrenAccountBalanceTimes[i] = accountCreateReq.SubAccounts[i].BalanceTime
|
||||||
}
|
}
|
||||||
|
|
||||||
return childrenAccounts
|
return childrenAccounts, childrenAccountBalanceTimes
|
||||||
|
}
|
||||||
|
|
||||||
|
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{
|
newAccount := &models.Account{
|
||||||
AccountId: oldAccount.AccountId,
|
AccountId: oldAccount.AccountId,
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
@@ -498,6 +558,7 @@ func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.Acc
|
|||||||
Icon: accountModifyReq.Icon,
|
Icon: accountModifyReq.Icon,
|
||||||
Color: accountModifyReq.Color,
|
Color: accountModifyReq.Color,
|
||||||
Comment: accountModifyReq.Comment,
|
Comment: accountModifyReq.Comment,
|
||||||
|
Extend: newAccountExtend,
|
||||||
Hidden: accountModifyReq.Hidden,
|
Hidden: accountModifyReq.Hidden,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,5 +571,16 @@ func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.Acc
|
|||||||
return newAccount
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,20 @@ const amapRestApiUrl = "https://restapi.amap.com/"
|
|||||||
|
|
||||||
// AmapApiProxy represents amap api proxy
|
// AmapApiProxy represents amap api proxy
|
||||||
type AmapApiProxy struct {
|
type AmapApiProxy struct {
|
||||||
|
ApiUsingConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a amap api proxy singleton instance
|
// Initialize a amap api proxy singleton instance
|
||||||
var (
|
var (
|
||||||
AmapApis = &AmapApiProxy{}
|
AmapApis = &AmapApiProxy{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// AmapApiProxyHandler returns amap api response
|
// AmapApiProxyHandler returns amap api response
|
||||||
func (p *AmapApiProxy) AmapApiProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
|
func (p *AmapApiProxy) AmapApiProxyHandler(c *core.WebContext) (*httputil.ReverseProxy, *errs.Error) {
|
||||||
var targetUrl string
|
var targetUrl string
|
||||||
|
|
||||||
if strings.HasPrefix(c.Request.RequestURI, "/_AMapService/v4/map/styles") {
|
if strings.HasPrefix(c.Request.RequestURI, "/_AMapService/v4/map/styles") {
|
||||||
@@ -38,7 +43,7 @@ func (p *AmapApiProxy) AmapApiProxyHandler(c *core.Context) (*httputil.ReversePr
|
|||||||
}
|
}
|
||||||
|
|
||||||
director := func(req *http.Request) {
|
director := func(req *http.Request) {
|
||||||
targetRawUrl := fmt.Sprintf("%s?%s&jscode=%s", targetUrl, req.URL.RawQuery, settings.Container.Current.AmapApplicationSecret)
|
targetRawUrl := fmt.Sprintf("%s?%s&jscode=%s", targetUrl, req.URL.RawQuery, p.CurrentConfig().AmapApplicationSecret)
|
||||||
targetUrl, _ := url.Parse(targetRawUrl)
|
targetUrl, _ := url.Parse(targetRawUrl)
|
||||||
|
|
||||||
oldCookies := req.Cookies()
|
oldCookies := req.Cookies()
|
||||||
|
|||||||
+50
-36
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
@@ -13,6 +14,8 @@ import (
|
|||||||
|
|
||||||
// AuthorizationsApi represents authorization api
|
// AuthorizationsApi represents authorization api
|
||||||
type AuthorizationsApi struct {
|
type AuthorizationsApi struct {
|
||||||
|
ApiUsingConfig
|
||||||
|
ApiWithUserInfo
|
||||||
users *services.UserService
|
users *services.UserService
|
||||||
tokens *services.TokenService
|
tokens *services.TokenService
|
||||||
twoFactorAuthorizations *services.TwoFactorAuthorizationService
|
twoFactorAuthorizations *services.TwoFactorAuthorizationService
|
||||||
@@ -21,6 +24,17 @@ type AuthorizationsApi struct {
|
|||||||
// Initialize a authorization api singleton instance
|
// Initialize a authorization api singleton instance
|
||||||
var (
|
var (
|
||||||
Authorizations = &AuthorizationsApi{
|
Authorizations = &AuthorizationsApi{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
ApiWithUserInfo: ApiWithUserInfo{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
ApiUsingAvatarProvider: ApiUsingAvatarProvider{
|
||||||
|
container: avatars.Container,
|
||||||
|
},
|
||||||
|
},
|
||||||
users: services.Users,
|
users: services.Users,
|
||||||
tokens: services.Tokens,
|
tokens: services.Tokens,
|
||||||
twoFactorAuthorizations: services.TwoFactorAuthorizations,
|
twoFactorAuthorizations: services.TwoFactorAuthorizations,
|
||||||
@@ -28,36 +42,36 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AuthorizeHandler verifies and authorizes current login request
|
// AuthorizeHandler verifies and authorizes current login request
|
||||||
func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error) {
|
func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var credential models.UserLoginRequest
|
var credential models.UserLoginRequest
|
||||||
err := c.ShouldBindJSON(&credential)
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[authorizations.AuthorizeHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.ErrLoginNameOrPasswordInvalid
|
return nil, errs.ErrLoginNameOrPasswordInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.users.GetUserByUsernameOrEmailAndPassword(c, credential.LoginName, credential.Password)
|
user, err := a.users.GetUserByUsernameOrEmailAndPassword(c, credential.LoginName, credential.Password)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error())
|
log.Warnf(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error())
|
||||||
return nil, errs.ErrLoginNameOrPasswordWrong
|
return nil, errs.ErrLoginNameOrPasswordWrong
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Disabled {
|
if user.Disabled {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because user is disabled", credential.LoginName)
|
log.Warnf(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because user is disabled", credential.LoginName)
|
||||||
return nil, errs.ErrUserIsDisabled
|
return nil, errs.ErrUserIsDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
|
if a.CurrentConfig().EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||||
hasValidEmailVerifyToken, err := a.tokens.ExistsValidTokenByType(c, user.Uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
|
hasValidEmailVerifyToken, err := a.tokens.ExistsValidTokenByType(c, user.Uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] failed check whether user \"uid:%d\" has valid verify email token, because %s", user.Uid, err.Error())
|
log.Warnf(c, "[authorizations.AuthorizeHandler] failed check whether user \"uid:%d\" has valid verify email token, because %s", user.Uid, err.Error())
|
||||||
hasValidEmailVerifyToken = false
|
hasValidEmailVerifyToken = false
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because user has not verified email", credential.LoginName)
|
log.Warnf(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{
|
return nil, errs.NewErrorWithContext(errs.ErrEmailIsNotVerified, map[string]any{
|
||||||
"email": user.Email,
|
"email": user.Email,
|
||||||
@@ -68,7 +82,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error)
|
|||||||
err = a.users.UpdateUserLastLoginTime(c, user.Uid)
|
err = a.users.UpdateUserLastLoginTime(c, user.Uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to update last login time for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Warnf(c, "[authorizations.AuthorizeHandler] failed to update last login time for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
twoFactorEnable := a.tokens.CurrentConfig().EnableTwoFactor
|
twoFactorEnable := a.tokens.CurrentConfig().EnableTwoFactor
|
||||||
@@ -77,7 +91,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error)
|
|||||||
twoFactorEnable, err = a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, user.Uid)
|
twoFactorEnable, err = a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, user.Uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to check two-factor setting for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrSystemError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +106,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[authorizations.AuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrTokenGenerating
|
return nil, errs.ErrTokenGenerating
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,19 +116,19 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error)
|
|||||||
|
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
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)
|
log.Infof(c, "[authorizations.AuthorizeHandler] user \"uid:%d\" has logined, token type is %d, token will be expired at %d", user.Uid, claims.Type, claims.ExpiresAt)
|
||||||
|
|
||||||
authResp := a.getAuthResponse(c, token, twoFactorEnable, user)
|
authResp := a.getAuthResponse(c, token, twoFactorEnable, user)
|
||||||
return authResp, nil
|
return authResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TwoFactorAuthorizeHandler verifies and authorizes current 2fa login by passcode
|
// TwoFactorAuthorizeHandler verifies and authorizes current 2fa login by passcode
|
||||||
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (any, *errs.Error) {
|
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var credential models.TwoFactorLoginRequest
|
var credential models.TwoFactorLoginRequest
|
||||||
err := c.ShouldBindJSON(&credential)
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.ErrPasscodeInvalid
|
return nil, errs.ErrPasscodeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,29 +136,29 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (any, *er
|
|||||||
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
|
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrSystemError)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !totp.Validate(credential.Passcode, twoFactorSetting.Secret) {
|
if !totp.Validate(credential.Passcode, twoFactorSetting.Secret) {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] passcode is invalid for user \"uid:%d\"", uid)
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] passcode is invalid for user \"uid:%d\"", uid)
|
||||||
return nil, errs.ErrPasscodeInvalid
|
return nil, errs.ErrPasscodeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.users.GetUserById(c, uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
log.Errorf(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Disabled {
|
if user.Disabled {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" is disabled", user.Uid)
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||||
return nil, errs.ErrUserIsDisabled
|
return nil, errs.ErrUserIsDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
|
if a.CurrentConfig().EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has not verified email", user.Uid)
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||||
return nil, errs.ErrEmailIsNotVerified
|
return nil, errs.ErrEmailIsNotVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,32 +166,32 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (any, *er
|
|||||||
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
log.Warnf(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)
|
token, claims, err := a.tokens.CreateToken(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[authorizations.TwoFactorAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrTokenGenerating
|
return nil, errs.ErrTokenGenerating
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has authorized two-factor via passcode, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
log.Infof(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(c, token, false, user)
|
||||||
return authResp, nil
|
return authResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TwoFactorAuthorizeByRecoveryCodeHandler verifies and authorizes current 2fa login by recovery code
|
// TwoFactorAuthorizeByRecoveryCodeHandler verifies and authorizes current 2fa login by recovery code
|
||||||
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Context) (any, *errs.Error) {
|
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var credential models.TwoFactorRecoveryCodeLoginRequest
|
var credential models.TwoFactorRecoveryCodeLoginRequest
|
||||||
err := c.ShouldBindJSON(&credential)
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.ErrTwoFactorRecoveryCodeInvalid
|
return nil, errs.ErrTwoFactorRecoveryCodeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +199,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
|
|||||||
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
|
log.Warnf(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)
|
return nil, errs.Or(err, errs.ErrSystemError)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,24 +210,24 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
|
|||||||
user, err := a.users.GetUserById(c, uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
log.Errorf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Disabled {
|
if user.Disabled {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" is disabled", user.Uid)
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||||
return nil, errs.ErrUserIsDisabled
|
return nil, errs.ErrUserIsDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
|
if a.CurrentConfig().EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has not verified email", user.Uid)
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||||
return nil, errs.ErrEmailIsNotVerified
|
return nil, errs.ErrEmailIsNotVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(c, uid, credential.RecoveryCode, user.Salt)
|
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(c, uid, credential.RecoveryCode, user.Salt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two-factor recovery code for user \"uid:%d\", because %s", uid, err.Error())
|
log.Warnf(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)
|
return nil, errs.Or(err, errs.ErrTwoFactorRecoveryCodeNotExist)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,30 +235,30 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
|
|||||||
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
log.Warnf(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)
|
token, claims, err := a.tokens.CreateToken(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrTokenGenerating
|
return nil, errs.ErrTokenGenerating
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
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)
|
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)
|
||||||
|
|
||||||
authResp := a.getAuthResponse(c, token, false, user)
|
authResp := a.getAuthResponse(c, token, false, user)
|
||||||
return authResp, nil
|
return authResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AuthorizationsApi) getAuthResponse(c *core.Context, token string, need2FA bool, user *models.User) *models.AuthResponse {
|
func (a *AuthorizationsApi) getAuthResponse(c *core.WebContext, token string, need2FA bool, user *models.User) *models.AuthResponse {
|
||||||
return &models.AuthResponse{
|
return &models.AuthResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
Need2FA: need2FA,
|
Need2FA: need2FA,
|
||||||
User: user.ToUserBasicInfo(),
|
User: a.GetUserBasicInfo(user),
|
||||||
NotificationContent: settings.Container.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale()),
|
NotificationContent: a.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
+66
-42
@@ -19,77 +19,93 @@ const pageCountForDataExport = 1000
|
|||||||
|
|
||||||
// DataManagementsApi represents data management api
|
// DataManagementsApi represents data management api
|
||||||
type DataManagementsApi struct {
|
type DataManagementsApi struct {
|
||||||
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
|
ApiUsingConfig
|
||||||
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
|
|
||||||
tokens *services.TokenService
|
tokens *services.TokenService
|
||||||
users *services.UserService
|
users *services.UserService
|
||||||
accounts *services.AccountService
|
accounts *services.AccountService
|
||||||
transactions *services.TransactionService
|
transactions *services.TransactionService
|
||||||
categories *services.TransactionCategoryService
|
categories *services.TransactionCategoryService
|
||||||
tags *services.TransactionTagService
|
tags *services.TransactionTagService
|
||||||
|
pictures *services.TransactionPictureService
|
||||||
templates *services.TransactionTemplateService
|
templates *services.TransactionTemplateService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a data management api singleton instance
|
// Initialize a data management api singleton instance
|
||||||
var (
|
var (
|
||||||
DataManagements = &DataManagementsApi{
|
DataManagements = &DataManagementsApi{
|
||||||
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
|
container: settings.Container,
|
||||||
|
},
|
||||||
tokens: services.Tokens,
|
tokens: services.Tokens,
|
||||||
users: services.Users,
|
users: services.Users,
|
||||||
accounts: services.Accounts,
|
accounts: services.Accounts,
|
||||||
transactions: services.Transactions,
|
transactions: services.Transactions,
|
||||||
categories: services.TransactionCategories,
|
categories: services.TransactionCategories,
|
||||||
tags: services.TransactionTags,
|
tags: services.TransactionTags,
|
||||||
|
pictures: services.TransactionPictures,
|
||||||
templates: services.TransactionTemplates,
|
templates: services.TransactionTemplates,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExportDataToEzbookkeepingCSVHandler returns exported data in csv format
|
// ExportDataToEzbookkeepingCSVHandler returns exported data in csv format
|
||||||
func (a *DataManagementsApi) ExportDataToEzbookkeepingCSVHandler(c *core.Context) ([]byte, string, *errs.Error) {
|
func (a *DataManagementsApi) ExportDataToEzbookkeepingCSVHandler(c *core.WebContext) ([]byte, string, *errs.Error) {
|
||||||
return a.getExportedFileContent(c, "csv")
|
return a.getExportedFileContent(c, "csv")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportDataToEzbookkeepingTSVHandler returns exported data in csv format
|
// ExportDataToEzbookkeepingTSVHandler returns exported data in csv format
|
||||||
func (a *DataManagementsApi) ExportDataToEzbookkeepingTSVHandler(c *core.Context) ([]byte, string, *errs.Error) {
|
func (a *DataManagementsApi) ExportDataToEzbookkeepingTSVHandler(c *core.WebContext) ([]byte, string, *errs.Error) {
|
||||||
return a.getExportedFileContent(c, "tsv")
|
return a.getExportedFileContent(c, "tsv")
|
||||||
}
|
}
|
||||||
|
|
||||||
// DataStatisticsHandler returns user data statistics
|
// DataStatisticsHandler returns user data statistics
|
||||||
func (a *DataManagementsApi) DataStatisticsHandler(c *core.Context) (any, *errs.Error) {
|
func (a *DataManagementsApi) DataStatisticsHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
totalAccountCount, err := a.accounts.GetTotalAccountCountByUid(c, uid)
|
totalAccountCount, err := a.accounts.GetTotalAccountCountByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total account count for user \"uid:%d\", because %s", uid, err.Error())
|
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
|
return nil, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
totalTransactionCategoryCount, err := a.categories.GetTotalCategoryCountByUid(c, uid)
|
totalTransactionCategoryCount, err := a.categories.GetTotalCategoryCountByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
return nil, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
totalTransactionTagCount, err := a.tags.GetTotalTagCountByUid(c, uid)
|
totalTransactionTagCount, err := a.tags.GetTotalTagCountByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
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())
|
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
|
return nil, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
totalTransactionCount, err := a.transactions.GetTotalTransactionCountByUid(c, uid)
|
totalTransactionCount, err := a.transactions.GetTotalTransactionCountByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction count for user \"uid:%d\", because %s", uid, err.Error())
|
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
|
return nil, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
totalTransactionTemplateCount, err := a.templates.GetTotalNormalTemplateCountByUid(c, uid)
|
totalTransactionTemplateCount, err := a.templates.GetTotalNormalTemplateCountByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction template count for user \"uid:%d\", because %s", uid, err.Error())
|
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
|
return nil, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,19 +114,21 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.Context) (any, *errs.
|
|||||||
TotalTransactionCategoryCount: totalTransactionCategoryCount,
|
TotalTransactionCategoryCount: totalTransactionCategoryCount,
|
||||||
TotalTransactionTagCount: totalTransactionTagCount,
|
TotalTransactionTagCount: totalTransactionTagCount,
|
||||||
TotalTransactionCount: totalTransactionCount,
|
TotalTransactionCount: totalTransactionCount,
|
||||||
|
TotalTransactionPictureCount: totalTransactionPictureCount,
|
||||||
TotalTransactionTemplateCount: totalTransactionTemplateCount,
|
TotalTransactionTemplateCount: totalTransactionTemplateCount,
|
||||||
|
TotalScheduledTransactionCount: totalScheduledTransactionCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataStatisticsResp, nil
|
return dataStatisticsResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearDataHandler deletes all user data
|
// ClearDataHandler deletes all user data
|
||||||
func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error) {
|
func (a *DataManagementsApi) ClearDataHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var clearDataReq models.ClearDataRequest
|
var clearDataReq models.ClearDataRequest
|
||||||
err := c.ShouldBindJSON(&clearDataReq)
|
err := c.ShouldBindJSON(&clearDataReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[data_managements.ClearDataHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[data_managements.ClearDataHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +137,7 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.WarnfWithRequestId(c, "[data_managements.ClearDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
log.Warnf(c, "[data_managements.ClearDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
@@ -129,40 +147,44 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error
|
|||||||
return nil, errs.ErrUserPasswordWrong
|
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)
|
err = a.transactions.DeleteAllTransactions(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transactions, because %s", err.Error())
|
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all transactions, because %s", err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.categories.DeleteAllCategories(c, uid)
|
err = a.categories.DeleteAllCategories(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction categories, because %s", err.Error())
|
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all transaction categories, because %s", err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.tags.DeleteAllTags(c, uid)
|
err = a.tags.DeleteAllTags(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction tags, because %s", err.Error())
|
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all transaction tags, because %s", err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.templates.DeleteAllTemplates(c, uid)
|
log.Infof(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction templates, 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
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *DataManagementsApi) getExportedFileContent(c *core.Context, fileType string) ([]byte, string, *errs.Error) {
|
func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType string) ([]byte, string, *errs.Error) {
|
||||||
if !settings.Container.Current.EnableDataExport {
|
if !a.CurrentConfig().EnableDataExport {
|
||||||
return nil, "", errs.ErrDataExportNotAllowed
|
return nil, "", errs.ErrDataExportNotAllowed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +192,7 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.Context, fileType st
|
|||||||
utcOffset, err := c.GetClientTimezoneOffset()
|
utcOffset, err := c.GetClientTimezoneOffset()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
|
log.Warnf(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
|
||||||
} else {
|
} else {
|
||||||
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
|
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
|
||||||
}
|
}
|
||||||
@@ -180,37 +202,41 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.Context, fileType st
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
log.Warnf(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, "", errs.ErrUserNotFound
|
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)
|
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, "", errs.ErrOperationFailed
|
return nil, "", errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1)
|
categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, "", errs.ErrOperationFailed
|
return nil, "", errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
tags, err := a.tags.GetAllTagsByUid(c, uid)
|
tags, err := a.tags.GetAllTagsByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, "", errs.ErrOperationFailed
|
return nil, "", errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
tagIndexes, err := a.tags.GetAllTagIdsMapOfAllTransactions(c, uid)
|
tagIndexes, err := a.tags.GetAllTagIdsMapOfAllTransactions(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, "", errs.ErrOperationFailed
|
return nil, "", errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,22 +247,20 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.Context, fileType st
|
|||||||
allTransactions, err := a.transactions.GetAllTransactions(c, uid, pageCountForDataExport, true)
|
allTransactions, err := a.transactions.GetAllTransactions(c, uid, pageCountForDataExport, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, "", errs.ErrOperationFailed
|
return nil, "", errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
var dataExporter converters.DataConverter
|
dataExporter := converters.GetTransactionDataExporter(fileType)
|
||||||
|
|
||||||
if fileType == "tsv" {
|
if dataExporter == nil {
|
||||||
dataExporter = a.ezBookKeepingTsvExporter
|
return nil, "", errs.ErrNotImplemented
|
||||||
} else {
|
|
||||||
dataExporter = a.ezBookKeepingCsvExporter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := dataExporter.ToExportedContent(uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexes)
|
result, err := dataExporter.ToExportedContent(c, uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexes)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, "", errs.Or(err, errs.ErrOperationFailed)
|
return nil, "", errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -14,11 +14,11 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ApiNotFound returns api not found error
|
// ApiNotFound returns api not found error
|
||||||
func (a *DefaultApi) ApiNotFound(c *core.Context) (any, *errs.Error) {
|
func (a *DefaultApi) ApiNotFound(c *core.WebContext) (any, *errs.Error) {
|
||||||
return nil, errs.ErrApiNotFound
|
return nil, errs.ErrApiNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// MethodNotAllowed returns method not allowed error
|
// MethodNotAllowed returns method not allowed error
|
||||||
func (a *DefaultApi) MethodNotAllowed(c *core.Context) (any, *errs.Error) {
|
func (a *DefaultApi) MethodNotAllowed(c *core.WebContext) (any, *errs.Error) {
|
||||||
return nil, errs.ErrMethodNotAllowed
|
return nil, errs.ErrMethodNotAllowed
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-13
@@ -18,15 +18,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ExchangeRatesApi represents exchange rate api
|
// ExchangeRatesApi represents exchange rate api
|
||||||
type ExchangeRatesApi struct{}
|
type ExchangeRatesApi struct {
|
||||||
|
ApiUsingConfig
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize a exchange rate api singleton instance
|
// Initialize a exchange rate api singleton instance
|
||||||
var (
|
var (
|
||||||
ExchangeRates = &ExchangeRatesApi{}
|
ExchangeRates = &ExchangeRatesApi{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// LatestExchangeRateHandler returns latest exchange rate data
|
// LatestExchangeRateHandler returns latest exchange rate data
|
||||||
func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (any, *errs.Error) {
|
func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
dataSource := exchangerates.Container.Current
|
dataSource := exchangerates.Container.Current
|
||||||
|
|
||||||
if dataSource == nil {
|
if dataSource == nil {
|
||||||
@@ -36,9 +42,9 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (any, *err
|
|||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
utils.SetProxyUrl(transport, settings.Container.Current.ExchangeRatesProxy)
|
utils.SetProxyUrl(transport, a.CurrentConfig().ExchangeRatesProxy)
|
||||||
|
|
||||||
if settings.Container.Current.ExchangeRatesSkipTLSVerify {
|
if a.CurrentConfig().ExchangeRatesSkipTLSVerify {
|
||||||
transport.TLSClientConfig = &tls.Config{
|
transport.TLSClientConfig = &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
}
|
}
|
||||||
@@ -46,34 +52,43 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (any, *err
|
|||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Timeout: time.Duration(settings.Container.Current.ExchangeRatesRequestTimeout) * time.Millisecond,
|
Timeout: time.Duration(a.CurrentConfig().ExchangeRatesRequestTimeout) * time.Millisecond,
|
||||||
}
|
}
|
||||||
|
|
||||||
urls := dataSource.GetRequestUrls()
|
requests, err := dataSource.BuildRequests()
|
||||||
exchangeRateResps := make([]*models.LatestExchangeRateResponse, 0, len(urls))
|
|
||||||
|
|
||||||
for i := 0; i < len(urls); i++ {
|
if err != nil {
|
||||||
req, _ := http.NewRequest("GET", urls[i], 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]
|
||||||
req.Header.Set("User-Agent", fmt.Sprintf("ezBookkeeping/%s ", settings.Version))
|
req.Header.Set("User-Agent", fmt.Sprintf("ezBookkeeping/%s ", settings.Version))
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[exchange_rates.LatestExchangeRateHandler] failed to request latest exchange rate data for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[exchange_rates.LatestExchangeRateHandler] failed to request latest exchange rate data for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.ErrFailedToRequestRemoteApi
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
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)
|
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)
|
||||||
return nil, errs.ErrFailedToRequestRemoteApi
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
log.Debugf(c, "[exchange_rates.LatestExchangeRateHandler] response#%d is %s", i, body)
|
||||||
|
|
||||||
exchangeRateResp, err := dataSource.Parse(c, body)
|
exchangeRateResp, err := dataSource.Parse(c, body)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[exchange_rates.LatestExchangeRateHandler] failed to parse response for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[exchange_rates.LatestExchangeRateHandler] failed to parse response for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrFailedToRequestRemoteApi)
|
return nil, errs.Or(err, errs.ErrFailedToRequestRemoteApi)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,351 @@
|
|||||||
|
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", "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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
-20
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
// ForgetPasswordsApi represents user forget password api
|
// ForgetPasswordsApi represents user forget password api
|
||||||
type ForgetPasswordsApi struct {
|
type ForgetPasswordsApi struct {
|
||||||
|
ApiUsingConfig
|
||||||
users *services.UserService
|
users *services.UserService
|
||||||
tokens *services.TokenService
|
tokens *services.TokenService
|
||||||
forgetPasswords *services.ForgetPasswordService
|
forgetPasswords *services.ForgetPasswordService
|
||||||
@@ -21,6 +22,9 @@ type ForgetPasswordsApi struct {
|
|||||||
// Initialize a user api singleton instance
|
// Initialize a user api singleton instance
|
||||||
var (
|
var (
|
||||||
ForgetPasswords = &ForgetPasswordsApi{
|
ForgetPasswords = &ForgetPasswordsApi{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
users: services.Users,
|
users: services.Users,
|
||||||
tokens: services.Tokens,
|
tokens: services.Tokens,
|
||||||
forgetPasswords: services.ForgetPasswords,
|
forgetPasswords: services.ForgetPasswords,
|
||||||
@@ -28,12 +32,12 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// UserForgetPasswordRequestHandler generates password reset link and send user an email with this link
|
// UserForgetPasswordRequestHandler generates password reset link and send user an email with this link
|
||||||
func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.Context) (any, *errs.Error) {
|
func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var request models.ForgetPasswordRequest
|
var request models.ForgetPasswordRequest
|
||||||
err := c.ShouldBindJSON(&request)
|
err := c.ShouldBindJSON(&request)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[forget_passwords.UserForgetPasswordRequestHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.ErrEmailIsEmptyOrInvalid
|
return nil, errs.ErrEmailIsEmptyOrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,30 +45,34 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.Context) (
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.ErrorfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to get user, because %s", err.Error())
|
log.Errorf(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to get user, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Disabled {
|
if user.Disabled {
|
||||||
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" is disabled", user.Uid)
|
log.Warnf(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||||
return nil, errs.ErrUserIsDisabled
|
return nil, errs.ErrUserIsDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_FORGET_PASSWORD) {
|
||||||
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" has not verified email", user.Uid)
|
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)
|
||||||
return nil, errs.ErrEmailIsNotVerified
|
return nil, errs.ErrEmailIsNotVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
if !settings.Container.Current.EnableSMTP {
|
if !a.CurrentConfig().EnableSMTP {
|
||||||
return nil, errs.ErrSMTPServerNotEnabled
|
return nil, errs.ErrSMTPServerNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
token, _, err := a.tokens.CreatePasswordResetToken(c, user)
|
token, _, err := a.tokens.CreatePasswordResetToken(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrTokenGenerating
|
return nil, errs.ErrTokenGenerating
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +80,7 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.Context) (
|
|||||||
err = a.forgetPasswords.SendPasswordResetEmail(c, user, token, c.GetClientLocale())
|
err = a.forgetPasswords.SendPasswordResetEmail(c, user, token, c.GetClientLocale())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
log.Warnf(c, "[forget_passwords.UserForgetPasswordRequestHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -80,12 +88,12 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.Context) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UserResetPasswordHandler resets user password by request parameters
|
// UserResetPasswordHandler resets user password by request parameters
|
||||||
func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.Context) (any, *errs.Error) {
|
func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var request models.PasswordResetRequest
|
var request models.PasswordResetRequest
|
||||||
err := c.ShouldBindJSON(&request)
|
err := c.ShouldBindJSON(&request)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,24 +102,28 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.Context) (any, *er
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.ErrorfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to get user, because %s", err.Error())
|
log.Errorf(c, "[forget_passwords.UserResetPasswordHandler] failed to get user, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Disabled {
|
if user.Disabled {
|
||||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" is disabled", user.Uid)
|
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||||
return nil, errs.ErrUserIsDisabled
|
return nil, errs.ErrUserIsDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_FORGET_PASSWORD) {
|
||||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" has not verified email", user.Uid)
|
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)
|
||||||
return nil, errs.ErrEmailIsNotVerified
|
return nil, errs.ErrEmailIsNotVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Email != request.Email {
|
if user.Email != request.Email {
|
||||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] request email not equals the user email")
|
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] request email not equals the user email")
|
||||||
return nil, errs.ErrEmptyIsInvalid
|
return nil, errs.ErrEmptyIsInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +132,7 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.Context) (any, *er
|
|||||||
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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())
|
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())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrNewPasswordEqualsOldInvalid
|
return nil, errs.ErrNewPasswordEqualsOldInvalid
|
||||||
@@ -135,7 +147,7 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.Context) (any, *er
|
|||||||
_, _, err = a.users.UpdateUser(c, userNew, false)
|
_, _, err = a.users.UpdateUser(c, userNew, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[forget_passwords.UserResetPasswordHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,9 +155,9 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.Context) (any, *er
|
|||||||
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
|
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.InfofWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
log.Infof(c, "[forget_passwords.UserResetPasswordHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||||
} else {
|
} else {
|
||||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// HealthStatusHandler returns the health status of current service
|
// HealthStatusHandler returns the health status of current service
|
||||||
func (a *HealthsApi) HealthStatusHandler(c *core.Context) (any, *errs.Error) {
|
func (a *HealthsApi) HealthStatusHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
|
|
||||||
result["version"] = settings.Version
|
result["version"] = settings.Version
|
||||||
|
|||||||
@@ -24,16 +24,21 @@ const tianDiTuMapAnnotationUrlFormat = "https://t0.tianditu.gov.cn/cva_w/wmts?SE
|
|||||||
|
|
||||||
// MapImageProxy represents map image proxy
|
// MapImageProxy represents map image proxy
|
||||||
type MapImageProxy struct {
|
type MapImageProxy struct {
|
||||||
|
ApiUsingConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a map image proxy singleton instance
|
// Initialize a map image proxy singleton instance
|
||||||
var (
|
var (
|
||||||
MapImages = &MapImageProxy{}
|
MapImages = &MapImageProxy{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// MapTileImageProxyHandler returns map tile image
|
// MapTileImageProxyHandler returns map tile image
|
||||||
func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
|
func (p *MapImageProxy) MapTileImageProxyHandler(c *core.WebContext) (*httputil.ReverseProxy, *errs.Error) {
|
||||||
return p.mapImageProxyHandler(c, func(c *core.Context, mapProvider string) (string, *errs.Error) {
|
return p.mapImageProxyHandler(c, func(c *core.WebContext, mapProvider string) (string, *errs.Error) {
|
||||||
if mapProvider == settings.OpenStreetMapProvider {
|
if mapProvider == settings.OpenStreetMapProvider {
|
||||||
return openStreetMapTileImageUrlFormat, nil
|
return openStreetMapTileImageUrlFormat, nil
|
||||||
} else if mapProvider == settings.OpenStreetMapHumanitarianStyleProvider {
|
} else if mapProvider == settings.OpenStreetMapHumanitarianStyleProvider {
|
||||||
@@ -47,7 +52,7 @@ func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.Rev
|
|||||||
} else if mapProvider == settings.CartoDBMapProvider {
|
} else if mapProvider == settings.CartoDBMapProvider {
|
||||||
return cartoDBMapTileImageUrlFormat, nil
|
return cartoDBMapTileImageUrlFormat, nil
|
||||||
} else if mapProvider == settings.TomTomMapProvider {
|
} else if mapProvider == settings.TomTomMapProvider {
|
||||||
targetUrl := tomtomMapTileImageUrlFormat + "?key=" + settings.Container.Current.TomTomMapAPIKey
|
targetUrl := tomtomMapTileImageUrlFormat + "?key=" + p.CurrentConfig().TomTomMapAPIKey
|
||||||
language := c.Query("language")
|
language := c.Query("language")
|
||||||
|
|
||||||
if language != "" {
|
if language != "" {
|
||||||
@@ -56,9 +61,9 @@ func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.Rev
|
|||||||
|
|
||||||
return targetUrl, nil
|
return targetUrl, nil
|
||||||
} else if mapProvider == settings.TianDiTuProvider {
|
} else if mapProvider == settings.TianDiTuProvider {
|
||||||
return tianDiTuMapTileImageUrlFormat + "&tk=" + settings.Container.Current.TianDiTuAPIKey, nil
|
return tianDiTuMapTileImageUrlFormat + "&tk=" + p.CurrentConfig().TianDiTuAPIKey, nil
|
||||||
} else if mapProvider == settings.CustomProvider {
|
} else if mapProvider == settings.CustomProvider {
|
||||||
return settings.Container.Current.CustomMapTileServerTileLayerUrl, nil
|
return p.CurrentConfig().CustomMapTileServerTileLayerUrl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", errs.ErrParameterInvalid
|
return "", errs.ErrParameterInvalid
|
||||||
@@ -66,23 +71,23 @@ func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.Rev
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MapAnnotationImageProxyHandler returns map annotation image
|
// MapAnnotationImageProxyHandler returns map annotation image
|
||||||
func (p *MapImageProxy) MapAnnotationImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
|
func (p *MapImageProxy) MapAnnotationImageProxyHandler(c *core.WebContext) (*httputil.ReverseProxy, *errs.Error) {
|
||||||
return p.mapImageProxyHandler(c, func(c *core.Context, mapProvider string) (string, *errs.Error) {
|
return p.mapImageProxyHandler(c, func(c *core.WebContext, mapProvider string) (string, *errs.Error) {
|
||||||
if mapProvider == settings.TianDiTuProvider {
|
if mapProvider == settings.TianDiTuProvider {
|
||||||
return tianDiTuMapAnnotationUrlFormat + "&tk=" + settings.Container.Current.TianDiTuAPIKey, nil
|
return tianDiTuMapAnnotationUrlFormat + "&tk=" + p.CurrentConfig().TianDiTuAPIKey, nil
|
||||||
} else if mapProvider == settings.CustomProvider {
|
} else if mapProvider == settings.CustomProvider {
|
||||||
return settings.Container.Current.CustomMapTileServerAnnotationLayerUrl, nil
|
return p.CurrentConfig().CustomMapTileServerAnnotationLayerUrl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", errs.ErrParameterInvalid
|
return "", errs.ErrParameterInvalid
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MapImageProxy) mapImageProxyHandler(c *core.Context, fn func(c *core.Context, mapProvider string) (string, *errs.Error)) (*httputil.ReverseProxy, *errs.Error) {
|
func (p *MapImageProxy) mapImageProxyHandler(c *core.WebContext, fn func(c *core.WebContext, mapProvider string) (string, *errs.Error)) (*httputil.ReverseProxy, *errs.Error) {
|
||||||
mapProvider := strings.Replace(c.Query("provider"), "-", "_", -1)
|
mapProvider := strings.Replace(c.Query("provider"), "-", "_", -1)
|
||||||
targetUrl := ""
|
targetUrl := ""
|
||||||
|
|
||||||
if mapProvider != settings.Container.Current.MapProvider {
|
if mapProvider != p.CurrentConfig().MapProvider {
|
||||||
return nil, errs.ErrMapProviderNotCurrent
|
return nil, errs.ErrMapProviderNotCurrent
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +110,7 @@ func (p *MapImageProxy) mapImageProxyHandler(c *core.Context, fn func(c *core.Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
utils.SetProxyUrl(transport, settings.Container.Current.MapProxy)
|
utils.SetProxyUrl(transport, p.CurrentConfig().MapProxy)
|
||||||
|
|
||||||
director := func(req *http.Request) {
|
director := func(req *http.Request) {
|
||||||
imageRawUrl := targetUrl
|
imageRawUrl := targetUrl
|
||||||
|
|||||||
+9
-4
@@ -19,16 +19,21 @@ const (
|
|||||||
|
|
||||||
// QrCodesApi represents qrcode generator api
|
// QrCodesApi represents qrcode generator api
|
||||||
type QrCodesApi struct {
|
type QrCodesApi struct {
|
||||||
|
ApiUsingConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a qrcode generator api singleton instance
|
// Initialize a qrcode generator api singleton instance
|
||||||
var (
|
var (
|
||||||
QrCodes = &QrCodesApi{}
|
QrCodes = &QrCodesApi{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// MobileUrlQrCodeHandler returns a mobile url qr code image
|
// MobileUrlQrCodeHandler returns a mobile url qr code image
|
||||||
func (a *QrCodesApi) MobileUrlQrCodeHandler(c *core.Context) ([]byte, string, *errs.Error) {
|
func (a *QrCodesApi) MobileUrlQrCodeHandler(c *core.WebContext) ([]byte, string, *errs.Error) {
|
||||||
fullUrl := settings.Container.Current.RootUrl + "mobile"
|
fullUrl := a.CurrentConfig().RootUrl + "mobile"
|
||||||
data, err := a.generateUrlQrCode(c, fullUrl)
|
data, err := a.generateUrlQrCode(c, fullUrl)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,7 +43,7 @@ func (a *QrCodesApi) MobileUrlQrCodeHandler(c *core.Context) ([]byte, string, *e
|
|||||||
return data, "image/png", nil
|
return data, "image/png", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *QrCodesApi) generateUrlQrCode(c *core.Context, url string) ([]byte, *errs.Error) {
|
func (a *QrCodesApi) generateUrlQrCode(c *core.WebContext, url string) ([]byte, *errs.Error) {
|
||||||
qrCodeImg, _ := qr.Encode(url, qr.M, qr.Auto)
|
qrCodeImg, _ := qr.Encode(url, qr.M, qr.Auto)
|
||||||
qrCodeImg, _ = barcode.Scale(qrCodeImg, qrCodeDefaultWidth, qrCodeDefaultHeight)
|
qrCodeImg, _ = barcode.Scale(qrCodeImg, qrCodeDefaultWidth, qrCodeDefaultHeight)
|
||||||
imgData := &bytes.Buffer{}
|
imgData := &bytes.Buffer{}
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
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('\'')
|
||||||
|
}
|
||||||
+76
-28
@@ -4,6 +4,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
@@ -15,6 +16,8 @@ import (
|
|||||||
|
|
||||||
// TokensApi represents token api
|
// TokensApi represents token api
|
||||||
type TokensApi struct {
|
type TokensApi struct {
|
||||||
|
ApiUsingConfig
|
||||||
|
ApiWithUserInfo
|
||||||
tokens *services.TokenService
|
tokens *services.TokenService
|
||||||
users *services.UserService
|
users *services.UserService
|
||||||
}
|
}
|
||||||
@@ -22,18 +25,29 @@ type TokensApi struct {
|
|||||||
// Initialize a token api singleton instance
|
// Initialize a token api singleton instance
|
||||||
var (
|
var (
|
||||||
Tokens = &TokensApi{
|
Tokens = &TokensApi{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
ApiWithUserInfo: ApiWithUserInfo{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
ApiUsingAvatarProvider: ApiUsingAvatarProvider{
|
||||||
|
container: avatars.Container,
|
||||||
|
},
|
||||||
|
},
|
||||||
tokens: services.Tokens,
|
tokens: services.Tokens,
|
||||||
users: services.Users,
|
users: services.Users,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// TokenListHandler returns available token list of current user
|
// TokenListHandler returns available token list of current user
|
||||||
func (a *TokensApi) TokenListHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
tokens, err := a.tokens.GetAllUnexpiredNormalTokensByUid(c, uid)
|
tokens, err := a.tokens.GetAllUnexpiredNormalTokensByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[tokens.TokenListHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[tokens.TokenListHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +76,7 @@ func (a *TokensApi) TokenListHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TokenRevokeCurrentHandler revokes current token of current user
|
// TokenRevokeCurrentHandler revokes current token of current user
|
||||||
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
_, claims, err := a.tokens.ParseTokenByHeader(c)
|
_, claims, err := a.tokens.ParseTokenByHeader(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -72,7 +86,7 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (any, *errs.Error
|
|||||||
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
|
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[tokens.TokenRevokeCurrentHandler] parse user token id failed, because %s", err.Error())
|
log.Warnf(c, "[tokens.TokenRevokeCurrentHandler] parse user token id failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,21 +100,21 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (any, *errs.Error
|
|||||||
err = a.tokens.DeleteToken(c, tokenRecord)
|
err = a.tokens.DeleteToken(c, tokenRecord)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[token.TokenRevokeCurrentHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenId, claims.Uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[token.TokenRevokeCurrentHandler] user \"uid:%d\" has revoked token \"id:%s\"", claims.Uid, tokenId)
|
log.Infof(c, "[token.TokenRevokeCurrentHandler] user \"uid:%d\" has revoked token \"id:%s\"", claims.Uid, tokenId)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenRevokeHandler revokes specific token of current user
|
// TokenRevokeHandler revokes specific token of current user
|
||||||
func (a *TokensApi) TokenRevokeHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TokensApi) TokenRevokeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var tokenRevokeReq models.TokenRevokeRequest
|
var tokenRevokeReq models.TokenRevokeRequest
|
||||||
err := c.ShouldBindJSON(&tokenRevokeReq)
|
err := c.ShouldBindJSON(&tokenRevokeReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[tokens.TokenRevokeHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[tokens.TokenRevokeHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +122,7 @@ func (a *TokensApi) TokenRevokeHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.ErrorfWithRequestId(c, "[token.TokenRevokeHandler] failed to parse token \"id:%s\", because %s", tokenRevokeReq.TokenId, err.Error())
|
log.Errorf(c, "[token.TokenRevokeHandler] failed to parse token \"id:%s\", because %s", tokenRevokeReq.TokenId, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.Or(err, errs.ErrInvalidTokenId)
|
return nil, errs.Or(err, errs.ErrInvalidTokenId)
|
||||||
@@ -117,28 +131,44 @@ func (a *TokensApi) TokenRevokeHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
if tokenRecord.Uid != uid {
|
if tokenRecord.Uid != uid {
|
||||||
log.WarnfWithRequestId(c, "[token.TokenRevokeHandler] token \"id:%s\" is not owned by user \"uid:%d\"", tokenRevokeReq.TokenId, uid)
|
log.Warnf(c, "[token.TokenRevokeHandler] token \"id:%s\" is not owned by user \"uid:%d\"", tokenRevokeReq.TokenId, uid)
|
||||||
return nil, errs.ErrInvalidTokenId
|
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)
|
err = a.tokens.DeleteToken(c, tokenRecord)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[token.TokenRevokeHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenRevokeReq.TokenId, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[token.TokenRevokeHandler] user \"uid:%d\" has revoked token \"id:%s\"", uid, tokenRevokeReq.TokenId)
|
log.Infof(c, "[token.TokenRevokeHandler] user \"uid:%d\" has revoked token \"id:%s\"", uid, tokenRevokeReq.TokenId)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenRevokeAllHandler revokes all tokens of current user except current token
|
// TokenRevokeAllHandler revokes all tokens of current user except current token
|
||||||
func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TokensApi) TokenRevokeAllHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
tokens, err := a.tokens.GetAllTokensByUid(c, uid)
|
tokens, err := a.tokens.GetAllTokensByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[tokens.TokenRevokeAllHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[tokens.TokenRevokeAllHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,37 +186,55 @@ func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
|
|
||||||
tokens = append(tokens[:currentTokenIndex], tokens[currentTokenIndex+1:]...)
|
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)
|
err = a.tokens.DeleteTokens(c, uid, tokens)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[token.TokenRevokeAllHandler] failed to revoke all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[token.TokenRevokeAllHandler] failed to revoke all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[token.TokenRevokeAllHandler] user \"uid:%d\" has revoked all tokens", uid)
|
log.Infof(c, "[token.TokenRevokeAllHandler] user \"uid:%d\" has revoked all tokens", uid)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenRefreshHandler refresh current token of current user
|
// TokenRefreshHandler refresh current token of current user
|
||||||
func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TokensApi) TokenRefreshHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(c, uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
|
log.Warnf(c, "[token.TokenRefreshHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
oldTokenClaims := c.GetTokenClaims()
|
oldTokenClaims := c.GetTokenClaims()
|
||||||
|
|
||||||
if now-oldTokenClaims.IssuedAt < int64(settings.Container.Current.TokenMinRefreshInterval) {
|
if now-oldTokenClaims.IssuedAt < int64(a.CurrentConfig().TokenMinRefreshInterval) {
|
||||||
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] token of user \"uid:%d\" does not need to be refreshed", uid)
|
log.Infof(c, "[token.TokenRefreshHandler] token of user \"uid:%d\" does not need to be refreshed", uid)
|
||||||
|
|
||||||
userTokenId, err := utils.StringToInt64(oldTokenClaims.UserTokenId)
|
userTokenId, err := utils.StringToInt64(oldTokenClaims.UserTokenId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[tokens.TokenRefreshHandler] parse user token id failed, because %s", err.Error())
|
log.Warnf(c, "[tokens.TokenRefreshHandler] parse user token id failed, because %s", err.Error())
|
||||||
} else {
|
} else {
|
||||||
tokenRecord := &models.TokenRecord{
|
tokenRecord := &models.TokenRecord{
|
||||||
Uid: oldTokenClaims.Uid,
|
Uid: oldTokenClaims.Uid,
|
||||||
@@ -199,13 +247,13 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
err = a.tokens.UpdateTokenLastSeen(c, tokenRecord)
|
err = a.tokens.UpdateTokenLastSeen(c, tokenRecord)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to update last seen of token \"id:%s\" for user \"uid:%d\", because %s", tokenId, uid, err.Error())
|
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{
|
refreshResp := &models.TokenRefreshResponse{
|
||||||
User: user.ToUserBasicInfo(),
|
User: a.GetUserBasicInfo(user),
|
||||||
NotificationContent: settings.Container.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
|
NotificationContent: a.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
|
||||||
}
|
}
|
||||||
|
|
||||||
return refreshResp, nil
|
return refreshResp, nil
|
||||||
@@ -214,7 +262,7 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
token, claims, err := a.tokens.CreateToken(c, user)
|
token, claims, err := a.tokens.CreateToken(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[token.TokenRefreshHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[token.TokenRefreshHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrTokenGenerating)
|
return nil, errs.Or(err, errs.ErrTokenGenerating)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,13 +276,13 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
log.Infof(c, "[token.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
|
||||||
refreshResp := &models.TokenRefreshResponse{
|
refreshResp := &models.TokenRefreshResponse{
|
||||||
NewToken: token,
|
NewToken: token,
|
||||||
OldTokenId: a.tokens.GenerateTokenId(oldTokenRecord),
|
OldTokenId: a.tokens.GenerateTokenId(oldTokenRecord),
|
||||||
User: user.ToUserBasicInfo(),
|
User: a.GetUserBasicInfo(user),
|
||||||
NotificationContent: settings.Container.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
|
NotificationContent: a.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
|
||||||
}
|
}
|
||||||
|
|
||||||
return refreshResp, nil
|
return refreshResp, nil
|
||||||
|
|||||||
@@ -17,23 +17,31 @@ import (
|
|||||||
|
|
||||||
// TransactionCategoriesApi represents transaction category api
|
// TransactionCategoriesApi represents transaction category api
|
||||||
type TransactionCategoriesApi struct {
|
type TransactionCategoriesApi struct {
|
||||||
|
ApiUsingConfig
|
||||||
|
ApiUsingDuplicateChecker
|
||||||
categories *services.TransactionCategoryService
|
categories *services.TransactionCategoryService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a transaction category api singleton instance
|
// Initialize a transaction category api singleton instance
|
||||||
var (
|
var (
|
||||||
TransactionCategories = &TransactionCategoriesApi{
|
TransactionCategories = &TransactionCategoriesApi{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||||
|
container: duplicatechecker.Container,
|
||||||
|
},
|
||||||
categories: services.TransactionCategories,
|
categories: services.TransactionCategories,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// CategoryListHandler returns transaction category list of current user
|
// CategoryListHandler returns transaction category list of current user
|
||||||
func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionCategoriesApi) CategoryListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var categoryListReq models.TransactionCategoryListRequest
|
var categoryListReq models.TransactionCategoryListRequest
|
||||||
err := c.ShouldBindQuery(&categoryListReq)
|
err := c.ShouldBindQuery(&categoryListReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryListHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_categories.CategoryListHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +49,7 @@ func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (any, *e
|
|||||||
categories, err := a.categories.GetAllCategoriesByUid(c, uid, categoryListReq.Type, categoryListReq.ParentId)
|
categories, err := a.categories.GetAllCategoriesByUid(c, uid, categoryListReq.Type, categoryListReq.ParentId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryListHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[transaction_categories.CategoryListHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,12 +57,12 @@ func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (any, *e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CategoryGetHandler returns one specific transaction category of current user
|
// CategoryGetHandler returns one specific transaction category of current user
|
||||||
func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var categoryGetReq models.TransactionCategoryGetRequest
|
var categoryGetReq models.TransactionCategoryGetRequest
|
||||||
err := c.ShouldBindQuery(&categoryGetReq)
|
err := c.ShouldBindQuery(&categoryGetReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryGetHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_categories.CategoryGetHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +70,7 @@ func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (any, *er
|
|||||||
category, err := a.categories.GetCategoryByCategoryId(c, uid, categoryGetReq.Id)
|
category, err := a.categories.GetCategoryByCategoryId(c, uid, categoryGetReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryGetHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", categoryGetReq.Id, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,17 +80,17 @@ func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (any, *er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CategoryCreateHandler saves a new transaction category by request parameters for current user
|
// CategoryCreateHandler saves a new transaction category by request parameters for current user
|
||||||
func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var categoryCreateReq models.TransactionCategoryCreateRequest
|
var categoryCreateReq models.TransactionCategoryCreateRequest
|
||||||
err := c.ShouldBindJSON(&categoryCreateReq)
|
err := c.ShouldBindJSON(&categoryCreateReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_categories.CategoryCreateHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if categoryCreateReq.Type < models.CATEGORY_TYPE_INCOME || categoryCreateReq.Type > models.CATEGORY_TYPE_TRANSFER {
|
if categoryCreateReq.Type < models.CATEGORY_TYPE_INCOME || categoryCreateReq.Type > models.CATEGORY_TYPE_TRANSFER {
|
||||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] category type invalid, type is %d", categoryCreateReq.Type)
|
log.Warnf(c, "[transaction_categories.CategoryCreateHandler] category type invalid, type is %d", categoryCreateReq.Type)
|
||||||
return nil, errs.ErrTransactionCategoryTypeInvalid
|
return nil, errs.ErrTransactionCategoryTypeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,17 +100,17 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any,
|
|||||||
parentCategory, err := a.categories.GetCategoryByCategoryId(c, uid, categoryCreateReq.ParentId)
|
parentCategory, err := a.categories.GetCategoryByCategoryId(c, uid, categoryCreateReq.ParentId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get parent category \"id:%d\" for user \"uid:%d\", because %s", categoryCreateReq.ParentId, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
if parentCategory == nil {
|
if parentCategory == nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] parent category \"id:%d\" does not exist for user \"uid:%d\"", categoryCreateReq.ParentId, uid)
|
log.Warnf(c, "[transaction_categories.CategoryCreateHandler] parent category \"id:%d\" does not exist for user \"uid:%d\"", categoryCreateReq.ParentId, uid)
|
||||||
return nil, errs.ErrParentTransactionCategoryNotFound
|
return nil, errs.ErrParentTransactionCategoryNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if parentCategory.ParentCategoryId > 0 {
|
if parentCategory.ParentCategoryId > 0 {
|
||||||
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)
|
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)
|
||||||
return nil, errs.ErrCannotAddToSecondaryTransactionCategory
|
return nil, errs.ErrCannotAddToSecondaryTransactionCategory
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,24 +124,24 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
category := a.createNewCategoryModel(uid, &categoryCreateReq, maxOrderId+1)
|
category := a.createNewCategoryModel(uid, &categoryCreateReq, maxOrderId+1)
|
||||||
|
|
||||||
if settings.Container.Current.EnableDuplicateSubmissionsCheck && categoryCreateReq.ClientSessionId != "" {
|
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && categoryCreateReq.ClientSessionId != "" {
|
||||||
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId)
|
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId)
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] another category \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
log.Infof(c, "[transaction_categories.CategoryCreateHandler] another category \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
||||||
categoryId, err := utils.StringToInt64(remark)
|
categoryId, err := utils.StringToInt64(remark)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
category, err = a.categories.GetCategoryByCategoryId(c, uid, categoryId)
|
category, err = a.categories.GetCategoryByCategoryId(c, uid, categoryId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get existed category \"id:%d\" for user \"uid:%d\", because %s", categoryId, uid, err.Error())
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,25 +155,25 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any,
|
|||||||
err = a.categories.CreateCategory(c, category)
|
err = a.categories.CreateCategory(c, category)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to create category \"id:%d\" for user \"uid:%d\", because %s", category.CategoryId, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] user \"uid:%d\" has created a new category \"id:%d\" successfully", uid, category.CategoryId)
|
log.Infof(c, "[transaction_categories.CategoryCreateHandler] user \"uid:%d\" has created a new category \"id:%d\" successfully", uid, category.CategoryId)
|
||||||
|
|
||||||
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId, utils.Int64ToString(category.CategoryId))
|
a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId, utils.Int64ToString(category.CategoryId))
|
||||||
categoryResp := category.ToTransactionCategoryInfoResponse()
|
categoryResp := category.ToTransactionCategoryInfoResponse()
|
||||||
|
|
||||||
return categoryResp, nil
|
return categoryResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CategoryCreateBatchHandler saves some new transaction category by request parameters for current user
|
// CategoryCreateBatchHandler saves some new transaction category by request parameters for current user
|
||||||
func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var categoryCreateBatchReq models.TransactionCategoryCreateBatchRequest
|
var categoryCreateBatchReq models.TransactionCategoryCreateBatchRequest
|
||||||
err := c.ShouldBindBodyWith(&categoryCreateBatchReq, binding.JSON)
|
err := c.ShouldBindBodyWith(&categoryCreateBatchReq, binding.JSON)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_categories.CategoryCreateBatchHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,12 +189,12 @@ func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.Context) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CategoryModifyHandler saves an existed transaction category by request parameters for current user
|
// CategoryModifyHandler saves an existed transaction category by request parameters for current user
|
||||||
func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var categoryModifyReq models.TransactionCategoryModifyRequest
|
var categoryModifyReq models.TransactionCategoryModifyRequest
|
||||||
err := c.ShouldBindJSON(&categoryModifyReq)
|
err := c.ShouldBindJSON(&categoryModifyReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_categories.CategoryModifyHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +202,7 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any,
|
|||||||
category, err := a.categories.GetCategoryByCategoryId(c, uid, categoryModifyReq.Id)
|
category, err := a.categories.GetCategoryByCategoryId(c, uid, categoryModifyReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", categoryModifyReq.Id, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,14 +238,14 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any,
|
|||||||
fromPrimaryCategory, err := a.categories.GetCategoryByCategoryId(c, uid, category.ParentCategoryId)
|
fromPrimaryCategory, err := a.categories.GetCategoryByCategoryId(c, uid, category.ParentCategoryId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(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())
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
toPrimaryCategory, err := a.categories.GetCategoryByCategoryId(c, uid, newCategory.ParentCategoryId)
|
toPrimaryCategory, err := a.categories.GetCategoryByCategoryId(c, uid, newCategory.ParentCategoryId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(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())
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,11 +261,11 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any,
|
|||||||
err = a.categories.ModifyCategory(c, newCategory)
|
err = a.categories.ModifyCategory(c, newCategory)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to update category \"id:%d\" for user \"uid:%d\", because %s", categoryModifyReq.Id, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_categories.CategoryModifyHandler] user \"uid:%d\" has updated category \"id:%d\" successfully", uid, categoryModifyReq.Id)
|
log.Infof(c, "[transaction_categories.CategoryModifyHandler] user \"uid:%d\" has updated category \"id:%d\" successfully", uid, categoryModifyReq.Id)
|
||||||
|
|
||||||
newCategory.Type = category.Type
|
newCategory.Type = category.Type
|
||||||
newCategory.DisplayOrder = category.DisplayOrder
|
newCategory.DisplayOrder = category.DisplayOrder
|
||||||
@@ -267,12 +275,12 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CategoryHideHandler hides an existed transaction category by request parameters for current user
|
// CategoryHideHandler hides an existed transaction category by request parameters for current user
|
||||||
func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var categoryHideReq models.TransactionCategoryHideRequest
|
var categoryHideReq models.TransactionCategoryHideRequest
|
||||||
err := c.ShouldBindJSON(&categoryHideReq)
|
err := c.ShouldBindJSON(&categoryHideReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryHideHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_categories.CategoryHideHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,21 +288,21 @@ func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (any, *e
|
|||||||
err = a.categories.HideCategory(c, uid, []int64{categoryHideReq.Id}, categoryHideReq.Hidden)
|
err = a.categories.HideCategory(c, uid, []int64{categoryHideReq.Id}, categoryHideReq.Hidden)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryHideHandler] failed to hide category \"id:%d\" for user \"uid:%d\", because %s", categoryHideReq.Id, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_categories.CategoryHideHandler] user \"uid:%d\" has hidden category \"id:%d\"", uid, categoryHideReq.Id)
|
log.Infof(c, "[transaction_categories.CategoryHideHandler] user \"uid:%d\" has hidden category \"id:%d\"", uid, categoryHideReq.Id)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CategoryMoveHandler moves display order of existed transaction categories by request parameters for current user
|
// CategoryMoveHandler moves display order of existed transaction categories by request parameters for current user
|
||||||
func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var categoryMoveReq models.TransactionCategoryMoveRequest
|
var categoryMoveReq models.TransactionCategoryMoveRequest
|
||||||
err := c.ShouldBindJSON(&categoryMoveReq)
|
err := c.ShouldBindJSON(&categoryMoveReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryMoveHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_categories.CategoryMoveHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,21 +323,21 @@ func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (any, *e
|
|||||||
err = a.categories.ModifyCategoryDisplayOrders(c, uid, categories)
|
err = a.categories.ModifyCategoryDisplayOrders(c, uid, categories)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryMoveHandler] failed to move categories for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[transaction_categories.CategoryMoveHandler] failed to move categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_categories.CategoryMoveHandler] user \"uid:%d\" has moved categories", uid)
|
log.Infof(c, "[transaction_categories.CategoryMoveHandler] user \"uid:%d\" has moved categories", uid)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CategoryDeleteHandler deletes an existed transaction category by request parameters for current user
|
// CategoryDeleteHandler deletes an existed transaction category by request parameters for current user
|
||||||
func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var categoryDeleteReq models.TransactionCategoryDeleteRequest
|
var categoryDeleteReq models.TransactionCategoryDeleteRequest
|
||||||
err := c.ShouldBindJSON(&categoryDeleteReq)
|
err := c.ShouldBindJSON(&categoryDeleteReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_categories.CategoryDeleteHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,15 +345,15 @@ func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (any,
|
|||||||
err = a.categories.DeleteCategory(c, uid, categoryDeleteReq.Id)
|
err = a.categories.DeleteCategory(c, uid, categoryDeleteReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] failed to delete category \"id:%d\" for user \"uid:%d\", because %s", categoryDeleteReq.Id, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] user \"uid:%d\" has deleted category \"id:%d\"", uid, categoryDeleteReq.Id)
|
log.Infof(c, "[transaction_categories.CategoryDeleteHandler] user \"uid:%d\" has deleted category \"id:%d\"", uid, categoryDeleteReq.Id)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TransactionCategoriesApi) createBatchCategories(c *core.Context, uid int64, categoryCreateBatchReq *models.TransactionCategoryCreateBatchRequest) ([]*models.TransactionCategory, error) {
|
func (a *TransactionCategoriesApi) createBatchCategories(c *core.WebContext, uid int64, categoryCreateBatchReq *models.TransactionCategoryCreateBatchRequest) ([]*models.TransactionCategory, error) {
|
||||||
var err error
|
var err error
|
||||||
categoryTypeMaxOrderMap := make(map[models.TransactionCategoryType]int32)
|
categoryTypeMaxOrderMap := make(map[models.TransactionCategoryType]int32)
|
||||||
categoriesMap := make(map[*models.TransactionCategory][]*models.TransactionCategory)
|
categoriesMap := make(map[*models.TransactionCategory][]*models.TransactionCategory)
|
||||||
@@ -360,7 +368,7 @@ func (a *TransactionCategoriesApi) createBatchCategories(c *core.Context, uid in
|
|||||||
maxOrderId, err = a.categories.GetMaxDisplayOrder(c, uid, categoryCreateReq.Type)
|
maxOrderId, err = a.categories.GetMaxDisplayOrder(c, uid, categoryCreateReq.Type)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,11 +396,11 @@ func (a *TransactionCategoriesApi) createBatchCategories(c *core.Context, uid in
|
|||||||
categories, err := a.categories.CreateCategories(c, uid, categoriesMap)
|
categories, err := a.categories.CreateCategories(c, uid, categoriesMap)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_categories.createBatchCategories] failed to create categories for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[transaction_categories.createBatchCategories] failed to create categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_categories.createBatchCategories] user \"uid:%d\" has created categories successfully", uid)
|
log.Infof(c, "[transaction_categories.createBatchCategories] user \"uid:%d\" has created categories successfully", uid)
|
||||||
|
|
||||||
return categories, nil
|
return categories, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
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{
|
||||||
|
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
|
// TagListHandler returns transaction tag list of current user
|
||||||
func (a *TransactionTagsApi) TagListHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTagsApi) TagListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
tags, err := a.tags.GetAllTagsByUid(c, uid)
|
tags, err := a.tags.GetAllTagsByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagListHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[transaction_tags.TagListHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,12 +44,12 @@ func (a *TransactionTagsApi) TagListHandler(c *core.Context) (any, *errs.Error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TagGetHandler returns one specific transaction tag of current user
|
// TagGetHandler returns one specific transaction tag of current user
|
||||||
func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTagsApi) TagGetHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var tagGetReq models.TransactionTagGetRequest
|
var tagGetReq models.TransactionTagGetRequest
|
||||||
err := c.ShouldBindQuery(&tagGetReq)
|
err := c.ShouldBindQuery(&tagGetReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_tags.TagGetHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_tags.TagGetHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
tag, err := a.tags.GetTagByTagId(c, uid, tagGetReq.Id)
|
tag, err := a.tags.GetTagByTagId(c, uid, tagGetReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagGetHandler] failed to get tag \"id:%d\" for user \"uid:%d\", because %s", tagGetReq.Id, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,12 +67,12 @@ func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TagCreateHandler saves a new transaction tag by request parameters for current user
|
// TagCreateHandler saves a new transaction tag by request parameters for current user
|
||||||
func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTagsApi) TagCreateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var tagCreateReq models.TransactionTagCreateRequest
|
var tagCreateReq models.TransactionTagCreateRequest
|
||||||
err := c.ShouldBindJSON(&tagCreateReq)
|
err := c.ShouldBindJSON(&tagCreateReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_tags.TagCreateHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_tags.TagCreateHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (any, *errs.Error
|
|||||||
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid)
|
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[transaction_tags.TagCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,11 +90,11 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (any, *errs.Error
|
|||||||
err = a.tags.CreateTag(c, tag)
|
err = a.tags.CreateTag(c, tag)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagCreateHandler] failed to create tag \"id:%d\" for user \"uid:%d\", because %s", tag.TagId, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_tags.TagCreateHandler] user \"uid:%d\" has created a new tag \"id:%d\" successfully", uid, tag.TagId)
|
log.Infof(c, "[transaction_tags.TagCreateHandler] user \"uid:%d\" has created a new tag \"id:%d\" successfully", uid, tag.TagId)
|
||||||
|
|
||||||
tagResp := tag.ToTransactionTagInfoResponse()
|
tagResp := tag.ToTransactionTagInfoResponse()
|
||||||
|
|
||||||
@@ -102,12 +102,12 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (any, *errs.Error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TagModifyHandler saves an existed transaction tag by request parameters for current user
|
// TagModifyHandler saves an existed transaction tag by request parameters for current user
|
||||||
func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var tagModifyReq models.TransactionTagModifyRequest
|
var tagModifyReq models.TransactionTagModifyRequest
|
||||||
err := c.ShouldBindJSON(&tagModifyReq)
|
err := c.ShouldBindJSON(&tagModifyReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_tags.TagModifyHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_tags.TagModifyHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (any, *errs.Error
|
|||||||
tag, err := a.tags.GetTagByTagId(c, uid, tagModifyReq.Id)
|
tag, err := a.tags.GetTagByTagId(c, uid, tagModifyReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagModifyHandler] failed to get tag \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.Id, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,11 +132,11 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (any, *errs.Error
|
|||||||
err = a.tags.ModifyTag(c, newTag)
|
err = a.tags.ModifyTag(c, newTag)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagModifyHandler] failed to update tag \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.Id, uid, err.Error())
|
log.Errorf(c, "[transaction_tags.TagModifyHandler] failed to update tag \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.Id, uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_tags.TagModifyHandler] user \"uid:%d\" has updated tag \"id:%d\" successfully", uid, tagModifyReq.Id)
|
log.Infof(c, "[transaction_tags.TagModifyHandler] user \"uid:%d\" has updated tag \"id:%d\" successfully", uid, tagModifyReq.Id)
|
||||||
|
|
||||||
tag.Name = newTag.Name
|
tag.Name = newTag.Name
|
||||||
tagResp := tag.ToTransactionTagInfoResponse()
|
tagResp := tag.ToTransactionTagInfoResponse()
|
||||||
@@ -144,13 +144,13 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (any, *errs.Error
|
|||||||
return tagResp, nil
|
return tagResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagHideHandler hides an transaction tag by request parameters for current user
|
// TagHideHandler hides a transaction tag by request parameters for current user
|
||||||
func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTagsApi) TagHideHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var tagHideReq models.TransactionTagHideRequest
|
var tagHideReq models.TransactionTagHideRequest
|
||||||
err := c.ShouldBindJSON(&tagHideReq)
|
err := c.ShouldBindJSON(&tagHideReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_tags.TagHideHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_tags.TagHideHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,21 +158,21 @@ func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (any, *errs.Error)
|
|||||||
err = a.tags.HideTag(c, uid, []int64{tagHideReq.Id}, tagHideReq.Hidden)
|
err = a.tags.HideTag(c, uid, []int64{tagHideReq.Id}, tagHideReq.Hidden)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagHideHandler] failed to hide tag \"id:%d\" for user \"uid:%d\", because %s", tagHideReq.Id, uid, err.Error())
|
log.Errorf(c, "[transaction_tags.TagHideHandler] failed to hide tag \"id:%d\" for user \"uid:%d\", because %s", tagHideReq.Id, uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_tags.TagHideHandler] user \"uid:%d\" has hidden tag \"id:%d\"", uid, tagHideReq.Id)
|
log.Infof(c, "[transaction_tags.TagHideHandler] user \"uid:%d\" has hidden tag \"id:%d\"", uid, tagHideReq.Id)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagMoveHandler moves display order of existed transaction tags by request parameters for current user
|
// TagMoveHandler moves display order of existed transaction tags by request parameters for current user
|
||||||
func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTagsApi) TagMoveHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var tagMoveReq models.TransactionTagMoveRequest
|
var tagMoveReq models.TransactionTagMoveRequest
|
||||||
err := c.ShouldBindJSON(&tagMoveReq)
|
err := c.ShouldBindJSON(&tagMoveReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_tags.TagMoveHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_tags.TagMoveHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,21 +193,21 @@ func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (any, *errs.Error)
|
|||||||
err = a.tags.ModifyTagDisplayOrders(c, uid, tags)
|
err = a.tags.ModifyTagDisplayOrders(c, uid, tags)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagMoveHandler] failed to move tags for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[transaction_tags.TagMoveHandler] failed to move tags for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_tags.TagMoveHandler] user \"uid:%d\" has moved tags", uid)
|
log.Infof(c, "[transaction_tags.TagMoveHandler] user \"uid:%d\" has moved tags", uid)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagDeleteHandler deletes an existed transaction tag by request parameters for current user
|
// TagDeleteHandler deletes an existed transaction tag by request parameters for current user
|
||||||
func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTagsApi) TagDeleteHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var tagDeleteReq models.TransactionTagDeleteRequest
|
var tagDeleteReq models.TransactionTagDeleteRequest
|
||||||
err := c.ShouldBindJSON(&tagDeleteReq)
|
err := c.ShouldBindJSON(&tagDeleteReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_tags.TagDeleteHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_tags.TagDeleteHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,11 +215,11 @@ func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (any, *errs.Error
|
|||||||
err = a.tags.DeleteTag(c, uid, tagDeleteReq.Id)
|
err = a.tags.DeleteTag(c, uid, tagDeleteReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagDeleteHandler] failed to delete tag \"id:%d\" for user \"uid:%d\", because %s", tagDeleteReq.Id, uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_tags.TagDeleteHandler] user \"uid:%d\" has deleted tag \"id:%d\"", uid, tagDeleteReq.Id)
|
log.Infof(c, "[transaction_tags.TagDeleteHandler] user \"uid:%d\" has deleted tag \"id:%d\"", uid, tagDeleteReq.Id)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
@@ -14,38 +15,52 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maximumTagsCountOfTemplate = 10
|
||||||
|
|
||||||
// TransactionTemplatesApi represents transaction template api
|
// TransactionTemplatesApi represents transaction template api
|
||||||
type TransactionTemplatesApi struct {
|
type TransactionTemplatesApi struct {
|
||||||
|
ApiUsingConfig
|
||||||
|
ApiUsingDuplicateChecker
|
||||||
templates *services.TransactionTemplateService
|
templates *services.TransactionTemplateService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a transaction template api singleton instance
|
// Initialize a transaction template api singleton instance
|
||||||
var (
|
var (
|
||||||
TransactionTemplates = &TransactionTemplatesApi{
|
TransactionTemplates = &TransactionTemplatesApi{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||||
|
container: duplicatechecker.Container,
|
||||||
|
},
|
||||||
templates: services.TransactionTemplates,
|
templates: services.TransactionTemplates,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// TemplateListHandler returns transaction template list of current user
|
// TemplateListHandler returns transaction template list of current user
|
||||||
func (a *TransactionTemplatesApi) TemplateListHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTemplatesApi) TemplateListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var templateListReq models.TransactionTemplateListRequest
|
var templateListReq models.TransactionTemplateListRequest
|
||||||
err := c.ShouldBindQuery(&templateListReq)
|
err := c.ShouldBindQuery(&templateListReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateListHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_templates.TemplateListHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if templateListReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateListReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
|
if templateListReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateListReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateListHandler] template type invalid, type is %d", templateListReq.TemplateType)
|
log.Warnf(c, "[transaction_templates.TemplateListHandler] template type invalid, type is %d", templateListReq.TemplateType)
|
||||||
return nil, errs.ErrTransactionTemplateTypeInvalid
|
return nil, errs.ErrTransactionTemplateTypeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if templateListReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||||
|
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
templates, err := a.templates.GetAllTemplatesByUid(c, uid, templateListReq.TemplateType)
|
templates, err := a.templates.GetAllTemplatesByUid(c, uid, templateListReq.TemplateType)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateListHandler] failed to get templates for user \"uid:%d\", because %s", uid, err.Error())
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,12 +77,12 @@ func (a *TransactionTemplatesApi) TemplateListHandler(c *core.Context) (any, *er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TemplateGetHandler returns one specific transaction template of current user
|
// TemplateGetHandler returns one specific transaction template of current user
|
||||||
func (a *TransactionTemplatesApi) TemplateGetHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTemplatesApi) TemplateGetHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var templateGetReq models.TransactionTemplateGetRequest
|
var templateGetReq models.TransactionTemplateGetRequest
|
||||||
err := c.ShouldBindQuery(&templateGetReq)
|
err := c.ShouldBindQuery(&templateGetReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateGetHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_templates.TemplateGetHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,10 +90,14 @@ func (a *TransactionTemplatesApi) TemplateGetHandler(c *core.Context) (any, *err
|
|||||||
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateGetReq.Id)
|
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateGetReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateGetHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateGetReq.Id, uid, err.Error())
|
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)
|
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()
|
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||||
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||||
|
|
||||||
@@ -86,49 +105,71 @@ func (a *TransactionTemplatesApi) TemplateGetHandler(c *core.Context) (any, *err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TemplateCreateHandler saves a new transaction template by request parameters for current user
|
// TemplateCreateHandler saves a new transaction template by request parameters for current user
|
||||||
func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var templateCreateReq models.TransactionTemplateCreateRequest
|
var templateCreateReq models.TransactionTemplateCreateRequest
|
||||||
err := c.ShouldBindJSON(&templateCreateReq)
|
err := c.ShouldBindJSON(&templateCreateReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_templates.TemplateCreateHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if templateCreateReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateCreateReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
|
if templateCreateReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateCreateReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] template type invalid, type is %d", templateCreateReq.TemplateType)
|
log.Warnf(c, "[transaction_templates.TemplateCreateHandler] template type invalid, type is %d", templateCreateReq.TemplateType)
|
||||||
return nil, errs.ErrTransactionTemplateTypeInvalid
|
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 {
|
if templateCreateReq.Type <= models.TRANSACTION_TYPE_MODIFY_BALANCE || templateCreateReq.Type > models.TRANSACTION_TYPE_TRANSFER {
|
||||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] transaction type invalid, type is %d", templateCreateReq.Type)
|
log.Warnf(c, "[transaction_templates.TemplateCreateHandler] transaction type invalid, type is %d", templateCreateReq.Type)
|
||||||
return nil, errs.ErrTransactionTypeInvalid
|
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()
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
maxOrderId, err := a.templates.GetMaxDisplayOrder(c, uid, templateCreateReq.TemplateType)
|
maxOrderId, err := a.templates.GetMaxDisplayOrder(c, uid, templateCreateReq.TemplateType)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||||
template := a.createNewTemplateModel(uid, &templateCreateReq, maxOrderId+1)
|
template := a.createNewTemplateModel(uid, &templateCreateReq, maxOrderId+1)
|
||||||
|
|
||||||
if settings.Container.Current.EnableDuplicateSubmissionsCheck && templateCreateReq.ClientSessionId != "" {
|
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && templateCreateReq.ClientSessionId != "" {
|
||||||
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId)
|
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId)
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateCreateHandler] another template \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
log.Infof(c, "[transaction_templates.TemplateCreateHandler] another template \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
||||||
templateId, err := utils.StringToInt64(remark)
|
templateId, err := utils.StringToInt64(remark)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
template, err = a.templates.GetTemplateByTemplateId(c, uid, templateId)
|
template, err = a.templates.GetTemplateByTemplateId(c, uid, templateId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to get existed template \"id:%d\" for user \"uid:%d\", because %s", templateId, uid, err.Error())
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,30 +183,30 @@ func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.Context) (any, *
|
|||||||
err = a.templates.CreateTemplate(c, template)
|
err = a.templates.CreateTemplate(c, template)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to create template \"id:%d\" for user \"uid:%d\", because %s", template.TemplateId, uid, err.Error())
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateCreateHandler] user \"uid:%d\" has created a new template \"id:%d\" successfully", uid, template.TemplateId)
|
log.Infof(c, "[transaction_templates.TemplateCreateHandler] user \"uid:%d\" has created a new template \"id:%d\" successfully", uid, template.TemplateId)
|
||||||
|
|
||||||
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId, utils.Int64ToString(template.TemplateId))
|
a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId, utils.Int64ToString(template.TemplateId))
|
||||||
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||||
|
|
||||||
return templateResp, nil
|
return templateResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateModifyHandler saves an existed transaction template by request parameters for current user
|
// TemplateModifyHandler saves an existed transaction template by request parameters for current user
|
||||||
func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var templateModifyReq models.TransactionTemplateModifyRequest
|
var templateModifyReq models.TransactionTemplateModifyRequest
|
||||||
err := c.ShouldBindJSON(&templateModifyReq)
|
err := c.ShouldBindJSON(&templateModifyReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_templates.TemplateModifyHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if templateModifyReq.Type <= models.TRANSACTION_TYPE_MODIFY_BALANCE || templateModifyReq.Type > models.TRANSACTION_TYPE_TRANSFER {
|
if templateModifyReq.Type <= models.TRANSACTION_TYPE_MODIFY_BALANCE || templateModifyReq.Type > models.TRANSACTION_TYPE_TRANSFER {
|
||||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] transaction type invalid, type is %d", templateModifyReq.Type)
|
log.Warnf(c, "[transaction_templates.TemplateModifyHandler] transaction type invalid, type is %d", templateModifyReq.Type)
|
||||||
return nil, errs.ErrTransactionTypeInvalid
|
return nil, errs.ErrTransactionTypeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,10 +214,32 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.Context) (any, *
|
|||||||
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateModifyReq.Id)
|
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateModifyReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error())
|
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)
|
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{
|
newTemplate := &models.TransactionTemplate{
|
||||||
TemplateId: template.TemplateId,
|
TemplateId: template.TemplateId,
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
@@ -192,6 +255,13 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.Context) (any, *
|
|||||||
Comment: templateModifyReq.Comment,
|
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 newTemplate.Name == template.Name &&
|
if newTemplate.Name == template.Name &&
|
||||||
newTemplate.Type == template.Type &&
|
newTemplate.Type == template.Type &&
|
||||||
newTemplate.CategoryId == template.CategoryId &&
|
newTemplate.CategoryId == template.CategoryId &&
|
||||||
@@ -202,17 +272,26 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.Context) (any, *
|
|||||||
newTemplate.RelatedAccountAmount == template.RelatedAccountAmount &&
|
newTemplate.RelatedAccountAmount == template.RelatedAccountAmount &&
|
||||||
newTemplate.HideAmount == template.HideAmount &&
|
newTemplate.HideAmount == template.HideAmount &&
|
||||||
newTemplate.Comment == template.Comment {
|
newTemplate.Comment == template.Comment {
|
||||||
|
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
|
||||||
return nil, errs.ErrNothingWillBeUpdated
|
return nil, errs.ErrNothingWillBeUpdated
|
||||||
|
} else if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
|
if newTemplate.ScheduledFrequencyType == template.ScheduledFrequencyType &&
|
||||||
|
newTemplate.ScheduledFrequency == template.ScheduledFrequency &&
|
||||||
|
newTemplate.ScheduledAt == template.ScheduledAt &&
|
||||||
|
newTemplate.ScheduledTimezoneUtcOffset == template.ScheduledTimezoneUtcOffset {
|
||||||
|
return nil, errs.ErrNothingWillBeUpdated
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.templates.ModifyTemplate(c, newTemplate)
|
err = a.templates.ModifyTemplate(c, newTemplate)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] failed to update template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error())
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateModifyHandler] user \"uid:%d\" has updated template \"id:%d\" successfully", uid, templateModifyReq.Id)
|
log.Infof(c, "[transaction_templates.TemplateModifyHandler] user \"uid:%d\" has updated template \"id:%d\" successfully", uid, templateModifyReq.Id)
|
||||||
|
|
||||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||||
newTemplate.TemplateType = template.TemplateType
|
newTemplate.TemplateType = template.TemplateType
|
||||||
@@ -223,39 +302,65 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.Context) (any, *
|
|||||||
return templateResp, nil
|
return templateResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateHideHandler hides an transaction template by request parameters for current user
|
// TemplateHideHandler hides a transaction template by request parameters for current user
|
||||||
func (a *TransactionTemplatesApi) TemplateHideHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTemplatesApi) TemplateHideHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var templateHideReq models.TransactionTemplateHideRequest
|
var templateHideReq models.TransactionTemplateHideRequest
|
||||||
err := c.ShouldBindJSON(&templateHideReq)
|
err := c.ShouldBindJSON(&templateHideReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateHideHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_templates.TemplateHideHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
err = a.templates.HideTemplate(c, uid, []int64{templateHideReq.Id}, templateHideReq.Hidden)
|
|
||||||
|
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateHideReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateHideHandler] failed to hide template \"id:%d\" for user \"uid:%d\", because %s", templateHideReq.Id, uid, err.Error())
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateHideHandler] user \"uid:%d\" has hidden template \"id:%d\"", uid, templateHideReq.Id)
|
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
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateMoveHandler moves display order of existed transaction templates by request parameters for current user
|
// TemplateMoveHandler moves display order of existed transaction templates by request parameters for current user
|
||||||
func (a *TransactionTemplatesApi) TemplateMoveHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTemplatesApi) TemplateMoveHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var templateMoveReq models.TransactionTemplateMoveRequest
|
var templateMoveReq models.TransactionTemplateMoveRequest
|
||||||
err := c.ShouldBindJSON(&templateMoveReq)
|
err := c.ShouldBindJSON(&templateMoveReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_templates.CategoryMoveHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_templates.TemplateMoveHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
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))
|
templates := make([]*models.TransactionTemplate, len(templateMoveReq.NewDisplayOrders))
|
||||||
|
|
||||||
for i := 0; i < len(templateMoveReq.NewDisplayOrders); i++ {
|
for i := 0; i < len(templateMoveReq.NewDisplayOrders); i++ {
|
||||||
@@ -272,38 +377,50 @@ func (a *TransactionTemplatesApi) TemplateMoveHandler(c *core.Context) (any, *er
|
|||||||
err = a.templates.ModifyTemplateDisplayOrders(c, uid, templates)
|
err = a.templates.ModifyTemplateDisplayOrders(c, uid, templates)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateMoveHandler] failed to move templates for user \"uid:%d\", because %s", uid, err.Error())
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateMoveHandler] user \"uid:%d\" has moved templates", uid)
|
log.Infof(c, "[transaction_templates.TemplateMoveHandler] user \"uid:%d\" has moved templates", uid)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateDeleteHandler deletes an existed transaction template by request parameters for current user
|
// TemplateDeleteHandler deletes an existed transaction template by request parameters for current user
|
||||||
func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var templateDeleteReq models.TransactionTemplateDeleteRequest
|
var templateDeleteReq models.TransactionTemplateDeleteRequest
|
||||||
err := c.ShouldBindJSON(&templateDeleteReq)
|
err := c.ShouldBindJSON(&templateDeleteReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[transaction_templates.TemplateDeleteHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
err = a.templates.DeleteTemplate(c, uid, templateDeleteReq.Id)
|
|
||||||
|
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateDeleteReq.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] failed to delete template \"id:%d\" for user \"uid:%d\", because %s", templateDeleteReq.Id, uid, err.Error())
|
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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] user \"uid:%d\" has deleted template \"id:%d\"", uid, templateDeleteReq.Id)
|
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
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) *models.TransactionTemplate {
|
func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) *models.TransactionTemplate {
|
||||||
return &models.TransactionTemplate{
|
template := &models.TransactionTemplate{
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
TemplateType: templateCreateReq.TemplateType,
|
TemplateType: templateCreateReq.TemplateType,
|
||||||
Name: templateCreateReq.Name,
|
Name: templateCreateReq.Name,
|
||||||
@@ -318,4 +435,60 @@ func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCrea
|
|||||||
Comment: templateCreateReq.Comment,
|
Comment: templateCreateReq.Comment,
|
||||||
DisplayOrder: order,
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return template
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
+518
-115
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TwoFactorStatusHandler returns 2fa status of current user
|
// TwoFactorStatusHandler returns 2fa status of current user
|
||||||
func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
|
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (an
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorStatusHandler] failed to get two-factor setting, because %s", err.Error())
|
log.Errorf(c, "[twofactor_authorizations.TwoFactorStatusHandler] failed to get two-factor setting, because %s", err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,12 +58,12 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (an
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TwoFactorEnableRequestHandler returns a new 2fa secret and qr code for current user to set 2fa and verify passcode next
|
// 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.Context) (any, *errs.Error) {
|
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two-factor setting, because %s", err.Error())
|
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two-factor setting, because %s", err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,23 +75,27 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to get user, because %s", err.Error())
|
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to get user, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
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)
|
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor secret, because %s", err.Error())
|
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor secret, because %s", err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
img, err := key.Image(240, 240)
|
img, err := key.Image(240, 240)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor qrcode, because %s", err.Error())
|
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor qrcode, because %s", err.Error())
|
||||||
return nil, errs.ErrOperationFailed
|
return nil, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,12 +114,12 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TwoFactorEnableConfirmHandler enables 2fa for current user
|
// TwoFactorEnableConfirmHandler enables 2fa for current user
|
||||||
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var confirmReq models.TwoFactorEnableConfirmRequest
|
var confirmReq models.TwoFactorEnableConfirmRequest
|
||||||
err := c.ShouldBindJSON(&confirmReq)
|
err := c.ShouldBindJSON(&confirmReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +127,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
|
|||||||
exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two-factor setting, because %s", err.Error())
|
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two-factor setting, because %s", err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,58 +139,62 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to get user, because %s", err.Error())
|
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to get user, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA) {
|
||||||
|
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||||
|
}
|
||||||
|
|
||||||
twoFactorSetting := &models.TwoFactor{
|
twoFactorSetting := &models.TwoFactor{
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
Secret: confirmReq.Secret,
|
Secret: confirmReq.Secret,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !totp.Validate(confirmReq.Passcode, confirmReq.Secret) {
|
if !totp.Validate(confirmReq.Passcode, confirmReq.Secret) {
|
||||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] passcode is invalid")
|
log.Warnf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] passcode is invalid")
|
||||||
return nil, errs.ErrPasscodeInvalid
|
return nil, errs.ErrPasscodeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to generate two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)
|
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.twoFactorAuthorizations.CreateTwoFactorSetting(c, twoFactorSetting)
|
err = a.twoFactorAuthorizations.CreateTwoFactorSetting(c, twoFactorSetting)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" has enabled two-factor authorization", uid)
|
log.Infof(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" has enabled two-factor authorization", uid)
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
|
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
log.Infof(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||||
} else {
|
} else {
|
||||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Warnf(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)
|
token, claims, err := a.tokens.CreateToken(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Warnf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
|
||||||
confirmResp := &models.TwoFactorEnableConfirmResponse{
|
confirmResp := &models.TwoFactorEnableConfirmResponse{
|
||||||
RecoveryCodes: recoveryCodes,
|
RecoveryCodes: recoveryCodes,
|
||||||
@@ -198,7 +206,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
|
|||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
log.Infof(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
|
||||||
confirmResp := &models.TwoFactorEnableConfirmResponse{
|
confirmResp := &models.TwoFactorEnableConfirmResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
@@ -209,12 +217,12 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TwoFactorDisableHandler disables 2fa for current user
|
// TwoFactorDisableHandler disables 2fa for current user
|
||||||
func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var disableReq models.TwoFactorDisableRequest
|
var disableReq models.TwoFactorDisableRequest
|
||||||
err := c.ShouldBindJSON(&disableReq)
|
err := c.ShouldBindJSON(&disableReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[twofactor_authorizations.TwoFactorDisableHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,12 +231,16 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (a
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
log.Warnf(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
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) {
|
if !a.users.IsPasswordEqualsUserPassword(disableReq.Password, user) {
|
||||||
return nil, errs.ErrUserPasswordWrong
|
return nil, errs.ErrUserPasswordWrong
|
||||||
}
|
}
|
||||||
@@ -236,7 +248,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (a
|
|||||||
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two-factor setting, because %s", err.Error())
|
log.Errorf(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two-factor setting, because %s", err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,29 +259,29 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (a
|
|||||||
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(c, uid)
|
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two-factor recovery codes for user \"uid:%d\"", uid)
|
log.Errorf(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two-factor recovery codes for user \"uid:%d\"", uid)
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(c, uid)
|
err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two-factor setting for user \"uid:%d\"", uid)
|
log.Errorf(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two-factor setting for user \"uid:%d\"", uid)
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] user \"uid:%d\" has disabled two-factor authorization", uid)
|
log.Infof(c, "[twofactor_authorizations.TwoFactorDisableHandler] user \"uid:%d\" has disabled two-factor authorization", uid)
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TwoFactorRecoveryCodeRegenerateHandler returns new 2fa recovery codes and revokes old recovery codes for current user
|
// TwoFactorRecoveryCodeRegenerateHandler returns new 2fa recovery codes and revokes old recovery codes for current user
|
||||||
func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *core.Context) (any, *errs.Error) {
|
func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var regenerateReq models.TwoFactorRegenerateRecoveryCodeRequest
|
var regenerateReq models.TwoFactorRegenerateRecoveryCodeRequest
|
||||||
err := c.ShouldBindJSON(®enerateReq)
|
err := c.ShouldBindJSON(®enerateReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +290,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
log.Warnf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
@@ -291,7 +303,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
|||||||
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two-factor setting, because %s", err.Error())
|
log.Errorf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two-factor setting, because %s", err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,14 +314,14 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
|||||||
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to generate two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)
|
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to create two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +329,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
|||||||
RecoveryCodes: recoveryCodes,
|
RecoveryCodes: recoveryCodes,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] user \"uid:%d\" has regenerated two-factor recovery codes", uid)
|
log.Infof(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] user \"uid:%d\" has regenerated two-factor recovery codes", uid)
|
||||||
|
|
||||||
return recoveryCodesResp, nil
|
return recoveryCodesResp, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+164
-167
@@ -1,13 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/locales"
|
"github.com/mayswind/ezbookkeeping/pkg/locales"
|
||||||
@@ -15,13 +14,14 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/storage"
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UsersApi represents user api
|
// UsersApi represents user api
|
||||||
type UsersApi struct {
|
type UsersApi struct {
|
||||||
|
ApiUsingConfig
|
||||||
|
ApiWithUserInfo
|
||||||
users *services.UserService
|
users *services.UserService
|
||||||
tokens *services.TokenService
|
tokens *services.TokenService
|
||||||
accounts *services.AccountService
|
accounts *services.AccountService
|
||||||
@@ -30,6 +30,17 @@ type UsersApi struct {
|
|||||||
// Initialize a user api singleton instance
|
// Initialize a user api singleton instance
|
||||||
var (
|
var (
|
||||||
Users = &UsersApi{
|
Users = &UsersApi{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
ApiWithUserInfo: ApiWithUserInfo{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
ApiUsingAvatarProvider: ApiUsingAvatarProvider{
|
||||||
|
container: avatars.Container,
|
||||||
|
},
|
||||||
|
},
|
||||||
users: services.Users,
|
users: services.Users,
|
||||||
tokens: services.Tokens,
|
tokens: services.Tokens,
|
||||||
accounts: services.Accounts,
|
accounts: services.Accounts,
|
||||||
@@ -37,8 +48,8 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// UserRegisterHandler saves a new user by request parameters
|
// UserRegisterHandler saves a new user by request parameters
|
||||||
func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) {
|
func (a *UsersApi) UserRegisterHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
if !settings.Container.Current.EnableUserRegister {
|
if !a.CurrentConfig().EnableUserRegister {
|
||||||
return nil, errs.ErrUserRegistrationNotAllowed
|
return nil, errs.ErrUserRegistrationNotAllowed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,12 +57,12 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
err := c.ShouldBindBodyWith(&userRegisterReq, binding.JSON)
|
err := c.ShouldBindBodyWith(&userRegisterReq, binding.JSON)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[users.UserRegisterHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if userRegisterReq.DefaultCurrency == validators.ParentAccountCurrencyPlaceholder {
|
if userRegisterReq.DefaultCurrency == validators.ParentAccountCurrencyPlaceholder {
|
||||||
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] user default currency is invalid")
|
log.Warnf(c, "[users.UserRegisterHandler] user default currency is invalid")
|
||||||
return nil, errs.ErrUserDefaultCurrencyIsInvalid
|
return nil, errs.ErrUserDefaultCurrencyIsInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,16 +79,17 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
DefaultCurrency: userRegisterReq.DefaultCurrency,
|
DefaultCurrency: userRegisterReq.DefaultCurrency,
|
||||||
FirstDayOfWeek: userRegisterReq.FirstDayOfWeek,
|
FirstDayOfWeek: userRegisterReq.FirstDayOfWeek,
|
||||||
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
|
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
|
||||||
|
FeatureRestriction: a.CurrentConfig().DefaultFeatureRestrictions,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.users.CreateUser(c, user)
|
err = a.users.CreateUser(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
|
log.Errorf(c, "[users.UserRegisterHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"%s\" has registered successfully, uid is %d", user.Username, user.Uid)
|
log.Infof(c, "[users.UserRegisterHandler] user \"%s\" has registered successfully, uid is %d", user.Username, user.Uid)
|
||||||
|
|
||||||
presetCategoriesSaved := false
|
presetCategoriesSaved := false
|
||||||
|
|
||||||
@@ -92,37 +104,37 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
authResp := &models.RegisterResponse{
|
authResp := &models.RegisterResponse{
|
||||||
AuthResponse: models.AuthResponse{
|
AuthResponse: models.AuthResponse{
|
||||||
Need2FA: false,
|
Need2FA: false,
|
||||||
User: user.ToUserBasicInfo(),
|
User: a.GetUserBasicInfo(user),
|
||||||
NotificationContent: settings.Container.GetAfterRegisterNotificationContent(user.Language, c.GetClientLocale()),
|
NotificationContent: a.GetAfterRegisterNotificationContent(user.Language, c.GetClientLocale()),
|
||||||
},
|
},
|
||||||
NeedVerifyEmail: settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableUserForceVerifyEmail,
|
NeedVerifyEmail: a.CurrentConfig().EnableUserVerifyEmail && a.CurrentConfig().EnableUserForceVerifyEmail,
|
||||||
PresetCategoriesSaved: presetCategoriesSaved,
|
PresetCategoriesSaved: presetCategoriesSaved,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableSMTP {
|
if a.CurrentConfig().EnableUserVerifyEmail && a.CurrentConfig().EnableSMTP {
|
||||||
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to create email verify token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[users.UserRegisterHandler] failed to create email verify token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
} else {
|
} else {
|
||||||
go func() {
|
go func() {
|
||||||
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] cannot send verify email to \"%s\", because %s", user.Email, err.Error())
|
log.Warnf(c, "[users.UserRegisterHandler] cannot send verify email to \"%s\", because %s", user.Email, err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.Container.Current.EnableUserForceVerifyEmail {
|
if a.CurrentConfig().EnableUserForceVerifyEmail {
|
||||||
return authResp, nil
|
return authResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
token, claims, err := a.tokens.CreateToken(c, user)
|
token, claims, err := a.tokens.CreateToken(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Warnf(c, "[users.UserRegisterHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return authResp, nil
|
return authResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,13 +142,13 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"uid:%d\" has logined, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
log.Infof(c, "[users.UserRegisterHandler] user \"uid:%d\" has logined, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
|
||||||
return authResp, nil
|
return authResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserEmailVerifyHandler sets user email address verified
|
// UserEmailVerifyHandler sets user email address verified
|
||||||
func (a *UsersApi) UserEmailVerifyHandler(c *core.Context) (any, *errs.Error) {
|
func (a *UsersApi) UserEmailVerifyHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var userVerifyEmailReq models.UserVerifyEmailRequest
|
var userVerifyEmailReq models.UserVerifyEmailRequest
|
||||||
err := c.ShouldBindJSON(&userVerifyEmailReq)
|
err := c.ShouldBindJSON(&userVerifyEmailReq)
|
||||||
|
|
||||||
@@ -145,35 +157,35 @@ func (a *UsersApi) UserEmailVerifyHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to get user, because %s", err.Error())
|
log.Errorf(c, "[users.UserEmailVerifyHandler] failed to get user, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Disabled {
|
if user.Disabled {
|
||||||
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" is disabled", user.Uid)
|
log.Warnf(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||||
return nil, errs.ErrUserIsDisabled
|
return nil, errs.ErrUserIsDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.EmailVerified {
|
if user.EmailVerified {
|
||||||
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" email has been verified", user.Uid)
|
log.Warnf(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" email has been verified", user.Uid)
|
||||||
return nil, errs.ErrEmailIsVerified
|
return nil, errs.ErrEmailIsVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.users.SetUserEmailVerified(c, user.Username)
|
err = a.users.SetUserEmailVerified(c, user.Username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to update user \"uid:%d\" email address verified, because %s", user.Uid, err.Error())
|
log.Errorf(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)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.tokens.DeleteTokensByType(c, uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
|
err = a.tokens.DeleteTokensByType(c, uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.InfofWithRequestId(c, "[users.UserEmailVerifyHandler] revoke old email verify tokens for user \"uid:%d\"", user.Uid)
|
log.Infof(c, "[users.UserEmailVerifyHandler] revoke old email verify tokens for user \"uid:%d\"", user.Uid)
|
||||||
} else {
|
} else {
|
||||||
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Warnf(c, "[users.UserEmailVerifyHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := &models.UserVerifyEmailResponse{}
|
resp := &models.UserVerifyEmailResponse{}
|
||||||
@@ -182,47 +194,47 @@ func (a *UsersApi) UserEmailVerifyHandler(c *core.Context) (any, *errs.Error) {
|
|||||||
token, claims, err := a.tokens.CreateToken(c, user)
|
token, claims, err := a.tokens.CreateToken(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Warnf(c, "[users.UserEmailVerifyHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.NewToken = token
|
resp.NewToken = token
|
||||||
resp.User = user.ToUserBasicInfo()
|
resp.User = a.GetUserBasicInfo(user)
|
||||||
resp.NotificationContent = settings.Container.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale())
|
resp.NotificationContent = a.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale())
|
||||||
|
|
||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" token created, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
log.Infof(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" token created, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserProfileHandler returns user profile of current user
|
// UserProfileHandler returns user profile of current user
|
||||||
func (a *UsersApi) UserProfileHandler(c *core.Context) (any, *errs.Error) {
|
func (a *UsersApi) UserProfileHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(c, uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to get user, because %s", err.Error())
|
log.Errorf(c, "[users.UserRegisterHandler] failed to get user, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
userResp := user.ToUserProfileResponse()
|
userResp := a.getUserProfileResponse(user)
|
||||||
return userResp, nil
|
return userResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserUpdateProfileHandler saves user profile by request parameters for current user
|
// UserUpdateProfileHandler saves user profile by request parameters for current user
|
||||||
func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error) {
|
func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var userUpdateReq models.UserProfileUpdateRequest
|
var userUpdateReq models.UserProfileUpdateRequest
|
||||||
err := c.ShouldBindJSON(&userUpdateReq)
|
err := c.ShouldBindJSON(&userUpdateReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] parse request failed, because %s", err.Error())
|
log.Warnf(c, "[users.UserUpdateProfileHandler] parse request failed, because %s", err.Error())
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +243,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to get user, because %s", err.Error())
|
log.Errorf(c, "[users.UserUpdateProfileHandler] failed to get user, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
@@ -240,6 +252,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
userUpdateReq.Email = strings.TrimSpace(userUpdateReq.Email)
|
userUpdateReq.Email = strings.TrimSpace(userUpdateReq.Email)
|
||||||
userUpdateReq.Nickname = strings.TrimSpace(userUpdateReq.Nickname)
|
userUpdateReq.Nickname = strings.TrimSpace(userUpdateReq.Nickname)
|
||||||
|
|
||||||
|
modifyProfileBasicInfo := false
|
||||||
anythingUpdate := false
|
anythingUpdate := false
|
||||||
userNew := &models.User{
|
userNew := &models.User{
|
||||||
Uid: user.Uid,
|
Uid: user.Uid,
|
||||||
@@ -247,12 +260,20 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if userUpdateReq.Email != "" && userUpdateReq.Email != user.Email {
|
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
|
user.Email = userUpdateReq.Email
|
||||||
userNew.Email = userUpdateReq.Email
|
userNew.Email = userUpdateReq.Email
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if userUpdateReq.Password != "" {
|
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) {
|
if !a.users.IsPasswordEqualsUserPassword(userUpdateReq.OldPassword, user) {
|
||||||
return nil, errs.ErrUserPasswordWrong
|
return nil, errs.ErrUserPasswordWrong
|
||||||
}
|
}
|
||||||
@@ -266,6 +287,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
if userUpdateReq.Nickname != "" && userUpdateReq.Nickname != user.Nickname {
|
if userUpdateReq.Nickname != "" && userUpdateReq.Nickname != user.Nickname {
|
||||||
user.Nickname = userUpdateReq.Nickname
|
user.Nickname = userUpdateReq.Nickname
|
||||||
userNew.Nickname = userUpdateReq.Nickname
|
userNew.Nickname = userUpdateReq.Nickname
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,23 +299,25 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, exists := accountMap[userUpdateReq.DefaultAccountId]; !exists {
|
if _, exists := accountMap[userUpdateReq.DefaultAccountId]; !exists {
|
||||||
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] account \"id:%d\" does not exist for user \"uid:%d\"", userUpdateReq.DefaultAccountId, uid)
|
log.Warnf(c, "[users.UserUpdateProfileHandler] account \"id:%d\" does not exist for user \"uid:%d\"", userUpdateReq.DefaultAccountId, uid)
|
||||||
return nil, errs.ErrUserDefaultAccountIsInvalid
|
return nil, errs.ErrUserDefaultAccountIsInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
if accountMap[userUpdateReq.DefaultAccountId].Hidden {
|
if accountMap[userUpdateReq.DefaultAccountId].Hidden {
|
||||||
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] account \"id:%d\" is hidden of user \"uid:%d\"", userUpdateReq.DefaultAccountId, uid)
|
log.Warnf(c, "[users.UserUpdateProfileHandler] account \"id:%d\" is hidden of user \"uid:%d\"", userUpdateReq.DefaultAccountId, uid)
|
||||||
return nil, errs.ErrUserDefaultAccountIsHidden
|
return nil, errs.ErrUserDefaultAccountIsHidden
|
||||||
}
|
}
|
||||||
|
|
||||||
user.DefaultAccountId = userUpdateReq.DefaultAccountId
|
user.DefaultAccountId = userUpdateReq.DefaultAccountId
|
||||||
userNew.DefaultAccountId = userUpdateReq.DefaultAccountId
|
userNew.DefaultAccountId = userUpdateReq.DefaultAccountId
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if userUpdateReq.TransactionEditScope != nil && *userUpdateReq.TransactionEditScope != user.TransactionEditScope {
|
if userUpdateReq.TransactionEditScope != nil && *userUpdateReq.TransactionEditScope != user.TransactionEditScope {
|
||||||
user.TransactionEditScope = *userUpdateReq.TransactionEditScope
|
user.TransactionEditScope = *userUpdateReq.TransactionEditScope
|
||||||
userNew.TransactionEditScope = *userUpdateReq.TransactionEditScope
|
userNew.TransactionEditScope = *userUpdateReq.TransactionEditScope
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} else {
|
||||||
userNew.TransactionEditScope = models.TRANSACTION_EDIT_SCOPE_INVALID
|
userNew.TransactionEditScope = models.TRANSACTION_EDIT_SCOPE_INVALID
|
||||||
@@ -305,58 +329,66 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
user.Language = userUpdateReq.Language
|
user.Language = userUpdateReq.Language
|
||||||
userNew.Language = userUpdateReq.Language
|
userNew.Language = userUpdateReq.Language
|
||||||
modifyUserLanguage = true
|
modifyUserLanguage = true
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if userUpdateReq.DefaultCurrency != "" && userUpdateReq.DefaultCurrency != user.DefaultCurrency {
|
if userUpdateReq.DefaultCurrency != "" && userUpdateReq.DefaultCurrency != user.DefaultCurrency {
|
||||||
user.DefaultCurrency = userUpdateReq.DefaultCurrency
|
user.DefaultCurrency = userUpdateReq.DefaultCurrency
|
||||||
userNew.DefaultCurrency = userUpdateReq.DefaultCurrency
|
userNew.DefaultCurrency = userUpdateReq.DefaultCurrency
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if userUpdateReq.FirstDayOfWeek != nil && *userUpdateReq.FirstDayOfWeek != user.FirstDayOfWeek {
|
if userUpdateReq.FirstDayOfWeek != nil && *userUpdateReq.FirstDayOfWeek != user.FirstDayOfWeek {
|
||||||
user.FirstDayOfWeek = *userUpdateReq.FirstDayOfWeek
|
user.FirstDayOfWeek = *userUpdateReq.FirstDayOfWeek
|
||||||
userNew.FirstDayOfWeek = *userUpdateReq.FirstDayOfWeek
|
userNew.FirstDayOfWeek = *userUpdateReq.FirstDayOfWeek
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} else {
|
||||||
userNew.FirstDayOfWeek = models.WEEKDAY_INVALID
|
userNew.FirstDayOfWeek = core.WEEKDAY_INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
if userUpdateReq.LongDateFormat != nil && *userUpdateReq.LongDateFormat != user.LongDateFormat {
|
if userUpdateReq.LongDateFormat != nil && *userUpdateReq.LongDateFormat != user.LongDateFormat {
|
||||||
user.LongDateFormat = *userUpdateReq.LongDateFormat
|
user.LongDateFormat = *userUpdateReq.LongDateFormat
|
||||||
userNew.LongDateFormat = *userUpdateReq.LongDateFormat
|
userNew.LongDateFormat = *userUpdateReq.LongDateFormat
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} else {
|
||||||
userNew.LongDateFormat = models.LONG_DATE_FORMAT_INVALID
|
userNew.LongDateFormat = core.LONG_DATE_FORMAT_INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
if userUpdateReq.ShortDateFormat != nil && *userUpdateReq.ShortDateFormat != user.ShortDateFormat {
|
if userUpdateReq.ShortDateFormat != nil && *userUpdateReq.ShortDateFormat != user.ShortDateFormat {
|
||||||
user.ShortDateFormat = *userUpdateReq.ShortDateFormat
|
user.ShortDateFormat = *userUpdateReq.ShortDateFormat
|
||||||
userNew.ShortDateFormat = *userUpdateReq.ShortDateFormat
|
userNew.ShortDateFormat = *userUpdateReq.ShortDateFormat
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} else {
|
||||||
userNew.ShortDateFormat = models.SHORT_DATE_FORMAT_INVALID
|
userNew.ShortDateFormat = core.SHORT_DATE_FORMAT_INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
if userUpdateReq.LongTimeFormat != nil && *userUpdateReq.LongTimeFormat != user.LongTimeFormat {
|
if userUpdateReq.LongTimeFormat != nil && *userUpdateReq.LongTimeFormat != user.LongTimeFormat {
|
||||||
user.LongTimeFormat = *userUpdateReq.LongTimeFormat
|
user.LongTimeFormat = *userUpdateReq.LongTimeFormat
|
||||||
userNew.LongTimeFormat = *userUpdateReq.LongTimeFormat
|
userNew.LongTimeFormat = *userUpdateReq.LongTimeFormat
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} else {
|
||||||
userNew.LongTimeFormat = models.LONG_TIME_FORMAT_INVALID
|
userNew.LongTimeFormat = core.LONG_TIME_FORMAT_INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
if userUpdateReq.ShortTimeFormat != nil && *userUpdateReq.ShortTimeFormat != user.ShortTimeFormat {
|
if userUpdateReq.ShortTimeFormat != nil && *userUpdateReq.ShortTimeFormat != user.ShortTimeFormat {
|
||||||
user.ShortTimeFormat = *userUpdateReq.ShortTimeFormat
|
user.ShortTimeFormat = *userUpdateReq.ShortTimeFormat
|
||||||
userNew.ShortTimeFormat = *userUpdateReq.ShortTimeFormat
|
userNew.ShortTimeFormat = *userUpdateReq.ShortTimeFormat
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} else {
|
||||||
userNew.ShortTimeFormat = models.SHORT_TIME_FORMAT_INVALID
|
userNew.ShortTimeFormat = core.SHORT_TIME_FORMAT_INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
if userUpdateReq.DecimalSeparator != nil && *userUpdateReq.DecimalSeparator != user.DecimalSeparator {
|
if userUpdateReq.DecimalSeparator != nil && *userUpdateReq.DecimalSeparator != user.DecimalSeparator {
|
||||||
user.DecimalSeparator = *userUpdateReq.DecimalSeparator
|
user.DecimalSeparator = *userUpdateReq.DecimalSeparator
|
||||||
userNew.DecimalSeparator = *userUpdateReq.DecimalSeparator
|
userNew.DecimalSeparator = *userUpdateReq.DecimalSeparator
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} else {
|
||||||
userNew.DecimalSeparator = core.DECIMAL_SEPARATOR_INVALID
|
userNew.DecimalSeparator = core.DECIMAL_SEPARATOR_INVALID
|
||||||
@@ -365,6 +397,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
if userUpdateReq.DigitGroupingSymbol != nil && *userUpdateReq.DigitGroupingSymbol != user.DigitGroupingSymbol {
|
if userUpdateReq.DigitGroupingSymbol != nil && *userUpdateReq.DigitGroupingSymbol != user.DigitGroupingSymbol {
|
||||||
user.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol
|
user.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol
|
||||||
userNew.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol
|
userNew.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} else {
|
||||||
userNew.DigitGroupingSymbol = core.DIGIT_GROUPING_SYMBOL_INVALID
|
userNew.DigitGroupingSymbol = core.DIGIT_GROUPING_SYMBOL_INVALID
|
||||||
@@ -373,6 +406,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
if userUpdateReq.DigitGrouping != nil && *userUpdateReq.DigitGrouping != user.DigitGrouping {
|
if userUpdateReq.DigitGrouping != nil && *userUpdateReq.DigitGrouping != user.DigitGrouping {
|
||||||
user.DigitGrouping = *userUpdateReq.DigitGrouping
|
user.DigitGrouping = *userUpdateReq.DigitGrouping
|
||||||
userNew.DigitGrouping = *userUpdateReq.DigitGrouping
|
userNew.DigitGrouping = *userUpdateReq.DigitGrouping
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} else {
|
||||||
userNew.DigitGrouping = core.DIGIT_GROUPING_TYPE_INVALID
|
userNew.DigitGrouping = core.DIGIT_GROUPING_TYPE_INVALID
|
||||||
@@ -381,14 +415,16 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
if userUpdateReq.CurrencyDisplayType != nil && *userUpdateReq.CurrencyDisplayType != user.CurrencyDisplayType {
|
if userUpdateReq.CurrencyDisplayType != nil && *userUpdateReq.CurrencyDisplayType != user.CurrencyDisplayType {
|
||||||
user.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
|
user.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
|
||||||
userNew.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
|
userNew.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} else {
|
||||||
userNew.CurrencyDisplayType = models.CURRENCY_DISPLAY_TYPE_INVALID
|
userNew.CurrencyDisplayType = core.CURRENCY_DISPLAY_TYPE_INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
if userUpdateReq.ExpenseAmountColor != nil && *userUpdateReq.ExpenseAmountColor != user.ExpenseAmountColor {
|
if userUpdateReq.ExpenseAmountColor != nil && *userUpdateReq.ExpenseAmountColor != user.ExpenseAmountColor {
|
||||||
user.ExpenseAmountColor = *userUpdateReq.ExpenseAmountColor
|
user.ExpenseAmountColor = *userUpdateReq.ExpenseAmountColor
|
||||||
userNew.ExpenseAmountColor = *userUpdateReq.ExpenseAmountColor
|
userNew.ExpenseAmountColor = *userUpdateReq.ExpenseAmountColor
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} else {
|
||||||
userNew.ExpenseAmountColor = models.AMOUNT_COLOR_TYPE_INVALID
|
userNew.ExpenseAmountColor = models.AMOUNT_COLOR_TYPE_INVALID
|
||||||
@@ -397,11 +433,16 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
if userUpdateReq.IncomeAmountColor != nil && *userUpdateReq.IncomeAmountColor != user.IncomeAmountColor {
|
if userUpdateReq.IncomeAmountColor != nil && *userUpdateReq.IncomeAmountColor != user.IncomeAmountColor {
|
||||||
user.IncomeAmountColor = *userUpdateReq.IncomeAmountColor
|
user.IncomeAmountColor = *userUpdateReq.IncomeAmountColor
|
||||||
userNew.IncomeAmountColor = *userUpdateReq.IncomeAmountColor
|
userNew.IncomeAmountColor = *userUpdateReq.IncomeAmountColor
|
||||||
|
modifyProfileBasicInfo = true
|
||||||
anythingUpdate = true
|
anythingUpdate = true
|
||||||
} else {
|
} else {
|
||||||
userNew.IncomeAmountColor = models.AMOUNT_COLOR_TYPE_INVALID
|
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 {
|
if modifyUserLanguage || userNew.DecimalSeparator != core.DECIMAL_SEPARATOR_INVALID || userNew.DigitGroupingSymbol != core.DIGIT_GROUPING_SYMBOL_INVALID {
|
||||||
decimalSeparator := userNew.DecimalSeparator
|
decimalSeparator := userNew.DecimalSeparator
|
||||||
digitGroupingSymbol := userNew.DigitGroupingSymbol
|
digitGroupingSymbol := userNew.DigitGroupingSymbol
|
||||||
@@ -436,7 +477,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
keyProfileUpdated, emailSetToUnverified, err := a.users.UpdateUser(c, userNew, modifyUserLanguage)
|
keyProfileUpdated, emailSetToUnverified, err := a.users.UpdateUser(c, userNew, modifyUserLanguage)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[users.UserUpdateProfileHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,28 +485,28 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
user.EmailVerified = false
|
user.EmailVerified = false
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" has updated successfully", user.Uid)
|
log.Infof(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" has updated successfully", user.Uid)
|
||||||
|
|
||||||
resp := &models.UserProfileUpdateResponse{
|
resp := &models.UserProfileUpdateResponse{
|
||||||
User: user.ToUserBasicInfo(),
|
User: a.GetUserBasicInfo(user),
|
||||||
}
|
}
|
||||||
|
|
||||||
if emailSetToUnverified && settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableSMTP {
|
if emailSetToUnverified && a.CurrentConfig().EnableUserVerifyEmail && a.CurrentConfig().EnableSMTP {
|
||||||
err = a.tokens.DeleteTokensByType(c, uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
|
err = a.tokens.DeleteTokensByType(c, uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[users.UserUpdateProfileHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
} else {
|
} else {
|
||||||
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to create email verify token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[users.UserUpdateProfileHandler] failed to create email verify token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
} else {
|
} else {
|
||||||
go func() {
|
go func() {
|
||||||
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] cannot send verify email to \"%s\", because %s", user.Email, err.Error())
|
log.Warnf(c, "[users.UserUpdateProfileHandler] cannot send verify email to \"%s\", because %s", user.Email, err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -477,15 +518,15 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
|
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
log.Infof(c, "[users.UserUpdateProfileHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||||
} else {
|
} else {
|
||||||
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Warnf(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)
|
token, claims, err := a.tokens.CreateToken(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Warnf(c, "[users.UserUpdateProfileHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +534,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
log.Infof(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -502,130 +543,108 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UserUpdateAvatarHandler saves user avatar by request parameters for current user
|
// UserUpdateAvatarHandler saves user avatar by request parameters for current user
|
||||||
func (a *UsersApi) UserUpdateAvatarHandler(c *core.Context) (any, *errs.Error) {
|
func (a *UsersApi) UserUpdateAvatarHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(c, uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get user, because %s", err.Error())
|
log.Errorf(c, "[users.UserUpdateAvatarHandler] failed to get user, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_UPDATE_AVATAR) {
|
||||||
|
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||||
|
}
|
||||||
|
|
||||||
form, err := c.MultipartForm()
|
form, err := c.MultipartForm()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get multi-part form data for user \"uid:%d\", because %s", user.Uid, err.Error())
|
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
|
return nil, errs.ErrParameterInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
avatars := form.File["avatar"]
|
avatarFiles := form.File["avatar"]
|
||||||
|
|
||||||
if len(avatars) < 1 {
|
if len(avatarFiles) < 1 {
|
||||||
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] there is no user avatar in request for user \"uid:%d\"", user.Uid)
|
log.Warnf(c, "[users.UserUpdateAvatarHandler] there is no user avatar in request for user \"uid:%d\"", user.Uid)
|
||||||
return nil, errs.ErrNoUserAvatar
|
return nil, errs.ErrNoUserAvatar
|
||||||
}
|
}
|
||||||
|
|
||||||
if avatars[0].Size < 1 {
|
if avatarFiles[0].Size < 1 {
|
||||||
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] the size of user avatar in request is zero for user \"uid:%d\"", user.Uid)
|
log.Warnf(c, "[users.UserUpdateAvatarHandler] the size of user avatar in request is zero for user \"uid:%d\"", user.Uid)
|
||||||
return nil, errs.ErrUserAvatarIsEmpty
|
return nil, errs.ErrUserAvatarIsEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
fileExtension := utils.GetFileNameExtension(avatars[0].Filename)
|
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) == "" {
|
if utils.GetImageContentType(fileExtension) == "" {
|
||||||
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] the file extension \"%s\" of user avatar in request is not supported for user \"uid:%d\"", fileExtension, user.Uid)
|
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
|
return nil, errs.ErrImageTypeNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
avatarFile, err := avatars[0].Open()
|
avatarFile, err := avatarFiles[0].Open()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get avatar file from request for user \"uid:%d\", because %s", user.Uid, err.Error())
|
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
|
return nil, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
defer avatarFile.Close()
|
err = a.users.UpdateUserAvatar(c, user.Uid, avatarFile, fileExtension, user.CustomAvatarType)
|
||||||
|
|
||||||
err = storage.Container.SaveAvatar(user.Uid, avatarFile, fileExtension)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to save avatar file for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[users.UserUpdateAvatarHandler] failed to update avatar for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrOperationFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
err = a.users.UpdateUserAvatar(c, user.Uid, fileExtension)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileExtension != user.CustomAvatarType {
|
|
||||||
err = storage.Container.DeleteAvatar(user.Uid, user.CustomAvatarType)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to delete old avatar with extension \"%s\" for user \"uid:%d\", because %s", user.CustomAvatarType, user.Uid, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user.CustomAvatarType = fileExtension
|
user.CustomAvatarType = fileExtension
|
||||||
userResp := user.ToUserProfileResponse()
|
userResp := a.getUserProfileResponse(user)
|
||||||
return userResp, nil
|
return userResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserRemoveAvatarHandler removes user avatar by request parameters for current user
|
// UserRemoveAvatarHandler removes user avatar by request parameters for current user
|
||||||
func (a *UsersApi) UserRemoveAvatarHandler(c *core.Context) (any, *errs.Error) {
|
func (a *UsersApi) UserRemoveAvatarHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
user, err := a.users.GetUserById(c, uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to get user, because %s", err.Error())
|
log.Errorf(c, "[users.UserRemoveAvatarHandler] failed to get user, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_UPDATE_AVATAR) {
|
||||||
|
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||||
|
}
|
||||||
|
|
||||||
if user.CustomAvatarType == "" {
|
if user.CustomAvatarType == "" {
|
||||||
return nil, errs.ErrNothingWillBeUpdated
|
return nil, errs.ErrNothingWillBeUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
err = storage.Container.DeleteAvatar(user.Uid, user.CustomAvatarType)
|
err = a.users.RemoveUserAvatar(c, user.Uid, user.CustomAvatarType)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to delete avatar file for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[users.UserRemoveAvatarHandler] failed to remove avatar for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
|
||||||
exists, err := storage.Container.ExistsAvatar(user.Uid, user.CustomAvatarType)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to check whether avatar file exist for user \"uid:%d\", because %s", user.Uid, err.Error())
|
|
||||||
return nil, errs.ErrOperationFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to delete whether avatar file exist for user \"uid:%d\", the avatar file still exist", user.Uid)
|
|
||||||
return nil, errs.ErrOperationFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = a.users.UpdateUserAvatar(c, user.Uid, "")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
|
||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
user.CustomAvatarType = ""
|
user.CustomAvatarType = ""
|
||||||
userResp := user.ToUserProfileResponse()
|
userResp := a.getUserProfileResponse(user)
|
||||||
return userResp, nil
|
return userResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserSendVerifyEmailByUnloginUserHandler sends unlogin user verify email
|
// UserSendVerifyEmailByUnloginUserHandler sends unlogin user verify email
|
||||||
func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any, *errs.Error) {
|
func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
if !settings.Container.Current.EnableUserVerifyEmail {
|
if !a.CurrentConfig().EnableUserVerifyEmail {
|
||||||
return nil, errs.ErrEmailValidationNotAllowed
|
return nil, errs.ErrEmailValidationNotAllowed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,35 +655,35 @@ func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to get user, because %s", err.Error())
|
log.Errorf(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to get user, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if !a.users.IsPasswordEqualsUserPassword(userResendVerifyEmailReq.Password, user) {
|
if !a.users.IsPasswordEqualsUserPassword(userResendVerifyEmailReq.Password, user) {
|
||||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] request password not equals to the user password")
|
log.Warnf(c, "[users.UserSendVerifyEmailByUnloginUserHandler] request password not equals to the user password")
|
||||||
return nil, errs.ErrUserPasswordWrong
|
return nil, errs.ErrUserPasswordWrong
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Disabled {
|
if user.Disabled {
|
||||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" is disabled", user.Uid)
|
log.Warnf(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||||
return nil, errs.ErrUserIsDisabled
|
return nil, errs.ErrUserIsDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.EmailVerified {
|
if user.EmailVerified {
|
||||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" email has been verified", user.Uid)
|
log.Warnf(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" email has been verified", user.Uid)
|
||||||
return nil, errs.ErrEmailIsVerified
|
return nil, errs.ErrEmailIsVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
if !settings.Container.Current.EnableSMTP {
|
if !a.CurrentConfig().EnableSMTP {
|
||||||
return nil, errs.ErrSMTPServerNotEnabled
|
return nil, errs.ErrSMTPServerNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrTokenGenerating
|
return nil, errs.ErrTokenGenerating
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +691,7 @@ func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any
|
|||||||
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
log.Warnf(c, "[users.UserSendVerifyEmailByUnloginUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -680,8 +699,8 @@ func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UserSendVerifyEmailByLoginedUserHandler sends logined user verify email
|
// UserSendVerifyEmailByLoginedUserHandler sends logined user verify email
|
||||||
func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any, *errs.Error) {
|
func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
if !settings.Container.Current.EnableUserVerifyEmail {
|
if !a.CurrentConfig().EnableUserVerifyEmail {
|
||||||
return nil, errs.ErrEmailValidationNotAllowed
|
return nil, errs.ErrEmailValidationNotAllowed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,25 +709,25 @@ func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errs.IsCustomError(err) {
|
if !errs.IsCustomError(err) {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to get user, because %s", err.Error())
|
log.Errorf(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to get user, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.EmailVerified {
|
if user.EmailVerified {
|
||||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] user \"uid:%d\" email has been verified", user.Uid)
|
log.Warnf(c, "[users.UserSendVerifyEmailByLoginedUserHandler] user \"uid:%d\" email has been verified", user.Uid)
|
||||||
return nil, errs.ErrEmailIsVerified
|
return nil, errs.ErrEmailIsVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
if !settings.Container.Current.EnableSMTP {
|
if !a.CurrentConfig().EnableSMTP {
|
||||||
return nil, errs.ErrSMTPServerNotEnabled
|
return nil, errs.ErrSMTPServerNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
return nil, errs.ErrTokenGenerating
|
return nil, errs.ErrTokenGenerating
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,7 +735,7 @@ func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any
|
|||||||
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
log.Warnf(c, "[users.UserSendVerifyEmailByLoginedUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -724,58 +743,36 @@ func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UserGetAvatarHandler returns user avatar data for current user
|
// UserGetAvatarHandler returns user avatar data for current user
|
||||||
func (a *UsersApi) UserGetAvatarHandler(c *core.Context) ([]byte, string, *errs.Error) {
|
func (a *UsersApi) UserGetAvatarHandler(c *core.WebContext) ([]byte, string, *errs.Error) {
|
||||||
uid := c.GetCurrentUid()
|
|
||||||
user, err := a.users.GetUserById(c, uid)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if !errs.IsCustomError(err) {
|
|
||||||
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to get user, because %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, "", errs.ErrUserNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.CustomAvatarType == "" {
|
|
||||||
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user does not have avatar for user \"uid:%d\"", user.Uid)
|
|
||||||
return nil, "", errs.ErrUserAvatarNoExists
|
|
||||||
}
|
|
||||||
|
|
||||||
fileName := c.Param("fileName")
|
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)
|
fileBaseName := utils.GetFileNameWithoutExtension(fileName)
|
||||||
|
|
||||||
if utils.Int64ToString(user.Uid) != fileBaseName {
|
if utils.Int64ToString(uid) != fileBaseName {
|
||||||
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] cannot get other user avatar \"uid:%s\" for user \"uid:%d\"", fileBaseName, user.Uid)
|
log.Warnf(c, "[users.UserGetAvatarHandler] cannot get other user avatar \"uid:%s\" for user \"uid:%d\"", fileBaseName, uid)
|
||||||
return nil, "", errs.ErrUserIdInvalid
|
return nil, "", errs.ErrUserIdInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
fileExtension := utils.GetFileNameExtension(fileName)
|
avatarData, err := a.users.GetUserAvatar(c, uid, fileExtension)
|
||||||
|
|
||||||
if user.CustomAvatarType != fileExtension {
|
|
||||||
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user avatar extension is invalid \"%s\" for user \"uid:%d\"", fileExtension, user.Uid)
|
|
||||||
return nil, "", errs.ErrUserAvatarNoExists
|
|
||||||
}
|
|
||||||
|
|
||||||
avatarFile, err := storage.Container.ReadAvatar(user.Uid, fileExtension)
|
|
||||||
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user avatar file not exist for user \"uid:%d\", because %s", user.Uid, err.Error())
|
|
||||||
return nil, "", errs.ErrUserAvatarNoExists
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to get user avatar object for user \"uid:%d\", because %s", user.Uid, err.Error())
|
if !errs.IsCustomError(err) {
|
||||||
return nil, "", errs.ErrOperationFailed
|
log.Errorf(c, "[users.UserGetAvatarHandler] failed to get user avatar, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
defer avatarFile.Close()
|
return nil, "", errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
|
||||||
avatarData, err := io.ReadAll(avatarFile)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to read user avatar object data for user \"uid:%d\", because %s", user.Uid, err.Error())
|
|
||||||
return nil, "", errs.ErrOperationFailed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return avatarData, utils.GetImageContentType(fileExtension), nil
|
return avatarData, contentType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *UsersApi) getUserProfileResponse(user *models.User) *models.UserProfileResponse {
|
||||||
|
return user.ToUserProfileResponse(a.GetUserBasicInfo(user))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package avatars
|
||||||
|
|
||||||
|
import "github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
|
|
||||||
|
// AvatarProvider is user avatar provider interface
|
||||||
|
type AvatarProvider interface {
|
||||||
|
GetAvatarUrl(user *models.User) string
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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 ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
+353
-156
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
|||||||
|
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: "备注",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,614 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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: "备注",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package converters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DataConverter defines the structure of data exporter
|
|
||||||
type DataConverter interface {
|
|
||||||
// ToExportedContent returns the exported data
|
|
||||||
ToExportedContent(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)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,572 @@
|
|||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
tagNameItems := strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS), c.transactionTagSeparator)
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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: ",",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,520 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package converters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EzBookKeepingCSVFileExporter defines the structure of CSV file exporter
|
|
||||||
type EzBookKeepingCSVFileExporter struct {
|
|
||||||
EzBookKeepingPlainFileExporter
|
|
||||||
}
|
|
||||||
|
|
||||||
const csvSeparator = ","
|
|
||||||
|
|
||||||
// ToExportedContent returns the exported CSV data
|
|
||||||
func (e *EzBookKeepingCSVFileExporter) ToExportedContent(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) {
|
|
||||||
return e.toExportedContent(uid, csvSeparator, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
package converters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EzBookKeepingPlainFileExporter defines the structure of plain file exporter
|
|
||||||
type EzBookKeepingPlainFileExporter struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
const lineSeparator = "\n"
|
|
||||||
const geoLocationSeparator = " "
|
|
||||||
const transactionTagSeparator = ";"
|
|
||||||
const headerLine = "Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description" + lineSeparator
|
|
||||||
const dataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" + lineSeparator
|
|
||||||
|
|
||||||
// toExportedContent returns the exported plain data
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) toExportedContent(uid int64, separator string, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
|
|
||||||
var ret strings.Builder
|
|
||||||
|
|
||||||
ret.Grow(len(transactions) * 100)
|
|
||||||
|
|
||||||
actualHeaderLine := headerLine
|
|
||||||
actualDataLineFormat := dataLineFormat
|
|
||||||
|
|
||||||
if separator != "," {
|
|
||||||
actualHeaderLine = strings.Replace(headerLine, ",", separator, -1)
|
|
||||||
actualDataLineFormat = strings.Replace(dataLineFormat, ",", separator, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.WriteString(actualHeaderLine)
|
|
||||||
|
|
||||||
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.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
|
|
||||||
transactionTimezone := utils.FormatTimezoneOffset(transactionTimeZone)
|
|
||||||
transactionType := e.getTransactionTypeName(transaction.Type)
|
|
||||||
category := e.replaceDelimiters(e.getTransactionCategoryName(transaction.CategoryId, categoryMap), separator)
|
|
||||||
subCategory := e.replaceDelimiters(e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap), separator)
|
|
||||||
account := e.replaceDelimiters(e.getAccountName(transaction.AccountId, accountMap), separator)
|
|
||||||
accountCurrency := e.getAccountCurrency(transaction.AccountId, accountMap)
|
|
||||||
amount := e.getDisplayAmount(transaction.Amount)
|
|
||||||
account2 := ""
|
|
||||||
account2Currency := ""
|
|
||||||
account2Amount := ""
|
|
||||||
geoLocation := ""
|
|
||||||
|
|
||||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
|
||||||
account2 = e.replaceDelimiters(e.getAccountName(transaction.RelatedAccountId, accountMap), separator)
|
|
||||||
account2Currency = e.getAccountCurrency(transaction.RelatedAccountId, accountMap)
|
|
||||||
account2Amount = e.getDisplayAmount(transaction.RelatedAccountAmount)
|
|
||||||
}
|
|
||||||
|
|
||||||
if transaction.GeoLongitude != 0 || transaction.GeoLatitude != 0 {
|
|
||||||
geoLocation = fmt.Sprintf("%f%s%f", transaction.GeoLongitude, geoLocationSeparator, transaction.GeoLatitude)
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := e.replaceDelimiters(e.getTags(transaction.TransactionId, allTagIndexes, tagMap), separator)
|
|
||||||
comment := e.replaceDelimiters(transaction.Comment, separator)
|
|
||||||
|
|
||||||
ret.WriteString(fmt.Sprintf(actualDataLineFormat, transactionTime, transactionTimezone, transactionType, category, subCategory, account, accountCurrency, amount, account2, account2Currency, account2Amount, geoLocation, tags, comment))
|
|
||||||
}
|
|
||||||
|
|
||||||
return []byte(ret.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) 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 *EzBookKeepingPlainFileExporter) 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 *EzBookKeepingPlainFileExporter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
|
||||||
category, exists := categoryMap[categoryId]
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
return category.Name
|
|
||||||
} else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string {
|
|
||||||
account, exists := accountMap[accountId]
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
return account.Name
|
|
||||||
} else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
|
|
||||||
account, exists := accountMap[accountId]
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
return account.Currency
|
|
||||||
} else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) 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 *EzBookKeepingPlainFileExporter) getTags(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++ {
|
|
||||||
if i > 0 {
|
|
||||||
ret.WriteString(transactionTagSeparator)
|
|
||||||
}
|
|
||||||
|
|
||||||
tagIndex := tagIndexes[i]
|
|
||||||
tag, exists := tagMap[tagIndex]
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.WriteString(tag.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EzBookKeepingPlainFileExporter) replaceDelimiters(text string, separator string) string {
|
|
||||||
text = strings.Replace(text, separator, " ", -1)
|
|
||||||
text = strings.Replace(text, "\r\n", " ", -1)
|
|
||||||
text = strings.Replace(text, "\n", " ", -1)
|
|
||||||
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package converters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EzBookKeepingTSVFileExporter defines the structure of TSV file exporter
|
|
||||||
type EzBookKeepingTSVFileExporter struct {
|
|
||||||
EzBookKeepingPlainFileExporter
|
|
||||||
}
|
|
||||||
|
|
||||||
const tsvSeparator = "\t"
|
|
||||||
|
|
||||||
// ToExportedContent returns the exported TSV data
|
|
||||||
func (e *EzBookKeepingTSVFileExporter) ToExportedContent(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) {
|
|
||||||
return e.toExportedContent(uid, tsvSeparator, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
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{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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
@@ -0,0 +1,370 @@
|
|||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package iif
|
||||||
|
|
||||||
|
// iifAccountDataset defines the structure of intuit interchange format (iif) account dataset
|
||||||
|
type iifAccountDataset struct {
|
||||||
|
accountDataColumnIndexes map[string]int
|
||||||
|
accounts []*iifAccountData
|
||||||
|
}
|
||||||
|
|
||||||
|
// iifAccountData defines the structure of intuit interchange format (iif) account data
|
||||||
|
type iifAccountData struct {
|
||||||
|
dataItems []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// iifTransactionDataset defines the structure of intuit interchange format (iif) transaction dataset
|
||||||
|
type iifTransactionDataset struct {
|
||||||
|
transactionDataColumnIndexes map[string]int
|
||||||
|
splitDataColumnIndexes map[string]int
|
||||||
|
transactions []*iifTransactionData
|
||||||
|
}
|
||||||
|
|
||||||
|
// iifTransactionData defines the structure of intuit interchange format (iif) transaction data
|
||||||
|
type iifTransactionData struct {
|
||||||
|
dataItems []string
|
||||||
|
splitData []*iifTransactionSplitData
|
||||||
|
}
|
||||||
|
|
||||||
|
// iifTransactionSplitData defines the structure of intuit interchange format (iif) transaction split data
|
||||||
|
type iifTransactionSplitData struct {
|
||||||
|
dataItems []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *iifTransactionDataset) getTransactionDataItemValue(transactionData *iifTransactionData, columnName string) (string, bool) {
|
||||||
|
if transactionData == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
index, exists := s.transactionDataColumnIndexes[columnName]
|
||||||
|
|
||||||
|
if !exists || index < 0 || index >= len(transactionData.dataItems) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactionData.dataItems[index], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *iifTransactionDataset) getSplitDataItemValue(splitData *iifTransactionSplitData, columnName string) (string, bool) {
|
||||||
|
if splitData == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
index, exists := s.splitDataColumnIndexes[columnName]
|
||||||
|
|
||||||
|
if !exists || index < 0 || index >= len(splitData.dataItems) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return splitData.dataItems[index], true
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
package iif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/csv"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const iifAccountSampleLineSignColumnName = "!ACCNT"
|
||||||
|
const iifTransactionSampleLineSignColumnName = "!TRNS"
|
||||||
|
const iifTransactionSplitSampleLineSignColumnName = "!SPL"
|
||||||
|
const iifTransactionEndSampleLineSignColumnName = "!ENDTRNS"
|
||||||
|
|
||||||
|
const iifAccountLineSignColumnName = "ACCNT"
|
||||||
|
const iifTransactionLineSignColumnName = "TRNS"
|
||||||
|
const iifTransactionSplitLineSignColumnName = "SPL"
|
||||||
|
const iifTransactionEndLineSignColumnName = "ENDTRNS"
|
||||||
|
|
||||||
|
// iifDataReader defines the structure of intuit interchange format (iif) data reader
|
||||||
|
type iifDataReader struct {
|
||||||
|
reader *csv.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// read returns the iif transaction dataset
|
||||||
|
func (r *iifDataReader) read(ctx core.Context) ([]*iifAccountDataset, []*iifTransactionDataset, error) {
|
||||||
|
allAccountDatasets := make([]*iifAccountDataset, 0)
|
||||||
|
allTransactionDatasets := make([]*iifTransactionDataset, 0)
|
||||||
|
|
||||||
|
currentDatasetType := ""
|
||||||
|
lastLineSign := ""
|
||||||
|
|
||||||
|
var currentAccountDataset *iifAccountDataset
|
||||||
|
var currentTransactionDataset *iifTransactionDataset
|
||||||
|
var currentTransactionData *iifTransactionData
|
||||||
|
|
||||||
|
for {
|
||||||
|
items, err := r.reader.Read()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] cannot parse tsv data, because %s", err.Error())
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) == 1 && items[0] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items[0]) < 1 {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] line first column is empty")
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// read sample line
|
||||||
|
if items[0][0] == '!' {
|
||||||
|
if lastLineSign != "" {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] iif missing transaction end line")
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentAccountDataset != nil {
|
||||||
|
allAccountDatasets = append(allAccountDatasets, currentAccountDataset)
|
||||||
|
currentAccountDataset = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentTransactionDataset != nil {
|
||||||
|
allTransactionDatasets = append(allTransactionDatasets, currentTransactionDataset)
|
||||||
|
currentTransactionDataset = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if items[0] == iifTransactionSplitSampleLineSignColumnName || items[0] == iifTransactionEndSampleLineSignColumnName {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] read transaction split sample line or transaction end sample line sign before transaction sample line sign")
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
} else {
|
||||||
|
currentDatasetType = items[0]
|
||||||
|
lastLineSign = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentDatasetType == iifAccountSampleLineSignColumnName {
|
||||||
|
currentAccountDataset, err = r.readAccountSampleLine(ctx, items)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
} else if currentDatasetType == iifTransactionSampleLineSignColumnName {
|
||||||
|
currentTransactionDataset, err = r.readTransactionSampleLines(ctx, items)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
} // not process (read sample line) for other dataset type
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// read data lines
|
||||||
|
if currentDatasetType == "" {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] cannot read data line before sample line")
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
} else if currentDatasetType == iifAccountSampleLineSignColumnName && currentAccountDataset != nil {
|
||||||
|
if items[0] == iifAccountLineSignColumnName {
|
||||||
|
accountData := &iifAccountData{
|
||||||
|
dataItems: items,
|
||||||
|
}
|
||||||
|
currentAccountDataset.accounts = append(currentAccountDataset.accounts, accountData)
|
||||||
|
} else {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] iif line expected reading account sign, but actual is \"%s\"", items[0])
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
} else if currentDatasetType == iifTransactionSampleLineSignColumnName && currentTransactionDataset != nil {
|
||||||
|
if lastLineSign == "" {
|
||||||
|
if items[0] == iifTransactionLineSignColumnName {
|
||||||
|
currentTransactionData = &iifTransactionData{
|
||||||
|
dataItems: items,
|
||||||
|
splitData: make([]*iifTransactionSplitData, 0),
|
||||||
|
}
|
||||||
|
lastLineSign = items[0]
|
||||||
|
} else {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] iif line expected reading transaction sign, but actual is \"%s\"", items[0])
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
} else if lastLineSign == iifTransactionLineSignColumnName || lastLineSign == iifTransactionSplitLineSignColumnName {
|
||||||
|
if items[0] == iifTransactionSplitLineSignColumnName {
|
||||||
|
currentTransactionData.splitData = append(currentTransactionData.splitData, &iifTransactionSplitData{
|
||||||
|
dataItems: items,
|
||||||
|
})
|
||||||
|
lastLineSign = items[0]
|
||||||
|
} else if items[0] == iifTransactionEndLineSignColumnName {
|
||||||
|
currentTransactionDataset.transactions = append(currentTransactionDataset.transactions, currentTransactionData)
|
||||||
|
lastLineSign = ""
|
||||||
|
} else {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] iif line expected reading split sign or transaction end sign, but actual is \"%s\"", items[0])
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] iif missing transaction sample end line")
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
} // not process (read data line) for other dataset type
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastLineSign != "" {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] iif missing transaction end line")
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentAccountDataset != nil {
|
||||||
|
allAccountDatasets = append(allAccountDatasets, currentAccountDataset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentTransactionDataset != nil {
|
||||||
|
allTransactionDatasets = append(allTransactionDatasets, currentTransactionDataset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allAccountDatasets, allTransactionDatasets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *iifDataReader) readAccountSampleLine(ctx core.Context, items []string) (*iifAccountDataset, error) {
|
||||||
|
accountSampleItems := items
|
||||||
|
accountDataColumnIndexes := make(map[string]int, len(accountSampleItems))
|
||||||
|
|
||||||
|
for i := 1; i < len(accountSampleItems); i++ {
|
||||||
|
columnName := accountSampleItems[i]
|
||||||
|
accountDataColumnIndexes[columnName] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
return &iifAccountDataset{
|
||||||
|
accountDataColumnIndexes: accountDataColumnIndexes,
|
||||||
|
accounts: make([]*iifAccountData, 0),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *iifDataReader) readTransactionSampleLines(ctx core.Context, items []string) (*iifTransactionDataset, error) {
|
||||||
|
transactionSampleItems := items
|
||||||
|
transactionDataColumnIndexes := make(map[string]int, len(transactionSampleItems))
|
||||||
|
|
||||||
|
for i := 1; i < len(transactionSampleItems); i++ {
|
||||||
|
columnName := transactionSampleItems[i]
|
||||||
|
transactionDataColumnIndexes[columnName] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
splitSampleItems, err := r.reader.Read()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.readTransactionSampleLines] expected reading transaction split sample line, but read eof")
|
||||||
|
return nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(splitSampleItems) < 1 || splitSampleItems[0] != iifTransactionSplitSampleLineSignColumnName {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.readTransactionSampleLines] expected reading transaction split sample line, but read \"%s\"", strings.Join(splitSampleItems, "\t"))
|
||||||
|
return nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
|
splitDataColumnIndexes := make(map[string]int, len(splitSampleItems))
|
||||||
|
|
||||||
|
for i := 1; i < len(splitSampleItems); i++ {
|
||||||
|
columnName := splitSampleItems[i]
|
||||||
|
splitDataColumnIndexes[columnName] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionEndSampleItems, err := r.reader.Read()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.readTransactionSampleLines] expected reading transaction end sample line, but read eof")
|
||||||
|
return nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(transactionEndSampleItems) < 1 || transactionEndSampleItems[0] != iifTransactionEndSampleLineSignColumnName {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.readTransactionSampleLines] expected reading transaction end sample line, but read \"%s\"", strings.Join(splitSampleItems, "\t"))
|
||||||
|
return nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
|
return &iifTransactionDataset{
|
||||||
|
transactionDataColumnIndexes: transactionDataColumnIndexes,
|
||||||
|
splitDataColumnIndexes: splitDataColumnIndexes,
|
||||||
|
transactions: make([]*iifTransactionData, 0),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNewIifDataReader(data []byte) *iifDataReader {
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
csvReader := csv.NewReader(reader)
|
||||||
|
csvReader.Comma = '\t'
|
||||||
|
csvReader.FieldsPerRecord = -1
|
||||||
|
|
||||||
|
return &iifDataReader{
|
||||||
|
reader: csvReader,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package iif
|
||||||
|
|
||||||
|
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 iifTransactionTypeNameMapping = 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)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// iifTransactionDataFileImporter defines the structure of intuit interchange format (iif) for transaction data
|
||||||
|
type iifTransactionDataFileImporter struct{}
|
||||||
|
|
||||||
|
// Initialize an intuit interchange format (iif) file importer singleton instance
|
||||||
|
var (
|
||||||
|
IifTransactionDataFileImporter = &iifTransactionDataFileImporter{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseImportedData returns the imported data by parsing the intuit interchange format (iif) data
|
||||||
|
func (c *iifTransactionDataFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*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) {
|
||||||
|
iifDataReader := createNewIifDataReader(data)
|
||||||
|
accountDatasets, transactionDatasets, err := iifDataReader.read(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionDataTable, err := createNewIIfTransactionDataTable(ctx, accountDatasets, transactionDatasets)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataTableImporter := datatable.CreateNewSimpleImporter(iifTransactionTypeNameMapping)
|
||||||
|
|
||||||
|
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
||||||
|
}
|
||||||
@@ -0,0 +1,680 @@
|
|||||||
|
package iif
|
||||||
|
|
||||||
|
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 TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!ACCNT\tNAME\tACCNTTYPE\n"+
|
||||||
|
"ACCNT\tTest Account\tBANK\n"+
|
||||||
|
"ACCNT\tTest Account2\tBANK\n"+
|
||||||
|
"ACCNT\tTest Category\tINC\n"+
|
||||||
|
"ACCNT\tTest Category2\tEXP\n"+
|
||||||
|
"!TRNS\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\t\n"+
|
||||||
|
"TRNS\tBEGINBALCHECK\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\tBEGINBALCHECK\t09/01/2024\txxx\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"+
|
||||||
|
"TRNS\tDEPOSIT\t09/02/2024\tTest Account\t0.12\n"+
|
||||||
|
"SPL\tDEPOSIT\t09/02/2024\tTest Category\t-0.12\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"+
|
||||||
|
"TRNS\tCREDIT CARD\t09/03/2024\tTest Account\t-1.00\n"+
|
||||||
|
"SPL\tCREDIT CARD\t09/03/2024\tTest Category2\t1.00\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"+
|
||||||
|
"TRNS\tTRANSFER\t09/04/2024\tTest Account\t-0.05\n"+
|
||||||
|
"SPL\tTRANSFER\t09/04/2024\tTest Account2\t0.05\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"+
|
||||||
|
"TRNS\tGENERAL JOURNAL\t09/05/2024\tTest Account\t0.06\n"+
|
||||||
|
"SPL\tGENERAL JOURNAL\t09/05/2024\tTest Account2\t-0.06\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"+
|
||||||
|
"TRNS\tDEPOSIT\t09/06/2024\tTest Category\t-23.45\n"+
|
||||||
|
"SPL\tDEPOSIT\t09/06/2024\tTest Account2\t23.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"+
|
||||||
|
"TRNS\tCREDIT CARD\t09/07/2024\tTest Category2\t34.56\n"+
|
||||||
|
"SPL\tCREDIT CARD\t09/07/2024\tTest Account2\t-34.56\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 7, 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(1725235200), 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(1725321600), 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(1725408000), 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, "", 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, int64(1725494400), utils.GetUnixTimeFromTransactionTime(allNewTransactions[4].TransactionTime))
|
||||||
|
assert.Equal(t, int64(6), allNewTransactions[4].Amount)
|
||||||
|
assert.Equal(t, "Test Account2", allNewTransactions[4].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[4].OriginalDestinationAccountName)
|
||||||
|
assert.Equal(t, "", 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, int64(1725580800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[5].TransactionTime))
|
||||||
|
assert.Equal(t, int64(2345), allNewTransactions[5].Amount)
|
||||||
|
assert.Equal(t, "Test Account2", allNewTransactions[5].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "", allNewTransactions[5].OriginalDestinationAccountName)
|
||||||
|
assert.Equal(t, "Test Category", 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, int64(1725667200), utils.GetUnixTimeFromTransactionTime(allNewTransactions[6].TransactionTime))
|
||||||
|
assert.Equal(t, int64(3456), allNewTransactions[6].Amount)
|
||||||
|
assert.Equal(t, "Test Account2", allNewTransactions[6].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "", allNewTransactions[6].OriginalDestinationAccountName)
|
||||||
|
assert.Equal(t, "Test Category2", 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, "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, "", allNewSubTransferCategories[0].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_MinimumValidDataWithoutAccountData(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Category\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, 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 Category", allNewTransactions[0].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalDestinationAccountName)
|
||||||
|
assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!ACCNT\tNAME\tACCNTTYPE\n"+
|
||||||
|
"ACCNT\tTest Account3\tBANK\n"+
|
||||||
|
"ACCNT\tTest Account4\tBANK\n"+
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/05/2024\tTest Account\t-0.05\n"+
|
||||||
|
"SPL\t09/05/2024\tTest Account2\t0.05\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"!TRNS\tTRNSID\tTRNSTYPE\tDATE\tACCNT\tNAME\tCLASS\tAMOUNT\tDOCNUM\tMEMO\tCLEAR\tTOPRINT\tADDR5\tDUEDATE\tTERMS\n"+
|
||||||
|
"!SPL\tSPLID\tTRNSTYPE\tDATE\tACCNT\tNAME\tCLASS\tAMOUNT\tDOCNUM\tMEMO\tCLEAR\tQNTY\tREIMBEXP\tSERVICEDATE\tOTHER2\n"+
|
||||||
|
"!ENDTRNS\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n"+
|
||||||
|
"TRNS\t\tTRANSFER\t09/04/2024\tTest Account3\t\tTest Class\t123.45\t\t\t\t\t\t\t\n"+
|
||||||
|
"SPL\t\tTRANSFER\t09/04/2024\tTest Account4\t\t\t-123.45\t\t\t\t\t\t\t\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n"+
|
||||||
|
"!CLASS\tNAME\tHIDDEN\n"+
|
||||||
|
"CLASS\tTest Class\tN\n"+
|
||||||
|
"!TRNS\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\t\n"+
|
||||||
|
"TRNS\tBEGINBALCHECK\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\tBEGINBALCHECK\t09/01/2024\txxx\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"+
|
||||||
|
"TRNS\tDEPOSIT\t09/02/2024\tTest Account\t0.12\n"+
|
||||||
|
"SPL\tDEPOSIT\t09/02/2024\tTest Category\t-0.12\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"+
|
||||||
|
"TRNS\tCREDIT CARD\t09/03/2024\tTest Account\t-1.00\n"+
|
||||||
|
"SPL\tCREDIT CARD\t09/03/2024\tTest Category2\t1.00\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"+
|
||||||
|
"!ACCNT\tTEST\tNAME\tACCNTTYPE\n"+
|
||||||
|
"ACCNT\t\tTest Category\tINC\n"+
|
||||||
|
"ACCNT\t\tTest Category2\tEXP\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 5, len(allNewTransactions))
|
||||||
|
|
||||||
|
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(1725235200), 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(1725321600), 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(1725408000), utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime))
|
||||||
|
assert.Equal(t, int64(12345), allNewTransactions[3].Amount)
|
||||||
|
assert.Equal(t, "Test Account4", allNewTransactions[3].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Account3", allNewTransactions[3].OriginalDestinationAccountName)
|
||||||
|
assert.Equal(t, "", 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, int64(1725494400), utils.GetUnixTimeFromTransactionTime(allNewTransactions[4].TransactionTime))
|
||||||
|
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, "", allNewTransactions[4].OriginalCategoryName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!ACCNT\tNAME\tACCNTTYPE\n"+
|
||||||
|
"ACCNT\tTest Parent Category:Test Category\tINC\n"+
|
||||||
|
"ACCNT\tTest Parent Category2:Test Category2\tEXP\n"+
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Parent Category:Test Category\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/02/2024\tTest Account2\t-123.45\n"+
|
||||||
|
"SPL\t09/02/2024\tTest Parent Category2:Test Category2\t123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, len(allNewTransactions))
|
||||||
|
assert.Equal(t, 1, len(allNewSubExpenseCategories))
|
||||||
|
assert.Equal(t, 1, len(allNewSubIncomeCategories))
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, 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].OriginalDestinationAccountName)
|
||||||
|
assert.Equal(t, "Test Category", allNewTransactions[0].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[1].Type)
|
||||||
|
assert.Equal(t, int64(1725235200), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
|
||||||
|
assert.Equal(t, int64(12345), allNewTransactions[1].Amount)
|
||||||
|
assert.Equal(t, "Test Account2", allNewTransactions[1].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "", allNewTransactions[1].OriginalDestinationAccountName)
|
||||||
|
assert.Equal(t, "Test Category2", allNewTransactions[1].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
|
||||||
|
assert.Equal(t, "Test Category", allNewSubIncomeCategories[0].Name)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
|
||||||
|
assert.Equal(t, "Test Category2", allNewSubExpenseCategories[0].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t2024/09/01\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t2024/09/01\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t2024/09/2\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t2024/09/2\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t2024/9/03\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t2024/9/03\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t2024/9/4\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t2024/9/4\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 4, len(allNewTransactions))
|
||||||
|
assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||||
|
assert.Equal(t, int64(1725235200), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
|
||||||
|
assert.Equal(t, int64(1725321600), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
|
||||||
|
assert.Equal(t, int64(1725408000), utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayFormatTime(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t9/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t9/01/2024\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/2/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/2/2024\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t9/3/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t9/3/2024\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 3, len(allNewTransactions))
|
||||||
|
assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||||
|
assert.Equal(t, int64(1725235200), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
|
||||||
|
assert.Equal(t, int64(1725321600), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t9/1/24\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t9/1/24\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||||
|
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t2024-09-01\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t2024-09-01\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||||
|
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t9/24\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t9/24\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparator(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t9/01/2024\tTest Account\t123,456.78\n"+
|
||||||
|
"SPL\t9/01/2024\tTest Account2\t-123,456.78\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
|
assert.Equal(t, int64(12345678), allNewTransactions[0].Amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123 45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||||
|
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-123 45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
|
||||||
|
"!ENDTRNS\t\t\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t\"Test\"\t123.45\t\"foo bar\t#test\"\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t\t-123.45\t\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
|
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment)
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
|
||||||
|
"!ENDTRNS\t\t\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\tTest\t123.45\t\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t\t-123.45\t\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
|
assert.Equal(t, "Test", allNewTransactions[0].Comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_NotSupportedToParseSplitTransaction(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-100.00\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account3\t-23.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing Transaction Line
|
||||||
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
|
// Missing Split Line
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
|
// Missing Transaction End Line
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-123.45\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
|
// Missing Transaction End Line (following is another header)
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
|
||||||
|
"!ACCNT\tNAME\tACCNTTYPE\n"+
|
||||||
|
"ACCNT\tTest Account\tBANK\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
|
// Invalid Line
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
|
||||||
|
"TEST\t\t\t\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
|
// Repeat Transaction Line
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
|
// Repeat Transaction End Line
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\t\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\t\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_InvalidHeaderLines(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing All Sample Lines
|
||||||
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
|
// Missing Transaction Sample Line
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
|
// Missing Split Sample Line
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
|
// Missing Transaction End Sample Line
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
|
// Missing Transaction End Sample Line (following is data line)
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
|
// Invalid Sample Line
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!TEST\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_MissingRequiredColumn(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing Date Column
|
||||||
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tACCNT\tAMOUNT\t\n"+
|
||||||
|
"!SPL\tACCNT\tAMOUNT\t\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\tTest Account\t123.45\n"+
|
||||||
|
"SPL\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||||
|
|
||||||
|
// Missing Account Column
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tAMOUNT\t\n"+
|
||||||
|
"!SPL\tDATE\tAMOUNT\t\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||||
|
|
||||||
|
// Missing Amount Column
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\t\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\t\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||||
|
}
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
package iif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
const iifAccountNameColumnName = "NAME"
|
||||||
|
const iifAccountTypeColumnName = "ACCNTTYPE"
|
||||||
|
|
||||||
|
const iifAccountTypeIncome = "INC"
|
||||||
|
const iifAccountTypeExpense = "EXP"
|
||||||
|
|
||||||
|
const iifTransactionTypeColumnName = "TRNSTYPE"
|
||||||
|
const iifTransactionDateColumnName = "DATE"
|
||||||
|
const iifTransactionAccountNameColumnName = "ACCNT"
|
||||||
|
const iifTransactionNameColumnName = "NAME"
|
||||||
|
const iifTransactionAmountColumnName = "AMOUNT"
|
||||||
|
const iifTransactionMemoColumnName = "MEMO"
|
||||||
|
|
||||||
|
const iifTransactionTypeBeginningBalance = "BEGINBALCHECK"
|
||||||
|
|
||||||
|
const iifTransactionCategorySeparator = ":"
|
||||||
|
|
||||||
|
var iifTransactionSupportedColumns = map[datatable.TransactionDataTableColumn]bool{
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: 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_AMOUNT: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// iifTransactionDataTable defines the structure of intuit interchange format (iif) transaction data table
|
||||||
|
type iifTransactionDataTable struct {
|
||||||
|
incomeAccountNames map[string]bool
|
||||||
|
expenseAccountNames map[string]bool
|
||||||
|
transactionDatasets []*iifTransactionDataset
|
||||||
|
}
|
||||||
|
|
||||||
|
// iifTransactionDataRow defines the structure of intuit interchange format (iif) transaction data row
|
||||||
|
type iifTransactionDataRow struct {
|
||||||
|
dataTable *iifTransactionDataTable
|
||||||
|
finalItems map[datatable.TransactionDataTableColumn]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// iifTransactionDataRowIterator defines the structure of intuit interchange format (iif) transaction data row iterator
|
||||||
|
type iifTransactionDataRowIterator struct {
|
||||||
|
dataTable *iifTransactionDataTable
|
||||||
|
currentDatasetIndex int
|
||||||
|
currentIndexInDataset int
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasColumn returns whether the transaction data table has specified column
|
||||||
|
func (t *iifTransactionDataTable) HasColumn(column datatable.TransactionDataTableColumn) bool {
|
||||||
|
_, exists := iifTransactionSupportedColumns[column]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionRowCount returns the total count of transaction data row
|
||||||
|
func (t *iifTransactionDataTable) TransactionRowCount() int {
|
||||||
|
totalDataRowCount := 0
|
||||||
|
|
||||||
|
for i := 0; i < len(t.transactionDatasets); i++ {
|
||||||
|
transactions := t.transactionDatasets[i]
|
||||||
|
totalDataRowCount += len(transactions.transactions)
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalDataRowCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionRowIterator returns the iterator of transaction data row
|
||||||
|
func (t *iifTransactionDataTable) TransactionRowIterator() datatable.TransactionDataRowIterator {
|
||||||
|
return &iifTransactionDataRowIterator{
|
||||||
|
dataTable: t,
|
||||||
|
currentDatasetIndex: 0,
|
||||||
|
currentIndexInDataset: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns whether this row is valid data for importing
|
||||||
|
func (r *iifTransactionDataRow) IsValid() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetData returns the data in the specified column type
|
||||||
|
func (r *iifTransactionDataRow) GetData(column datatable.TransactionDataTableColumn) string {
|
||||||
|
_, exists := iifTransactionSupportedColumns[column]
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return r.finalItems[column]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNext returns whether the iterator does not reach the end
|
||||||
|
func (t *iifTransactionDataRowIterator) HasNext() bool {
|
||||||
|
allDatasets := t.dataTable.transactionDatasets
|
||||||
|
|
||||||
|
if t.currentDatasetIndex >= len(allDatasets) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDataset := allDatasets[t.currentDatasetIndex]
|
||||||
|
|
||||||
|
if t.currentIndexInDataset+1 < len(currentDataset.transactions) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := t.currentDatasetIndex + 1; i < len(allDatasets); i++ {
|
||||||
|
dataset := allDatasets[i]
|
||||||
|
|
||||||
|
if len(dataset.transactions) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the next imported data row
|
||||||
|
func (t *iifTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) {
|
||||||
|
allDatasets := t.dataTable.transactionDatasets
|
||||||
|
currentIndexInDataset := t.currentIndexInDataset
|
||||||
|
|
||||||
|
for i := t.currentDatasetIndex; i < len(allDatasets); i++ {
|
||||||
|
dataset := allDatasets[i]
|
||||||
|
|
||||||
|
if currentIndexInDataset+1 < len(dataset.transactions) {
|
||||||
|
t.currentIndexInDataset++
|
||||||
|
currentIndexInDataset = t.currentIndexInDataset
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
t.currentDatasetIndex++
|
||||||
|
t.currentIndexInDataset = -1
|
||||||
|
currentIndexInDataset = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.currentDatasetIndex >= len(allDatasets) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDataset := allDatasets[t.currentDatasetIndex]
|
||||||
|
|
||||||
|
if t.currentIndexInDataset >= len(currentDataset.transactions) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := currentDataset.transactions[t.currentIndexInDataset]
|
||||||
|
rowItems, err := t.parseTransaction(ctx, user, currentDataset, data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &iifTransactionDataRow{
|
||||||
|
dataTable: t.dataTable,
|
||||||
|
finalItems: rowItems,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user *models.User, dataset *iifTransactionDataset, transactionData *iifTransactionData) (map[datatable.TransactionDataTableColumn]string, error) {
|
||||||
|
if len(transactionData.splitData) < 1 {
|
||||||
|
return nil, errs.ErrInvalidIIFFile
|
||||||
|
} else if len(transactionData.splitData) > 1 {
|
||||||
|
return nil, errs.ErrNotSupportedSplitTransactions
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
data := make(map[datatable.TransactionDataTableColumn]string, len(iifTransactionSupportedColumns))
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME], err = t.parseTransactionTime(dataset, transactionData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionType, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionTypeColumnName)
|
||||||
|
accountName1, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionAccountNameColumnName)
|
||||||
|
accountName2, _ := dataset.getSplitDataItemValue(transactionData.splitData[0], iifTransactionAccountNameColumnName)
|
||||||
|
amount1, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionAmountColumnName)
|
||||||
|
amount2, _ := dataset.getSplitDataItemValue(transactionData.splitData[0], iifTransactionAmountColumnName)
|
||||||
|
amountNum1, err := utils.ParseAmount(strings.ReplaceAll(amount1, ",", ""))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.ErrAmountInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
amountNum2, err := utils.ParseAmount(strings.ReplaceAll(amount2, ",", ""))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.ErrAmountInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if transactionType == iifTransactionTypeBeginningBalance { // balance modification
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE]
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName1
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amountNum1)
|
||||||
|
} else if t.dataTable.incomeAccountNames[accountName1] || t.dataTable.incomeAccountNames[accountName2] { // income
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
|
||||||
|
categoryName := ""
|
||||||
|
accountName := ""
|
||||||
|
amountNum := int64(0)
|
||||||
|
|
||||||
|
if t.dataTable.incomeAccountNames[accountName1] && !t.dataTable.incomeAccountNames[accountName2] {
|
||||||
|
categoryName = accountName1
|
||||||
|
accountName = accountName2
|
||||||
|
amountNum = amountNum2
|
||||||
|
} else if t.dataTable.incomeAccountNames[accountName2] && !t.dataTable.incomeAccountNames[accountName1] {
|
||||||
|
categoryName = accountName2
|
||||||
|
accountName = accountName1
|
||||||
|
amountNum = amountNum1
|
||||||
|
} else {
|
||||||
|
log.Errorf(ctx, "[iif_transaction_data_table.parseTransaction] cannot parse transaction, because two accounts \"%s\" and \"%s\" are all income account", accountName1, accountName2)
|
||||||
|
return nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryNames := strings.Split(categoryName, iifTransactionCategorySeparator)
|
||||||
|
|
||||||
|
if len(categoryNames) > 1 {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = categoryNames[0]
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = categoryNames[len(categoryNames)-1]
|
||||||
|
} else {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = categoryName
|
||||||
|
}
|
||||||
|
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amountNum)
|
||||||
|
} else if t.dataTable.expenseAccountNames[accountName1] || t.dataTable.expenseAccountNames[accountName2] { // expense
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
|
||||||
|
categoryName := ""
|
||||||
|
accountName := ""
|
||||||
|
amountNum := int64(0)
|
||||||
|
|
||||||
|
if t.dataTable.expenseAccountNames[accountName1] && !t.dataTable.expenseAccountNames[accountName2] {
|
||||||
|
categoryName = accountName1
|
||||||
|
accountName = accountName2
|
||||||
|
amountNum = amountNum2
|
||||||
|
} else if t.dataTable.expenseAccountNames[accountName2] && !t.dataTable.expenseAccountNames[accountName1] {
|
||||||
|
categoryName = accountName2
|
||||||
|
accountName = accountName1
|
||||||
|
amountNum = amountNum1
|
||||||
|
} else {
|
||||||
|
log.Errorf(ctx, "[iif_transaction_data_table.parseTransaction] cannot parse transaction, because two accounts \"%s\" and \"%s\" are all expense account", accountName1, accountName2)
|
||||||
|
return nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryNames := strings.Split(categoryName, iifTransactionCategorySeparator)
|
||||||
|
|
||||||
|
if len(categoryNames) > 1 {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = categoryNames[0]
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = categoryNames[len(categoryNames)-1]
|
||||||
|
} else {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = categoryName
|
||||||
|
}
|
||||||
|
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amountNum)
|
||||||
|
} else {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
|
||||||
|
|
||||||
|
if amountNum1 >= 0 {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName2
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amountNum2)
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = accountName1
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(amountNum1)
|
||||||
|
} else if amountNum2 >= 0 {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName1
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amountNum1)
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = accountName2
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(amountNum2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if memo, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionMemoColumnName); memo != "" {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = memo
|
||||||
|
} else if name, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionNameColumnName); name != "" {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = name
|
||||||
|
} else {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *iifTransactionDataRowIterator) parseTransactionTime(dataset *iifTransactionDataset, transactionData *iifTransactionData) (string, error) {
|
||||||
|
date, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionDateColumnName)
|
||||||
|
dateParts := strings.Split(date, "/")
|
||||||
|
|
||||||
|
if len(dateParts) != 3 {
|
||||||
|
return "", errs.ErrTransactionTimeInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
month := dateParts[0]
|
||||||
|
day := dateParts[1]
|
||||||
|
year := dateParts[2]
|
||||||
|
|
||||||
|
if utils.IsValidYearMonthDayLongOrShortDateFormat(date) {
|
||||||
|
year = dateParts[0]
|
||||||
|
month = dateParts[1]
|
||||||
|
day = dateParts[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(month) < 2 {
|
||||||
|
month = "0" + month
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(day) < 2 {
|
||||||
|
day = "0" + day
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s-%s-%s 00:00:00", year, month, day), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNewIIfTransactionDataTable(ctx core.Context, accountDatasets []*iifAccountDataset, transactionDatasets []*iifTransactionDataset) (*iifTransactionDataTable, error) {
|
||||||
|
if len(transactionDatasets) < 1 {
|
||||||
|
return nil, errs.ErrNotFoundTransactionDataInFile
|
||||||
|
}
|
||||||
|
|
||||||
|
incomeAccountNames, expenseAccountNames := getIncomeAndExpenseAccountNameMap(accountDatasets)
|
||||||
|
|
||||||
|
for i := 0; i < len(transactionDatasets); i++ {
|
||||||
|
transactionDataset := transactionDatasets[i]
|
||||||
|
|
||||||
|
for _, requiredColumnName := range []string{
|
||||||
|
iifTransactionDateColumnName,
|
||||||
|
iifTransactionAccountNameColumnName,
|
||||||
|
iifTransactionAmountColumnName,
|
||||||
|
} {
|
||||||
|
if _, exists := transactionDataset.transactionDataColumnIndexes[requiredColumnName]; !exists {
|
||||||
|
return nil, errs.ErrMissingRequiredFieldInHeaderRow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &iifTransactionDataTable{
|
||||||
|
incomeAccountNames: incomeAccountNames,
|
||||||
|
expenseAccountNames: expenseAccountNames,
|
||||||
|
transactionDatasets: transactionDatasets,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIncomeAndExpenseAccountNameMap(accountDatasets []*iifAccountDataset) (incomeAccountNames map[string]bool, expenseAccountNames map[string]bool) {
|
||||||
|
incomeAccountNames = make(map[string]bool)
|
||||||
|
expenseAccountNames = make(map[string]bool)
|
||||||
|
|
||||||
|
for i := 0; i < len(accountDatasets); i++ {
|
||||||
|
accountDataset := accountDatasets[i]
|
||||||
|
accountNameColumnIndex, accountNameColumnExists := accountDataset.accountDataColumnIndexes[iifAccountNameColumnName]
|
||||||
|
accountTypeColumnIndex, accountTypeColumnExists := accountDataset.accountDataColumnIndexes[iifAccountTypeColumnName]
|
||||||
|
|
||||||
|
if !accountNameColumnExists || accountNameColumnIndex < 0 ||
|
||||||
|
!accountTypeColumnExists || accountTypeColumnIndex < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for j := 0; j < len(accountDataset.accounts); j++ {
|
||||||
|
items := accountDataset.accounts[j].dataItems
|
||||||
|
|
||||||
|
if accountNameColumnIndex >= len(items) ||
|
||||||
|
accountTypeColumnIndex >= len(items) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
accountName := items[accountNameColumnIndex]
|
||||||
|
accountType := items[accountTypeColumnIndex]
|
||||||
|
|
||||||
|
if accountType == iifAccountTypeIncome {
|
||||||
|
incomeAccountNames[accountName] = true
|
||||||
|
} else if accountType == iifAccountTypeExpense {
|
||||||
|
expenseAccountNames[accountName] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return incomeAccountNames, expenseAccountNames
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user