Compare commits
127
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b42d141927 | ||
|
|
e6078e3c0b | ||
|
|
dbc143954c | ||
|
|
0726fcecf0 | ||
|
|
9ae5be2c33 | ||
|
|
a230199ce0 | ||
|
|
4f7e774043 | ||
|
|
793ef10ce8 | ||
|
|
a7c2d6cbb3 | ||
|
|
1d412959d7 | ||
|
|
b9ac97178f | ||
|
|
7b46da7007 | ||
|
|
2b80c92caa | ||
|
|
f7dd0bd5e8 | ||
|
|
1533914e4d | ||
|
|
bfce4b865b | ||
|
|
581e4a5453 | ||
|
|
9b0fc9a63b | ||
|
|
793919cf10 | ||
|
|
25024c210a | ||
|
|
3977370079 | ||
|
|
37bcb90ab0 | ||
|
|
e345aeee97 | ||
|
|
486d94335b | ||
|
|
7d5acb33e8 | ||
|
|
2138b44c53 | ||
|
|
503fb5734f | ||
|
|
ac06a4bbe9 | ||
|
|
87540fa1b7 | ||
|
|
66d4c9f0af | ||
|
|
55119b3e91 | ||
|
|
67a5c076e7 | ||
|
|
c1d859ebdd | ||
|
|
7a2060efdc | ||
|
|
2ad9e2f85f | ||
|
|
a8d62bc5e8 | ||
|
|
8816e4addc | ||
|
|
2921631ac3 | ||
|
|
0db838e2c4 | ||
|
|
4b6cbb69c1 | ||
|
|
9e0ba0ce56 | ||
|
|
6c19876a47 | ||
|
|
b2db56f855 | ||
|
|
8c4d9c69d0 | ||
|
|
657a201fc0 | ||
|
|
7a7ac25dcf | ||
|
|
d0a548f44e | ||
|
|
f4172fcea7 | ||
|
|
d5216e7994 | ||
|
|
1718c5adab | ||
|
|
d0c090a657 | ||
|
|
c3db919531 | ||
|
|
e09feff36d | ||
|
|
e7dd9660da | ||
|
|
c57620beeb | ||
|
|
29aeb58fc0 | ||
|
|
69d8c8ce5e | ||
|
|
6e3fc43d86 | ||
|
|
021ec152e9 | ||
|
|
ea9dceb333 | ||
|
|
afa1aa5b75 | ||
|
|
328336ea5a | ||
|
|
094e8b2a47 | ||
|
|
b90a3cde4a | ||
|
|
7c8c7fb7db | ||
|
|
343aa25230 | ||
|
|
47fa21d739 | ||
|
|
dc13bb5f93 | ||
|
|
d26f5c5354 | ||
|
|
1b7bec4f81 | ||
|
|
f7399fd0e7 | ||
|
|
cd61ad0eb9 | ||
|
|
84222fc7f4 | ||
|
|
23ac572e3f | ||
|
|
66a8126da0 | ||
|
|
a55a3f4ee2 | ||
|
|
024d9ee637 | ||
|
|
957c1ae53d | ||
|
|
b8be29e110 | ||
|
|
da87d659df | ||
|
|
d0378b5ac4 | ||
|
|
91960f0e57 | ||
|
|
72f67cb519 | ||
|
|
35c4cf2f2f | ||
|
|
c9ef90f5ea | ||
|
|
35e7152abb | ||
|
|
d9b51a7616 | ||
|
|
b27e40b531 | ||
|
|
44aca7641f | ||
|
|
a89ff00d94 | ||
|
|
227f11359c | ||
|
|
7a737d7f83 | ||
|
|
312ea94e62 | ||
|
|
6d735c20cb | ||
|
|
cb8a830345 | ||
|
|
9c143a43aa | ||
|
|
222cadd8df | ||
|
|
cad4e6c816 | ||
|
|
50d633ee3a | ||
|
|
21fa21d089 | ||
|
|
5f379faf17 | ||
|
|
3b62504798 | ||
|
|
4130cb66df | ||
|
|
30dff17272 | ||
|
|
fa037b4d2a | ||
|
|
d38a770ec5 | ||
|
|
015292e830 | ||
|
|
f8c732f0ff | ||
|
|
e92c487815 | ||
|
|
c573b23d76 | ||
|
|
f73a5d533c | ||
|
|
4ea8e32a8e | ||
|
|
06d79046d0 | ||
|
|
8eb19c57ba | ||
|
|
f123de7233 | ||
|
|
7ba776555e | ||
|
|
bccf02b5f7 | ||
|
|
c24a8a8c13 | ||
|
|
6064ef8fde | ||
|
|
1834dd0b19 | ||
|
|
9f653eef37 | ||
|
|
ddaf6df436 | ||
|
|
0708ce816e | ||
|
|
2c34578a98 | ||
|
|
9d71b339b5 | ||
|
|
bbd6666299 | ||
|
|
23b6293502 |
+6
-1
@@ -7,4 +7,9 @@ config/env/*
|
|||||||
|
|
||||||
vendor
|
vendor
|
||||||
|
|
||||||
*firebase-adminsdk*.json
|
# Firebase service account credentials
|
||||||
|
infra/firebase-service-account.json
|
||||||
|
|
||||||
|
# Config files containing secrets (manage manually on each server)
|
||||||
|
# infra/production.yaml
|
||||||
|
# infra/staging.yaml
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# 1) Build stage
|
# 1) Build stage
|
||||||
FROM golang:1.21-alpine AS build
|
FROM golang:1.24-alpine AS build
|
||||||
RUN apk --no-cache add ca-certificates tzdata git curl
|
RUN apk --no-cache add ca-certificates tzdata git curl
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
#PROJECT_NAME = "enaklo-pos-backend"
|
#PROJECT_NAME = "enaklo-pos-backend"
|
||||||
DB_USERNAME :=apskel
|
|
||||||
DB_PASSWORD :=7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk
|
# ─── Environment (default: staging) ──────────────────────────────────────────
|
||||||
DB_HOST :=62.72.45.250
|
ENV ?= staging
|
||||||
DB_PORT :=5433
|
|
||||||
DB_NAME :=apskel_pos
|
ifeq ($(ENV),production)
|
||||||
|
DB_USERNAME :=apskel
|
||||||
|
DB_PASSWORD :=7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk
|
||||||
|
DB_HOST :=62.72.45.250
|
||||||
|
DB_PORT :=5433
|
||||||
|
DB_NAME :=apskel_pos
|
||||||
|
else
|
||||||
|
DB_USERNAME :=apskel
|
||||||
|
DB_PASSWORD :=7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk
|
||||||
|
DB_HOST :=62.72.45.250
|
||||||
|
DB_PORT :=5433
|
||||||
|
DB_NAME :=apskel_pos_staging
|
||||||
|
endif
|
||||||
|
|
||||||
DB_URL = postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
|
DB_URL = postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
|
||||||
|
|
||||||
@@ -16,15 +28,19 @@ endif
|
|||||||
.SILENT: help
|
.SILENT: help
|
||||||
help:
|
help:
|
||||||
@echo
|
@echo
|
||||||
@echo "Usage: make [command]"
|
@echo "Usage: make [command] [ENV=staging|production]"
|
||||||
@echo
|
@echo
|
||||||
@echo "Commands:"
|
@echo "Commands:"
|
||||||
|
@echo " run Run server (default: staging)"
|
||||||
|
@echo " run ENV=production Run server with production config"
|
||||||
|
@echo
|
||||||
@echo " rename-project name={name} Rename project"
|
@echo " rename-project name={name} Rename project"
|
||||||
@echo
|
@echo
|
||||||
@echo " build-http Build http server"
|
@echo " build-http Build http server"
|
||||||
@echo
|
@echo
|
||||||
@echo " migration-create name={name} Create migration"
|
@echo " migration-create name={name} Create migration"
|
||||||
@echo " migration-up Up migrations"
|
@echo " migration-up Up migrations"
|
||||||
|
@echo " migration-up ENV=production Up migrations (production DB)"
|
||||||
@echo " migration-down Down last migration"
|
@echo " migration-down Down last migration"
|
||||||
@echo
|
@echo
|
||||||
@echo " docker-up Up docker services"
|
@echo " docker-up Up docker services"
|
||||||
@@ -83,6 +99,12 @@ migration-up:
|
|||||||
migration-down:
|
migration-down:
|
||||||
@migrate -database $(DB_URL) -path ./migrations down 1
|
@migrate -database $(DB_URL) -path ./migrations down 1
|
||||||
|
|
||||||
|
# Force migration to specific version
|
||||||
|
|
||||||
|
.SILENT: migration-force
|
||||||
|
migration-force:
|
||||||
|
@migrate -database $(DB_URL) -path ./migrations force $(version)
|
||||||
|
|
||||||
.SILENT: seeder-create
|
.SILENT: seeder-create
|
||||||
seeder-create:
|
seeder-create:
|
||||||
@migrate create -ext sql -dir ./seeders -seq $(name)
|
@migrate create -ext sql -dir ./seeders -seq $(name)
|
||||||
@@ -108,7 +130,11 @@ fmt:
|
|||||||
@go fmt ./...
|
@go fmt ./...
|
||||||
|
|
||||||
start:
|
start:
|
||||||
go run main.go --env-path .env
|
ENV_MODE=$(ENV) go run cmd/server/main.go
|
||||||
|
|
||||||
|
.SILENT: run
|
||||||
|
run:
|
||||||
|
ENV_MODE=$(ENV) go run cmd/server/main.go
|
||||||
|
|
||||||
# Default
|
# Default
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -12,13 +12,14 @@ import (
|
|||||||
const (
|
const (
|
||||||
YAML_PATH = "infra/%s"
|
YAML_PATH = "infra/%s"
|
||||||
ENV_MODE = "ENV_MODE"
|
ENV_MODE = "ENV_MODE"
|
||||||
DEFAULT_ENV_MODE = "development"
|
DEFAULT_ENV_MODE = "staging"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
validEnvMode = map[string]struct{}{
|
validEnvMode = map[string]struct{}{
|
||||||
"local": {},
|
"local": {},
|
||||||
"development": {},
|
"development": {},
|
||||||
|
"staging": {},
|
||||||
"production": {},
|
"production": {},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -31,7 +32,7 @@ type Config struct {
|
|||||||
Log Log `mapstructure:"log"`
|
Log Log `mapstructure:"log"`
|
||||||
S3Config S3Config `mapstructure:"s3"`
|
S3Config S3Config `mapstructure:"s3"`
|
||||||
Fonnte Fonnte `mapstructure:"fonnte"`
|
Fonnte Fonnte `mapstructure:"fonnte"`
|
||||||
Firebase Firebase `mapstructure:"firebase"`
|
FCM FCM `mapstructure:"fcm"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -97,6 +98,6 @@ func (c *Config) GetFonnte() *Fonnte {
|
|||||||
return &c.Fonnte
|
return &c.Fonnte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) GetFirebase() *Firebase {
|
func (c *Config) GetFCM() *FCM {
|
||||||
return &c.Firebase
|
return &c.FCM
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type FCM struct {
|
||||||
|
CredentialsFile string `mapstructure:"credentials_file"`
|
||||||
|
ProjectID string `mapstructure:"project_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FCM) GetCredentialsFile() string {
|
||||||
|
return f.CredentialsFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FCM) GetProjectID() string {
|
||||||
|
return f.ProjectID
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
type Firebase struct {
|
|
||||||
CredentialsFile string `mapstructure:"credentials_file"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Firebase) GetCredentialsFile() string {
|
|
||||||
return f.CredentialsFile
|
|
||||||
}
|
|
||||||
+4
-3
@@ -1,7 +1,8 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
Port string `mapstructure:"port"`
|
Port string `mapstructure:"port"`
|
||||||
BaseUrl string `mapstructure:"common-url"`
|
BaseUrl string `mapstructure:"common-url"`
|
||||||
LocalUrl string `mapstructure:"local-url"`
|
LocalUrl string `mapstructure:"local-url"`
|
||||||
|
SelfOrderUrl string `mapstructure:"self-order-url"`
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-8
@@ -2,23 +2,61 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
APP_NAME="apskel-pos"
|
APP_NAME="apskel-pos"
|
||||||
PORT="4000"
|
|
||||||
|
# ─── Deteksi environment dari branch aktif ───────────────────────────────────
|
||||||
|
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
|
||||||
|
case "$CURRENT_BRANCH" in
|
||||||
|
main)
|
||||||
|
ENV_MODE="production"
|
||||||
|
PORT="4000"
|
||||||
|
;;
|
||||||
|
staging)
|
||||||
|
ENV_MODE="staging"
|
||||||
|
PORT="4001"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ Branch '$CURRENT_BRANCH' tidak dikenali untuk deployment."
|
||||||
|
echo " Gunakan branch 'main' (production) atau 'staging' (staging)."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CONTAINER_NAME="$APP_NAME"
|
||||||
|
IMAGE_NAME="$APP_NAME:$ENV_MODE"
|
||||||
|
|
||||||
|
echo "📦 Environment : $ENV_MODE"
|
||||||
|
echo "🌿 Branch : $CURRENT_BRANCH"
|
||||||
|
echo "🐳 Container : $CONTAINER_NAME"
|
||||||
|
echo "🔌 Port : $PORT"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ─── Pastikan config file ada ─────────────────────────────────────────────────
|
||||||
|
CONFIG_FILE="infra/$ENV_MODE.yaml"
|
||||||
|
if [ ! -f "$CONFIG_FILE" ]; then
|
||||||
|
echo "❌ Config file '$CONFIG_FILE' tidak ditemukan."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "🔄 Pulling latest code..."
|
echo "🔄 Pulling latest code..."
|
||||||
git pull
|
git pull
|
||||||
|
|
||||||
echo "🐳 Building Docker image (production target)..."
|
echo "🐳 Building Docker image ($ENV_MODE)..."
|
||||||
docker build --target production -t $APP_NAME:latest .
|
docker build --target production -t "$IMAGE_NAME" .
|
||||||
|
|
||||||
echo "🛑 Stopping and removing old container..."
|
echo "🛑 Stopping and removing old container..."
|
||||||
docker rm -f $APP_NAME 2>/dev/null || true
|
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
||||||
|
|
||||||
echo "🚀 Running new container..."
|
echo "🚀 Running new container..."
|
||||||
docker run -d --name $APP_NAME \
|
docker run -d --name "$CONTAINER_NAME" \
|
||||||
-p $PORT:$PORT \
|
-p "$PORT:4000" \
|
||||||
-e TZ=Asia/Jakarta \
|
-e TZ=Asia/Jakarta \
|
||||||
|
-e ENV_MODE="$ENV_MODE" \
|
||||||
-v "$(pwd)/infra":/infra:ro \
|
-v "$(pwd)/infra":/infra:ro \
|
||||||
-v "$(pwd)/templates":/templates:ro \
|
-v "$(pwd)/templates":/templates:ro \
|
||||||
$APP_NAME:latest
|
"$IMAGE_NAME"
|
||||||
|
|
||||||
echo "✅ Deployment complete."
|
echo ""
|
||||||
|
echo "✅ Deployment $ENV_MODE complete."
|
||||||
|
echo " Container : $CONTAINER_NAME"
|
||||||
|
echo " Port : $PORT"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
server:
|
server:
|
||||||
base-url:
|
base-url:
|
||||||
local-url:
|
local-url:
|
||||||
|
self-order-url:
|
||||||
port: 4000
|
port: 4000
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
@@ -8,7 +9,7 @@ jwt:
|
|||||||
expires-ttl: 144000
|
expires-ttl: 144000
|
||||||
secret: "5Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
|
secret: "5Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
|
||||||
refresh_token:
|
refresh_token:
|
||||||
expires-ttl: 7776000 # 3 months in minutes (90 days * 24 hours * 60 minutes)
|
expires-ttl: 7776000
|
||||||
secret: "R3fr3sh_T0k3n_S3cr3t_K3y_2024_P0S"
|
secret: "R3fr3sh_T0k3n_S3cr3t_K3y_2024_P0S"
|
||||||
customer:
|
customer:
|
||||||
expires-ttl: 7776000
|
expires-ttl: 7776000
|
||||||
@@ -20,7 +21,7 @@ postgresql:
|
|||||||
driver: postgres
|
driver: postgres
|
||||||
db: apskel_pos
|
db: apskel_pos
|
||||||
username: apskel
|
username: apskel
|
||||||
password: '7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk'
|
password: "7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk"
|
||||||
ssl-mode: disable
|
ssl-mode: disable
|
||||||
max-idle-connections-in-second: 600
|
max-idle-connections-in-second: 600
|
||||||
max-open-connections-in-second: 600
|
max-open-connections-in-second: 600
|
||||||
@@ -28,7 +29,7 @@ postgresql:
|
|||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
host: 127.0.0.1
|
host: 194.233.78.1
|
||||||
port: 6379
|
port: 6379
|
||||||
password: "CmICdmnX1EZPhVBYzQPEGw==U"
|
password: "CmICdmnX1EZPhVBYzQPEGw==U"
|
||||||
db: 0
|
db: 0
|
||||||
@@ -44,16 +45,17 @@ s3:
|
|||||||
endpoint: sin1.contabostorage.com
|
endpoint: sin1.contabostorage.com
|
||||||
bucket_name: enaklo
|
bucket_name: enaklo
|
||||||
log_level: Error
|
log_level: Error
|
||||||
host_url: 'https://sin1.contabostorage.com/fda98c2228f246f29a7e466b86b3b9e7:'
|
host_url: "https://sin1.contabostorage.com/fda98c2228f246f29a7e466b86b3b9e7:"
|
||||||
|
|
||||||
log:
|
log:
|
||||||
log_format: 'json'
|
log_format: "json"
|
||||||
log_level: 'debug'
|
log_level: "info"
|
||||||
|
|
||||||
fonnte:
|
fonnte:
|
||||||
api_url: "https://api.fonnte.com/send"
|
api_url: "https://api.fonnte.com/send"
|
||||||
token: "bADQrf9NTXfLZQCK2wGg"
|
token: "bADQrf9NTXfLZQCK2wGg"
|
||||||
timeout: 30
|
timeout: 30
|
||||||
|
|
||||||
firebase:
|
fcm:
|
||||||
credentials_file: "apskel-pos-v2-firebase-adminsdk-fbsvc-ae00499526.json"
|
credentials_file: "infra/firebase-service-account.json"
|
||||||
|
project_id: "apskel-pos-v2"
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
server:
|
||||||
|
base-url:
|
||||||
|
local-url:
|
||||||
|
self-order-url:
|
||||||
|
port: 4000
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
token:
|
||||||
|
expires-ttl: 144000
|
||||||
|
secret: "eZ7LAZJuSOGSHxb1ZYaZCkrBo5YBvc"
|
||||||
|
refresh_token:
|
||||||
|
expires-ttl: 7776000
|
||||||
|
secret: "EMx2DKPtMp0jQNpLvzzCsZkoUHe0d9"
|
||||||
|
customer:
|
||||||
|
expires-ttl: 7776000
|
||||||
|
secret: "layCV2rne0X57acWzSS3NxENmYJs7B"
|
||||||
|
|
||||||
|
postgresql:
|
||||||
|
host: 62.72.45.250
|
||||||
|
port: 5433
|
||||||
|
driver: postgres
|
||||||
|
db: apskel_pos_staging
|
||||||
|
username: apskel
|
||||||
|
password: "7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk"
|
||||||
|
ssl-mode: disable
|
||||||
|
max-idle-connections-in-second: 600
|
||||||
|
max-open-connections-in-second: 600
|
||||||
|
connection-max-life-time-in-second: 600
|
||||||
|
debug: false
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: 62.72.45.250
|
||||||
|
port: 6380
|
||||||
|
password: "CmICdmnX1EZPhVBYzQPEGw==U"
|
||||||
|
db: 1
|
||||||
|
dial_timeout: 5s
|
||||||
|
read_timeout: 3s
|
||||||
|
write_timeout: 3s
|
||||||
|
pool_size: 10
|
||||||
|
min_idle_connections: 5
|
||||||
|
|
||||||
|
s3:
|
||||||
|
access_key_id: cf9a475e18bc7626cbdbf09709d82a64
|
||||||
|
access_key_secret: 91f3321294d3e23035427a0ecb893ada
|
||||||
|
endpoint: sin1.contabostorage.com
|
||||||
|
bucket_name: enaklo
|
||||||
|
log_level: Error
|
||||||
|
host_url: "https://sin1.contabostorage.com/fda98c2228f246f29a7e466b86b3b9e7:"
|
||||||
|
|
||||||
|
log:
|
||||||
|
log_format: "json"
|
||||||
|
log_level: "info"
|
||||||
|
|
||||||
|
fonnte:
|
||||||
|
api_url: "https://api.fonnte.com/send"
|
||||||
|
token: "bADQrf9NTXfLZQCK2wGg"
|
||||||
|
timeout: 30
|
||||||
|
|
||||||
|
fcm:
|
||||||
|
credentials_file: "infra/firebase-service-account.json"
|
||||||
|
project_id: "apskel-pos-v2"
|
||||||
+112
-16
@@ -25,11 +25,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
redisClient *redis.Client
|
redisClient *redis.Client
|
||||||
router *router.Router
|
router *router.Router
|
||||||
shutdown chan os.Signal
|
shutdown chan os.Signal
|
||||||
|
omsetScheduler *service.OmsetMilestoneScheduler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
|
func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
|
||||||
@@ -43,6 +44,15 @@ func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
|
|||||||
func (a *App) Initialize(cfg *config.Config) error {
|
func (a *App) Initialize(cfg *config.Config) error {
|
||||||
repos := a.initRepositories()
|
repos := a.initRepositories()
|
||||||
processors := a.initProcessors(cfg, repos)
|
processors := a.initProcessors(cfg, repos)
|
||||||
|
|
||||||
|
// Initialize omset milestone scheduler
|
||||||
|
a.omsetScheduler = service.NewOmsetMilestoneScheduler(
|
||||||
|
repos.organizationRepo,
|
||||||
|
repos.outletRepo,
|
||||||
|
repos.userRepo,
|
||||||
|
processors.notificationProcessor,
|
||||||
|
)
|
||||||
|
|
||||||
services := a.initServices(processors, repos, cfg)
|
services := a.initServices(processors, repos, cfg)
|
||||||
validators := a.initValidators()
|
validators := a.initValidators()
|
||||||
middleware := a.initMiddleware(services, cfg)
|
middleware := a.initMiddleware(services, cfg)
|
||||||
@@ -56,7 +66,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
repos.userRepo,
|
repos.userRepo,
|
||||||
repos.sessionRepo,
|
repos.sessionRepo,
|
||||||
repos.orderRepo,
|
repos.orderRepo,
|
||||||
processors.fcmClient,
|
services.productOutletPriceService,
|
||||||
)
|
)
|
||||||
|
|
||||||
a.router = router.NewRouter(
|
a.router = router.NewRouter(
|
||||||
@@ -98,6 +108,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
validators.vendorValidator,
|
validators.vendorValidator,
|
||||||
services.purchaseOrderService,
|
services.purchaseOrderService,
|
||||||
validators.purchaseOrderValidator,
|
validators.purchaseOrderValidator,
|
||||||
|
services.purchaseCategoryService,
|
||||||
|
validators.purchaseCategoryValidator,
|
||||||
services.unitConverterService,
|
services.unitConverterService,
|
||||||
validators.unitConverterValidator,
|
validators.unitConverterValidator,
|
||||||
services.chartOfAccountTypeService,
|
services.chartOfAccountTypeService,
|
||||||
@@ -119,13 +131,27 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
services.customerPointsService,
|
services.customerPointsService,
|
||||||
services.spinGameService,
|
services.spinGameService,
|
||||||
middleware.customerAuthMiddleware,
|
middleware.customerAuthMiddleware,
|
||||||
|
services.userDeviceService,
|
||||||
|
validators.userDeviceValidator,
|
||||||
|
services.notificationService,
|
||||||
|
validators.notificationValidator,
|
||||||
|
services.productOutletPriceService,
|
||||||
|
validators.productOutletPriceValidator,
|
||||||
selfOrderHandler,
|
selfOrderHandler,
|
||||||
|
services.expenseService,
|
||||||
|
validators.expenseValidator,
|
||||||
|
a.redisClient,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Start(port string) error {
|
func (a *App) Start(port string) error {
|
||||||
|
// Start the omset milestone scheduler (checks every 5 minutes for daily omset milestones)
|
||||||
|
if a.omsetScheduler != nil {
|
||||||
|
a.omsetScheduler.Start(5 * time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
engine := a.router.Init()
|
engine := a.router.Init()
|
||||||
|
|
||||||
a.server = &http.Server{
|
a.server = &http.Server{
|
||||||
@@ -161,6 +187,9 @@ func (a *App) Start(port string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Shutdown() {
|
func (a *App) Shutdown() {
|
||||||
|
if a.omsetScheduler != nil {
|
||||||
|
a.omsetScheduler.Stop()
|
||||||
|
}
|
||||||
close(a.shutdown)
|
close(a.shutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +218,7 @@ type repositories struct {
|
|||||||
productRecipeRepo *repository.ProductRecipeRepository
|
productRecipeRepo *repository.ProductRecipeRepository
|
||||||
vendorRepo *repository.VendorRepositoryImpl
|
vendorRepo *repository.VendorRepositoryImpl
|
||||||
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
|
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
|
||||||
|
purchaseCategoryRepo *repository.PurchaseCategoryRepositoryImpl
|
||||||
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
|
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
|
||||||
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
|
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
|
||||||
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
||||||
@@ -208,6 +238,12 @@ type repositories struct {
|
|||||||
otpRepo repository.OtpRepository
|
otpRepo repository.OtpRepository
|
||||||
sessionRepo repository.SessionRepository
|
sessionRepo repository.SessionRepository
|
||||||
txManager *repository.TxManager
|
txManager *repository.TxManager
|
||||||
|
userDeviceRepo *repository.UserDeviceRepositoryImpl
|
||||||
|
notificationRepo *repository.NotificationRepositoryImpl
|
||||||
|
notificationReceiverRepo *repository.NotificationReceiverRepositoryImpl
|
||||||
|
notificationDeliveryRepo *repository.NotificationDeliveryRepositoryImpl
|
||||||
|
productOutletPriceRepo *repository.ProductOutletPriceRepositoryImpl
|
||||||
|
expenseRepo *repository.ExpenseRepositoryImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initRepositories() *repositories {
|
func (a *App) initRepositories() *repositories {
|
||||||
@@ -236,6 +272,7 @@ func (a *App) initRepositories() *repositories {
|
|||||||
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
||||||
vendorRepo: repository.NewVendorRepositoryImpl(a.db),
|
vendorRepo: repository.NewVendorRepositoryImpl(a.db),
|
||||||
purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db),
|
purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db),
|
||||||
|
purchaseCategoryRepo: repository.NewPurchaseCategoryRepositoryImpl(a.db),
|
||||||
unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl),
|
unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl),
|
||||||
chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db),
|
chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db),
|
||||||
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
||||||
@@ -255,6 +292,12 @@ func (a *App) initRepositories() *repositories {
|
|||||||
otpRepo: repository.NewOtpRepository(a.db),
|
otpRepo: repository.NewOtpRepository(a.db),
|
||||||
sessionRepo: repository.NewSessionRepository(a.redisClient),
|
sessionRepo: repository.NewSessionRepository(a.redisClient),
|
||||||
txManager: repository.NewTxManager(a.db),
|
txManager: repository.NewTxManager(a.db),
|
||||||
|
userDeviceRepo: repository.NewUserDeviceRepositoryImpl(a.db),
|
||||||
|
notificationRepo: repository.NewNotificationRepository(a.db),
|
||||||
|
notificationReceiverRepo: repository.NewNotificationReceiverRepository(a.db),
|
||||||
|
notificationDeliveryRepo: repository.NewNotificationDeliveryRepository(a.db),
|
||||||
|
productOutletPriceRepo: repository.NewProductOutletPriceRepositoryImpl(a.db),
|
||||||
|
expenseRepo: repository.NewExpenseRepositoryImpl(a.db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +321,7 @@ type processors struct {
|
|||||||
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
||||||
vendorProcessor *processor.VendorProcessorImpl
|
vendorProcessor *processor.VendorProcessorImpl
|
||||||
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
|
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
|
||||||
|
purchaseCategoryProcessor *processor.PurchaseCategoryProcessorImpl
|
||||||
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
|
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
|
||||||
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
|
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
|
||||||
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
||||||
@@ -296,14 +340,16 @@ type processors struct {
|
|||||||
customerPointsProcessor *processor.CustomerPointsProcessor
|
customerPointsProcessor *processor.CustomerPointsProcessor
|
||||||
otpProcessor processor.OtpProcessor
|
otpProcessor processor.OtpProcessor
|
||||||
fileClient processor.FileClient
|
fileClient processor.FileClient
|
||||||
fcmClient client.FcmClient
|
|
||||||
inventoryMovementService service.InventoryMovementService
|
inventoryMovementService service.InventoryMovementService
|
||||||
|
userDeviceProcessor *processor.UserDeviceProcessorImpl
|
||||||
|
notificationProcessor *processor.NotificationProcessorImpl
|
||||||
|
productOutletPriceProcessor processor.ProductOutletPriceProcessor
|
||||||
|
expenseProcessor *processor.ExpenseProcessorImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||||
fileClient := client.NewFileClient(cfg.S3Config)
|
fileClient := client.NewFileClient(cfg.S3Config)
|
||||||
fonnteClient := client.NewFonnteClient(cfg.GetFonnte())
|
fonnteClient := client.NewFonnteClient(cfg.GetFonnte())
|
||||||
fcmClient := client.NewFcmClient(cfg.GetFirebase())
|
|
||||||
otpProcessor := processor.NewOtpProcessor(fonnteClient, repos.otpRepo)
|
otpProcessor := processor.NewOtpProcessor(fonnteClient, repos.otpRepo)
|
||||||
inventoryMovementService := service.NewInventoryMovementService(repos.inventoryMovementRepo, repos.ingredientRepo)
|
inventoryMovementService := service.NewInventoryMovementService(repos.inventoryMovementRepo, repos.ingredientRepo)
|
||||||
|
|
||||||
@@ -313,20 +359,21 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
outletProcessor: processor.NewOutletProcessorImpl(repos.outletRepo),
|
outletProcessor: processor.NewOutletProcessorImpl(repos.outletRepo),
|
||||||
outletSettingProcessor: processor.NewOutletSettingProcessorImpl(repos.outletSettingRepo, repos.outletRepo),
|
outletSettingProcessor: processor.NewOutletSettingProcessorImpl(repos.outletSettingRepo, repos.outletRepo),
|
||||||
categoryProcessor: processor.NewCategoryProcessorImpl(repos.categoryRepo),
|
categoryProcessor: processor.NewCategoryProcessorImpl(repos.categoryRepo),
|
||||||
productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo),
|
productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo, repos.productOutletPriceRepo),
|
||||||
productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo),
|
productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo),
|
||||||
inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo, repos.ingredientRepo, repos.inventoryMovementRepo),
|
inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo, repos.ingredientRepo, repos.inventoryMovementRepo),
|
||||||
orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.paymentOrderItemRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.inventoryMovementRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo, repos.txManager, repos.productRecipeRepo, repos.ingredientRepo, inventoryMovementService),
|
orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.paymentOrderItemRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.inventoryMovementRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo, repos.txManager, repos.productRecipeRepo, repos.ingredientRepo, inventoryMovementService, repos.productOutletPriceRepo),
|
||||||
paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo),
|
paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo),
|
||||||
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
|
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
|
||||||
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
|
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
|
||||||
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo),
|
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo, repos.expenseRepo),
|
||||||
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
|
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
|
||||||
unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
|
unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
|
||||||
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
|
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
|
||||||
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
||||||
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
|
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
|
||||||
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.purchaseCategoryRepo, repos.categoryRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
||||||
|
purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo),
|
||||||
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
|
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
|
||||||
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
|
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
|
||||||
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
|
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
|
||||||
@@ -345,8 +392,11 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo, repos.gameRepo),
|
customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo, repos.gameRepo),
|
||||||
otpProcessor: otpProcessor,
|
otpProcessor: otpProcessor,
|
||||||
fileClient: fileClient,
|
fileClient: fileClient,
|
||||||
fcmClient: fcmClient,
|
|
||||||
inventoryMovementService: inventoryMovementService,
|
inventoryMovementService: inventoryMovementService,
|
||||||
|
userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo),
|
||||||
|
notificationProcessor: buildNotificationProcessor(cfg, repos),
|
||||||
|
productOutletPriceProcessor: processor.NewProductOutletPriceProcessorImpl(repos.productOutletPriceRepo, repos.productRepo, repos.outletRepo),
|
||||||
|
expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo, repos.purchaseCategoryRepo),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +422,7 @@ type services struct {
|
|||||||
productRecipeService *service.ProductRecipeServiceImpl
|
productRecipeService *service.ProductRecipeServiceImpl
|
||||||
vendorService *service.VendorServiceImpl
|
vendorService *service.VendorServiceImpl
|
||||||
purchaseOrderService *service.PurchaseOrderServiceImpl
|
purchaseOrderService *service.PurchaseOrderServiceImpl
|
||||||
|
purchaseCategoryService service.PurchaseCategoryService
|
||||||
unitConverterService *service.IngredientUnitConverterServiceImpl
|
unitConverterService *service.IngredientUnitConverterServiceImpl
|
||||||
chartOfAccountTypeService service.ChartOfAccountTypeService
|
chartOfAccountTypeService service.ChartOfAccountTypeService
|
||||||
chartOfAccountService service.ChartOfAccountService
|
chartOfAccountService service.ChartOfAccountService
|
||||||
@@ -383,11 +434,15 @@ type services struct {
|
|||||||
customerAuthService service.CustomerAuthService
|
customerAuthService service.CustomerAuthService
|
||||||
customerPointsService service.CustomerPointsService
|
customerPointsService service.CustomerPointsService
|
||||||
spinGameService service.SpinGameService
|
spinGameService service.SpinGameService
|
||||||
|
userDeviceService service.UserDeviceService
|
||||||
|
notificationService service.NotificationService
|
||||||
|
productOutletPriceService service.ProductOutletPriceService
|
||||||
|
expenseService *service.ExpenseServiceImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||||
authConfig := cfg.Auth()
|
authConfig := cfg.Auth()
|
||||||
authService := service.NewAuthService(processors.userProcessor, authConfig)
|
authService := service.NewAuthService(processors.userProcessor, processors.userDeviceProcessor, authConfig)
|
||||||
organizationService := service.NewOrganizationService(processors.organizationProcessor)
|
organizationService := service.NewOrganizationService(processors.organizationProcessor)
|
||||||
outletService := service.NewOutletService(processors.outletProcessor)
|
outletService := service.NewOutletService(processors.outletProcessor)
|
||||||
outletSettingService := service.NewOutletSettingService(processors.outletSettingProcessor)
|
outletSettingService := service.NewOutletSettingService(processors.outletSettingProcessor)
|
||||||
@@ -395,7 +450,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
productService := service.NewProductService(processors.productProcessor)
|
productService := service.NewProductService(processors.productProcessor)
|
||||||
productVariantService := service.NewProductVariantService(processors.productVariantProcessor)
|
productVariantService := service.NewProductVariantService(processors.productVariantProcessor)
|
||||||
inventoryService := service.NewInventoryService(processors.inventoryProcessor)
|
inventoryService := service.NewInventoryService(processors.inventoryProcessor)
|
||||||
orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, nil, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo) // Will be updated after orderIngredientTransactionService is created
|
orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, nil, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo, processors.notificationProcessor, repos.userRepo) // Will be updated after orderIngredientTransactionService is created
|
||||||
paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor)
|
paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor)
|
||||||
fileService := service.NewFileServiceImpl(processors.fileProcessor)
|
fileService := service.NewFileServiceImpl(processors.fileProcessor)
|
||||||
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
|
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
|
||||||
@@ -407,6 +462,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor)
|
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor)
|
||||||
vendorService := service.NewVendorService(processors.vendorProcessor)
|
vendorService := service.NewVendorService(processors.vendorProcessor)
|
||||||
purchaseOrderService := service.NewPurchaseOrderService(processors.purchaseOrderProcessor)
|
purchaseOrderService := service.NewPurchaseOrderService(processors.purchaseOrderProcessor)
|
||||||
|
purchaseCategoryService := service.NewPurchaseCategoryService(processors.purchaseCategoryProcessor)
|
||||||
unitConverterService := service.NewIngredientUnitConverterService(processors.unitConverterProcessor)
|
unitConverterService := service.NewIngredientUnitConverterService(processors.unitConverterProcessor)
|
||||||
chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor)
|
chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor)
|
||||||
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
|
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
|
||||||
@@ -418,9 +474,11 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
customerAuthService := service.NewCustomerAuthService(processors.customerAuthProcessor)
|
customerAuthService := service.NewCustomerAuthService(processors.customerAuthProcessor)
|
||||||
customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor)
|
customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor)
|
||||||
spinGameService := service.NewSpinGameService(processors.gamePlayProcessor, repos.txManager)
|
spinGameService := service.NewSpinGameService(processors.gamePlayProcessor, repos.txManager)
|
||||||
|
userDeviceService := service.NewUserDeviceService(processors.userDeviceProcessor)
|
||||||
|
notificationService := service.NewNotificationService(processors.notificationProcessor)
|
||||||
|
|
||||||
// Update order service with order ingredient transaction service
|
// Update order service with order ingredient transaction service
|
||||||
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo)
|
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo, processors.notificationProcessor, repos.userRepo)
|
||||||
|
|
||||||
return &services{
|
return &services{
|
||||||
userService: service.NewUserService(processors.userProcessor),
|
userService: service.NewUserService(processors.userProcessor),
|
||||||
@@ -444,6 +502,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
productRecipeService: productRecipeService,
|
productRecipeService: productRecipeService,
|
||||||
vendorService: vendorService,
|
vendorService: vendorService,
|
||||||
purchaseOrderService: purchaseOrderService,
|
purchaseOrderService: purchaseOrderService,
|
||||||
|
purchaseCategoryService: purchaseCategoryService,
|
||||||
unitConverterService: unitConverterService,
|
unitConverterService: unitConverterService,
|
||||||
chartOfAccountTypeService: chartOfAccountTypeService,
|
chartOfAccountTypeService: chartOfAccountTypeService,
|
||||||
chartOfAccountService: chartOfAccountService,
|
chartOfAccountService: chartOfAccountService,
|
||||||
@@ -455,6 +514,10 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
customerAuthService: customerAuthService,
|
customerAuthService: customerAuthService,
|
||||||
customerPointsService: customerPointsService,
|
customerPointsService: customerPointsService,
|
||||||
spinGameService: spinGameService,
|
spinGameService: spinGameService,
|
||||||
|
userDeviceService: userDeviceService,
|
||||||
|
notificationService: notificationService,
|
||||||
|
productOutletPriceService: service.NewProductOutletPriceService(processors.productOutletPriceProcessor),
|
||||||
|
expenseService: service.NewExpenseService(processors.expenseProcessor),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,6 +548,7 @@ type validators struct {
|
|||||||
tableValidator *validator.TableValidator
|
tableValidator *validator.TableValidator
|
||||||
vendorValidator *validator.VendorValidatorImpl
|
vendorValidator *validator.VendorValidatorImpl
|
||||||
purchaseOrderValidator *validator.PurchaseOrderValidatorImpl
|
purchaseOrderValidator *validator.PurchaseOrderValidatorImpl
|
||||||
|
purchaseCategoryValidator *validator.PurchaseCategoryValidatorImpl
|
||||||
unitConverterValidator *validator.IngredientUnitConverterValidatorImpl
|
unitConverterValidator *validator.IngredientUnitConverterValidatorImpl
|
||||||
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
|
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
|
||||||
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
||||||
@@ -494,6 +558,10 @@ type validators struct {
|
|||||||
rewardValidator validator.RewardValidator
|
rewardValidator validator.RewardValidator
|
||||||
campaignValidator validator.CampaignValidator
|
campaignValidator validator.CampaignValidator
|
||||||
customerAuthValidator validator.CustomerAuthValidator
|
customerAuthValidator validator.CustomerAuthValidator
|
||||||
|
userDeviceValidator *validator.UserDeviceValidatorImpl
|
||||||
|
notificationValidator *validator.NotificationValidatorImpl
|
||||||
|
productOutletPriceValidator *validator.ProductOutletPriceValidatorImpl
|
||||||
|
expenseValidator *validator.ExpenseValidatorImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initValidators() *validators {
|
func (a *App) initValidators() *validators {
|
||||||
@@ -512,6 +580,7 @@ func (a *App) initValidators() *validators {
|
|||||||
tableValidator: validator.NewTableValidator(),
|
tableValidator: validator.NewTableValidator(),
|
||||||
vendorValidator: validator.NewVendorValidator(),
|
vendorValidator: validator.NewVendorValidator(),
|
||||||
purchaseOrderValidator: validator.NewPurchaseOrderValidator(),
|
purchaseOrderValidator: validator.NewPurchaseOrderValidator(),
|
||||||
|
purchaseCategoryValidator: validator.NewPurchaseCategoryValidator(),
|
||||||
unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl),
|
unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl),
|
||||||
chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl),
|
chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl),
|
||||||
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
||||||
@@ -521,5 +590,32 @@ func (a *App) initValidators() *validators {
|
|||||||
rewardValidator: validator.NewRewardValidator(),
|
rewardValidator: validator.NewRewardValidator(),
|
||||||
campaignValidator: validator.NewCampaignValidator(),
|
campaignValidator: validator.NewCampaignValidator(),
|
||||||
customerAuthValidator: validator.NewCustomerAuthValidator(),
|
customerAuthValidator: validator.NewCustomerAuthValidator(),
|
||||||
|
userDeviceValidator: validator.NewUserDeviceValidator(),
|
||||||
|
notificationValidator: validator.NewNotificationValidator(),
|
||||||
|
productOutletPriceValidator: validator.NewProductOutletPriceValidator(),
|
||||||
|
expenseValidator: validator.NewExpenseValidator(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildNotificationProcessor creates the notification processor with FCM integration.
|
||||||
|
// If FCM is not configured, it returns a processor with a nil FCM client (FCM dispatch will be skipped).
|
||||||
|
func buildNotificationProcessor(cfg *config.Config, repos *repositories) *processor.NotificationProcessorImpl {
|
||||||
|
var fcmClient client.FCMClient
|
||||||
|
if cfg.FCM.CredentialsFile != "" {
|
||||||
|
var err error
|
||||||
|
fcmClient, err = client.NewFCMClient(&cfg.FCM)
|
||||||
|
if err != nil {
|
||||||
|
// FCM init failure is non-fatal; notifications will still be persisted.
|
||||||
|
fcmClient = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processor.NewNotificationProcessor(
|
||||||
|
repos.notificationRepo,
|
||||||
|
repos.notificationReceiverRepo,
|
||||||
|
repos.notificationDeliveryRepo,
|
||||||
|
repos.userDeviceRepo,
|
||||||
|
repos.userRepo,
|
||||||
|
fcmClient,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,81 +3,140 @@ package client
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
|
||||||
"apskel-pos-be/config"
|
|
||||||
|
|
||||||
firebase "firebase.google.com/go/v4"
|
firebase "firebase.google.com/go/v4"
|
||||||
"firebase.google.com/go/v4/messaging"
|
"firebase.google.com/go/v4/messaging"
|
||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FcmClient interface {
|
type FCMConfig interface {
|
||||||
SendMulticastNotification(ctx context.Context, tokens []string, title string, body string) error
|
GetCredentialsFile() string
|
||||||
|
GetProjectID() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FCMClient interface {
|
||||||
|
SendNotification(ctx context.Context, token string, title string, body string, data map[string]string) error
|
||||||
|
SendMulticastNotification(ctx context.Context, tokens []string, title string, body string, data map[string]string) error
|
||||||
|
SendToTopic(ctx context.Context, topic string, title string, body string, data map[string]string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type fcmClient struct {
|
type fcmClient struct {
|
||||||
messagingClient *messaging.Client
|
messaging *messaging.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFcmClient(cfg *config.Firebase) FcmClient {
|
func NewFCMClient(cfg FCMConfig) (FCMClient, error) {
|
||||||
if cfg == nil || cfg.GetCredentialsFile() == "" {
|
ctx := context.Background()
|
||||||
log.Println("FCM: credentials file not configured, FCM client is disabled")
|
|
||||||
return &fcmClient{messagingClient: nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
opt := option.WithCredentialsFile(cfg.GetCredentialsFile())
|
opt := option.WithCredentialsFile(cfg.GetCredentialsFile())
|
||||||
app, err := firebase.NewApp(context.Background(), nil, opt)
|
|
||||||
|
app, err := firebase.NewApp(ctx, &firebase.Config{
|
||||||
|
ProjectID: cfg.GetProjectID(),
|
||||||
|
}, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("FCM: failed to initialize Firebase app: %v", err)
|
return nil, fmt.Errorf("failed to initialize firebase app: %w", err)
|
||||||
return &fcmClient{messagingClient: nil}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := app.Messaging(context.Background())
|
msgClient, err := app.Messaging(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("FCM: failed to create messaging client: %v", err)
|
return nil, fmt.Errorf("failed to initialize firebase messaging client: %w", err)
|
||||||
return &fcmClient{messagingClient: nil}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("FCM: client initialized successfully")
|
return &fcmClient{
|
||||||
return &fcmClient{messagingClient: client}
|
messaging: msgClient,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fcmClient) SendMulticastNotification(ctx context.Context, tokens []string, title string, body string) error {
|
// SendNotification sends a push notification to a single device token.
|
||||||
if c.messagingClient == nil {
|
func (f *fcmClient) SendNotification(ctx context.Context, token string, title string, body string, data map[string]string) error {
|
||||||
log.Println("FCM: client not initialized, skipping notification")
|
message := &messaging.Message{
|
||||||
return nil
|
Token: token,
|
||||||
|
Notification: &messaging.Notification{
|
||||||
|
Title: title,
|
||||||
|
Body: body,
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
Android: &messaging.AndroidConfig{
|
||||||
|
Priority: "high",
|
||||||
|
},
|
||||||
|
APNS: &messaging.APNSConfig{
|
||||||
|
Payload: &messaging.APNSPayload{
|
||||||
|
Aps: &messaging.Aps{
|
||||||
|
Sound: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err := f.messaging.Send(ctx, message)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send FCM notification: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMulticastNotification sends a push notification to multiple device tokens.
|
||||||
|
func (f *fcmClient) SendMulticastNotification(ctx context.Context, tokens []string, title string, body string, data map[string]string) error {
|
||||||
if len(tokens) == 0 {
|
if len(tokens) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
message := &messaging.MulticastMessage{
|
message := &messaging.MulticastMessage{
|
||||||
|
Tokens: tokens,
|
||||||
Notification: &messaging.Notification{
|
Notification: &messaging.Notification{
|
||||||
Title: title,
|
Title: title,
|
||||||
Body: body,
|
Body: body,
|
||||||
},
|
},
|
||||||
Tokens: tokens,
|
Data: data,
|
||||||
Android: &messaging.AndroidConfig{
|
Android: &messaging.AndroidConfig{
|
||||||
Priority: "high",
|
Priority: "high",
|
||||||
},
|
},
|
||||||
|
APNS: &messaging.APNSConfig{
|
||||||
|
Payload: &messaging.APNSPayload{
|
||||||
|
Aps: &messaging.Aps{
|
||||||
|
Sound: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := c.messagingClient.SendMulticast(ctx, message)
|
batchResp, err := f.messaging.SendEachForMulticast(ctx, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("FCM: failed to send multicast notification: %w", err)
|
return fmt.Errorf("failed to send FCM multicast notification: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.FailureCount > 0 {
|
if batchResp.FailureCount > 0 {
|
||||||
log.Printf("FCM: %d tokens failed out of %d", response.FailureCount, len(tokens))
|
return fmt.Errorf("FCM multicast: %d/%d messages failed to send", batchResp.FailureCount, len(tokens))
|
||||||
for i, resp := range response.Responses {
|
}
|
||||||
if !resp.Success {
|
|
||||||
log.Printf("FCM: token[%d] failed: %v", i, resp.Error)
|
return nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// SendToTopic sends a push notification to all devices subscribed to a topic.
|
||||||
|
func (f *fcmClient) SendToTopic(ctx context.Context, topic string, title string, body string, data map[string]string) error {
|
||||||
|
message := &messaging.Message{
|
||||||
|
Topic: topic,
|
||||||
|
Notification: &messaging.Notification{
|
||||||
|
Title: title,
|
||||||
|
Body: body,
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
Android: &messaging.AndroidConfig{
|
||||||
|
Priority: "high",
|
||||||
|
},
|
||||||
|
APNS: &messaging.APNSConfig{
|
||||||
|
Payload: &messaging.APNSPayload{
|
||||||
|
Aps: &messaging.Aps{
|
||||||
|
Sound: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := f.messaging.Send(ctx, message)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send FCM topic notification: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("FCM: sent %d/%d notifications successfully", response.SuccessCount, len(tokens))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
// Budget allocation of revenue used by the parent category cut-off report.
|
||||||
|
// The three shares are expected to add up to 100.
|
||||||
|
const (
|
||||||
|
BudgetLimitPurchasePercent = 60.0
|
||||||
|
BudgetLimitOwnerPercent = 20.0
|
||||||
|
BudgetLimitTeamPercent = 20.0
|
||||||
|
)
|
||||||
+18
-12
@@ -40,22 +40,28 @@ const (
|
|||||||
OutletServiceEntity = "outlet_service"
|
OutletServiceEntity = "outlet_service"
|
||||||
VendorServiceEntity = "vendor_service"
|
VendorServiceEntity = "vendor_service"
|
||||||
PurchaseOrderServiceEntity = "purchase_order_service"
|
PurchaseOrderServiceEntity = "purchase_order_service"
|
||||||
|
PurchaseCategoryServiceEntity = "purchase_category_service"
|
||||||
IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service"
|
IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service"
|
||||||
IngredientCompositionServiceEntity = "ingredient_composition_service"
|
IngredientCompositionServiceEntity = "ingredient_composition_service"
|
||||||
TableEntity = "table"
|
TableEntity = "table"
|
||||||
// Gamification entities
|
// Gamification entities
|
||||||
CustomerPointsEntity = "customer_points"
|
CustomerPointsEntity = "customer_points"
|
||||||
CustomerTokensEntity = "customer_tokens"
|
CustomerTokensEntity = "customer_tokens"
|
||||||
TierEntity = "tier"
|
TierEntity = "tier"
|
||||||
GameEntity = "game"
|
GameEntity = "game"
|
||||||
GamePrizeEntity = "game_prize"
|
GamePrizeEntity = "game_prize"
|
||||||
GamePlayEntity = "game_play"
|
GamePlayEntity = "game_play"
|
||||||
OmsetTrackerEntity = "omset_tracker"
|
OmsetTrackerEntity = "omset_tracker"
|
||||||
RewardEntity = "reward"
|
RewardEntity = "reward"
|
||||||
CampaignEntity = "campaign"
|
CampaignEntity = "campaign"
|
||||||
CampaignRuleEntity = "campaign_rule"
|
CampaignRuleEntity = "campaign_rule"
|
||||||
CustomerEntity = "customer"
|
CustomerEntity = "customer"
|
||||||
SpinGameHandlerEntity = "spin_game_handler"
|
SpinGameHandlerEntity = "spin_game_handler"
|
||||||
|
UserDeviceServiceEntity = "user_device_service"
|
||||||
|
NotificationServiceEntity = "notification_service"
|
||||||
|
NotificationHandlerEntity = "notification_handler"
|
||||||
|
ProductOutletPriceServiceEntity = "product_outlet_price_service"
|
||||||
|
ExpenseServiceEntity = "expense_service"
|
||||||
)
|
)
|
||||||
|
|
||||||
var HttpErrorMap = map[string]int{
|
var HttpErrorMap = map[string]int{
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
type ExpenseStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ExpenseStatusDraft ExpenseStatus = "draft"
|
||||||
|
ExpenseStatusSent ExpenseStatus = "sent"
|
||||||
|
ExpenseStatusApproved ExpenseStatus = "approved"
|
||||||
|
ExpenseStatusCancel ExpenseStatus = "cancel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAllExpenseStatuses() []ExpenseStatus {
|
||||||
|
return []ExpenseStatus{
|
||||||
|
ExpenseStatusDraft,
|
||||||
|
ExpenseStatusSent,
|
||||||
|
ExpenseStatusApproved,
|
||||||
|
ExpenseStatusCancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidExpenseStatus(status ExpenseStatus) bool {
|
||||||
|
for _, validStatus := range GetAllExpenseStatuses() {
|
||||||
|
if status == validStatus {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
// A purchase order is charged to a team. Teams come from the parent product
|
||||||
|
// categories, plus Pusat for spending that belongs to no single team.
|
||||||
|
const (
|
||||||
|
PurchaseTeamScopeCategory = "category"
|
||||||
|
PurchaseTeamScopeCentral = "central"
|
||||||
|
|
||||||
|
// PurchaseTeamCentralName is what Pusat is called in the picker. Pusat has no
|
||||||
|
// row of its own, so the name lives here rather than in the database.
|
||||||
|
PurchaseTeamCentralName = "Pusat"
|
||||||
|
|
||||||
|
// PurchaseTeamNone is the value the list filter takes to ask for purchases
|
||||||
|
// that have not been charged to any team yet.
|
||||||
|
PurchaseTeamNone = "none"
|
||||||
|
|
||||||
|
// PurchaseTeamNoneName labels those purchases in the reports.
|
||||||
|
PurchaseTeamNoneName = "Tanpa Team"
|
||||||
|
)
|
||||||
@@ -3,10 +3,12 @@ package constants
|
|||||||
type UserRole string
|
type UserRole string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RoleAdmin UserRole = "admin"
|
RoleAdmin UserRole = "admin"
|
||||||
RoleManager UserRole = "manager"
|
RoleManager UserRole = "manager"
|
||||||
RoleCashier UserRole = "cashier"
|
RoleCashier UserRole = "cashier"
|
||||||
RoleWaiter UserRole = "waiter"
|
RoleWaiter UserRole = "waiter"
|
||||||
|
RoleOwner UserRole = "owner"
|
||||||
|
RolePurchasing UserRole = "purchasing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAllUserRoles() []UserRole {
|
func GetAllUserRoles() []UserRole {
|
||||||
@@ -15,6 +17,8 @@ func GetAllUserRoles() []UserRole {
|
|||||||
RoleManager,
|
RoleManager,
|
||||||
RoleCashier,
|
RoleCashier,
|
||||||
RoleWaiter,
|
RoleWaiter,
|
||||||
|
RoleOwner,
|
||||||
|
RolePurchasing,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateAccountRequest struct {
|
type CreateAccountRequest struct {
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
Number string `json:"number" validate:"required,min=1,max=50"`
|
Number string `json:"number" validate:"required,min=1,max=50"`
|
||||||
AccountType string `json:"account_type" validate:"required,oneof=cash wallet bank credit debit asset liability equity revenue expense"`
|
AccountType string `json:"account_type" validate:"required,oneof=cash wallet bank credit debit asset liability equity revenue expense"`
|
||||||
OpeningBalance float64 `json:"opening_balance"`
|
OpeningBalance float64 `json:"opening_balance"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateAccountRequest struct {
|
type UpdateAccountRequest struct {
|
||||||
@@ -24,21 +24,21 @@ type UpdateAccountRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AccountResponse struct {
|
type AccountResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Number string `json:"number"`
|
Number string `json:"number"`
|
||||||
AccountType string `json:"account_type"`
|
AccountType string `json:"account_type"`
|
||||||
OpeningBalance float64 `json:"opening_balance"`
|
OpeningBalance float64 `json:"opening_balance"`
|
||||||
CurrentBalance float64 `json:"current_balance"`
|
CurrentBalance float64 `json:"current_balance"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
IsSystem bool `json:"is_system"`
|
IsSystem bool `json:"is_system"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
ChartOfAccount *ChartOfAccountResponse `json:"chart_of_account,omitempty"`
|
ChartOfAccount *ChartOfAccountResponse `json:"chart_of_account,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListAccountsRequest struct {
|
type ListAccountsRequest struct {
|
||||||
|
|||||||
@@ -7,17 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PaymentMethodAnalyticsRequest struct {
|
type PaymentMethodAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID `form:"organization_id"`
|
OrganizationID uuid.UUID `form:"organization_id"`
|
||||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PaymentMethodAnalyticsResponse represents the response for payment method analytics
|
// PaymentMethodAnalyticsResponse represents the response for payment method analytics
|
||||||
type PaymentMethodAnalyticsResponse struct {
|
type PaymentMethodAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
GroupBy string `json:"group_by"`
|
GroupBy string `json:"group_by"`
|
||||||
@@ -45,15 +46,16 @@ type PaymentMethodAnalyticsData struct {
|
|||||||
|
|
||||||
type SalesAnalyticsRequest struct {
|
type SalesAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SalesAnalyticsResponse struct {
|
type SalesAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
GroupBy string `json:"group_by"`
|
GroupBy string `json:"group_by"`
|
||||||
@@ -83,19 +85,105 @@ type SalesAnalyticsData struct {
|
|||||||
NetSales float64 `json:"net_sales"`
|
NetSales float64 `json:"net_sales"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PurchasingAnalyticsRequest struct {
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
|
// Team narrows the report to one team: a parent category id, "central" for
|
||||||
|
// Pusat, or "none" for purchases charged to no team. Empty covers all teams.
|
||||||
|
Team string `form:"team,omitempty"`
|
||||||
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
|
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchasingAnalyticsResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
|
Team string `json:"team,omitempty"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
GroupBy string `json:"group_by"`
|
||||||
|
Summary PurchasingSummary `json:"summary"`
|
||||||
|
Data []PurchasingAnalyticsData `json:"data"`
|
||||||
|
IngredientData []PurchasingIngredientData `json:"ingredient_data"`
|
||||||
|
VendorData []PurchasingVendorData `json:"vendor_data"`
|
||||||
|
TeamData []PurchasingTeamData `json:"team_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchasingTeamData is one team's share of the purchases. Scope and CategoryID
|
||||||
|
// are exactly what the team filter takes, so a row doubles as a drill-down link.
|
||||||
|
type PurchasingTeamData struct {
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
CategoryID *uuid.UUID `json:"category_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
|
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Percentage float64 `json:"percentage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchasingSummary struct {
|
||||||
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||||
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
TotalQuantity float64 `json:"total_quantity"`
|
||||||
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||||
|
TotalIngredients int64 `json:"total_ingredients"`
|
||||||
|
TotalVendors int64 `json:"total_vendors"`
|
||||||
|
TotalTeams int64 `json:"total_teams"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchasingAnalyticsData struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Purchases float64 `json:"purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Ingredients int64 `json:"ingredients"`
|
||||||
|
Vendors int64 `json:"vendors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchasingIngredientData struct {
|
||||||
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
|
IngredientName string `json:"ingredient_name"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
AverageUnitCost float64 `json:"average_unit_cost"`
|
||||||
|
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchasingVendorData struct {
|
||||||
|
VendorID *uuid.UUID `json:"vendor_id"`
|
||||||
|
VendorName string `json:"vendor_name"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||||
|
IngredientCount int64 `json:"ingredient_count"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
}
|
||||||
|
|
||||||
// ProductAnalyticsRequest represents the request for product analytics
|
// ProductAnalyticsRequest represents the request for product analytics
|
||||||
type ProductAnalyticsRequest struct {
|
type ProductAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
Limit int `form:"limit,default=1000" validate:"min=1,max=1000"`
|
Limit int `form:"limit,default=1000" validate:"min=1,max=1000"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductAnalyticsResponse represents the response for product analytics
|
// ProductAnalyticsResponse represents the response for product analytics
|
||||||
type ProductAnalyticsResponse struct {
|
type ProductAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
Data []ProductAnalyticsData `json:"data"`
|
Data []ProductAnalyticsData `json:"data"`
|
||||||
@@ -105,6 +193,7 @@ type ProductAnalyticsData struct {
|
|||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
ProductSku string `json:"product_sku"`
|
ProductSku string `json:"product_sku"`
|
||||||
|
ProductPrice float64 `json:"product_price"`
|
||||||
CategoryID uuid.UUID `json:"category_id"`
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
CategoryName string `json:"category_name"`
|
CategoryName string `json:"category_name"`
|
||||||
CategoryOrder int `json:"category_order"`
|
CategoryOrder int `json:"category_order"`
|
||||||
@@ -123,15 +212,16 @@ type ProductAnalyticsData struct {
|
|||||||
// ProductAnalyticsPerCategoryRequest represents the request for product analytics per category
|
// ProductAnalyticsPerCategoryRequest represents the request for product analytics per category
|
||||||
type ProductAnalyticsPerCategoryRequest struct {
|
type ProductAnalyticsPerCategoryRequest struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductAnalyticsPerCategoryResponse represents the response for product analytics per category
|
// ProductAnalyticsPerCategoryResponse represents the response for product analytics per category
|
||||||
type ProductAnalyticsPerCategoryResponse struct {
|
type ProductAnalyticsPerCategoryResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
Data []ProductAnalyticsPerCategoryData `json:"data"`
|
Data []ProductAnalyticsPerCategoryData `json:"data"`
|
||||||
@@ -149,18 +239,148 @@ type ProductAnalyticsPerCategoryData struct {
|
|||||||
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProductAnalyticsPerParentCategoryRequest represents the request for product analytics per parent category
|
||||||
|
type ProductAnalyticsPerParentCategoryRequest struct {
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProductAnalyticsPerParentCategoryResponse represents the response for product analytics per parent category
|
||||||
|
type ProductAnalyticsPerParentCategoryResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
Data []ProductAnalyticsPerParentCategoryData `json:"data"`
|
||||||
|
Budget BudgetCutOff `json:"budget"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductAnalyticsPerParentCategoryData struct {
|
||||||
|
ParentCategoryID uuid.UUID `json:"parent_category_id"`
|
||||||
|
ParentCategoryName string `json:"parent_category_name"`
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
TotalQuantity int64 `json:"total_quantity"`
|
||||||
|
CategoryCount int64 `json:"category_count"`
|
||||||
|
ProductCount int64 `json:"product_count"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||||
|
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||||
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentCategoryAnalyticsDetailRequest represents the request for the drill-down of one parent category
|
||||||
|
type ParentCategoryAnalyticsDetailRequest struct {
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
ParentCategoryID string
|
||||||
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentCategoryAnalyticsDetailResponse represents the drill-down of one parent category
|
||||||
|
type ParentCategoryAnalyticsDetailResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
ParentCategoryID uuid.UUID `json:"parent_category_id"`
|
||||||
|
ParentCategoryName string `json:"parent_category_name"`
|
||||||
|
Summary ParentCategoryAnalyticsDetailSummary `json:"summary"`
|
||||||
|
Categories []ParentCategoryAnalyticsDetailData `json:"categories"`
|
||||||
|
Budget BudgetCutOff `json:"budget"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParentCategoryAnalyticsDetailSummary struct {
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
TotalQuantity int64 `json:"total_quantity"`
|
||||||
|
CategoryCount int64 `json:"category_count"`
|
||||||
|
ProductCount int64 `json:"product_count"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||||
|
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||||
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParentCategoryAnalyticsDetailData struct {
|
||||||
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
TotalQuantity int64 `json:"total_quantity"`
|
||||||
|
ProductCount int64 `json:"product_count"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||||
|
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||||
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
|
Products []ParentCategoryAnalyticsProductData `json:"products"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParentCategoryAnalyticsProductData struct {
|
||||||
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
ProductSku string `json:"product_sku"`
|
||||||
|
ProductPrice float64 `json:"product_price"`
|
||||||
|
QuantitySold int64 `json:"quantity_sold"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
AveragePrice float64 `json:"average_price"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
StandardHppPerUnit float64 `json:"standard_hpp_per_unit"`
|
||||||
|
StandardHppTotal float64 `json:"standard_hpp_total"`
|
||||||
|
FifoHppPerUnit float64 `json:"fifo_hpp_per_unit"`
|
||||||
|
FifoHppTotal float64 `json:"fifo_hpp_total"`
|
||||||
|
MovingAverageHppPerUnit float64 `json:"moving_average_hpp_per_unit"`
|
||||||
|
MovingAverageHppTotal float64 `json:"moving_average_hpp_total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BudgetCutOff is the Monday-to-Sunday spending limit breakdown attached to the
|
||||||
|
// parent category reports.
|
||||||
|
type BudgetCutOff struct {
|
||||||
|
Percentages BudgetPercentages `json:"percentages"`
|
||||||
|
CutOffFrom time.Time `json:"cut_off_from"`
|
||||||
|
CutOffTo time.Time `json:"cut_off_to"`
|
||||||
|
Total BudgetPeriod `json:"total"`
|
||||||
|
Weekly []BudgetPeriod `json:"weekly"`
|
||||||
|
Monthly []BudgetMonthPeriod `json:"monthly"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BudgetPercentages struct {
|
||||||
|
Purchase float64 `json:"purchase"`
|
||||||
|
Owner float64 `json:"owner"`
|
||||||
|
Team float64 `json:"team"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BudgetPeriod struct {
|
||||||
|
PeriodStart time.Time `json:"period_start"`
|
||||||
|
PeriodEnd time.Time `json:"period_end"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
LimitPurchase float64 `json:"limit_purchase"`
|
||||||
|
LimitOwner float64 `json:"limit_owner"`
|
||||||
|
LimitTeam float64 `json:"limit_team"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BudgetMonthPeriod struct {
|
||||||
|
Month string `json:"month"`
|
||||||
|
WeekCount int `json:"week_count"`
|
||||||
|
BudgetPeriod
|
||||||
|
}
|
||||||
|
|
||||||
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
||||||
type DashboardAnalyticsRequest struct {
|
type DashboardAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DashboardAnalyticsResponse represents the response for dashboard analytics
|
// DashboardAnalyticsResponse represents the response for dashboard analytics
|
||||||
type DashboardAnalyticsResponse struct {
|
type DashboardAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
Overview DashboardOverview `json:"overview"`
|
Overview DashboardOverview `json:"overview"`
|
||||||
@@ -171,36 +391,58 @@ type DashboardAnalyticsResponse struct {
|
|||||||
|
|
||||||
// DashboardOverview represents the overview data for dashboard
|
// DashboardOverview represents the overview data for dashboard
|
||||||
type DashboardOverview struct {
|
type DashboardOverview struct {
|
||||||
TotalSales float64 `json:"total_sales"`
|
TotalSales float64 `json:"total_sales"`
|
||||||
TotalOrders int64 `json:"total_orders"`
|
TotalOrders int64 `json:"total_orders"`
|
||||||
AverageOrderValue float64 `json:"average_order_value"`
|
AverageOrderValue float64 `json:"average_order_value"`
|
||||||
TotalCustomers int64 `json:"total_customers"`
|
TotalCustomers int64 `json:"total_customers"`
|
||||||
VoidedOrders int64 `json:"voided_orders"`
|
VoidedOrders int64 `json:"voided_orders"`
|
||||||
RefundedOrders int64 `json:"refunded_orders"`
|
RefundedOrders int64 `json:"refunded_orders"`
|
||||||
|
TotalItemSold int64 `json:"total_item_sold"`
|
||||||
|
TotalLowStock int64 `json:"total_low_stock"`
|
||||||
|
TotalProductActive int64 `json:"total_product_active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossAnalyticsRequest represents the request for profit and loss analytics
|
|
||||||
type ProfitLossAnalyticsRequest struct {
|
type ProfitLossAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossAnalyticsResponse represents the response for profit and loss analytics
|
|
||||||
type ProfitLossAnalyticsResponse struct {
|
type ProfitLossAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
GroupBy string `json:"group_by"`
|
DateTo time.Time `json:"date_to"`
|
||||||
Summary ProfitLossSummary `json:"summary"`
|
GroupBy string `json:"group_by"`
|
||||||
Data []ProfitLossData `json:"data"`
|
Summary ProfitLossSummary `json:"summary"`
|
||||||
ProductData []ProductProfitData `json:"product_data"`
|
Data []ProfitLossData `json:"data"`
|
||||||
|
ProductData []ProductProfitData `json:"product_data"`
|
||||||
|
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
||||||
|
Purchasing ProfitLossPurchasing `json:"purchasing"`
|
||||||
|
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
||||||
|
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfitLossPurchasing struct {
|
||||||
|
TodayTotal float64 `json:"today_total"`
|
||||||
|
MtdTotal float64 `json:"mtd_total"`
|
||||||
|
TodayRawMaterial float64 `json:"today_raw_material"`
|
||||||
|
MtdRawMaterial float64 `json:"mtd_raw_material"`
|
||||||
|
TodayExpense float64 `json:"today_expense"`
|
||||||
|
MtdExpense float64 `json:"mtd_expense"`
|
||||||
|
Items []ProfitLossPurchasingItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfitLossPurchasingItem struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Item string `json:"item"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Nominal float64 `json:"nominal"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossSummary represents the summary of profit and loss analytics
|
|
||||||
type ProfitLossSummary struct {
|
type ProfitLossSummary struct {
|
||||||
TotalRevenue float64 `json:"total_revenue"`
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
TotalCost float64 `json:"total_cost"`
|
TotalCost float64 `json:"total_cost"`
|
||||||
@@ -215,7 +457,6 @@ type ProfitLossSummary struct {
|
|||||||
ProfitabilityRatio float64 `json:"profitability_ratio"`
|
ProfitabilityRatio float64 `json:"profitability_ratio"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossData represents individual profit and loss data point by time period
|
|
||||||
type ProfitLossData struct {
|
type ProfitLossData struct {
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Revenue float64 `json:"revenue"`
|
Revenue float64 `json:"revenue"`
|
||||||
@@ -229,7 +470,6 @@ type ProfitLossData struct {
|
|||||||
Orders int64 `json:"orders"`
|
Orders int64 `json:"orders"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductProfitData represents profit data for individual products
|
|
||||||
type ProductProfitData struct {
|
type ProductProfitData struct {
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
@@ -244,3 +484,139 @@ type ProductProfitData struct {
|
|||||||
AverageCost float64 `json:"average_cost"`
|
AverageCost float64 `json:"average_cost"`
|
||||||
ProfitPerUnit float64 `json:"profit_per_unit"`
|
ProfitPerUnit float64 `json:"profit_per_unit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProfitLossSummaryRow struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
IsBold bool `json:"is_bold"`
|
||||||
|
TodayNominal float64 `json:"today_nominal"`
|
||||||
|
TodayPct float64 `json:"today_pct"`
|
||||||
|
MtdNominal float64 `json:"mtd_nominal"`
|
||||||
|
MtdPct float64 `json:"mtd_pct"`
|
||||||
|
SubItems []ProfitLossSummaryRow `json:"sub_items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperationalExpenseItem struct {
|
||||||
|
Item string `json:"item"`
|
||||||
|
Nominal float64 `json:"nominal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryPeriodRequest struct {
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
|
ExcludeGajiStaffFromReimburse bool `form:"exclude_gaji_staff_from_reimburse"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryMonthlyRequest struct {
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
|
Month string `form:"month" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryMTDRequest struct {
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
|
ExcludeGajiStaffFromReimburse bool `form:"exclude_gaji_staff_from_reimburse"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryPeriodResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
|
Period ExclusiveSummaryPeriodRange `json:"period"`
|
||||||
|
Summary ExclusiveSummaryPeriodSummary `json:"summary"`
|
||||||
|
Reimburse ExclusiveSummaryReimburse `json:"reimburse"`
|
||||||
|
HPPBreakdown []ExclusiveSummaryCategoryBreakdown `json:"hpp_breakdown"`
|
||||||
|
OperationalExpenseBreakdown []ExclusiveSummaryCategoryBreakdown `json:"operational_expense_breakdown"`
|
||||||
|
DailySummary []ExclusiveSummaryDailySummary `json:"daily_summary"`
|
||||||
|
DailyTransactions []ExclusiveSummaryDailyTransaction `json:"daily_transactions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryPeriodRange struct {
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryPeriodSummary struct {
|
||||||
|
Sales float64 `json:"sales"`
|
||||||
|
HPP float64 `json:"hpp"`
|
||||||
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
|
SalaryTotal float64 `json:"salary_total"`
|
||||||
|
SalaryDW float64 `json:"salary_dw"`
|
||||||
|
SalaryStaff float64 `json:"salary_staff"`
|
||||||
|
SalaryOther float64 `json:"salary_other"`
|
||||||
|
OtherOperationalExpenses float64 `json:"other_operational_expenses"`
|
||||||
|
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
NetProfit float64 `json:"net_profit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryReimburse struct {
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
ExcludedSalaryStaff float64 `json:"excluded_salary_staff"`
|
||||||
|
TotalReimburse float64 `json:"total_reimburse"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryCategoryBreakdown struct {
|
||||||
|
CategoryCode string `json:"category_code"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Percentage float64 `json:"percentage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryDailySummary struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
TransactionCount int64 `json:"transaction_count"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryDailyTransaction struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
CategoryCode string `json:"category_code"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryMonthlyResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
|
Month string `json:"month"`
|
||||||
|
Summary ExclusiveSummaryMonthlySummary `json:"summary"`
|
||||||
|
Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"`
|
||||||
|
BankBalance []ExclusiveSummaryBankBalance `json:"bank_balance"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryMonthlySummary struct {
|
||||||
|
TotalSales float64 `json:"total_sales"`
|
||||||
|
HPP float64 `json:"hpp"`
|
||||||
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
|
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
NetProfit float64 `json:"net_profit"`
|
||||||
|
NetProfitMargin float64 `json:"net_profit_margin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryMonthlyPeriod struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
Sales float64 `json:"sales"`
|
||||||
|
HPP float64 `json:"hpp"`
|
||||||
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
|
GrossMargin float64 `json:"gross_margin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryBankBalance struct {
|
||||||
|
Bank string `json:"bank"`
|
||||||
|
OpeningBalance *float64 `json:"opening_balance"`
|
||||||
|
IncomingMutation *float64 `json:"incoming_mutation"`
|
||||||
|
OutgoingMutation *float64 `json:"outgoing_mutation"`
|
||||||
|
ClosingBalance *float64 `json:"closing_balance"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ type CreateCategoryRequest struct {
|
|||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
BusinessType *string `json:"business_type,omitempty"`
|
BusinessType *string `json:"business_type,omitempty"`
|
||||||
Order *int `json:"order,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Order *int `json:"order,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,12 +20,17 @@ type UpdateCategoryRequest struct {
|
|||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
BusinessType *string `json:"business_type,omitempty"`
|
BusinessType *string `json:"business_type,omitempty"`
|
||||||
Order *int `json:"order,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Order *int `json:"order,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListCategoriesRequest struct {
|
type ListCategoriesRequest struct {
|
||||||
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
|
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty" validate:"omitempty,oneof=parent child"`
|
||||||
BusinessType string `json:"business_type,omitempty"`
|
BusinessType string `json:"business_type,omitempty"`
|
||||||
Search string `json:"search,omitempty"`
|
Search string `json:"search,omitempty"`
|
||||||
Page int `json:"page" validate:"required,min=1"`
|
Page int `json:"page" validate:"required,min=1"`
|
||||||
@@ -34,10 +41,13 @@ type ListCategoriesRequest struct {
|
|||||||
type CategoryResponse struct {
|
type CategoryResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
ParentName *string `json:"parent_name,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
BusinessType string `json:"business_type"`
|
BusinessType string `json:"business_type"`
|
||||||
Order int `json:"order"`
|
Order int `json:"order"`
|
||||||
Metadata map[string]interface{} `json:"metadata"`
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateExpenseRequest struct {
|
||||||
|
Receiver string `json:"receiver" validate:"required"`
|
||||||
|
TransactionDate string `json:"transaction_date" validate:"required"`
|
||||||
|
CodeNumber string `json:"code_number" validate:"required"`
|
||||||
|
OutletID string `json:"outlet_id" validate:"required"`
|
||||||
|
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved cancel"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
Total float64 `json:"total" validate:"required"`
|
||||||
|
Items []CreateExpenseItemRequest `json:"items" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateExpenseItemRequest struct {
|
||||||
|
ChartOfAccountID string `json:"chart_of_account_id" validate:"required"`
|
||||||
|
PurchaseCategoryID string `json:"purchase_category_id" validate:"required"`
|
||||||
|
Item string `json:"item" validate:"required"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Amount float64 `json:"amount" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateExpenseRequest struct {
|
||||||
|
Receiver *string `json:"receiver,omitempty"`
|
||||||
|
TransactionDate *string `json:"transaction_date,omitempty"`
|
||||||
|
CodeNumber *string `json:"code_number,omitempty"`
|
||||||
|
OutletID *string `json:"outlet_id,omitempty"`
|
||||||
|
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved cancel"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Tax *float64 `json:"tax,omitempty"`
|
||||||
|
Total *float64 `json:"total,omitempty"`
|
||||||
|
Reserved1 *string `json:"reserved1,omitempty"`
|
||||||
|
Items []UpdateExpenseItemRequest `json:"items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateExpenseItemRequest struct {
|
||||||
|
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
||||||
|
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"`
|
||||||
|
Item *string `json:"item,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Amount *float64 `json:"amount,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID uuid.UUID `json:"outlet_id"`
|
||||||
|
Receiver string `json:"receiver"`
|
||||||
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
|
CodeNumber string `json:"code_number"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
Reserved1 *string `json:"reserved1,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Items []ExpenseItemResponse `json:"items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseItemResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
ExpenseID uuid.UUID `json:"expense_id"`
|
||||||
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
|
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
||||||
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
|
PurchaseCategoryName string `json:"purchase_category_name,omitempty"`
|
||||||
|
PurchaseCategoryType string `json:"purchase_category_type,omitempty"`
|
||||||
|
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||||
|
Item string `json:"item"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListExpenseRequest struct {
|
||||||
|
Page int `json:"page" validate:"min=1"`
|
||||||
|
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||||
|
Search string `json:"search,omitempty"`
|
||||||
|
OutletID string `json:"outlet_id,omitempty"`
|
||||||
|
Status string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved cancel"`
|
||||||
|
StartDate string `json:"start_date,omitempty"`
|
||||||
|
EndDate string `json:"end_date,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListExpenseResponse struct {
|
||||||
|
Expenses []ExpenseResponse `json:"expenses"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsRequest struct {
|
||||||
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
|
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
GroupBy string `json:"group_by"`
|
||||||
|
Summary ExpenseAnalyticsSummary `json:"summary"`
|
||||||
|
Data []ExpenseAnalyticsData `json:"data"`
|
||||||
|
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
||||||
|
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData `json:"chart_of_account_data"`
|
||||||
|
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsSummary struct {
|
||||||
|
TotalExpenses float64 `json:"total_expenses"`
|
||||||
|
TotalExpenseCount int64 `json:"total_expense_count"`
|
||||||
|
TotalTax float64 `json:"total_tax"`
|
||||||
|
AverageExpenseValue float64 `json:"average_expense_value"`
|
||||||
|
TotalCategories int64 `json:"total_categories"`
|
||||||
|
TotalItems int64 `json:"total_items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsData struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Expenses float64 `json:"expenses"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
Items int64 `json:"items"`
|
||||||
|
Categories int64 `json:"categories"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsCategoryData struct {
|
||||||
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
|
PurchaseCategoryName string `json:"purchase_category_name"`
|
||||||
|
PurchaseCategoryType string `json:"purchase_category_type"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
ItemCount int64 `json:"item_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsChartOfAccountData struct {
|
||||||
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
|
ChartOfAccountName string `json:"chart_of_account_name"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
ItemCount int64 `json:"item_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsItemData struct {
|
||||||
|
Item string `json:"item"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
ItemCount int64 `json:"item_count"`
|
||||||
|
}
|
||||||
@@ -77,8 +77,7 @@ type ListIngredientUnitConvertersResponse struct {
|
|||||||
type IngredientUnitsResponse struct {
|
type IngredientUnitsResponse struct {
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
IngredientName string `json:"ingredient_name"`
|
IngredientName string `json:"ingredient_name"`
|
||||||
BaseUnitID uuid.UUID `json:"base_unit_id"`
|
BaseUnitID *uuid.UUID `json:"base_unit_id"`
|
||||||
BaseUnitName string `json:"base_unit_name"`
|
BaseUnitName string `json:"base_unit_name"`
|
||||||
Units []*UnitResponse `json:"units"`
|
Units []*UnitResponse `json:"units"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ type AdjustInventoryRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RestockInventoryRequest struct {
|
type RestockInventoryRequest struct {
|
||||||
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||||
Items []RestockItem `json:"items" validate:"required,min=1,dive"`
|
Items []RestockItem `json:"items" validate:"required,min=1,dive"`
|
||||||
Reason string `json:"reason" validate:"required,min=1,max=255"`
|
Reason string `json:"reason" validate:"required,min=1,max=255"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RestockItem struct {
|
type RestockItem struct {
|
||||||
@@ -82,10 +82,10 @@ type InventoryAdjustmentResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RestockInventoryResponse struct {
|
type RestockInventoryResponse struct {
|
||||||
OutletID uuid.UUID `json:"outlet_id"`
|
OutletID uuid.UUID `json:"outlet_id"`
|
||||||
Items []RestockItemResult `json:"items"`
|
Items []RestockItemResult `json:"items"`
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
RestockedAt time.Time `json:"restocked_at"`
|
RestockedAt time.Time `json:"restocked_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RestockItemResult struct {
|
type RestockItemResult struct {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- Request contracts ----
|
||||||
|
|
||||||
|
type SendNotificationRequest struct {
|
||||||
|
Title string `json:"title" validate:"required,min=1,max=255"`
|
||||||
|
Body string `json:"body" validate:"required"`
|
||||||
|
Type string `json:"type,omitempty" validate:"omitempty,max=100"`
|
||||||
|
Category string `json:"category,omitempty" validate:"omitempty,max=100"`
|
||||||
|
Priority entities.NotificationPriority `json:"priority,omitempty" validate:"omitempty,oneof=low normal high"`
|
||||||
|
ImageURL string `json:"image_url,omitempty" validate:"omitempty,max=512"`
|
||||||
|
ActionURL string `json:"action_url,omitempty" validate:"omitempty,max=512"`
|
||||||
|
NotifiableType string `json:"notifiable_type,omitempty" validate:"omitempty,max=100"`
|
||||||
|
NotifiableID *uuid.UUID `json:"notifiable_id,omitempty"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
ReceiverIDs []uuid.UUID `json:"receiver_ids" validate:"required,min=1"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BroadcastNotificationRequest struct {
|
||||||
|
Title string `json:"title" validate:"required,min=1,max=255"`
|
||||||
|
Body string `json:"body" validate:"required"`
|
||||||
|
Type string `json:"type,omitempty" validate:"omitempty,max=100"`
|
||||||
|
Category string `json:"category,omitempty" validate:"omitempty,max=100"`
|
||||||
|
Priority entities.NotificationPriority `json:"priority,omitempty" validate:"omitempty,oneof=low normal high"`
|
||||||
|
ImageURL string `json:"image_url,omitempty" validate:"omitempty,max=512"`
|
||||||
|
ActionURL string `json:"action_url,omitempty" validate:"omitempty,max=512"`
|
||||||
|
NotifiableType string `json:"notifiable_type,omitempty" validate:"omitempty,max=100"`
|
||||||
|
NotifiableID *uuid.UUID `json:"notifiable_id,omitempty"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListNotificationsRequest struct {
|
||||||
|
Page int `form:"page" validate:"min=1"`
|
||||||
|
Limit int `form:"limit" validate:"min=1,max=100"`
|
||||||
|
IsRead *bool `form:"is_read"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Response contracts ----
|
||||||
|
|
||||||
|
type NotificationResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Priority entities.NotificationPriority `json:"priority"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
ActionURL string `json:"action_url"`
|
||||||
|
NotifiableType string `json:"notifiable_type"`
|
||||||
|
NotifiableID *uuid.UUID `json:"notifiable_id"`
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||||
|
SentAt *time.Time `json:"sent_at"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at"`
|
||||||
|
CreatedBy *uuid.UUID `json:"created_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationReceiverResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
NotificationID uuid.UUID `json:"notification_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
IsRead bool `json:"is_read"`
|
||||||
|
ReadAt *time.Time `json:"read_at"`
|
||||||
|
IsDeleted bool `json:"is_deleted"`
|
||||||
|
DeletedAt *time.Time `json:"deleted_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Notification *NotificationResponse `json:"notification,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListNotificationsResponse struct {
|
||||||
|
Notifications []*NotificationReceiverResponse `json:"notifications"`
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
UnreadCount int64 `json:"unread_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
@@ -98,6 +98,8 @@ type OrderItemResponse struct {
|
|||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
ProductVariantID *uuid.UUID `json:"product_variant_id"`
|
ProductVariantID *uuid.UUID `json:"product_variant_id"`
|
||||||
ProductVariantName *string `json:"product_variant_name,omitempty"`
|
ProductVariantName *string `json:"product_variant_name,omitempty"`
|
||||||
|
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||||
|
CategoryName *string `json:"category_name,omitempty"`
|
||||||
Quantity int `json:"quantity"`
|
Quantity int `json:"quantity"`
|
||||||
UnitPrice float64 `json:"unit_price"`
|
UnitPrice float64 `json:"unit_price"`
|
||||||
TotalPrice float64 `json:"total_price"`
|
TotalPrice float64 `json:"total_price"`
|
||||||
@@ -108,6 +110,7 @@ type OrderItemResponse struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
PrinterType string `json:"printer_type"`
|
PrinterType string `json:"printer_type"`
|
||||||
|
PrintToChecker bool `json:"print_to_checker"`
|
||||||
PaidQuantity int `json:"paid_quantity"`
|
PaidQuantity int `json:"paid_quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
type CreateProductRequest struct {
|
type CreateProductRequest struct {
|
||||||
CategoryID uuid.UUID `json:"category_id" validate:"required"`
|
CategoryID uuid.UUID `json:"category_id" validate:"required"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
SKU *string `json:"sku,omitempty"`
|
SKU *string `json:"sku,omitempty"`
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
@@ -16,28 +17,30 @@ type CreateProductRequest struct {
|
|||||||
BusinessType *string `json:"business_type,omitempty"`
|
BusinessType *string `json:"business_type,omitempty"`
|
||||||
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
||||||
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
||||||
|
PrintToChecker *bool `json:"print_to_checker,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
Variants []CreateProductVariantRequest `json:"variants,omitempty"`
|
Variants []CreateProductVariantRequest `json:"variants,omitempty"`
|
||||||
InitialStock *int `json:"initial_stock,omitempty" validate:"omitempty,min=0"` // Initial stock quantity for all outlets
|
InitialStock *int `json:"initial_stock,omitempty" validate:"omitempty,min=0"`
|
||||||
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` // Reorder level for all outlets
|
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"`
|
||||||
CreateInventory bool `json:"create_inventory,omitempty"` // Whether to create inventory records for all outlets
|
CreateInventory bool `json:"create_inventory,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateProductRequest struct {
|
type UpdateProductRequest struct {
|
||||||
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
SKU *string `json:"sku,omitempty"`
|
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
SKU *string `json:"sku,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Price *float64 `json:"price,omitempty" validate:"omitempty,min=0"`
|
Description *string `json:"description,omitempty"`
|
||||||
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
|
Price *float64 `json:"price,omitempty" validate:"omitempty,min=0"`
|
||||||
BusinessType *string `json:"business_type,omitempty"`
|
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
|
||||||
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
BusinessType *string `json:"business_type,omitempty"`
|
||||||
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
PrintToChecker *bool `json:"print_to_checker,omitempty"`
|
||||||
// Stock management fields
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` // Update reorder level for all existing inventory records
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateProductVariantRequest struct {
|
type CreateProductVariantRequest struct {
|
||||||
@@ -56,24 +59,27 @@ type UpdateProductVariantRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProductResponse struct {
|
type ProductResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
CategoryID uuid.UUID `json:"category_id"`
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
CategoryName string `json:"category_name"`
|
CategoryName string `json:"category_name"`
|
||||||
SKU *string `json:"sku"`
|
SKU *string `json:"sku"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
Cost float64 `json:"cost"`
|
OutletPrice *float64 `json:"outlet_price,omitempty"`
|
||||||
BusinessType string `json:"business_type"`
|
OutletPrices []ProductOutletPriceResponse `json:"outlet_prices,omitempty"`
|
||||||
ImageURL *string `json:"image_url"`
|
Cost float64 `json:"cost"`
|
||||||
PrinterType string `json:"printer_type"`
|
BusinessType string `json:"business_type"`
|
||||||
Metadata map[string]interface{} `json:"metadata"`
|
ImageURL *string `json:"image_url"`
|
||||||
IsActive bool `json:"is_active"`
|
PrinterType string `json:"printer_type"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
PrintToChecker bool `json:"print_to_checker"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
Category *CategoryResponse `json:"category,omitempty"`
|
IsActive bool `json:"is_active"`
|
||||||
Variants []ProductVariantResponse `json:"variants,omitempty"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Category *CategoryResponse `json:"category,omitempty"`
|
||||||
|
Variants []ProductVariantResponse `json:"variants,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductVariantResponse struct {
|
type ProductVariantResponse struct {
|
||||||
@@ -89,6 +95,7 @@ type ProductVariantResponse struct {
|
|||||||
|
|
||||||
type ListProductsRequest struct {
|
type ListProductsRequest struct {
|
||||||
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
|
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||||
BusinessType string `json:"business_type,omitempty"`
|
BusinessType string `json:"business_type,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateProductOutletPriceRequest struct {
|
||||||
|
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||||
|
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||||
|
Price float64 `json:"price" validate:"required,min=0"`
|
||||||
|
PrintToChecker bool `json:"print_to_checker"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProductOutletPriceRequest struct {
|
||||||
|
Price float64 `json:"price" validate:"required,min=0"`
|
||||||
|
PrintToChecker *bool `json:"print_to_checker"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductOutletPriceResponse struct {
|
||||||
|
ID uuid.UUID `json:"id,omitempty"`
|
||||||
|
ProductID uuid.UUID `json:"product_id,omitempty"`
|
||||||
|
OutletID uuid.UUID `json:"outlet_id"`
|
||||||
|
OutletName string `json:"outlet_name,omitempty"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
PrintToChecker bool `json:"print_to_checker"`
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListProductOutletPricesResponse struct {
|
||||||
|
Prices []ProductOutletPriceResponse `json:"prices"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkCreateProductOutletPriceRequest struct {
|
||||||
|
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||||
|
Prices []CreateProductOutletPricePerOutletRequest `json:"prices" validate:"required,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateProductOutletPricePerOutletRequest struct {
|
||||||
|
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||||
|
Price float64 `json:"price" validate:"required,min=0"`
|
||||||
|
PrintToChecker bool `json:"print_to_checker"`
|
||||||
|
}
|
||||||
@@ -34,34 +34,34 @@ type BulkCreateProductRecipeRequest struct {
|
|||||||
|
|
||||||
// Response structures
|
// Response structures
|
||||||
type ProductRecipeResponse struct {
|
type ProductRecipeResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
VariantID *uuid.UUID `json:"variant_id"`
|
VariantID *uuid.UUID `json:"variant_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
WastePercentage float64 `json:"waste_percentage"`
|
WastePercentage float64 `json:"waste_percentage"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Product *ProductResponse `json:"product,omitempty"`
|
Product *ProductResponse `json:"product,omitempty"`
|
||||||
ProductVariant *ProductVariantResponse `json:"product_variant,omitempty"`
|
ProductVariant *ProductVariantResponse `json:"product_variant,omitempty"`
|
||||||
Ingredient *ProductRecipeIngredientResponse `json:"ingredient,omitempty"`
|
Ingredient *ProductRecipeIngredientResponse `json:"ingredient,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductRecipeIngredientResponse struct {
|
type ProductRecipeIngredientResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
UnitID *uuid.UUID `json:"unit_id"`
|
||||||
Cost float64 `json:"cost"`
|
Cost float64 `json:"cost"`
|
||||||
Stock float64 `json:"stock"`
|
Stock float64 `json:"stock"`
|
||||||
IsSemiFinished bool `json:"is_semi_finished"`
|
IsSemiFinished bool `json:"is_semi_finished"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
Metadata map[string]interface{} `json:"metadata"`
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Unit *ProductRecipeUnitResponse `json:"unit,omitempty"`
|
Unit *ProductRecipeUnitResponse `json:"unit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,4 +71,4 @@ type ProductRecipeUnitResponse struct {
|
|||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreatePurchaseCategoryRequest struct {
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Code *string `json:"code,omitempty"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=raw_material expense"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePurchaseCategoryRequest struct {
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Code *string `json:"code,omitempty"`
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
|
Type *string `json:"type,omitempty" validate:"omitempty,oneof=raw_material expense"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPurchaseCategoriesRequest struct {
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty" validate:"omitempty,oneof=raw_material expense"`
|
||||||
|
Search string `json:"search,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
Page int `json:"page" validate:"required,min=1"`
|
||||||
|
Limit int `json:"limit" validate:"required,min=1,max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategoryResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
PresetID *uuid.UUID `json:"preset_id"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
IsSystem bool `json:"is_system"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPurchaseCategoriesResponse struct {
|
||||||
|
PurchaseCategories []PurchaseCategoryResponse `json:"purchase_categories"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
@@ -7,76 +7,102 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreatePurchaseOrderRequest struct {
|
type CreatePurchaseOrderRequest struct {
|
||||||
VendorID uuid.UUID `json:"vendor_id" validate:"required"`
|
VendorID *uuid.UUID `json:"vendor_id,omitempty" validate:"omitempty"`
|
||||||
PONumber string `json:"po_number" validate:"required,min=1,max=50"`
|
PONumber string `json:"po_number" validate:"required,min=1,max=50"`
|
||||||
TransactionDate string `json:"transaction_date" validate:"required"` // Format: YYYY-MM-DD
|
TransactionDate string `json:"transaction_date" validate:"required"` // Format: YYYY-MM-DD
|
||||||
DueDate string `json:"due_date" validate:"required"` // Format: YYYY-MM-DD
|
DueDate *string `json:"due_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD
|
||||||
Reference *string `json:"reference,omitempty" validate:"omitempty,max=100"`
|
Reference *string `json:"reference,omitempty" validate:"omitempty,max=100"`
|
||||||
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"`
|
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"`
|
||||||
Message *string `json:"message,omitempty" validate:"omitempty"`
|
Message *string `json:"message,omitempty" validate:"omitempty"`
|
||||||
|
TeamScope *string `json:"team_scope,omitempty" validate:"omitempty,oneof=category central"`
|
||||||
|
TeamCategoryID *uuid.UUID `json:"team_category_id,omitempty" validate:"omitempty"`
|
||||||
Items []CreatePurchaseOrderItemRequest `json:"items" validate:"required,min=1,dive"`
|
Items []CreatePurchaseOrderItemRequest `json:"items" validate:"required,min=1,dive"`
|
||||||
AttachmentFileIDs []uuid.UUID `json:"attachment_file_ids,omitempty"`
|
AttachmentFileIDs []uuid.UUID `json:"attachment_file_ids,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreatePurchaseOrderItemRequest struct {
|
type CreatePurchaseOrderItemRequest struct {
|
||||||
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id" validate:"required"`
|
||||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||||
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
||||||
Amount float64 `json:"amount" validate:"required,gte=0"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
||||||
|
Amount float64 `json:"amount" validate:"required,gte=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePurchaseOrderRequest struct {
|
type UpdatePurchaseOrderRequest struct {
|
||||||
VendorID *uuid.UUID `json:"vendor_id,omitempty" validate:"omitempty"`
|
VendorID *uuid.UUID `json:"vendor_id,omitempty" validate:"omitempty"`
|
||||||
PONumber *string `json:"po_number,omitempty" validate:"omitempty,min=1,max=50"`
|
PONumber *string `json:"po_number,omitempty" validate:"omitempty,min=1,max=50"`
|
||||||
TransactionDate *string `json:"transaction_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD
|
TransactionDate *string `json:"transaction_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD
|
||||||
DueDate *string `json:"due_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD
|
DueDate *string `json:"due_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD
|
||||||
Reference *string `json:"reference,omitempty" validate:"omitempty,max=100"`
|
Reference *string `json:"reference,omitempty" validate:"omitempty,max=100"`
|
||||||
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"`
|
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"`
|
||||||
Message *string `json:"message,omitempty" validate:"omitempty"`
|
Message *string `json:"message,omitempty" validate:"omitempty"`
|
||||||
|
// An empty string clears the team; omitting the field leaves it untouched.
|
||||||
|
TeamScope *string `json:"team_scope,omitempty" validate:"omitempty"`
|
||||||
|
TeamCategoryID *uuid.UUID `json:"team_category_id,omitempty" validate:"omitempty"`
|
||||||
Items []UpdatePurchaseOrderItemRequest `json:"items,omitempty" validate:"omitempty,dive"`
|
Items []UpdatePurchaseOrderItemRequest `json:"items,omitempty" validate:"omitempty,dive"`
|
||||||
AttachmentFileIDs []uuid.UUID `json:"attachment_file_ids,omitempty"`
|
AttachmentFileIDs []uuid.UUID `json:"attachment_file_ids,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePurchaseOrderItemRequest struct {
|
type UpdatePurchaseOrderItemRequest struct {
|
||||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty" validate:"omitempty"`
|
||||||
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||||
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
||||||
Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
||||||
|
Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderResponse struct {
|
type PurchaseOrderResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
VendorID uuid.UUID `json:"vendor_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
VendorID *uuid.UUID `json:"vendor_id"`
|
||||||
PONumber string `json:"po_number"`
|
PONumber string `json:"po_number"`
|
||||||
TransactionDate time.Time `json:"transaction_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
DueDate time.Time `json:"due_date"`
|
DueDate *time.Time `json:"due_date"`
|
||||||
Reference *string `json:"reference"`
|
Reference *string `json:"reference"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message *string `json:"message"`
|
Message *string `json:"message"`
|
||||||
TotalAmount float64 `json:"total_amount"`
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
TeamScope *string `json:"team_scope"`
|
||||||
|
TeamCategoryID *uuid.UUID `json:"team_category_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Team *PurchaseTeamResponse `json:"team,omitempty"`
|
||||||
Vendor *VendorResponse `json:"vendor,omitempty"`
|
Vendor *VendorResponse `json:"vendor,omitempty"`
|
||||||
Items []PurchaseOrderItemResponse `json:"items,omitempty"`
|
Items []PurchaseOrderItemResponse `json:"items,omitempty"`
|
||||||
Attachments []PurchaseOrderAttachmentResponse `json:"attachments,omitempty"`
|
Attachments []PurchaseOrderAttachmentResponse `json:"attachments,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PurchaseTeamResponse is one entry of the team picker. Teams come from the parent
|
||||||
|
// product categories; Pusat is the extra entry that has no category behind it, so
|
||||||
|
// its CategoryID is null.
|
||||||
|
type PurchaseTeamResponse struct {
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
CategoryID *uuid.UUID `json:"category_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPurchaseTeamsResponse struct {
|
||||||
|
Teams []PurchaseTeamResponse `json:"teams"`
|
||||||
|
}
|
||||||
|
|
||||||
type PurchaseOrderItemResponse struct {
|
type PurchaseOrderItemResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID *uuid.UUID `json:"ingredient_id"`
|
||||||
Description *string `json:"description"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Description *string `json:"description"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
Quantity *float64 `json:"quantity"`
|
||||||
Amount float64 `json:"amount"`
|
UnitID *uuid.UUID `json:"unit_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Amount float64 `json:"amount"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Unit *UnitResponse `json:"unit,omitempty"`
|
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||||
|
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||||
|
Unit *UnitResponse `json:"unit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderAttachmentResponse struct {
|
type PurchaseOrderAttachmentResponse struct {
|
||||||
@@ -88,13 +114,20 @@ type PurchaseOrderAttachmentResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ListPurchaseOrdersRequest struct {
|
type ListPurchaseOrdersRequest struct {
|
||||||
Page int `json:"page" validate:"min=1"`
|
Page int `json:"page" validate:"min=1"`
|
||||||
Limit int `json:"limit" validate:"min=1,max=100"`
|
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||||
Search string `json:"search,omitempty"`
|
Search string `json:"search,omitempty"`
|
||||||
Status string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"`
|
Status string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"`
|
||||||
VendorID *uuid.UUID `json:"vendor_id,omitempty"`
|
VendorID *uuid.UUID `json:"vendor_id,omitempty"`
|
||||||
StartDate *time.Time `json:"start_date,omitempty"`
|
// Team is the single-value form of the two filters below, so the team picker
|
||||||
EndDate *time.Time `json:"end_date,omitempty"`
|
// can send back what it was given: a parent category id, "central" for Pusat,
|
||||||
|
// or "none" for purchases with no team yet. It replaces them rather than
|
||||||
|
// narrowing alongside them.
|
||||||
|
Team string `json:"team,omitempty"`
|
||||||
|
TeamScope string `json:"team_scope,omitempty" validate:"omitempty,oneof=category central"`
|
||||||
|
TeamCategoryID *uuid.UUID `json:"team_category_id,omitempty"`
|
||||||
|
StartDate *time.Time `json:"start_date,omitempty"`
|
||||||
|
EndDate *time.Time `json:"end_date,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListPurchaseOrdersResponse struct {
|
type ListPurchaseOrdersResponse struct {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package contract
|
package contract
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,8 +48,10 @@ type SelfOrderMenuVariant struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SelfOrderCreateOrderRequest struct {
|
type SelfOrderCreateOrderRequest struct {
|
||||||
SessionID string `json:"session_id" validate:"required"`
|
SessionID string `json:"session_id" validate:"required"`
|
||||||
OrderItems []SelfOrderCreateOrderItem `json:"order_items" validate:"required,min=1,dive"`
|
CustomerName string `json:"customer_name" validate:"required"`
|
||||||
|
OrderType string `json:"order_type" validate:"required,oneof=dine_in takeaway delivery"`
|
||||||
|
OrderItems []SelfOrderCreateOrderItem `json:"order_items" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelfOrderCreateOrderItem struct {
|
type SelfOrderCreateOrderItem struct {
|
||||||
@@ -62,7 +62,7 @@ type SelfOrderCreateOrderItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SelfOrderListCategoriesRequest struct {
|
type SelfOrderListCategoriesRequest struct {
|
||||||
OrganizationID string `form:"organisasi_id" validate:"required"`
|
OrganizationID string `form:"organization_id" validate:"required"`
|
||||||
OutletID string `form:"outlet_id" validate:"required"`
|
OutletID string `form:"outlet_id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,35 +78,5 @@ type SelfOrderListCategoriesResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SelfOrderListOrdersResponse struct {
|
type SelfOrderListOrdersResponse struct {
|
||||||
Orders []SelfOrderOrderItem `json:"orders"`
|
Orders []OrderResponse `json:"orders"`
|
||||||
}
|
|
||||||
|
|
||||||
type SelfOrderOrderItem struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
OrderNumber string `json:"order_number"`
|
|
||||||
TableNumber *string `json:"table_number,omitempty"`
|
|
||||||
OrderType string `json:"order_type"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Subtotal float64 `json:"subtotal"`
|
|
||||||
TaxAmount float64 `json:"tax_amount"`
|
|
||||||
DiscountAmount float64 `json:"discount_amount"`
|
|
||||||
TotalAmount float64 `json:"total_amount"`
|
|
||||||
RemainingAmount float64 `json:"remaining_amount"`
|
|
||||||
PaymentStatus string `json:"payment_status"`
|
|
||||||
IsVoid bool `json:"is_void"`
|
|
||||||
IsRefund bool `json:"is_refund"`
|
|
||||||
Items []SelfOrderOrderLineItem `json:"items,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelfOrderOrderLineItem struct {
|
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"`
|
|
||||||
ProductVariantNam *string `json:"product_variant_name,omitempty"`
|
|
||||||
Quantity int `json:"quantity"`
|
|
||||||
UnitPrice float64 `json:"unit_price"`
|
|
||||||
TotalPrice float64 `json:"total_price"`
|
|
||||||
Notes *string `json:"notes,omitempty"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ type CreateUserRequest struct {
|
|||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
Email string `json:"email" validate:"required,email"`
|
Email string `json:"email" validate:"required,email"`
|
||||||
Password string `json:"password" validate:"required,min=6"`
|
Password string `json:"password" validate:"required,min=6"`
|
||||||
Role string `json:"role" validate:"required,oneof=admin manager cashier waiter"`
|
Role string `json:"role" validate:"required,oneof=admin manager cashier waiter owner purchasing"`
|
||||||
Permissions map[string]interface{} `json:"permissions,omitempty"`
|
Permissions map[string]interface{} `json:"permissions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserRequest struct {
|
type UpdateUserRequest struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Email *string `json:"email,omitempty" validate:"omitempty,email"`
|
Email *string `json:"email,omitempty" validate:"omitempty,email"`
|
||||||
Role *string `json:"role,omitempty" validate:"omitempty,oneof=admin manager cashier waiter"`
|
Role *string `json:"role,omitempty" validate:"omitempty,oneof=admin manager cashier waiter owner purchasing"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
Permissions *map[string]interface{} `json:"permissions,omitempty"`
|
Permissions *map[string]interface{} `json:"permissions,omitempty"`
|
||||||
@@ -35,9 +35,15 @@ type UpdateUserOutletRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
Email string `json:"email" validate:"required,email"`
|
Email string `json:"email" validate:"required,email"`
|
||||||
Password string `json:"password" validate:"required"`
|
Password string `json:"password" validate:"required"`
|
||||||
FcmToken *string `json:"fcm_token,omitempty"`
|
DeviceID string `json:"device_id,omitempty"`
|
||||||
|
DeviceName string `json:"device_name,omitempty"`
|
||||||
|
DeviceType string `json:"device_type,omitempty"`
|
||||||
|
Platform string `json:"platform,omitempty"`
|
||||||
|
FCMToken string `json:"fcm_token,omitempty"`
|
||||||
|
AppVersion string `json:"app_version,omitempty"`
|
||||||
|
OsVersion string `json:"os_version,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoginResponse struct {
|
type LoginResponse struct {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegisterUserDeviceRequest struct {
|
||||||
|
DeviceID string `json:"device_id" validate:"required,min=1,max=255"`
|
||||||
|
DeviceName string `json:"device_name,omitempty" validate:"omitempty,max=255"`
|
||||||
|
DeviceType entities.DeviceType `json:"device_type,omitempty" validate:"omitempty,oneof=mobile tablet desktop"`
|
||||||
|
Platform entities.DevicePlatform `json:"platform,omitempty" validate:"omitempty,oneof=android ios web"`
|
||||||
|
FCMToken string `json:"fcm_token,omitempty" validate:"omitempty,max=512"`
|
||||||
|
AppVersion string `json:"app_version,omitempty" validate:"omitempty,max=50"`
|
||||||
|
OsVersion string `json:"os_version,omitempty" validate:"omitempty,max=50"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUserDeviceRequest struct {
|
||||||
|
DeviceName string `json:"device_name,omitempty" validate:"omitempty,max=255"`
|
||||||
|
DeviceType entities.DeviceType `json:"device_type,omitempty" validate:"omitempty,oneof=mobile tablet desktop"`
|
||||||
|
Platform entities.DevicePlatform `json:"platform,omitempty" validate:"omitempty,oneof=android ios web"`
|
||||||
|
FCMToken string `json:"fcm_token,omitempty" validate:"omitempty,max=512"`
|
||||||
|
AppVersion string `json:"app_version,omitempty" validate:"omitempty,max=50"`
|
||||||
|
OsVersion string `json:"os_version,omitempty" validate:"omitempty,max=50"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserDeviceResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
|
DeviceName string `json:"device_name"`
|
||||||
|
DeviceType entities.DeviceType `json:"device_type"`
|
||||||
|
Platform entities.DevicePlatform `json:"platform"`
|
||||||
|
FCMToken string `json:"fcm_token"`
|
||||||
|
AppVersion string `json:"app_version"`
|
||||||
|
OsVersion string `json:"os_version"`
|
||||||
|
IPAddress string `json:"ip_address"`
|
||||||
|
LastActiveAt *time.Time `json:"last_active_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListUserDevicesRequest struct {
|
||||||
|
Page int `json:"page" validate:"min=1"`
|
||||||
|
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||||
|
UserID string `json:"user_id,omitempty"`
|
||||||
|
Platform string `json:"platform,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListUserDevicesResponse struct {
|
||||||
|
Devices []UserDeviceResponse `json:"devices"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
+221
-40
@@ -27,10 +27,89 @@ type SalesAnalytics struct {
|
|||||||
NetSales float64 `json:"net_sales"`
|
NetSales float64 `json:"net_sales"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PurchaseTeamFilter narrows purchasing figures to a single team: a parent
|
||||||
|
// category, Pusat, or the purchases that carry no team at all. A nil filter
|
||||||
|
// leaves the figures spanning every team.
|
||||||
|
type PurchaseTeamFilter struct {
|
||||||
|
Scope string
|
||||||
|
CategoryID *uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchasingAnalytics represents purchasing analytics data
|
||||||
|
type PurchasingAnalytics struct {
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
|
Summary PurchasingSummary `json:"summary"`
|
||||||
|
Data []PurchasingAnalyticsData `json:"data"`
|
||||||
|
IngredientData []PurchasingIngredientData `json:"ingredient_data"`
|
||||||
|
VendorData []PurchasingVendorData `json:"vendor_data"`
|
||||||
|
TeamData []PurchasingTeamData `json:"team_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchasingTeamData is one team's share of the purchases: a parent category,
|
||||||
|
// Pusat, or the purchases charged to no team at all. Scope and CategoryID are
|
||||||
|
// what the team filter takes back, so a row can be clicked straight through.
|
||||||
|
type PurchasingTeamData struct {
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
CategoryID *uuid.UUID `json:"category_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
|
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Percentage float64 `json:"percentage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchasingSummary struct {
|
||||||
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||||
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
TotalQuantity float64 `json:"total_quantity"`
|
||||||
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||||
|
TotalIngredients int64 `json:"total_ingredients"`
|
||||||
|
TotalVendors int64 `json:"total_vendors"`
|
||||||
|
TotalTeams int64 `json:"total_teams"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchasingAnalyticsData struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Purchases float64 `json:"purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Ingredients int64 `json:"ingredients"`
|
||||||
|
Vendors int64 `json:"vendors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchasingIngredientData struct {
|
||||||
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
|
IngredientName string `json:"ingredient_name"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
AverageUnitCost float64 `json:"average_unit_cost"`
|
||||||
|
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchasingVendorData struct {
|
||||||
|
VendorID *uuid.UUID `json:"vendor_id"`
|
||||||
|
VendorName string `json:"vendor_name"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||||
|
IngredientCount int64 `json:"ingredient_count"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProductAnalytics struct {
|
type ProductAnalytics struct {
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
ProductSku string `json:"product_sku"`
|
ProductSku string `json:"product_sku"`
|
||||||
|
ProductPrice float64 `json:"product_price"`
|
||||||
CategoryID uuid.UUID `json:"category_id"`
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
CategoryName string `json:"category_name"`
|
CategoryName string `json:"category_name"`
|
||||||
CategoryOrder int `json:"category_order"`
|
CategoryOrder int `json:"category_order"`
|
||||||
@@ -58,6 +137,39 @@ type ProductAnalyticsPerCategory struct {
|
|||||||
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProductAnalyticsPerParentCategory rolls the per-category figures up to the
|
||||||
|
// top-level category. A category without a parent is its own group.
|
||||||
|
type ProductAnalyticsPerParentCategory struct {
|
||||||
|
ParentCategoryID uuid.UUID `json:"parent_category_id"`
|
||||||
|
ParentCategoryName string `json:"parent_category_name"`
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
TotalQuantity int64 `json:"total_quantity"`
|
||||||
|
CategoryCount int64 `json:"category_count"`
|
||||||
|
ProductCount int64 `json:"product_count"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||||
|
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||||
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentCategoryAnalyticsDetail is the drill-down for a single parent category:
|
||||||
|
// its own totals, the sub-categories underneath it, and the products in each.
|
||||||
|
type ParentCategoryAnalyticsDetail struct {
|
||||||
|
ParentCategoryID uuid.UUID
|
||||||
|
ParentCategoryName string
|
||||||
|
Summary *ProductAnalyticsPerParentCategory
|
||||||
|
Categories []*ProductAnalyticsPerCategory
|
||||||
|
Products []*ProductAnalytics
|
||||||
|
}
|
||||||
|
|
||||||
|
// BudgetCutOffWeek is one Monday-to-Sunday bucket of revenue, used to derive the
|
||||||
|
// weekly spending limits.
|
||||||
|
type BudgetCutOffWeek struct {
|
||||||
|
WeekStart time.Time `json:"week_start"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
}
|
||||||
|
|
||||||
// DashboardOverview represents dashboard overview data
|
// DashboardOverview represents dashboard overview data
|
||||||
type DashboardOverview struct {
|
type DashboardOverview struct {
|
||||||
TotalSales float64 `json:"total_sales"`
|
TotalSales float64 `json:"total_sales"`
|
||||||
@@ -66,56 +178,125 @@ type DashboardOverview struct {
|
|||||||
TotalCustomers int64 `json:"total_customers"`
|
TotalCustomers int64 `json:"total_customers"`
|
||||||
VoidedOrders int64 `json:"voided_orders"`
|
VoidedOrders int64 `json:"voided_orders"`
|
||||||
RefundedOrders int64 `json:"refunded_orders"`
|
RefundedOrders int64 `json:"refunded_orders"`
|
||||||
|
TotalItemSold int64 `json:"total_item_sold"`
|
||||||
|
TotalLowStock int64 `json:"total_low_stock"`
|
||||||
|
TotalProductActive int64 `json:"total_product_active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossAnalytics represents profit and loss analytics data
|
|
||||||
type ProfitLossAnalytics struct {
|
type ProfitLossAnalytics struct {
|
||||||
Summary ProfitLossSummary `json:"summary"`
|
Summary ProfitLossSummary
|
||||||
Data []ProfitLossData `json:"data"`
|
Data []ProfitLossData
|
||||||
ProductData []ProductProfitData `json:"product_data"`
|
ProductData []ProductProfitData
|
||||||
|
TodayRevenue float64
|
||||||
|
TodayCost float64
|
||||||
|
MtdRevenue float64
|
||||||
|
MtdCost float64
|
||||||
|
TodayPurchasing float64
|
||||||
|
MtdPurchasing float64
|
||||||
|
TodayPurchasingRawMaterial float64
|
||||||
|
MtdPurchasingRawMaterial float64
|
||||||
|
TodayPurchasingExpense float64
|
||||||
|
MtdPurchasingExpense float64
|
||||||
|
PurchasingItems []PurchasingItemDetail
|
||||||
|
TodayExpenseByCategory []ExpenseCategoryTotal
|
||||||
|
MtdExpenseByCategory []ExpenseCategoryTotal
|
||||||
|
OperationalExpenseItems []OperationalExpenseItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchasingItemDetail struct {
|
||||||
|
Date time.Time
|
||||||
|
Item string
|
||||||
|
Quantity float64
|
||||||
|
Amount float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossSummary represents profit and loss summary data
|
|
||||||
type ProfitLossSummary struct {
|
type ProfitLossSummary struct {
|
||||||
TotalRevenue float64 `json:"total_revenue"`
|
TotalRevenue float64
|
||||||
TotalCost float64 `json:"total_cost"`
|
TotalCost float64
|
||||||
GrossProfit float64 `json:"gross_profit"`
|
GrossProfit float64
|
||||||
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
GrossProfitMargin float64
|
||||||
TotalTax float64 `json:"total_tax"`
|
TotalTax float64
|
||||||
TotalDiscount float64 `json:"total_discount"`
|
TotalDiscount float64
|
||||||
NetProfit float64 `json:"net_profit"`
|
NetProfit float64
|
||||||
NetProfitMargin float64 `json:"net_profit_margin"`
|
NetProfitMargin float64
|
||||||
TotalOrders int64 `json:"total_orders"`
|
TotalOrders int64
|
||||||
AverageProfit float64 `json:"average_profit"`
|
AverageProfit float64
|
||||||
ProfitabilityRatio float64 `json:"profitability_ratio"`
|
ProfitabilityRatio float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossData represents profit and loss data by time period
|
|
||||||
type ProfitLossData struct {
|
type ProfitLossData struct {
|
||||||
Date time.Time `json:"date"`
|
Date time.Time
|
||||||
Revenue float64 `json:"revenue"`
|
Revenue float64
|
||||||
Cost float64 `json:"cost"`
|
Cost float64
|
||||||
GrossProfit float64 `json:"gross_profit"`
|
GrossProfit float64
|
||||||
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
GrossProfitMargin float64
|
||||||
Tax float64 `json:"tax"`
|
Tax float64
|
||||||
Discount float64 `json:"discount"`
|
Discount float64
|
||||||
NetProfit float64 `json:"net_profit"`
|
NetProfit float64
|
||||||
NetProfitMargin float64 `json:"net_profit_margin"`
|
NetProfitMargin float64
|
||||||
Orders int64 `json:"orders"`
|
Orders int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductProfitData represents profit data for individual products
|
|
||||||
type ProductProfitData struct {
|
type ProductProfitData struct {
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID
|
||||||
ProductName string `json:"product_name"`
|
ProductName string
|
||||||
CategoryID uuid.UUID `json:"category_id"`
|
CategoryID uuid.UUID
|
||||||
CategoryName string `json:"category_name"`
|
CategoryName string
|
||||||
QuantitySold int64 `json:"quantity_sold"`
|
QuantitySold int64
|
||||||
Revenue float64 `json:"revenue"`
|
Revenue float64
|
||||||
Cost float64 `json:"cost"`
|
Cost float64
|
||||||
GrossProfit float64 `json:"gross_profit"`
|
GrossProfit float64
|
||||||
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
GrossProfitMargin float64
|
||||||
AveragePrice float64 `json:"average_price"`
|
AveragePrice float64
|
||||||
AverageCost float64 `json:"average_cost"`
|
AverageCost float64
|
||||||
ProfitPerUnit float64 `json:"profit_per_unit"`
|
ProfitPerUnit float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseCategoryTotal struct {
|
||||||
|
CategoryName string
|
||||||
|
Amount float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperationalExpenseItem struct {
|
||||||
|
Item string
|
||||||
|
Amount float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryAnalytics struct {
|
||||||
|
SalesTotal float64
|
||||||
|
SalesCount int64
|
||||||
|
HPPBreakdown []ExclusiveSummaryCategoryTotal
|
||||||
|
OperationalExpenseBreakdown []ExclusiveSummaryCategoryTotal
|
||||||
|
DailySummary []ExclusiveSummaryDailySummary
|
||||||
|
DailyTransactions []ExclusiveSummaryDailyTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryCategoryTotal struct {
|
||||||
|
CategoryCode string
|
||||||
|
CategoryName string
|
||||||
|
Amount float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryDailySummary struct {
|
||||||
|
Date time.Time
|
||||||
|
TransactionCount int64
|
||||||
|
TotalCost float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryDailyTransaction struct {
|
||||||
|
Date time.Time
|
||||||
|
CategoryCode string
|
||||||
|
CategoryName string
|
||||||
|
Description string
|
||||||
|
Amount float64
|
||||||
|
Source string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryBankBalance struct {
|
||||||
|
Bank string
|
||||||
|
OpeningBalance *float64
|
||||||
|
IncomingMutation *float64
|
||||||
|
OutgoingMutation *float64
|
||||||
|
ClosingBalance *float64
|
||||||
|
Notes *string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,15 +31,18 @@ func (m *Metadata) Scan(value interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Category struct {
|
type Category struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
|
||||||
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
|
||||||
Description *string `gorm:"type:text" json:"description"`
|
ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"`
|
||||||
Order int `gorm:"default:0" json:"order"`
|
Parent *Category `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||||
BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
|
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
Description *string `gorm:"type:text" json:"description"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
Order int `gorm:"default:0" json:"order"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
|
||||||
|
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"`
|
Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"`
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ func GetAllEntities() []interface{} {
|
|||||||
&CampaignRule{},
|
&CampaignRule{},
|
||||||
&OtpSession{},
|
&OtpSession{},
|
||||||
// Analytics entities are not database tables, they are query results
|
// Analytics entities are not database tables, they are query results
|
||||||
|
&UserDevice{},
|
||||||
|
// Notification entities
|
||||||
|
&Notification{},
|
||||||
|
&NotificationReceiver{},
|
||||||
|
&NotificationDelivery{},
|
||||||
|
&ProductOutletPrice{},
|
||||||
|
&Expense{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Expense struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"`
|
||||||
|
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"`
|
||||||
|
Receiver string `gorm:"not null;size:255" json:"receiver"`
|
||||||
|
TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date"`
|
||||||
|
CodeNumber string `gorm:"not null;size:50" json:"code_number"`
|
||||||
|
Status string `gorm:"not null;size:20;default:'draft'" json:"status"`
|
||||||
|
Description *string `gorm:"type:text" json:"description"`
|
||||||
|
Tax float64 `gorm:"type:decimal(15,2);not null;default:0" json:"tax"`
|
||||||
|
Total float64 `gorm:"type:decimal(15,2);not null;default:0" json:"total"`
|
||||||
|
Reserved1 *string `gorm:"type:text" json:"reserved1"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
|
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||||
|
Items []ExpenseItem `gorm:"foreignKey:ExpenseID" json:"items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalytics struct {
|
||||||
|
Summary ExpenseAnalyticsSummary
|
||||||
|
Data []ExpenseAnalyticsData
|
||||||
|
CategoryData []ExpenseAnalyticsCategoryData
|
||||||
|
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData
|
||||||
|
ItemData []ExpenseAnalyticsItemData
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsSummary struct {
|
||||||
|
TotalExpenses float64
|
||||||
|
TotalExpenseCount int64
|
||||||
|
TotalTax float64
|
||||||
|
AverageExpenseValue float64
|
||||||
|
TotalCategories int64
|
||||||
|
TotalItems int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsData struct {
|
||||||
|
Date time.Time
|
||||||
|
Expenses float64
|
||||||
|
ExpenseCount int64
|
||||||
|
Tax float64
|
||||||
|
Items int64
|
||||||
|
Categories int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsCategoryData struct {
|
||||||
|
PurchaseCategoryID uuid.UUID
|
||||||
|
PurchaseCategoryName string
|
||||||
|
PurchaseCategoryType string
|
||||||
|
TotalAmount float64
|
||||||
|
ExpenseCount int64
|
||||||
|
ItemCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsChartOfAccountData struct {
|
||||||
|
ChartOfAccountID uuid.UUID
|
||||||
|
ChartOfAccountName string
|
||||||
|
TotalAmount float64
|
||||||
|
ExpenseCount int64
|
||||||
|
ItemCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsItemData struct {
|
||||||
|
Item string
|
||||||
|
TotalAmount float64
|
||||||
|
ExpenseCount int64
|
||||||
|
ItemCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Expense) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if e.ID == uuid.Nil {
|
||||||
|
e.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Expense) TableName() string {
|
||||||
|
return "expenses"
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExpenseItem struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
ExpenseID uuid.UUID `gorm:"type:uuid;not null;index" json:"expense_id"`
|
||||||
|
ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id"`
|
||||||
|
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id"`
|
||||||
|
Item string `gorm:"not null;size:255" json:"item"`
|
||||||
|
Description *string `gorm:"type:text" json:"description"`
|
||||||
|
Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Expense *Expense `gorm:"foreignKey:ExpenseID" json:"expense,omitempty"`
|
||||||
|
ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"`
|
||||||
|
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExpenseItem) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if e.ID == uuid.Nil {
|
||||||
|
e.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ExpenseItem) TableName() string {
|
||||||
|
return "expense_items"
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ type Ingredient struct {
|
|||||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"`
|
||||||
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
|
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
|
||||||
Name string `gorm:"not null;size:255" json:"name"`
|
Name string `gorm:"not null;size:255" json:"name"`
|
||||||
UnitID uuid.UUID `gorm:"type:uuid;not null;index" json:"unit_id"`
|
UnitID *uuid.UUID `gorm:"type:uuid;index" json:"unit_id"`
|
||||||
Cost float64 `gorm:"type:decimal(10,2);default:0.00" json:"cost"`
|
Cost float64 `gorm:"type:decimal(10,2);default:0.00" json:"cost"`
|
||||||
Stock float64 `gorm:"type:decimal(10,2);default:0.00" json:"stock"`
|
Stock float64 `gorm:"type:decimal(10,2);default:0.00" json:"stock"`
|
||||||
IsSemiFinished bool `gorm:"default:false" json:"is_semi_finished"`
|
IsSemiFinished bool `gorm:"default:false" json:"is_semi_finished"`
|
||||||
|
|||||||
@@ -39,4 +39,3 @@ func (iuc *IngredientUnitConverter) BeforeCreate() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,34 +36,36 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type InventoryMovement struct {
|
type InventoryMovement struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
|
||||||
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"`
|
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"`
|
||||||
ItemID uuid.UUID `gorm:"type:uuid;not null;index" json:"item_id" validate:"required"`
|
ItemID uuid.UUID `gorm:"type:uuid;not null;index" json:"item_id" validate:"required"`
|
||||||
ItemType string `gorm:"not null;size:20" json:"item_type" validate:"required"` // "PRODUCT" or "INGREDIENT"
|
ItemType string `gorm:"not null;size:20" json:"item_type" validate:"required"` // "PRODUCT" or "INGREDIENT"
|
||||||
MovementType InventoryMovementType `gorm:"not null;size:50" json:"movement_type" validate:"required"`
|
MovementType InventoryMovementType `gorm:"not null;size:50" json:"movement_type" validate:"required"`
|
||||||
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
|
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
|
||||||
PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
|
PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
|
||||||
NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
|
NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
|
||||||
UnitCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"unit_cost"`
|
UnitCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"unit_cost"`
|
||||||
TotalCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"total_cost"`
|
TotalCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"total_cost"`
|
||||||
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
|
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
|
||||||
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
|
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
|
||||||
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
|
PurchaseOrderItemID *uuid.UUID `gorm:"type:uuid;index" json:"purchase_order_item_id"`
|
||||||
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
|
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
|
||||||
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
|
||||||
Reason *string `gorm:"size:255" json:"reason"`
|
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
||||||
Notes *string `gorm:"type:text" json:"notes"`
|
Reason *string `gorm:"size:255" json:"reason"`
|
||||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
Notes *string `gorm:"type:text" json:"notes"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
|
||||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||||
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
|
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
|
||||||
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
|
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
|
||||||
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
PurchaseOrderItem *PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderItemID" json:"purchase_order_item,omitempty"`
|
||||||
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
|
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (im *InventoryMovement) BeforeCreate(tx *gorm.DB) error {
|
func (im *InventoryMovement) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationPriority string
|
||||||
|
type NotificationDeliveryStatus string
|
||||||
|
type NotificationChannel string
|
||||||
|
type NotificationProvider string
|
||||||
|
|
||||||
|
const (
|
||||||
|
NotificationPriorityLow NotificationPriority = "low"
|
||||||
|
NotificationPriorityNormal NotificationPriority = "normal"
|
||||||
|
NotificationPriorityHigh NotificationPriority = "high"
|
||||||
|
|
||||||
|
NotificationDeliveryStatusPending NotificationDeliveryStatus = "pending"
|
||||||
|
NotificationDeliveryStatusSent NotificationDeliveryStatus = "sent"
|
||||||
|
NotificationDeliveryStatusDelivered NotificationDeliveryStatus = "delivered"
|
||||||
|
NotificationDeliveryStatusFailed NotificationDeliveryStatus = "failed"
|
||||||
|
|
||||||
|
NotificationChannelPush NotificationChannel = "push"
|
||||||
|
NotificationChannelWebsocket NotificationChannel = "websocket"
|
||||||
|
NotificationChannelEmail NotificationChannel = "email"
|
||||||
|
|
||||||
|
NotificationProviderFirebase NotificationProvider = "firebase"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotificationData is a JSON-serializable map for extra notification payload.
|
||||||
|
type NotificationData map[string]interface{}
|
||||||
|
|
||||||
|
func (d NotificationData) Value() (driver.Value, error) {
|
||||||
|
if d == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return json.Marshal(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *NotificationData) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
*d = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bytes, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("type assertion to []byte failed")
|
||||||
|
}
|
||||||
|
return json.Unmarshal(bytes, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification is the master notification record.
|
||||||
|
type Notification struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
Title string `gorm:"not null;size:255" json:"title"`
|
||||||
|
Body string `gorm:"type:text" json:"body"`
|
||||||
|
Type string `gorm:"size:100" json:"type"`
|
||||||
|
Category string `gorm:"size:100" json:"category"`
|
||||||
|
Priority NotificationPriority `gorm:"size:50;default:'normal'" json:"priority"`
|
||||||
|
ImageURL string `gorm:"size:512" json:"image_url"`
|
||||||
|
ActionURL string `gorm:"size:512" json:"action_url"`
|
||||||
|
NotifiableType string `gorm:"size:100" json:"notifiable_type"`
|
||||||
|
NotifiableID *uuid.UUID `gorm:"type:uuid" json:"notifiable_id"`
|
||||||
|
Data NotificationData `gorm:"type:jsonb" json:"data"`
|
||||||
|
ScheduledAt *time.Time `gorm:"type:timestamptz" json:"scheduled_at"`
|
||||||
|
SentAt *time.Time `gorm:"type:timestamptz" json:"sent_at"`
|
||||||
|
ExpiredAt *time.Time `gorm:"type:timestamptz" json:"expired_at"`
|
||||||
|
CreatedBy *uuid.UUID `gorm:"type:uuid" json:"created_by"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Creator *User `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"`
|
||||||
|
Receivers []*NotificationReceiver `gorm:"foreignKey:NotificationID" json:"receivers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notification) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if n.ID == uuid.Nil {
|
||||||
|
n.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Notification) TableName() string {
|
||||||
|
return "notifications"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationReceiver links a notification to a specific user.
|
||||||
|
type NotificationReceiver struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
NotificationID uuid.UUID `gorm:"type:uuid;not null;index" json:"notification_id"`
|
||||||
|
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"`
|
||||||
|
IsRead bool `gorm:"default:false" json:"is_read"`
|
||||||
|
ReadAt *time.Time `gorm:"type:timestamptz" json:"read_at"`
|
||||||
|
IsDeleted bool `gorm:"default:false" json:"is_deleted"`
|
||||||
|
DeletedAt *time.Time `gorm:"type:timestamptz" json:"deleted_at"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Notification *Notification `gorm:"foreignKey:NotificationID" json:"notification,omitempty"`
|
||||||
|
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
Deliveries []*NotificationDelivery `gorm:"foreignKey:NotificationReceiverID" json:"deliveries,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NotificationReceiver) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if n.ID == uuid.Nil {
|
||||||
|
n.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (NotificationReceiver) TableName() string {
|
||||||
|
return "notification_receivers"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationDelivery tracks per-device delivery attempts.
|
||||||
|
type NotificationDelivery struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
NotificationReceiverID uuid.UUID `gorm:"type:uuid;not null;index" json:"notification_receiver_id"`
|
||||||
|
UserDeviceID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_device_id"`
|
||||||
|
Channel NotificationChannel `gorm:"size:50;default:'push'" json:"channel"`
|
||||||
|
DeliveryStatus NotificationDeliveryStatus `gorm:"size:50;default:'pending'" json:"delivery_status"`
|
||||||
|
Provider NotificationProvider `gorm:"size:50" json:"provider"`
|
||||||
|
ProviderMessageID string `gorm:"size:255" json:"provider_message_id"`
|
||||||
|
SentAt *time.Time `gorm:"type:timestamptz" json:"sent_at"`
|
||||||
|
DeliveredAt *time.Time `gorm:"type:timestamptz" json:"delivered_at"`
|
||||||
|
FailedAt *time.Time `gorm:"type:timestamptz" json:"failed_at"`
|
||||||
|
FailureReason string `gorm:"type:text" json:"failure_reason"`
|
||||||
|
RetryCount int `gorm:"default:0" json:"retry_count"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
NotificationReceiver *NotificationReceiver `gorm:"foreignKey:NotificationReceiverID" json:"notification_receiver,omitempty"`
|
||||||
|
UserDevice *UserDevice `gorm:"foreignKey:UserDeviceID" json:"user_device,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NotificationDelivery) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if n.ID == uuid.Nil {
|
||||||
|
n.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (NotificationDelivery) TableName() string {
|
||||||
|
return "notification_deliveries"
|
||||||
|
}
|
||||||
@@ -26,14 +26,14 @@ type OrderIngredientTransaction struct {
|
|||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||||
Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||||
OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"`
|
OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"`
|
||||||
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||||
ProductVariant *ProductVariant `gorm:"foreignKey:ProductVariantID" json:"product_variant,omitempty"`
|
ProductVariant *ProductVariant `gorm:"foreignKey:ProductVariantID" json:"product_variant,omitempty"`
|
||||||
Ingredient Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
Ingredient Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||||
CreatedByUser User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"`
|
CreatedByUser User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oit *OrderIngredientTransaction) BeforeCreate(tx *gorm.DB) error {
|
func (oit *OrderIngredientTransaction) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
|||||||
@@ -26,13 +26,14 @@ type Product struct {
|
|||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||||
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
|
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
|
||||||
ProductRecipes []ProductRecipe `gorm:"foreignKey:ProductID" json:"product_recipes,omitempty"`
|
ProductRecipes []ProductRecipe `gorm:"foreignKey:ProductID" json:"product_recipes,omitempty"`
|
||||||
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"`
|
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"`
|
||||||
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"`
|
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"`
|
||||||
|
ProductOutletPrices []ProductOutletPrice `gorm:"foreignKey:ProductID" json:"product_outlet_prices,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Product) BeforeCreate(tx *gorm.DB) error {
|
func (p *Product) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProductIngredient struct {
|
type ProductIngredient struct {
|
||||||
ID uuid.UUID `json:"id" db:"id"`
|
ID uuid.UUID `json:"id" db:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
||||||
ProductID uuid.UUID `json:"product_id" db:"product_id"`
|
ProductID uuid.UUID `json:"product_id" db:"product_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
|
||||||
Quantity float64 `json:"quantity" db:"quantity"`
|
Quantity float64 `json:"quantity" db:"quantity"`
|
||||||
WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
|
WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
|
||||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Product *Product `json:"product,omitempty"`
|
Product *Product `json:"product,omitempty"`
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductOutletPrice struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id"`
|
||||||
|
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"`
|
||||||
|
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
|
||||||
|
PrintToChecker bool `gorm:"not null;default:true" json:"print_to_checker"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||||
|
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProductOutletPrice) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if p.ID == uuid.Nil {
|
||||||
|
p.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ProductOutletPrice) TableName() string {
|
||||||
|
return "product_outlet_prices"
|
||||||
|
}
|
||||||
@@ -34,4 +34,4 @@ func (pr *ProductRecipe) BeforeCreate(tx *gorm.DB) error {
|
|||||||
|
|
||||||
func (ProductRecipe) TableName() string {
|
func (ProductRecipe) TableName() string {
|
||||||
return "product_recipes"
|
return "product_recipes"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PurchaseCategoryTypeRawMaterial PurchaseCategoryType = "raw_material"
|
||||||
|
PurchaseCategoryTypeExpense PurchaseCategoryType = "expense"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryPreset struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"`
|
||||||
|
Code string `gorm:"not null;unique;size:100" json:"code"`
|
||||||
|
Name string `gorm:"not null;size:255" json:"name"`
|
||||||
|
Type PurchaseCategoryType `gorm:"not null;size:20" json:"type"`
|
||||||
|
SortOrder int `gorm:"not null;default:0" json:"sort_order"`
|
||||||
|
IsActive bool `gorm:"not null;default:true" json:"is_active"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Parent *PurchaseCategoryPreset `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryPreset) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if p.ID == uuid.Nil {
|
||||||
|
p.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PurchaseCategoryPreset) TableName() string {
|
||||||
|
return "purchase_category_presets"
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategory struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"`
|
||||||
|
PresetID *uuid.UUID `gorm:"type:uuid;index" json:"preset_id"`
|
||||||
|
ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"`
|
||||||
|
Code string `gorm:"not null;size:100" json:"code"`
|
||||||
|
Name string `gorm:"not null;size:255" json:"name"`
|
||||||
|
Type PurchaseCategoryType `gorm:"not null;size:20" json:"type"`
|
||||||
|
SortOrder int `gorm:"not null;default:0" json:"sort_order"`
|
||||||
|
IsSystem bool `gorm:"not null;default:false" json:"is_system"`
|
||||||
|
IsActive bool `gorm:"not null;default:true" json:"is_active"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
|
Preset *PurchaseCategoryPreset `gorm:"foreignKey:PresetID" json:"preset,omitempty"`
|
||||||
|
Parent *PurchaseCategory `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||||
|
Children []PurchaseCategory `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PurchaseCategory) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if c.ID == uuid.Nil {
|
||||||
|
c.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PurchaseCategory) TableName() string {
|
||||||
|
return "purchase_categories"
|
||||||
|
}
|
||||||
@@ -9,21 +9,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseOrder struct {
|
type PurchaseOrder struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id" validate:"required"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id" validate:"required"`
|
||||||
VendorID uuid.UUID `gorm:"type:uuid;not null" json:"vendor_id" validate:"required"`
|
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id" validate:"omitempty"`
|
||||||
PONumber string `gorm:"not null;size:50" json:"po_number" validate:"required,min=1,max=50"`
|
VendorID *uuid.UUID `gorm:"type:uuid" json:"vendor_id" validate:"omitempty"`
|
||||||
TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date" validate:"required"`
|
PONumber string `gorm:"not null;size:50" json:"po_number" validate:"required,min=1,max=50"`
|
||||||
DueDate time.Time `gorm:"type:date;not null" json:"due_date" validate:"required"`
|
TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date" validate:"required"`
|
||||||
Reference *string `gorm:"size:100" json:"reference" validate:"omitempty,max=100"`
|
DueDate *time.Time `gorm:"type:date" json:"due_date" validate:"omitempty"`
|
||||||
Status string `gorm:"not null;size:20;default:'draft'" json:"status" validate:"required,oneof=draft sent approved received cancelled"`
|
Reference *string `gorm:"size:100" json:"reference" validate:"omitempty,max=100"`
|
||||||
Message *string `gorm:"type:text" json:"message" validate:"omitempty"`
|
Status string `gorm:"not null;size:20;default:'draft'" json:"status" validate:"required,oneof=draft sent approved received cancelled"`
|
||||||
TotalAmount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"total_amount"`
|
Message *string `gorm:"type:text" json:"message" validate:"omitempty"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
TotalAmount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"total_amount"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
// TeamScope is 'category' when the purchase is charged to a parent category, or
|
||||||
|
// 'central' for Pusat. Nil means no team was chosen, which is not the same as Pusat.
|
||||||
|
TeamScope *string `gorm:"size:20;index" json:"team_scope" validate:"omitempty,oneof=category central"`
|
||||||
|
TeamCategoryID *uuid.UUID `gorm:"type:uuid;index" json:"team_category_id" validate:"omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
|
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||||
Vendor *Vendor `gorm:"foreignKey:VendorID" json:"vendor,omitempty"`
|
Vendor *Vendor `gorm:"foreignKey:VendorID" json:"vendor,omitempty"`
|
||||||
|
TeamCategory *Category `gorm:"foreignKey:TeamCategoryID" json:"team_category,omitempty"`
|
||||||
Items []PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderID" json:"items,omitempty"`
|
Items []PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderID" json:"items,omitempty"`
|
||||||
Attachments []PurchaseOrderAttachment `gorm:"foreignKey:PurchaseOrderID" json:"attachments,omitempty"`
|
Attachments []PurchaseOrderAttachment `gorm:"foreignKey:PurchaseOrderID" json:"attachments,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -41,19 +48,21 @@ func (PurchaseOrder) TableName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderItem struct {
|
type PurchaseOrderItem struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
PurchaseOrderID uuid.UUID `gorm:"type:uuid;not null" json:"purchase_order_id" validate:"required"`
|
PurchaseOrderID uuid.UUID `gorm:"type:uuid;not null" json:"purchase_order_id" validate:"required"`
|
||||||
IngredientID uuid.UUID `gorm:"type:uuid;not null" json:"ingredient_id" validate:"required"`
|
IngredientID *uuid.UUID `gorm:"type:uuid" json:"ingredient_id" validate:"omitempty"`
|
||||||
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id" validate:"required"`
|
||||||
Quantity float64 `gorm:"type:decimal(10,3);not null" json:"quantity" validate:"required,gt=0"`
|
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
||||||
UnitID uuid.UUID `gorm:"type:uuid;not null" json:"unit_id" validate:"required"`
|
Quantity *float64 `gorm:"type:decimal(10,3)" json:"quantity" validate:"omitempty,gt=0"`
|
||||||
Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"`
|
UnitID *uuid.UUID `gorm:"type:uuid" json:"unit_id" validate:"omitempty"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"`
|
PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"`
|
||||||
Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,omitempty"`
|
||||||
|
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (poi *PurchaseOrderItem) BeforeCreate(tx *gorm.DB) error {
|
func (poi *PurchaseOrderItem) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ import (
|
|||||||
type UserRole string
|
type UserRole string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RoleAdmin UserRole = "admin"
|
RoleAdmin UserRole = "admin"
|
||||||
RoleManager UserRole = "manager"
|
RoleManager UserRole = "manager"
|
||||||
RoleCashier UserRole = "cashier"
|
RoleCashier UserRole = "cashier"
|
||||||
RoleWaiter UserRole = "waiter"
|
RoleWaiter UserRole = "waiter"
|
||||||
|
RoleOwner UserRole = "owner"
|
||||||
|
RolePurchasing UserRole = "purchasing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Permissions map[string]interface{}
|
type Permissions map[string]interface{}
|
||||||
@@ -46,10 +48,9 @@ type User struct {
|
|||||||
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||||
Email string `gorm:"uniqueIndex;not null;size:255" json:"email" validate:"required,email"`
|
Email string `gorm:"uniqueIndex;not null;size:255" json:"email" validate:"required,email"`
|
||||||
PasswordHash string `gorm:"not null;size:255" json:"-"`
|
PasswordHash string `gorm:"not null;size:255" json:"-"`
|
||||||
Role UserRole `gorm:"not null;size:50" json:"role" validate:"required,oneof=admin manager cashier waiter"`
|
Role UserRole `gorm:"not null;size:50" json:"role" validate:"required,oneof=admin manager cashier waiter owner purchasing"`
|
||||||
Permissions Permissions `gorm:"type:jsonb;default:'{}'" json:"permissions"`
|
Permissions Permissions `gorm:"type:jsonb;default:'{}'" json:"permissions"`
|
||||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||||
FcmToken *string `gorm:"size:512" json:"fcm_token,omitempty"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeviceType string
|
||||||
|
type DevicePlatform string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DeviceTypeMobile DeviceType = "mobile"
|
||||||
|
DeviceTypeTablet DeviceType = "tablet"
|
||||||
|
DeviceTypeDesktop DeviceType = "desktop"
|
||||||
|
|
||||||
|
DevicePlatformAndroid DevicePlatform = "android"
|
||||||
|
DevicePlatformIOS DevicePlatform = "ios"
|
||||||
|
DevicePlatformWeb DevicePlatform = "web"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserDevice struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"`
|
||||||
|
DeviceID string `gorm:"not null;size:255;index" json:"device_id"`
|
||||||
|
DeviceName string `gorm:"size:255" json:"device_name"`
|
||||||
|
DeviceType DeviceType `gorm:"size:50" json:"device_type"`
|
||||||
|
Platform DevicePlatform `gorm:"size:50" json:"platform"`
|
||||||
|
FCMToken string `gorm:"size:512" json:"fcm_token"`
|
||||||
|
AppVersion string `gorm:"size:50" json:"app_version"`
|
||||||
|
OsVersion string `gorm:"size:50" json:"os_version"`
|
||||||
|
IPAddress string `gorm:"size:45" json:"ip_address"`
|
||||||
|
LastActiveAt *time.Time `gorm:"type:timestamptz" json:"last_active_at"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserDevice) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if u.ID == uuid.Nil {
|
||||||
|
u.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UserDevice) TableName() string {
|
||||||
|
return "user_devices"
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"apskel-pos-be/internal/util"
|
"apskel-pos-be/internal/util"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AnalyticsHandler struct {
|
type AnalyticsHandler struct {
|
||||||
@@ -25,6 +26,17 @@ func NewAnalyticsHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *AnalyticsHandler) resolveOutletID(c *gin.Context, contextOutletID uuid.UUID) *string {
|
||||||
|
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||||
|
return &outletIDStr
|
||||||
|
}
|
||||||
|
if contextOutletID != uuid.Nil {
|
||||||
|
s := contextOutletID.String()
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *AnalyticsHandler) GetPaymentMethodAnalytics(c *gin.Context) {
|
func (h *AnalyticsHandler) GetPaymentMethodAnalytics(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
@@ -36,7 +48,7 @@ func (h *AnalyticsHandler) GetPaymentMethodAnalytics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
req.OutletID = &contextInfo.OutletID
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
modelReq := transformer.PaymentMethodAnalyticsContractToModel(&req)
|
modelReq := transformer.PaymentMethodAnalyticsContractToModel(&req)
|
||||||
|
|
||||||
response, err := h.analyticsService.GetPaymentMethodAnalytics(ctx, modelReq)
|
response, err := h.analyticsService.GetPaymentMethodAnalytics(ctx, modelReq)
|
||||||
@@ -60,7 +72,7 @@ func (h *AnalyticsHandler) GetSalesAnalytics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
req.OutletID = &contextInfo.OutletID
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
modelReq := transformer.SalesAnalyticsContractToModel(&req)
|
modelReq := transformer.SalesAnalyticsContractToModel(&req)
|
||||||
|
|
||||||
response, err := h.analyticsService.GetSalesAnalytics(ctx, modelReq)
|
response, err := h.analyticsService.GetSalesAnalytics(ctx, modelReq)
|
||||||
@@ -73,6 +85,30 @@ func (h *AnalyticsHandler) GetSalesAnalytics(c *gin.Context) {
|
|||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetSalesAnalytics")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetSalesAnalytics")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *AnalyticsHandler) GetPurchasingAnalytics(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.PurchasingAnalyticsRequest
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetPurchasingAnalytics", err.Error())}), "AnalyticsHandler::GetPurchasingAnalytics")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
|
modelReq := transformer.PurchasingAnalyticsContractToModel(&req)
|
||||||
|
|
||||||
|
response, err := h.analyticsService.GetPurchasingAnalytics(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetPurchasingAnalytics", err.Error())}), "AnalyticsHandler::GetPurchasingAnalytics")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contractResp := transformer.PurchasingAnalyticsModelToContract(response)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetPurchasingAnalytics")
|
||||||
|
}
|
||||||
|
|
||||||
func (h *AnalyticsHandler) GetProductAnalytics(c *gin.Context) {
|
func (h *AnalyticsHandler) GetProductAnalytics(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
@@ -84,7 +120,7 @@ func (h *AnalyticsHandler) GetProductAnalytics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
req.OutletID = &contextInfo.OutletID
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
modelReq := transformer.ProductAnalyticsContractToModel(&req)
|
modelReq := transformer.ProductAnalyticsContractToModel(&req)
|
||||||
|
|
||||||
response, err := h.analyticsService.GetProductAnalytics(ctx, modelReq)
|
response, err := h.analyticsService.GetProductAnalytics(ctx, modelReq)
|
||||||
@@ -108,7 +144,7 @@ func (h *AnalyticsHandler) GetProductAnalyticsPerCategory(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
req.OutletID = &contextInfo.OutletID
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
modelReq := transformer.ProductAnalyticsPerCategoryContractToModel(&req)
|
modelReq := transformer.ProductAnalyticsPerCategoryContractToModel(&req)
|
||||||
|
|
||||||
response, err := h.analyticsService.GetProductAnalyticsPerCategory(ctx, modelReq)
|
response, err := h.analyticsService.GetProductAnalyticsPerCategory(ctx, modelReq)
|
||||||
@@ -121,6 +157,55 @@ func (h *AnalyticsHandler) GetProductAnalyticsPerCategory(c *gin.Context) {
|
|||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetProductAnalyticsPerCategory")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetProductAnalyticsPerCategory")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *AnalyticsHandler) GetProductAnalyticsPerParentCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.ProductAnalyticsPerParentCategoryRequest
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetProductAnalyticsPerParentCategory", err.Error())}), "AnalyticsHandler::GetProductAnalyticsPerParentCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
|
modelReq := transformer.ProductAnalyticsPerParentCategoryContractToModel(&req)
|
||||||
|
|
||||||
|
response, err := h.analyticsService.GetProductAnalyticsPerParentCategory(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetProductAnalyticsPerParentCategory", err.Error())}), "AnalyticsHandler::GetProductAnalyticsPerParentCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contractResp := transformer.ProductAnalyticsPerParentCategoryModelToContract(response)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetProductAnalyticsPerParentCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnalyticsHandler) GetParentCategoryAnalyticsDetail(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.ParentCategoryAnalyticsDetailRequest
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetParentCategoryAnalyticsDetail", err.Error())}), "AnalyticsHandler::GetParentCategoryAnalyticsDetail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
|
req.ParentCategoryID = c.Param("parent_category_id")
|
||||||
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
|
modelReq := transformer.ParentCategoryAnalyticsDetailContractToModel(&req)
|
||||||
|
|
||||||
|
response, err := h.analyticsService.GetParentCategoryAnalyticsDetail(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetParentCategoryAnalyticsDetail", err.Error())}), "AnalyticsHandler::GetParentCategoryAnalyticsDetail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contractResp := transformer.ParentCategoryAnalyticsDetailModelToContract(response)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetParentCategoryAnalyticsDetail")
|
||||||
|
}
|
||||||
|
|
||||||
func (h *AnalyticsHandler) GetDashboardAnalytics(c *gin.Context) {
|
func (h *AnalyticsHandler) GetDashboardAnalytics(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
@@ -132,7 +217,7 @@ func (h *AnalyticsHandler) GetDashboardAnalytics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
req.OutletID = &contextInfo.OutletID
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
modelReq := transformer.DashboardAnalyticsContractToModel(&req)
|
modelReq := transformer.DashboardAnalyticsContractToModel(&req)
|
||||||
|
|
||||||
response, err := h.analyticsService.GetDashboardAnalytics(ctx, modelReq)
|
response, err := h.analyticsService.GetDashboardAnalytics(ctx, modelReq)
|
||||||
@@ -156,6 +241,7 @@ func (h *AnalyticsHandler) GetProfitLossAnalytics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
modelReq, err := transformer.ProfitLossAnalyticsContractToModel(&req)
|
modelReq, err := transformer.ProfitLossAnalyticsContractToModel(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetProfitLossAnalytics", err.Error())}), "AnalyticsHandler::GetProfitLossAnalytics")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetProfitLossAnalytics", err.Error())}), "AnalyticsHandler::GetProfitLossAnalytics")
|
||||||
@@ -173,3 +259,87 @@ func (h *AnalyticsHandler) GetProfitLossAnalytics(c *gin.Context) {
|
|||||||
contractResp := transformer.ProfitLossAnalyticsModelToContract(response)
|
contractResp := transformer.ProfitLossAnalyticsModelToContract(response)
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetProfitLossAnalytics")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetProfitLossAnalytics")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *AnalyticsHandler) GetExclusiveSummaryPeriod(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.ExclusiveSummaryPeriodRequest
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryPeriod", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryPeriod")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
|
modelReq, err := transformer.ExclusiveSummaryPeriodContractToModel(&req)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryPeriod", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryPeriod")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := h.analyticsService.GetExclusiveSummaryPeriod(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetExclusiveSummaryPeriod", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryPeriod")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contractResp := transformer.ExclusiveSummaryPeriodModelToContract(response)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetExclusiveSummaryPeriod")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnalyticsHandler) GetExclusiveSummaryMonthly(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.ExclusiveSummaryMonthlyRequest
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryMonthly", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMonthly")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
|
modelReq, err := transformer.ExclusiveSummaryMonthlyContractToModel(&req)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryMonthly", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMonthly")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := h.analyticsService.GetExclusiveSummaryMonthly(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetExclusiveSummaryMonthly", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMonthly")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contractResp := transformer.ExclusiveSummaryMonthlyModelToContract(response)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetExclusiveSummaryMonthly")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnalyticsHandler) GetExclusiveSummaryMTD(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.ExclusiveSummaryMTDRequest
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryMTD", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMTD")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
|
modelReq, err := transformer.ExclusiveSummaryMTDContractToModel(&req)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryMTD", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMTD")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := h.analyticsService.GetExclusiveSummaryMTD(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetExclusiveSummaryMTD", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMTD")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contractResp := transformer.ExclusiveSummaryPeriodModelToContract(response)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetExclusiveSummaryMTD")
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (h *CategoryHandler) CreateCategory(c *gin.Context) {
|
|||||||
contextInfo := appcontext.FromGinContext(ctx)
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
var req contract.CreateCategoryRequest
|
var req contract.CreateCategoryRequest
|
||||||
fmt.Printf("CategoryHandler::CreateCategory -> Request: %+v\n", req)
|
fmt.Printf("CategoryHandler::CreateCategory -> Request: %+v\n", req)
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
logger.FromContext(c.Request.Context()).WithError(err).Error("CategoryHandler::CreateCategory -> request binding failed")
|
logger.FromContext(c.Request.Context()).WithError(err).Error("CategoryHandler::CreateCategory -> request binding failed")
|
||||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
@@ -44,6 +44,11 @@ func (h *CategoryHandler) CreateCategory(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject outlet_id from context if user has one and request doesn't provide it
|
||||||
|
if req.OutletID == nil && contextInfo.OutletID != uuid.Nil {
|
||||||
|
req.OutletID = &contextInfo.OutletID
|
||||||
|
}
|
||||||
|
|
||||||
validationError, validationErrorCode := h.categoryValidator.ValidateCreateCategoryRequest(&req)
|
validationError, validationErrorCode := h.categoryValidator.ValidateCreateCategoryRequest(&req)
|
||||||
if validationError != nil {
|
if validationError != nil {
|
||||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
@@ -149,6 +154,11 @@ func (h *CategoryHandler) ListCategories(c *gin.Context) {
|
|||||||
OrganizationID: &contextInfo.OrganizationID,
|
OrganizationID: &contextInfo.OrganizationID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject outlet_id from context if user has one
|
||||||
|
if contextInfo.OutletID != uuid.Nil {
|
||||||
|
req.OutletID = &contextInfo.OutletID
|
||||||
|
}
|
||||||
|
|
||||||
// Parse query parameters
|
// Parse query parameters
|
||||||
if pageStr := c.Query("page"); pageStr != "" {
|
if pageStr := c.Query("page"); pageStr != "" {
|
||||||
if page, err := strconv.Atoi(pageStr); err == nil {
|
if page, err := strconv.Atoi(pageStr); err == nil {
|
||||||
@@ -176,6 +186,23 @@ func (h *CategoryHandler) ListCategories(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||||
|
if outletID, err := uuid.Parse(outletIDStr); err == nil {
|
||||||
|
req.OutletID = &outletID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if parentIDStr := c.Query("parent_id"); parentIDStr != "" {
|
||||||
|
if parentID, err := uuid.Parse(parentIDStr); err == nil {
|
||||||
|
req.ParentID = &parentID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// type=parent -> top level categories only
|
||||||
|
// type=child -> leaf categories (sub categories + top level ones without children)
|
||||||
|
if categoryType := c.Query("type"); categoryType != "" {
|
||||||
|
req.Type = categoryType
|
||||||
|
}
|
||||||
|
|
||||||
validationError, validationErrorCode := h.categoryValidator.ValidateListCategoriesRequest(req)
|
validationError, validationErrorCode := h.categoryValidator.ValidateListCategoriesRequest(req)
|
||||||
if validationError != nil {
|
if validationError != nil {
|
||||||
logger.FromContext(ctx).WithError(validationError).Error("CategoryHandler::ListCategories -> request validation failed")
|
logger.FromContext(ctx).WithError(validationError).Error("CategoryHandler::ListCategories -> request validation failed")
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func (h *ChartOfAccountTypeHandler) DeleteChartOfAccountType(c *gin.Context) {
|
|||||||
func (h *ChartOfAccountTypeHandler) ListChartOfAccountTypes(c *gin.Context) {
|
func (h *ChartOfAccountTypeHandler) ListChartOfAccountTypes(c *gin.Context) {
|
||||||
// Parse query parameters
|
// Parse query parameters
|
||||||
filters := make(map[string]interface{})
|
filters := make(map[string]interface{})
|
||||||
|
|
||||||
if isActive := c.Query("is_active"); isActive != "" {
|
if isActive := c.Query("is_active"); isActive != "" {
|
||||||
if isActiveBool, err := strconv.ParseBool(isActive); err == nil {
|
if isActiveBool, err := strconv.ParseBool(isActive); err == nil {
|
||||||
filters["is_active"] = isActiveBool
|
filters["is_active"] = isActiveBool
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -47,7 +49,7 @@ func (m *CommonMiddleware) Recovery(next http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
|
logger.FromContext(r.Context()).Error("Recovery", fmt.Sprintf("panic recovered: %v", err))
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/service"
|
||||||
|
"apskel-pos-be/internal/validator"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExpenseHandler struct {
|
||||||
|
expenseService service.ExpenseService
|
||||||
|
expenseValidator validator.ExpenseValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExpenseHandler(
|
||||||
|
expenseService service.ExpenseService,
|
||||||
|
expenseValidator validator.ExpenseValidator,
|
||||||
|
) *ExpenseHandler {
|
||||||
|
return &ExpenseHandler{
|
||||||
|
expenseService: expenseService,
|
||||||
|
expenseValidator: expenseValidator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExpenseHandler) CreateExpense(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.CreateExpenseRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::CreateExpense -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::CreateExpense")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validationError, validationErrorCode := h.expenseValidator.ValidateCreateExpenseRequest(&req)
|
||||||
|
if validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::CreateExpense")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseResponse := h.expenseService.CreateExpense(ctx, contextInfo, &req)
|
||||||
|
if expenseResponse.HasErrors() {
|
||||||
|
errorResp := expenseResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::CreateExpense -> Failed to create expense from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::CreateExpense")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExpenseHandler) UpdateExpense(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
expenseIDStr := c.Param("id")
|
||||||
|
expenseID, err := uuid.Parse(expenseIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::UpdateExpense -> Invalid expense ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid expense ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::UpdateExpense")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.UpdateExpenseRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::UpdateExpense -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::UpdateExpense")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validationError, validationErrorCode := h.expenseValidator.ValidateUpdateExpenseRequest(&req)
|
||||||
|
if validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::UpdateExpense")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseResponse := h.expenseService.UpdateExpense(ctx, contextInfo, expenseID, &req)
|
||||||
|
if expenseResponse.HasErrors() {
|
||||||
|
errorResp := expenseResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::UpdateExpense -> Failed to update expense from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::UpdateExpense")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExpenseHandler) DeleteExpense(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
expenseIDStr := c.Param("id")
|
||||||
|
expenseID, err := uuid.Parse(expenseIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::DeleteExpense -> Invalid expense ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid expense ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::DeleteExpense")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseResponse := h.expenseService.DeleteExpense(ctx, contextInfo, expenseID)
|
||||||
|
if expenseResponse.HasErrors() {
|
||||||
|
errorResp := expenseResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::DeleteExpense -> Failed to delete expense from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::DeleteExpense")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExpenseHandler) GetExpense(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
expenseIDStr := c.Param("id")
|
||||||
|
expenseID, err := uuid.Parse(expenseIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::GetExpense -> Invalid expense ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid expense ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::GetExpense")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseResponse := h.expenseService.GetExpenseByID(ctx, contextInfo, expenseID)
|
||||||
|
if expenseResponse.HasErrors() {
|
||||||
|
errorResp := expenseResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::GetExpense -> Failed to get expense from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::GetExpense")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExpenseHandler) ListExpenses(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
req := &contract.ListExpenseRequest{
|
||||||
|
Page: 1,
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageStr := c.Query("page"); pageStr != "" {
|
||||||
|
if page, err := strconv.Atoi(pageStr); err == nil {
|
||||||
|
req.Page = page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if limitStr := c.Query("limit"); limitStr != "" {
|
||||||
|
if limit, err := strconv.Atoi(limitStr); err == nil {
|
||||||
|
req.Limit = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if search := c.Query("search"); search != "" {
|
||||||
|
req.Search = search
|
||||||
|
}
|
||||||
|
|
||||||
|
if status := c.Query("status"); status != "" {
|
||||||
|
req.Status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritize outlet_id from context (e.g. outlet-scoped user),
|
||||||
|
// fall back to query param if context has no outlet.
|
||||||
|
if contextInfo.OutletID != uuid.Nil {
|
||||||
|
req.OutletID = contextInfo.OutletID.String()
|
||||||
|
} else if outletID := c.Query("outlet_id"); outletID != "" {
|
||||||
|
req.OutletID = outletID
|
||||||
|
}
|
||||||
|
|
||||||
|
if startDate := c.Query("start_date"); startDate != "" {
|
||||||
|
req.StartDate = startDate
|
||||||
|
}
|
||||||
|
|
||||||
|
if endDate := c.Query("end_date"); endDate != "" {
|
||||||
|
req.EndDate = endDate
|
||||||
|
}
|
||||||
|
|
||||||
|
validationError, validationErrorCode := h.expenseValidator.ValidateListExpenseRequest(req)
|
||||||
|
if validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::ListExpenses")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseResponse := h.expenseService.ListExpenses(ctx, contextInfo, req)
|
||||||
|
if expenseResponse.HasErrors() {
|
||||||
|
errorResp := expenseResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::ListExpenses -> Failed to list expenses from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::ListExpenses")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExpenseHandler) GetExpenseAnalytics(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.ExpenseAnalyticsRequest
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::GetExpenseAnalytics -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::GetExpenseAnalytics")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if contextInfo.OutletID != uuid.Nil {
|
||||||
|
outletID := contextInfo.OutletID.String()
|
||||||
|
req.OutletID = &outletID
|
||||||
|
} else if outletID := c.Query("outlet_id"); outletID != "" {
|
||||||
|
req.OutletID = &outletID
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseResponse := h.expenseService.GetExpenseAnalytics(ctx, contextInfo, &req)
|
||||||
|
if expenseResponse.HasErrors() {
|
||||||
|
errorResp := expenseResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::GetExpenseAnalytics -> Failed to get expense analytics from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::GetExpenseAnalytics")
|
||||||
|
}
|
||||||
@@ -275,4 +275,3 @@ func (h *IngredientUnitConverterHandler) GetUnitsByIngredientID(c *gin.Context)
|
|||||||
|
|
||||||
util.HandleResponse(c.Writer, c.Request, unitsResponse, "IngredientUnitConverterHandler::GetUnitsByIngredientID")
|
util.HandleResponse(c.Writer, c.Request, unitsResponse, "IngredientUnitConverterHandler::GetUnitsByIngredientID")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/service"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
"apskel-pos-be/internal/validator"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationHandler struct {
|
||||||
|
notificationService service.NotificationService
|
||||||
|
notificationValidator validator.NotificationValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationHandler(
|
||||||
|
notificationService service.NotificationService,
|
||||||
|
notificationValidator validator.NotificationValidator,
|
||||||
|
) *NotificationHandler {
|
||||||
|
return &NotificationHandler{
|
||||||
|
notificationService: notificationService,
|
||||||
|
notificationValidator: notificationValidator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send godoc
|
||||||
|
// POST /api/v1/notifications/send
|
||||||
|
// Sends a notification to specific users.
|
||||||
|
func (h *NotificationHandler) Send(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.SendNotificationRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::Send -> request binding failed")
|
||||||
|
validationErr := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::Send")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErr, errCode := h.notificationValidator.ValidateSendRequest(&req); validationErr != nil {
|
||||||
|
respErr := contract.NewResponseError(errCode, constants.RequestEntity, validationErr.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{respErr}), "NotificationHandler::Send")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.notificationService.Send(ctx, &req, contextInfo.UserID)
|
||||||
|
if resp.HasErrors() {
|
||||||
|
logger.FromContext(ctx).WithError(resp.GetErrors()[0]).Error("NotificationHandler::Send -> service error")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::Send")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast godoc
|
||||||
|
// POST /api/v1/notifications/broadcast
|
||||||
|
// Sends a notification to all active users in the caller's organization.
|
||||||
|
func (h *NotificationHandler) Broadcast(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.BroadcastNotificationRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::Broadcast -> request binding failed")
|
||||||
|
validationErr := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::Broadcast")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErr, errCode := h.notificationValidator.ValidateBroadcastRequest(&req); validationErr != nil {
|
||||||
|
respErr := contract.NewResponseError(errCode, constants.RequestEntity, validationErr.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{respErr}), "NotificationHandler::Broadcast")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.notificationService.Broadcast(ctx, &req, contextInfo.OrganizationID, contextInfo.UserID)
|
||||||
|
if resp.HasErrors() {
|
||||||
|
logger.FromContext(ctx).WithError(resp.GetErrors()[0]).Error("NotificationHandler::Broadcast -> service error")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::Broadcast")
|
||||||
|
}
|
||||||
|
|
||||||
|
// List godoc
|
||||||
|
// GET /api/v1/notifications
|
||||||
|
// Returns paginated notifications for the authenticated user.
|
||||||
|
func (h *NotificationHandler) List(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
req := contract.ListNotificationsRequest{
|
||||||
|
Page: 1,
|
||||||
|
Limit: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::List -> query binding failed")
|
||||||
|
validationErr := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::List")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErr, errCode := h.notificationValidator.ValidateListRequest(&req); validationErr != nil {
|
||||||
|
respErr := contract.NewResponseError(errCode, constants.RequestEntity, validationErr.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{respErr}), "NotificationHandler::List")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.notificationService.ListForUser(ctx, &req, contextInfo.UserID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::List")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID godoc
|
||||||
|
// GET /api/v1/notifications/:id
|
||||||
|
func (h *NotificationHandler) GetByID(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::GetByID -> invalid notification ID")
|
||||||
|
validationErr := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid notification ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::GetByID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.notificationService.GetByID(ctx, id)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::GetByID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAsRead godoc
|
||||||
|
// PUT /api/v1/notifications/:id/read
|
||||||
|
func (h *NotificationHandler) MarkAsRead(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
receiverID, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::MarkAsRead -> invalid receiver ID")
|
||||||
|
validationErr := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid notification receiver ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::MarkAsRead")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.notificationService.MarkAsRead(ctx, receiverID, contextInfo.UserID)
|
||||||
|
if resp.HasErrors() {
|
||||||
|
logger.FromContext(ctx).WithError(resp.GetErrors()[0]).Error("NotificationHandler::MarkAsRead -> service error")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::MarkAsRead")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAllAsRead godoc
|
||||||
|
// PUT /api/v1/notifications/read-all
|
||||||
|
func (h *NotificationHandler) MarkAllAsRead(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
resp := h.notificationService.MarkAllAsRead(ctx, contextInfo.UserID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::MarkAllAsRead")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete godoc
|
||||||
|
// DELETE /api/v1/notifications/:id
|
||||||
|
func (h *NotificationHandler) Delete(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
receiverID, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("NotificationHandler::Delete -> invalid receiver ID")
|
||||||
|
validationErr := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid notification receiver ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationErr}), "NotificationHandler::Delete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.notificationService.DeleteForUser(ctx, receiverID, contextInfo.UserID)
|
||||||
|
if resp.HasErrors() {
|
||||||
|
logger.FromContext(ctx).WithError(resp.GetErrors()[0]).Error("NotificationHandler::Delete -> service error")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "NotificationHandler::Delete")
|
||||||
|
}
|
||||||
@@ -137,6 +137,10 @@ func (h *OrderHandler) ListOrders(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modelReq.OrganizationID = &contextInfo.OrganizationID
|
modelReq.OrganizationID = &contextInfo.OrganizationID
|
||||||
|
if modelReq.OutletID == nil && contextInfo.OutletID != uuid.Nil {
|
||||||
|
modelReq.OutletID = &contextInfo.OutletID
|
||||||
|
}
|
||||||
|
|
||||||
response, err := h.orderService.ListOrders(c.Request.Context(), modelReq)
|
response, err := h.orderService.ListOrders(c.Request.Context(), modelReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::ListOrders", err.Error())}), "OrderHandler::ListOrders")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::ListOrders", err.Error())}), "OrderHandler::ListOrders")
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ func (h *ProductHandler) CreateProduct(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *ProductHandler) UpdateProduct(c *gin.Context) {
|
func (h *ProductHandler) UpdateProduct(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
productIDStr := c.Param("id")
|
productIDStr := c.Param("id")
|
||||||
productID, err := uuid.Parse(productIDStr)
|
productID, err := uuid.Parse(productIDStr)
|
||||||
@@ -85,7 +86,7 @@ func (h *ProductHandler) UpdateProduct(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
productResponse := h.productService.UpdateProduct(ctx, productID, &req)
|
productResponse := h.productService.UpdateProduct(ctx, contextInfo, productID, &req)
|
||||||
if productResponse.HasErrors() {
|
if productResponse.HasErrors() {
|
||||||
errorResp := productResponse.GetErrors()[0]
|
errorResp := productResponse.GetErrors()[0]
|
||||||
logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::UpdateProduct -> Failed to update product from service")
|
logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::UpdateProduct -> Failed to update product from service")
|
||||||
@@ -117,6 +118,7 @@ func (h *ProductHandler) DeleteProduct(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *ProductHandler) GetProduct(c *gin.Context) {
|
func (h *ProductHandler) GetProduct(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
productIDStr := c.Param("id")
|
productIDStr := c.Param("id")
|
||||||
productID, err := uuid.Parse(productIDStr)
|
productID, err := uuid.Parse(productIDStr)
|
||||||
@@ -127,7 +129,7 @@ func (h *ProductHandler) GetProduct(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
productResponse := h.productService.GetProductByID(ctx, productID)
|
productResponse := h.productService.GetProductByID(ctx, productID, contextInfo.OutletID)
|
||||||
if productResponse.HasErrors() {
|
if productResponse.HasErrors() {
|
||||||
errorResp := productResponse.GetErrors()[0]
|
errorResp := productResponse.GetErrors()[0]
|
||||||
logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::GetProduct -> Failed to get product from service")
|
logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::GetProduct -> Failed to get product from service")
|
||||||
@@ -184,6 +186,97 @@ func (h *ProductHandler) ListProducts(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||||
|
if outletID, err := uuid.Parse(outletIDStr); err == nil {
|
||||||
|
req.OutletID = &outletID
|
||||||
|
}
|
||||||
|
} else if contextInfo.OutletID != uuid.Nil {
|
||||||
|
req.OutletID = &contextInfo.OutletID
|
||||||
|
}
|
||||||
|
|
||||||
|
if minPriceStr := c.Query("min_price"); minPriceStr != "" {
|
||||||
|
if minPrice, err := strconv.ParseFloat(minPriceStr, 64); err == nil {
|
||||||
|
req.MinPrice = &minPrice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxPriceStr := c.Query("max_price"); maxPriceStr != "" {
|
||||||
|
if maxPrice, err := strconv.ParseFloat(maxPriceStr, 64); err == nil {
|
||||||
|
req.MaxPrice = &maxPrice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validationError, validationErrorCode := h.productValidator.ValidateListProductsRequest(req)
|
||||||
|
if validationError != nil {
|
||||||
|
logger.FromContext(ctx).WithError(validationError).Error("ProductHandler::ListProducts -> request validation failed")
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductHandler::ListProducts")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
productsResponse := h.productService.ListProducts(ctx, req)
|
||||||
|
if productsResponse.HasErrors() {
|
||||||
|
errorResp := productsResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::ListProducts -> Failed to list products from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, productsResponse, "ProductHandler::ListProducts")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) ListProductAll(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
req := &contract.ListProductsRequest{
|
||||||
|
Page: 1,
|
||||||
|
Limit: 10,
|
||||||
|
OrganizationID: &contextInfo.OrganizationID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageStr := c.Query("page"); pageStr != "" {
|
||||||
|
if page, err := strconv.Atoi(pageStr); err == nil {
|
||||||
|
req.Page = page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if limitStr := c.Query("limit"); limitStr != "" {
|
||||||
|
if limit, err := strconv.Atoi(limitStr); err == nil {
|
||||||
|
req.Limit = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if search := c.Query("search"); search != "" {
|
||||||
|
req.Search = search
|
||||||
|
}
|
||||||
|
|
||||||
|
if businessType := c.Query("business_type"); businessType != "" {
|
||||||
|
req.BusinessType = businessType
|
||||||
|
}
|
||||||
|
|
||||||
|
if organizationIDStr := c.Query("organization_id"); organizationIDStr != "" {
|
||||||
|
if organizationID, err := uuid.Parse(organizationIDStr); err == nil {
|
||||||
|
req.OrganizationID = &organizationID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if categoryIDStr := c.Query("category_id"); categoryIDStr != "" {
|
||||||
|
if categoryID, err := uuid.Parse(categoryIDStr); err == nil {
|
||||||
|
req.CategoryID = &categoryID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
|
||||||
|
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
||||||
|
req.IsActive = &isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||||
|
if outletID, err := uuid.Parse(outletIDStr); err == nil {
|
||||||
|
req.OutletID = &outletID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if minPriceStr := c.Query("min_price"); minPriceStr != "" {
|
if minPriceStr := c.Query("min_price"); minPriceStr != "" {
|
||||||
if minPrice, err := strconv.ParseFloat(minPriceStr, 64); err == nil {
|
if minPrice, err := strconv.ParseFloat(minPriceStr, 64); err == nil {
|
||||||
req.MinPrice = &minPrice
|
req.MinPrice = &minPrice
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/service"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
"apskel-pos-be/internal/validator"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductOutletPriceHandler struct {
|
||||||
|
service service.ProductOutletPriceService
|
||||||
|
validator validator.ProductOutletPriceValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductOutletPriceHandler(svc service.ProductOutletPriceService, v validator.ProductOutletPriceValidator) *ProductOutletPriceHandler {
|
||||||
|
return &ProductOutletPriceHandler{
|
||||||
|
service: svc,
|
||||||
|
validator: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductOutletPriceHandler) Upsert(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
var req contract.CreateProductOutletPriceRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductOutletPriceHandler::Upsert -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::Upsert")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErr, code := h.validator.ValidateCreateRequest(&req); validationErr != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(code, constants.RequestEntity, validationErr.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::Upsert")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.service.Upsert(ctx, &req)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "ProductOutletPriceHandler::Upsert")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductOutletPriceHandler) GetByProductAndOutlet(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
productIDStr := c.Param("product_id")
|
||||||
|
productID, err := uuid.Parse(productIDStr)
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid product ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::GetByProductAndOutlet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outletIDStr := c.Param("outlet_id")
|
||||||
|
outletID, err := uuid.Parse(outletIDStr)
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::GetByProductAndOutlet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.service.GetByProductAndOutlet(ctx, productID, outletID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "ProductOutletPriceHandler::GetByProductAndOutlet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductOutletPriceHandler) GetByProduct(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
productIDStr := c.Param("product_id")
|
||||||
|
productID, err := uuid.Parse(productIDStr)
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid product ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::GetByProduct")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.service.GetByProduct(ctx, productID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "ProductOutletPriceHandler::GetByProduct")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductOutletPriceHandler) GetByOutlet(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
outletIDStr := c.Param("outlet_id")
|
||||||
|
outletID, err := uuid.Parse(outletIDStr)
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::GetByOutlet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.service.GetByOutlet(ctx, outletID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "ProductOutletPriceHandler::GetByOutlet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductOutletPriceHandler) Delete(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::Delete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.service.Delete(ctx, id)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "ProductOutletPriceHandler::Delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductOutletPriceHandler) BulkUpsert(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
var req contract.BulkCreateProductOutletPriceRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductOutletPriceHandler::BulkUpsert -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::BulkUpsert")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErr, code := h.validator.ValidateBulkCreateRequest(&req); validationErr != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(code, constants.RequestEntity, validationErr.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductOutletPriceHandler::BulkUpsert")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := h.service.BulkUpsert(ctx, &req)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, resp, "ProductOutletPriceHandler::BulkUpsert")
|
||||||
|
}
|
||||||
@@ -219,4 +219,4 @@ func (h *ProductRecipeHandler) BulkCreate(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(recipes))
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(recipes))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/service"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
"apskel-pos-be/internal/validator"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryHandler struct {
|
||||||
|
purchaseCategoryService service.PurchaseCategoryService
|
||||||
|
purchaseCategoryValidator validator.PurchaseCategoryValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseCategoryHandler(purchaseCategoryService service.PurchaseCategoryService, purchaseCategoryValidator validator.PurchaseCategoryValidator) *PurchaseCategoryHandler {
|
||||||
|
return &PurchaseCategoryHandler{
|
||||||
|
purchaseCategoryService: purchaseCategoryService,
|
||||||
|
purchaseCategoryValidator: purchaseCategoryValidator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) CreatePurchaseCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.CreatePurchaseCategoryRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("PurchaseCategoryHandler::CreatePurchaseCategory -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::CreatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationError, validationErrorCode := h.purchaseCategoryValidator.ValidateCreatePurchaseCategoryRequest(&req); validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::CreatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.CreatePurchaseCategory(ctx, contextInfo, &req)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::CreatePurchaseCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) UpdatePurchaseCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
categoryID, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase category ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.UpdatePurchaseCategoryRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("PurchaseCategoryHandler::UpdatePurchaseCategory -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationError, validationErrorCode := h.purchaseCategoryValidator.ValidateUpdatePurchaseCategoryRequest(&req); validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.UpdatePurchaseCategory(ctx, contextInfo, categoryID, &req)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) DeletePurchaseCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
categoryID, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase category ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::DeletePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.DeletePurchaseCategory(ctx, contextInfo, categoryID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::DeletePurchaseCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) GetPurchaseCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
categoryID, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase category ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::GetPurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.GetPurchaseCategoryByID(ctx, contextInfo, categoryID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::GetPurchaseCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) ListPurchaseCategories(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
req := &contract.ListPurchaseCategoriesRequest{
|
||||||
|
Page: 1,
|
||||||
|
Limit: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageStr := c.Query("page"); pageStr != "" {
|
||||||
|
if page, err := strconv.Atoi(pageStr); err == nil {
|
||||||
|
req.Page = page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if limitStr := c.Query("limit"); limitStr != "" {
|
||||||
|
if limit, err := strconv.Atoi(limitStr); err == nil {
|
||||||
|
req.Limit = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parentIDStr := c.Query("parent_id"); parentIDStr != "" {
|
||||||
|
if parentID, err := uuid.Parse(parentIDStr); err == nil {
|
||||||
|
req.ParentID = &parentID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if categoryType := c.Query("type"); categoryType != "" {
|
||||||
|
req.Type = categoryType
|
||||||
|
}
|
||||||
|
|
||||||
|
if search := c.Query("search"); search != "" {
|
||||||
|
req.Search = search
|
||||||
|
}
|
||||||
|
|
||||||
|
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
|
||||||
|
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
||||||
|
req.IsActive = &isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationError, validationErrorCode := h.purchaseCategoryValidator.ValidateListPurchaseCategoriesRequest(req); validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::ListPurchaseCategories")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.ListPurchaseCategories(ctx, contextInfo, req)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::ListPurchaseCategories")
|
||||||
|
}
|
||||||
@@ -176,6 +176,20 @@ func (h *PurchaseOrderHandler) ListPurchaseOrders(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if team := c.Query("team"); team != "" {
|
||||||
|
req.Team = team
|
||||||
|
}
|
||||||
|
|
||||||
|
if teamScope := c.Query("team_scope"); teamScope != "" {
|
||||||
|
req.TeamScope = teamScope
|
||||||
|
}
|
||||||
|
|
||||||
|
if teamCategoryIDStr := c.Query("team_category_id"); teamCategoryIDStr != "" {
|
||||||
|
if teamCategoryID, err := uuid.Parse(teamCategoryIDStr); err == nil {
|
||||||
|
req.TeamCategoryID = &teamCategoryID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if startDateStr := c.Query("start_date"); startDateStr != "" {
|
if startDateStr := c.Query("start_date"); startDateStr != "" {
|
||||||
if startDate, err := time.Parse("2006-01-02", startDateStr); err == nil {
|
if startDate, err := time.Parse("2006-01-02", startDateStr); err == nil {
|
||||||
req.StartDate = &startDate
|
req.StartDate = &startDate
|
||||||
@@ -224,6 +238,21 @@ func (h *PurchaseOrderHandler) GetPurchaseOrdersByStatus(c *gin.Context) {
|
|||||||
util.HandleResponse(c.Writer, c.Request, poResponse, "PurchaseOrderHandler::GetPurchaseOrdersByStatus")
|
util.HandleResponse(c.Writer, c.Request, poResponse, "PurchaseOrderHandler::GetPurchaseOrdersByStatus")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListPurchaseTeams serves the team picker for the purchase form: the parent
|
||||||
|
// categories of the caller's outlet, plus Pusat.
|
||||||
|
func (h *PurchaseOrderHandler) ListPurchaseTeams(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
teamsResponse := h.purchaseOrderService.ListPurchaseTeams(ctx, contextInfo)
|
||||||
|
if teamsResponse.HasErrors() {
|
||||||
|
errorResp := teamsResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("PurchaseOrderHandler::ListPurchaseTeams -> Failed to list purchase teams from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, teamsResponse, "PurchaseOrderHandler::ListPurchaseTeams")
|
||||||
|
}
|
||||||
|
|
||||||
func (h *PurchaseOrderHandler) GetOverduePurchaseOrders(c *gin.Context) {
|
func (h *PurchaseOrderHandler) GetOverduePurchaseOrders(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReportHandler struct {
|
type ReportHandler struct {
|
||||||
@@ -19,11 +20,26 @@ func NewReportHandler(reportService service.ReportService, userService UserServi
|
|||||||
return &ReportHandler{reportService: reportService, userService: userService}
|
return &ReportHandler{reportService: reportService, userService: userService}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *ReportHandler) resolveOutletID(c *gin.Context, contextOutletID uuid.UUID) string {
|
||||||
|
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||||
|
if _, err := uuid.Parse(outletIDStr); err == nil {
|
||||||
|
return outletIDStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pathOutletID := c.Param("outlet_id"); pathOutletID != "" {
|
||||||
|
return pathOutletID
|
||||||
|
}
|
||||||
|
if contextOutletID != uuid.Nil {
|
||||||
|
return contextOutletID.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
|
func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
ci := appcontext.FromGinContext(ctx)
|
ci := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
outletID := c.Param("outlet_id")
|
outletID := h.resolveOutletID(c, ci.OutletID)
|
||||||
var dayPtr *time.Time
|
var dayPtr *time.Time
|
||||||
if d := c.Query("date"); d != "" {
|
if d := c.Query("date"); d != "" {
|
||||||
if t, err := time.Parse("2006-01-02", d); err == nil {
|
if t, err := time.Parse("2006-01-02", d); err == nil {
|
||||||
@@ -50,3 +66,35 @@ func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
|
|||||||
"file_name": fileName,
|
"file_name": fileName,
|
||||||
}), "ReportHandler::GetDailyTransactionReportPDF")
|
}), "ReportHandler::GetDailyTransactionReportPDF")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *ReportHandler) GetProfitLossReportPDF(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
ci := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
outletID := h.resolveOutletID(c, ci.OutletID)
|
||||||
|
var dayPtr *time.Time
|
||||||
|
if d := c.Query("date"); d != "" {
|
||||||
|
if t, err := time.Parse("2006-01-02", d); err == nil {
|
||||||
|
dayPtr = &t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.userService.GetUserByID(ctx, ci.UserID)
|
||||||
|
var genBy string
|
||||||
|
if err != nil {
|
||||||
|
genBy = ci.UserID.String()
|
||||||
|
} else {
|
||||||
|
genBy = user.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
publicURL, fileName, err := h.reportService.GenerateProfitLossPDF(ctx, ci.OrganizationID.String(), outletID, dayPtr, genBy)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "ReportHandler::GetProfitLossReportPDF", err.Error())}), "ReportHandler::GetProfitLossReportPDF")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(map[string]string{
|
||||||
|
"url": publicURL,
|
||||||
|
"file_name": fileName,
|
||||||
|
}), "ReportHandler::GetProfitLossReportPDF")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"apskel-pos-be/internal/client"
|
|
||||||
"apskel-pos-be/internal/constants"
|
"apskel-pos-be/internal/constants"
|
||||||
"apskel-pos-be/internal/contract"
|
"apskel-pos-be/internal/contract"
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
"apskel-pos-be/internal/logger"
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/mappers"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
"apskel-pos-be/internal/pkg/tabletoken"
|
"apskel-pos-be/internal/pkg/tabletoken"
|
||||||
"apskel-pos-be/internal/processor"
|
"apskel-pos-be/internal/processor"
|
||||||
@@ -15,22 +15,21 @@ import (
|
|||||||
"apskel-pos-be/internal/util"
|
"apskel-pos-be/internal/util"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SelfOrderHandler struct {
|
type SelfOrderHandler struct {
|
||||||
orderService service.OrderService
|
orderService service.OrderService
|
||||||
categoryService service.CategoryService
|
categoryService service.CategoryService
|
||||||
productService service.ProductService
|
productService service.ProductService
|
||||||
tableRepo repository.TableRepositoryInterface
|
tableRepo repository.TableRepositoryInterface
|
||||||
outletRepo processor.OutletRepository
|
outletRepo processor.OutletRepository
|
||||||
userRepo processor.UserRepository
|
userRepo processor.UserRepository
|
||||||
sessionRepo repository.SessionRepository
|
sessionRepo repository.SessionRepository
|
||||||
orderRepo repository.OrderRepository
|
orderRepo repository.OrderRepository
|
||||||
fcmClient client.FcmClient
|
productOutletPriceService service.ProductOutletPriceService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSelfOrderHandler(
|
func NewSelfOrderHandler(
|
||||||
@@ -42,18 +41,18 @@ func NewSelfOrderHandler(
|
|||||||
userRepo processor.UserRepository,
|
userRepo processor.UserRepository,
|
||||||
sessionRepo repository.SessionRepository,
|
sessionRepo repository.SessionRepository,
|
||||||
orderRepo repository.OrderRepository,
|
orderRepo repository.OrderRepository,
|
||||||
fcmClient client.FcmClient,
|
productOutletPriceService service.ProductOutletPriceService,
|
||||||
) *SelfOrderHandler {
|
) *SelfOrderHandler {
|
||||||
return &SelfOrderHandler{
|
return &SelfOrderHandler{
|
||||||
orderService: orderService,
|
orderService: orderService,
|
||||||
categoryService: categoryService,
|
categoryService: categoryService,
|
||||||
productService: productService,
|
productService: productService,
|
||||||
tableRepo: tableRepo,
|
tableRepo: tableRepo,
|
||||||
outletRepo: outletRepo,
|
outletRepo: outletRepo,
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
sessionRepo: sessionRepo,
|
sessionRepo: sessionRepo,
|
||||||
orderRepo: orderRepo,
|
orderRepo: orderRepo,
|
||||||
fcmClient: fcmClient,
|
productOutletPriceService: productOutletPriceService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,16 +219,29 @@ func (h *SelfOrderHandler) GetMenu(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
menu := h.buildMenuResponse(outlet, table, catList.Categories, prodList.Products)
|
menu := h.buildMenuResponse(ctx, outlet, table, catList.Categories, prodList.Products)
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(menu), "SelfOrderHandler::GetMenu")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(menu), "SelfOrderHandler::GetMenu")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SelfOrderHandler) buildMenuResponse(
|
func (h *SelfOrderHandler) buildMenuResponse(
|
||||||
|
ctx context.Context,
|
||||||
outlet *entities.Outlet,
|
outlet *entities.Outlet,
|
||||||
table *entities.Table,
|
table *entities.Table,
|
||||||
categories []contract.CategoryResponse,
|
categories []contract.CategoryResponse,
|
||||||
products []contract.ProductResponse,
|
products []contract.ProductResponse,
|
||||||
) *contract.SelfOrderMenuResponse {
|
) *contract.SelfOrderMenuResponse {
|
||||||
|
outletPriceMap := make(map[uuid.UUID]float64)
|
||||||
|
if h.productOutletPriceService != nil {
|
||||||
|
priceResp := h.productOutletPriceService.GetByOutlet(ctx, outlet.ID)
|
||||||
|
if priceResp != nil && !priceResp.HasErrors() {
|
||||||
|
if priceList, ok := priceResp.Data.(*contract.ListProductOutletPricesResponse); ok {
|
||||||
|
for _, p := range priceList.Prices {
|
||||||
|
outletPriceMap[p.ProductID] = p.Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
productMap := make(map[uuid.UUID][]contract.ProductResponse)
|
productMap := make(map[uuid.UUID][]contract.ProductResponse)
|
||||||
for _, p := range products {
|
for _, p := range products {
|
||||||
productMap[p.CategoryID] = append(productMap[p.CategoryID], p)
|
productMap[p.CategoryID] = append(productMap[p.CategoryID], p)
|
||||||
@@ -240,11 +252,15 @@ func (h *SelfOrderHandler) buildMenuResponse(
|
|||||||
menuItems := make([]contract.SelfOrderMenuItem, 0)
|
menuItems := make([]contract.SelfOrderMenuItem, 0)
|
||||||
if prods, ok := productMap[cat.ID]; ok {
|
if prods, ok := productMap[cat.ID]; ok {
|
||||||
for _, p := range prods {
|
for _, p := range prods {
|
||||||
|
price := p.Price
|
||||||
|
if outletPrice, exists := outletPriceMap[p.ID]; exists {
|
||||||
|
price = outletPrice
|
||||||
|
}
|
||||||
item := contract.SelfOrderMenuItem{
|
item := contract.SelfOrderMenuItem{
|
||||||
ID: p.ID,
|
ID: p.ID,
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
Description: p.Description,
|
Description: p.Description,
|
||||||
Price: p.Price,
|
Price: price,
|
||||||
ImageURL: p.ImageURL,
|
ImageURL: p.ImageURL,
|
||||||
}
|
}
|
||||||
for _, v := range p.Variants {
|
for _, v := range p.Variants {
|
||||||
@@ -335,6 +351,7 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
|
|||||||
metadata := make(map[string]interface{})
|
metadata := make(map[string]interface{})
|
||||||
metadata["self_order"] = true
|
metadata["self_order"] = true
|
||||||
metadata["session_id"] = session.ID
|
metadata["session_id"] = session.ID
|
||||||
|
metadata["customer_name"] = req.CustomerName
|
||||||
|
|
||||||
tableID := table.ID
|
tableID := table.ID
|
||||||
modelReq := &models.CreateOrderRequest{
|
modelReq := &models.CreateOrderRequest{
|
||||||
@@ -342,7 +359,7 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
|
|||||||
UserID: userID,
|
UserID: userID,
|
||||||
TableID: &tableID,
|
TableID: &tableID,
|
||||||
TableNumber: &table.TableName,
|
TableNumber: &table.TableName,
|
||||||
OrderType: constants.OrderTypeDineIn,
|
OrderType: constants.OrderType(req.OrderType),
|
||||||
OrderItems: orderItems,
|
OrderItems: orderItems,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
}
|
}
|
||||||
@@ -356,41 +373,13 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go h.sendNewOrderNotification(context.Background(), table.OrganizationID, table.TableName, len(req.OrderItems))
|
|
||||||
|
|
||||||
contractResp := transformer.OrderModelToContract(response)
|
contractResp := transformer.OrderModelToContract(response)
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "SelfOrderHandler::CreateOrder")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "SelfOrderHandler::CreateOrder")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SelfOrderHandler) sendNewOrderNotification(ctx context.Context, organizationID uuid.UUID, tableName string, itemCount int) {
|
|
||||||
users, err := h.userRepo.GetUsersWithFcmTokenByOrganization(ctx, organizationID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("SelfOrderHandler::sendNewOrderNotification -> failed to get users with FCM token: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens := make([]string, 0, len(users))
|
|
||||||
for _, u := range users {
|
|
||||||
if u.FcmToken != nil && *u.FcmToken != "" {
|
|
||||||
tokens = append(tokens, *u.FcmToken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tokens) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
title := "Order Baru"
|
|
||||||
body := fmt.Sprintf("Order baru dari Meja %s — %d item", tableName, itemCount)
|
|
||||||
|
|
||||||
if err := h.fcmClient.SendMulticastNotification(ctx, tokens, title, body); err != nil {
|
|
||||||
log.Printf("SelfOrderHandler::sendNewOrderNotification -> failed to send FCM notification: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) {
|
func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
sessionID := c.Param("sessionId")
|
sessionID := c.Param("session_id")
|
||||||
|
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
@@ -423,47 +412,15 @@ func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := &contract.SelfOrderListOrdersResponse{
|
modelOrders := mappers.OrderEntitiesToResponses(orders)
|
||||||
Orders: make([]contract.SelfOrderOrderItem, 0, len(orders)),
|
contractOrders := make([]contract.OrderResponse, len(modelOrders))
|
||||||
}
|
for i := range modelOrders {
|
||||||
for _, o := range orders {
|
contractOrders[i] = *transformer.OrderModelToContract(&modelOrders[i])
|
||||||
item := contract.SelfOrderOrderItem{
|
|
||||||
ID: o.ID,
|
|
||||||
OrderNumber: o.OrderNumber,
|
|
||||||
TableNumber: o.TableNumber,
|
|
||||||
OrderType: string(o.OrderType),
|
|
||||||
Status: string(o.Status),
|
|
||||||
Subtotal: o.Subtotal,
|
|
||||||
TaxAmount: o.TaxAmount,
|
|
||||||
DiscountAmount: o.DiscountAmount,
|
|
||||||
TotalAmount: o.TotalAmount,
|
|
||||||
RemainingAmount: o.RemainingAmount,
|
|
||||||
PaymentStatus: string(o.PaymentStatus),
|
|
||||||
IsVoid: o.IsVoid,
|
|
||||||
IsRefund: o.IsRefund,
|
|
||||||
CreatedAt: o.CreatedAt,
|
|
||||||
}
|
|
||||||
for _, oi := range o.OrderItems {
|
|
||||||
lineItem := contract.SelfOrderOrderLineItem{
|
|
||||||
ProductID: oi.ProductID,
|
|
||||||
Quantity: oi.Quantity,
|
|
||||||
UnitPrice: oi.UnitPrice,
|
|
||||||
TotalPrice: oi.TotalPrice,
|
|
||||||
Notes: oi.Notes,
|
|
||||||
Status: string(oi.Status),
|
|
||||||
ProductVariantID: oi.ProductVariantID,
|
|
||||||
}
|
|
||||||
if oi.Product.ID != uuid.Nil {
|
|
||||||
lineItem.ProductName = oi.Product.Name
|
|
||||||
}
|
|
||||||
if oi.ProductVariant != nil {
|
|
||||||
lineItem.ProductVariantNam = &oi.ProductVariant.Name
|
|
||||||
}
|
|
||||||
item.Items = append(item.Items, lineItem)
|
|
||||||
}
|
|
||||||
resp.Orders = append(resp.Orders, item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resp := &contract.SelfOrderListOrdersResponse{
|
||||||
|
Orders: contractOrders,
|
||||||
|
}
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "SelfOrderHandler::GetOrdersBySession")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "SelfOrderHandler::GetOrdersBySession")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,6 +429,7 @@ func (h *SelfOrderHandler) validateCreateOrderRequest(req *contract.SelfOrderCre
|
|||||||
return fmt.Errorf("session_id is required")
|
return fmt.Errorf("session_id is required")
|
||||||
}
|
}
|
||||||
if len(req.OrderItems) == 0 {
|
if len(req.OrderItems) == 0 {
|
||||||
|
|
||||||
return fmt.Errorf("at least one order item is required")
|
return fmt.Errorf("at least one order item is required")
|
||||||
}
|
}
|
||||||
for i, item := range req.OrderItems {
|
for i, item := range req.OrderItems {
|
||||||
@@ -499,7 +457,7 @@ func (h *SelfOrderHandler) ListCategories(c *gin.Context) {
|
|||||||
|
|
||||||
if req.OrganizationID == "" {
|
if req.OrganizationID == "" {
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "organisasi_id is required"),
|
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "organization_id is required"),
|
||||||
}), "SelfOrderHandler::ListCategories")
|
}), "SelfOrderHandler::ListCategories")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -514,7 +472,7 @@ func (h *SelfOrderHandler) ListCategories(c *gin.Context) {
|
|||||||
orgID, err := uuid.Parse(req.OrganizationID)
|
orgID, err := uuid.Parse(req.OrganizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid organisasi_id format"),
|
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid organization_id format"),
|
||||||
}), "SelfOrderHandler::ListCategories")
|
}), "SelfOrderHandler::ListCategories")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ import (
|
|||||||
type TableHandler struct {
|
type TableHandler struct {
|
||||||
tableService TableService
|
tableService TableService
|
||||||
tableValidator *validator.TableValidator
|
tableValidator *validator.TableValidator
|
||||||
baseURL string
|
selfOrderURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator, baseURL string) *TableHandler {
|
func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator, selfOrderURL string) *TableHandler {
|
||||||
return &TableHandler{
|
return &TableHandler{
|
||||||
tableService: tableService,
|
tableService: tableService,
|
||||||
tableValidator: tableValidator,
|
tableValidator: tableValidator,
|
||||||
baseURL: baseURL,
|
selfOrderURL: selfOrderURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +150,11 @@ func (h *TableHandler) List(c *gin.Context) {
|
|||||||
Limit: 100,
|
Limit: 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to context outlet ID if not provided in query
|
||||||
|
if query.OutletID == "" && contextInfo.OutletID != uuid.Nil {
|
||||||
|
query.OutletID = contextInfo.OutletID.String()
|
||||||
|
}
|
||||||
|
|
||||||
if pageStr := c.Query("page"); pageStr != "" {
|
if pageStr := c.Query("page"); pageStr != "" {
|
||||||
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 {
|
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 {
|
||||||
query.Page = page
|
query.Page = page
|
||||||
@@ -312,7 +317,7 @@ func (h *TableHandler) GenerateQRCode(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
selfOrderURL := fmt.Sprintf("%s/api/v1/self-order/table/%s", h.baseURL, token)
|
selfOrderURLResult := fmt.Sprintf("%s/menu?token=%s", h.selfOrderURL, token)
|
||||||
|
|
||||||
size := 256
|
size := 256
|
||||||
if sizeStr := c.Query("size"); sizeStr != "" {
|
if sizeStr := c.Query("size"); sizeStr != "" {
|
||||||
@@ -321,7 +326,7 @@ func (h *TableHandler) GenerateQRCode(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pngBytes, err := qrcode.GeneratePNG(selfOrderURL, size)
|
pngBytes, err := qrcode.GeneratePNG(selfOrderURLResult, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::GenerateQRCode -> QR generation failed")
|
logger.FromContext(ctx).WithError(err).Error("TableHandler::GenerateQRCode -> QR generation failed")
|
||||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, "Failed to generate QR code")
|
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, "Failed to generate QR code")
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/service"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
"apskel-pos-be/internal/validator"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserDeviceHandler struct {
|
||||||
|
userDeviceService service.UserDeviceService
|
||||||
|
userDeviceValidator validator.UserDeviceValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserDeviceHandler(
|
||||||
|
userDeviceService service.UserDeviceService,
|
||||||
|
userDeviceValidator validator.UserDeviceValidator,
|
||||||
|
) *UserDeviceHandler {
|
||||||
|
return &UserDeviceHandler{
|
||||||
|
userDeviceService: userDeviceService,
|
||||||
|
userDeviceValidator: userDeviceValidator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserDeviceHandler) RegisterDevice(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.RegisterUserDeviceRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("UserDeviceHandler::RegisterDevice -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::RegisterDevice")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validationError, validationErrorCode := h.userDeviceValidator.ValidateRegisterDeviceRequest(&req)
|
||||||
|
if validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::RegisterDevice")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceResponse := h.userDeviceService.RegisterDevice(ctx, contextInfo.UserID, &req)
|
||||||
|
if deviceResponse.HasErrors() {
|
||||||
|
errorResp := deviceResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::RegisterDevice -> Failed to register device from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::RegisterDevice")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserDeviceHandler) UpdateDevice(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
deviceIDStr := c.Param("id")
|
||||||
|
deviceID, err := uuid.Parse(deviceIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("UserDeviceHandler::UpdateDevice -> Invalid device ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid device ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::UpdateDevice")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.UpdateUserDeviceRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("UserDeviceHandler::UpdateDevice -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::UpdateDevice")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validationError, validationErrorCode := h.userDeviceValidator.ValidateUpdateDeviceRequest(&req)
|
||||||
|
if validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::UpdateDevice")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceResponse := h.userDeviceService.UpdateDevice(ctx, deviceID, &req)
|
||||||
|
if deviceResponse.HasErrors() {
|
||||||
|
errorResp := deviceResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::UpdateDevice -> Failed to update device from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::UpdateDevice")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserDeviceHandler) DeleteDevice(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
deviceIDStr := c.Param("id")
|
||||||
|
deviceID, err := uuid.Parse(deviceIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("UserDeviceHandler::DeleteDevice -> Invalid device ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid device ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::DeleteDevice")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceResponse := h.userDeviceService.DeleteDevice(ctx, deviceID)
|
||||||
|
if deviceResponse.HasErrors() {
|
||||||
|
errorResp := deviceResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::DeleteDevice -> Failed to delete device from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::DeleteDevice")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserDeviceHandler) GetDevice(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
deviceIDStr := c.Param("id")
|
||||||
|
deviceID, err := uuid.Parse(deviceIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("UserDeviceHandler::GetDevice -> Invalid device ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid device ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::GetDevice")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceResponse := h.userDeviceService.GetDeviceByID(ctx, deviceID)
|
||||||
|
if deviceResponse.HasErrors() {
|
||||||
|
errorResp := deviceResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::GetDevice -> Failed to get device from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::GetDevice")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserDeviceHandler) GetMyDevices(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
deviceResponse := h.userDeviceService.GetDevicesByUserID(ctx, contextInfo.UserID)
|
||||||
|
if deviceResponse.HasErrors() {
|
||||||
|
errorResp := deviceResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::GetMyDevices -> Failed to get devices from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::GetMyDevices")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserDeviceHandler) GetDevicesByUser(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
userIDStr := c.Param("user_id")
|
||||||
|
userID, err := uuid.Parse(userIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("UserDeviceHandler::GetDevicesByUser -> Invalid user ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid user ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::GetDevicesByUser")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceResponse := h.userDeviceService.GetDevicesByUserID(ctx, userID)
|
||||||
|
if deviceResponse.HasErrors() {
|
||||||
|
errorResp := deviceResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::GetDevicesByUser -> Failed to get devices from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::GetDevicesByUser")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserDeviceHandler) ListDevices(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
req := &contract.ListUserDevicesRequest{
|
||||||
|
Page: 1,
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageStr := c.Query("page"); pageStr != "" {
|
||||||
|
if page, err := strconv.Atoi(pageStr); err == nil {
|
||||||
|
req.Page = page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if limitStr := c.Query("limit"); limitStr != "" {
|
||||||
|
if limit, err := strconv.Atoi(limitStr); err == nil {
|
||||||
|
req.Limit = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if userID := c.Query("user_id"); userID != "" {
|
||||||
|
req.UserID = userID
|
||||||
|
}
|
||||||
|
|
||||||
|
if platform := c.Query("platform"); platform != "" {
|
||||||
|
req.Platform = platform
|
||||||
|
}
|
||||||
|
|
||||||
|
validationError, validationErrorCode := h.userDeviceValidator.ValidateListDevicesRequest(req)
|
||||||
|
if validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UserDeviceHandler::ListDevices")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceResponse := h.userDeviceService.ListDevices(ctx, req)
|
||||||
|
if deviceResponse.HasErrors() {
|
||||||
|
errorResp := deviceResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("UserDeviceHandler::ListDevices -> Failed to list devices from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, deviceResponse, "UserDeviceHandler::ListDevices")
|
||||||
|
}
|
||||||
@@ -13,11 +13,12 @@ func CategoryEntityToModel(entity *entities.Category) *models.Category {
|
|||||||
return &models.Category{
|
return &models.Category{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
OrganizationID: entity.OrganizationID,
|
OrganizationID: entity.OrganizationID,
|
||||||
|
OutletID: entity.OutletID,
|
||||||
Name: entity.Name,
|
Name: entity.Name,
|
||||||
Description: entity.Description,
|
Description: entity.Description,
|
||||||
ImageURL: nil, // Entity doesn't have ImageURL, model does
|
ImageURL: nil,
|
||||||
Order: entity.Order, // Entity doesn't have SortOrder, model does
|
Order: entity.Order,
|
||||||
IsActive: true, // Entity doesn't have IsActive, default to true
|
IsActive: true,
|
||||||
CreatedAt: entity.CreatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
UpdatedAt: entity.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -32,14 +33,14 @@ func CategoryModelToEntity(model *models.Category) *entities.Category {
|
|||||||
if model.ImageURL != nil {
|
if model.ImageURL != nil {
|
||||||
metadata["image_url"] = *model.ImageURL
|
metadata["image_url"] = *model.ImageURL
|
||||||
}
|
}
|
||||||
// metadata["sort_order"] = model.SortOrder
|
|
||||||
|
|
||||||
return &entities.Category{
|
return &entities.Category{
|
||||||
ID: model.ID,
|
ID: model.ID,
|
||||||
OrganizationID: model.OrganizationID,
|
OrganizationID: model.OrganizationID,
|
||||||
|
OutletID: model.OutletID,
|
||||||
Name: model.Name,
|
Name: model.Name,
|
||||||
Description: model.Description,
|
Description: model.Description,
|
||||||
BusinessType: "restaurant", // Default business type
|
BusinessType: "restaurant",
|
||||||
Order: model.Order,
|
Order: model.Order,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
CreatedAt: model.CreatedAt,
|
CreatedAt: model.CreatedAt,
|
||||||
@@ -56,14 +57,15 @@ func CreateCategoryRequestToEntity(req *models.CreateCategoryRequest) *entities.
|
|||||||
if req.ImageURL != nil {
|
if req.ImageURL != nil {
|
||||||
metadata["image_url"] = *req.ImageURL
|
metadata["image_url"] = *req.ImageURL
|
||||||
}
|
}
|
||||||
// metadata["sort_order"] = req.SortOrder
|
|
||||||
|
|
||||||
return &entities.Category{
|
return &entities.Category{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
|
OutletID: req.OutletID,
|
||||||
|
ParentID: req.ParentID,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
Order: req.Order,
|
Order: req.Order,
|
||||||
BusinessType: "restaurant", // Default business type
|
BusinessType: "restaurant",
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,14 +86,24 @@ func CategoryEntityToResponse(entity *entities.Category) *models.CategoryRespons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parent name is only available when the Parent association is preloaded
|
||||||
|
var parentName *string
|
||||||
|
if entity.Parent != nil {
|
||||||
|
name := entity.Parent.Name
|
||||||
|
parentName = &name
|
||||||
|
}
|
||||||
|
|
||||||
return &models.CategoryResponse{
|
return &models.CategoryResponse{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
OrganizationID: entity.OrganizationID,
|
OrganizationID: entity.OrganizationID,
|
||||||
|
OutletID: entity.OutletID,
|
||||||
|
ParentID: entity.ParentID,
|
||||||
|
ParentName: parentName,
|
||||||
Name: entity.Name,
|
Name: entity.Name,
|
||||||
Description: entity.Description,
|
Description: entity.Description,
|
||||||
ImageURL: imageURL,
|
ImageURL: imageURL,
|
||||||
Order: entity.Order,
|
Order: entity.Order,
|
||||||
IsActive: true, // Default to true since entity doesn't have this field
|
IsActive: true,
|
||||||
CreatedAt: entity.CreatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
UpdatedAt: entity.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -121,6 +133,14 @@ func UpdateCategoryEntityFromRequest(entity *entities.Category, req *models.Upda
|
|||||||
if req.Order != nil {
|
if req.Order != nil {
|
||||||
entity.Order = *req.Order
|
entity.Order = *req.Order
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.OutletID != nil {
|
||||||
|
entity.OutletID = req.OutletID
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ParentID != nil {
|
||||||
|
entity.ParentID = req.ParentID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CategoryEntitiesToModels(entities []*entities.Category) []*models.Category {
|
func CategoryEntitiesToModels(entities []*entities.Category) []*models.Category {
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExpenseEntityToModel(entity *entities.Expense) *models.Expense {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.Expense{
|
||||||
|
ID: entity.ID,
|
||||||
|
OrganizationID: entity.OrganizationID,
|
||||||
|
OutletID: entity.OutletID,
|
||||||
|
Receiver: entity.Receiver,
|
||||||
|
TransactionDate: entity.TransactionDate,
|
||||||
|
CodeNumber: entity.CodeNumber,
|
||||||
|
Status: entity.Status,
|
||||||
|
Description: entity.Description,
|
||||||
|
Tax: entity.Tax,
|
||||||
|
Total: entity.Total,
|
||||||
|
Reserved1: entity.Reserved1,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpenseModelToEntity(model *models.Expense) *entities.Expense {
|
||||||
|
if model == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entities.Expense{
|
||||||
|
ID: model.ID,
|
||||||
|
OrganizationID: model.OrganizationID,
|
||||||
|
OutletID: model.OutletID,
|
||||||
|
Receiver: model.Receiver,
|
||||||
|
TransactionDate: model.TransactionDate,
|
||||||
|
CodeNumber: model.CodeNumber,
|
||||||
|
Status: model.Status,
|
||||||
|
Description: model.Description,
|
||||||
|
Tax: model.Tax,
|
||||||
|
Total: model.Total,
|
||||||
|
Reserved1: model.Reserved1,
|
||||||
|
CreatedAt: model.CreatedAt,
|
||||||
|
UpdatedAt: model.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpenseEntityToResponse(entity *entities.Expense) *models.ExpenseResponse {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &models.ExpenseResponse{
|
||||||
|
ID: entity.ID,
|
||||||
|
OrganizationID: entity.OrganizationID,
|
||||||
|
OutletID: entity.OutletID,
|
||||||
|
Receiver: entity.Receiver,
|
||||||
|
TransactionDate: entity.TransactionDate,
|
||||||
|
CodeNumber: entity.CodeNumber,
|
||||||
|
Status: entity.Status,
|
||||||
|
Description: entity.Description,
|
||||||
|
Tax: entity.Tax,
|
||||||
|
Total: entity.Total,
|
||||||
|
Reserved1: entity.Reserved1,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity.Items != nil {
|
||||||
|
resp.Items = ExpenseItemEntitiesToResponses(entity.Items)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpenseEntitiesToResponses(entities []*entities.Expense) []*models.ExpenseResponse {
|
||||||
|
if entities == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
responses := make([]*models.ExpenseResponse, len(entities))
|
||||||
|
for i, entity := range entities {
|
||||||
|
responses[i] = ExpenseEntityToResponse(entity)
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpenseItemEntityToResponse(entity *entities.ExpenseItem) *models.ExpenseItemResponse {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &models.ExpenseItemResponse{
|
||||||
|
ID: entity.ID,
|
||||||
|
ExpenseID: entity.ExpenseID,
|
||||||
|
ChartOfAccountID: entity.ChartOfAccountID,
|
||||||
|
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||||
|
Item: entity.Item,
|
||||||
|
Description: entity.Description,
|
||||||
|
Amount: entity.Amount,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity.ChartOfAccount != nil {
|
||||||
|
response.ChartOfAccountName = entity.ChartOfAccount.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity.PurchaseCategory != nil {
|
||||||
|
response.PurchaseCategoryName = entity.PurchaseCategory.Name
|
||||||
|
response.PurchaseCategoryType = string(entity.PurchaseCategory.Type)
|
||||||
|
response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpenseItemEntitiesToResponses(entities []entities.ExpenseItem) []models.ExpenseItemResponse {
|
||||||
|
if entities == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
responses := make([]models.ExpenseItemResponse, len(entities))
|
||||||
|
for i, entity := range entities {
|
||||||
|
response := ExpenseItemEntityToResponse(&entity)
|
||||||
|
if response != nil {
|
||||||
|
responses[i] = *response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NotificationEntityToResponse(e *entities.Notification) *models.NotificationResponse {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &models.NotificationResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
Title: e.Title,
|
||||||
|
Body: e.Body,
|
||||||
|
Type: e.Type,
|
||||||
|
Category: e.Category,
|
||||||
|
Priority: e.Priority,
|
||||||
|
ImageURL: e.ImageURL,
|
||||||
|
ActionURL: e.ActionURL,
|
||||||
|
NotifiableType: e.NotifiableType,
|
||||||
|
NotifiableID: e.NotifiableID,
|
||||||
|
Data: e.Data,
|
||||||
|
ScheduledAt: e.ScheduledAt,
|
||||||
|
SentAt: e.SentAt,
|
||||||
|
ExpiredAt: e.ExpiredAt,
|
||||||
|
CreatedBy: e.CreatedBy,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotificationReceiverEntityToResponse(e *entities.NotificationReceiver) *models.NotificationReceiverResponse {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resp := &models.NotificationReceiverResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
NotificationID: e.NotificationID,
|
||||||
|
UserID: e.UserID,
|
||||||
|
IsRead: e.IsRead,
|
||||||
|
ReadAt: e.ReadAt,
|
||||||
|
IsDeleted: e.IsDeleted,
|
||||||
|
DeletedAt: e.DeletedAt,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
}
|
||||||
|
if e.Notification != nil {
|
||||||
|
resp.Notification = NotificationEntityToResponse(e.Notification)
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotificationReceiverEntitiesToResponses(entities []*entities.NotificationReceiver) []*models.NotificationReceiverResponse {
|
||||||
|
if entities == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
responses := make([]*models.NotificationReceiverResponse, len(entities))
|
||||||
|
for i, e := range entities {
|
||||||
|
responses[i] = NotificationReceiverEntityToResponse(e)
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotificationDeliveryEntityToResponse(e *entities.NotificationDelivery) *models.NotificationDeliveryResponse {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &models.NotificationDeliveryResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
NotificationReceiverID: e.NotificationReceiverID,
|
||||||
|
UserDeviceID: e.UserDeviceID,
|
||||||
|
Channel: e.Channel,
|
||||||
|
DeliveryStatus: e.DeliveryStatus,
|
||||||
|
Provider: e.Provider,
|
||||||
|
ProviderMessageID: e.ProviderMessageID,
|
||||||
|
SentAt: e.SentAt,
|
||||||
|
DeliveredAt: e.DeliveredAt,
|
||||||
|
FailedAt: e.FailedAt,
|
||||||
|
FailureReason: e.FailureReason,
|
||||||
|
RetryCount: e.RetryCount,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ func OrderEntityToResponse(order *entities.Order) *models.OrderResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, item := range order.OrderItems {
|
for i, item := range order.OrderItems {
|
||||||
resp := OrderItemEntityToResponse(&item)
|
resp := OrderItemEntityToResponse(&item, order.OutletID)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
resp.PaidQuantity = paidQtyByOrderItem[item.ID]
|
resp.PaidQuantity = paidQtyByOrderItem[item.ID]
|
||||||
response.OrderItems[i] = *resp
|
response.OrderItems[i] = *resp
|
||||||
@@ -101,11 +101,20 @@ func OrderEntityToResponse(order *entities.Order) *models.OrderResponse {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemResponse {
|
func OrderItemEntityToResponse(item *entities.OrderItem, outletID uuid.UUID) *models.OrderItemResponse {
|
||||||
if item == nil {
|
if item == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve print_to_checker from preloaded outlet prices
|
||||||
|
printToChecker := true // default
|
||||||
|
for _, op := range item.Product.ProductOutletPrices {
|
||||||
|
if op.OutletID == outletID {
|
||||||
|
printToChecker = op.PrintToChecker
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response := &models.OrderItemResponse{
|
response := &models.OrderItemResponse{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
OrderID: item.OrderID,
|
OrderID: item.OrderID,
|
||||||
@@ -130,10 +139,19 @@ func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemRespon
|
|||||||
CreatedAt: item.CreatedAt,
|
CreatedAt: item.CreatedAt,
|
||||||
UpdatedAt: item.UpdatedAt,
|
UpdatedAt: item.UpdatedAt,
|
||||||
PrinterType: item.Product.PrinterType,
|
PrinterType: item.Product.PrinterType,
|
||||||
|
PrintToChecker: printToChecker,
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Product.ID != uuid.Nil {
|
if item.Product.ID != uuid.Nil {
|
||||||
response.ProductName = item.Product.Name
|
response.ProductName = item.Product.Name
|
||||||
|
if item.Product.CategoryID != uuid.Nil {
|
||||||
|
categoryID := item.Product.CategoryID
|
||||||
|
response.CategoryID = &categoryID
|
||||||
|
}
|
||||||
|
if item.Product.Category.ID != uuid.Nil {
|
||||||
|
categoryName := item.Product.Category.Name
|
||||||
|
response.CategoryName = &categoryName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.ProductVariant != nil {
|
if item.ProductVariant != nil {
|
||||||
@@ -316,14 +334,14 @@ func OrderEntitiesToResponses(orders []*entities.Order) []models.OrderResponse {
|
|||||||
return responses
|
return responses
|
||||||
}
|
}
|
||||||
|
|
||||||
func OrderItemEntitiesToResponses(items []*entities.OrderItem) []models.OrderItemResponse {
|
func OrderItemEntitiesToResponses(items []*entities.OrderItem, outletID uuid.UUID) []models.OrderItemResponse {
|
||||||
if items == nil {
|
if items == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
responses := make([]models.OrderItemResponse, len(items))
|
responses := make([]models.OrderItemResponse, len(items))
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
response := OrderItemEntityToResponse(item)
|
response := OrderItemEntityToResponse(item, outletID)
|
||||||
if response != nil {
|
if response != nil {
|
||||||
responses[i] = *response
|
responses[i] = *response
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func TestOrderItemEntityToResponse_WithProductNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
result := OrderItemEntityToResponse(orderItem)
|
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.NotNil(t, result)
|
assert.NotNil(t, result)
|
||||||
@@ -89,7 +89,7 @@ func TestOrderItemEntityToResponse_WithoutProductVariant(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
result := OrderItemEntityToResponse(orderItem)
|
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.NotNil(t, result)
|
assert.NotNil(t, result)
|
||||||
@@ -129,7 +129,7 @@ func TestOrderItemEntityToResponse_WithoutProductPreload(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
result := OrderItemEntityToResponse(orderItem)
|
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.NotNil(t, result)
|
assert.NotNil(t, result)
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ func MapProductIngredientEntityToModel(entity *entities.ProductIngredient) *mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &models.ProductIngredient{
|
return &models.ProductIngredient{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
OrganizationID: entity.OrganizationID,
|
OrganizationID: entity.OrganizationID,
|
||||||
OutletID: entity.OutletID,
|
OutletID: entity.OutletID,
|
||||||
ProductID: entity.ProductID,
|
ProductID: entity.ProductID,
|
||||||
IngredientID: entity.IngredientID,
|
IngredientID: entity.IngredientID,
|
||||||
Quantity: entity.Quantity,
|
Quantity: entity.Quantity,
|
||||||
WastePercentage: entity.WastePercentage,
|
WastePercentage: entity.WastePercentage,
|
||||||
CreatedAt: entity.CreatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
UpdatedAt: entity.UpdatedAt,
|
||||||
Product: ProductEntityToModel(entity.Product),
|
Product: ProductEntityToModel(entity.Product),
|
||||||
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
|
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,17 +31,17 @@ func MapProductIngredientModelToEntity(model *models.ProductIngredient) *entitie
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &entities.ProductIngredient{
|
return &entities.ProductIngredient{
|
||||||
ID: model.ID,
|
ID: model.ID,
|
||||||
OrganizationID: model.OrganizationID,
|
OrganizationID: model.OrganizationID,
|
||||||
OutletID: model.OutletID,
|
OutletID: model.OutletID,
|
||||||
ProductID: model.ProductID,
|
ProductID: model.ProductID,
|
||||||
IngredientID: model.IngredientID,
|
IngredientID: model.IngredientID,
|
||||||
Quantity: model.Quantity,
|
Quantity: model.Quantity,
|
||||||
WastePercentage: model.WastePercentage,
|
WastePercentage: model.WastePercentage,
|
||||||
CreatedAt: model.CreatedAt,
|
CreatedAt: model.CreatedAt,
|
||||||
UpdatedAt: model.UpdatedAt,
|
UpdatedAt: model.UpdatedAt,
|
||||||
Product: ProductModelToEntity(model.Product),
|
Product: ProductModelToEntity(model.Product),
|
||||||
Ingredient: MapIngredientModelToEntity(model.Ingredient),
|
Ingredient: MapIngredientModelToEntity(model.Ingredient),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ func ProductEntityToResponse(entity *entities.Product) *models.ProductResponse {
|
|||||||
Name: entity.Name,
|
Name: entity.Name,
|
||||||
Description: entity.Description,
|
Description: entity.Description,
|
||||||
Price: entity.Price,
|
Price: entity.Price,
|
||||||
|
OutletPrice: nil, // populated by processor when outletID is available
|
||||||
Cost: entity.Cost,
|
Cost: entity.Cost,
|
||||||
BusinessType: constants.BusinessType(entity.BusinessType),
|
BusinessType: constants.BusinessType(entity.BusinessType),
|
||||||
ImageURL: entity.ImageURL,
|
ImageURL: entity.ImageURL,
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProductOutletPriceEntityToModel(entity *entities.ProductOutletPrice) *models.ProductOutletPrice {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.ProductOutletPrice{
|
||||||
|
ID: entity.ID,
|
||||||
|
ProductID: entity.ProductID,
|
||||||
|
OutletID: entity.OutletID,
|
||||||
|
Price: entity.Price,
|
||||||
|
PrintToChecker: entity.PrintToChecker,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProductOutletPriceModelToEntity(model *models.ProductOutletPrice) *entities.ProductOutletPrice {
|
||||||
|
if model == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entities.ProductOutletPrice{
|
||||||
|
ID: model.ID,
|
||||||
|
ProductID: model.ProductID,
|
||||||
|
OutletID: model.OutletID,
|
||||||
|
Price: model.Price,
|
||||||
|
PrintToChecker: model.PrintToChecker,
|
||||||
|
CreatedAt: model.CreatedAt,
|
||||||
|
UpdatedAt: model.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProductOutletPriceEntitiesToModels(entities []*entities.ProductOutletPrice) []*models.ProductOutletPrice {
|
||||||
|
if entities == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
models := make([]*models.ProductOutletPrice, len(entities))
|
||||||
|
for i, entity := range entities {
|
||||||
|
models[i] = ProductOutletPriceEntityToModel(entity)
|
||||||
|
}
|
||||||
|
return models
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreatePurchaseCategoryRequestToEntity(req *models.CreatePurchaseCategoryRequest) *entities.PurchaseCategory {
|
||||||
|
if req == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entities.PurchaseCategory{
|
||||||
|
OrganizationID: req.OrganizationID,
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
Name: req.Name,
|
||||||
|
Type: entities.PurchaseCategoryType(req.Type),
|
||||||
|
SortOrder: req.SortOrder,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurchaseCategoryEntityToResponse(entity *entities.PurchaseCategory) *models.PurchaseCategoryResponse {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.PurchaseCategoryResponse{
|
||||||
|
ID: entity.ID,
|
||||||
|
OrganizationID: entity.OrganizationID,
|
||||||
|
PresetID: entity.PresetID,
|
||||||
|
ParentID: entity.ParentID,
|
||||||
|
Code: entity.Code,
|
||||||
|
Name: entity.Name,
|
||||||
|
Type: string(entity.Type),
|
||||||
|
SortOrder: entity.SortOrder,
|
||||||
|
IsSystem: entity.IsSystem,
|
||||||
|
IsActive: entity.IsActive,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurchaseCategoryEntitiesToResponses(categoryEntities []*entities.PurchaseCategory) []models.PurchaseCategoryResponse {
|
||||||
|
responses := make([]models.PurchaseCategoryResponse, len(categoryEntities))
|
||||||
|
for i, entity := range categoryEntities {
|
||||||
|
response := PurchaseCategoryEntityToResponse(entity)
|
||||||
|
if response != nil {
|
||||||
|
responses[i] = *response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
@@ -1,10 +1,33 @@
|
|||||||
package mappers
|
package mappers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// purchaseTeamFromEntity renders the team a purchase order is charged to. It returns
|
||||||
|
// nil when no team was chosen, which is distinct from a purchase charged to Pusat.
|
||||||
|
// The category name is only filled in when TeamCategory was preloaded.
|
||||||
|
func purchaseTeamFromEntity(entity *entities.PurchaseOrder) *models.PurchaseTeam {
|
||||||
|
if entity.TeamScope == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
team := &models.PurchaseTeam{Scope: *entity.TeamScope}
|
||||||
|
switch *entity.TeamScope {
|
||||||
|
case constants.PurchaseTeamScopeCentral:
|
||||||
|
team.Name = constants.PurchaseTeamCentralName
|
||||||
|
case constants.PurchaseTeamScopeCategory:
|
||||||
|
team.CategoryID = entity.TeamCategoryID
|
||||||
|
if entity.TeamCategory != nil {
|
||||||
|
team.Name = entity.TeamCategory.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return team
|
||||||
|
}
|
||||||
|
|
||||||
func PurchaseOrderEntityToModel(entity *entities.PurchaseOrder) *models.PurchaseOrder {
|
func PurchaseOrderEntityToModel(entity *entities.PurchaseOrder) *models.PurchaseOrder {
|
||||||
if entity == nil {
|
if entity == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -13,6 +36,7 @@ func PurchaseOrderEntityToModel(entity *entities.PurchaseOrder) *models.Purchase
|
|||||||
return &models.PurchaseOrder{
|
return &models.PurchaseOrder{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
OrganizationID: entity.OrganizationID,
|
OrganizationID: entity.OrganizationID,
|
||||||
|
OutletID: entity.OutletID,
|
||||||
VendorID: entity.VendorID,
|
VendorID: entity.VendorID,
|
||||||
PONumber: entity.PONumber,
|
PONumber: entity.PONumber,
|
||||||
TransactionDate: entity.TransactionDate,
|
TransactionDate: entity.TransactionDate,
|
||||||
@@ -21,6 +45,8 @@ func PurchaseOrderEntityToModel(entity *entities.PurchaseOrder) *models.Purchase
|
|||||||
Status: entity.Status,
|
Status: entity.Status,
|
||||||
Message: entity.Message,
|
Message: entity.Message,
|
||||||
TotalAmount: entity.TotalAmount,
|
TotalAmount: entity.TotalAmount,
|
||||||
|
TeamScope: entity.TeamScope,
|
||||||
|
TeamCategoryID: entity.TeamCategoryID,
|
||||||
CreatedAt: entity.CreatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
UpdatedAt: entity.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -34,6 +60,7 @@ func PurchaseOrderModelToEntity(model *models.PurchaseOrder) *entities.PurchaseO
|
|||||||
return &entities.PurchaseOrder{
|
return &entities.PurchaseOrder{
|
||||||
ID: model.ID,
|
ID: model.ID,
|
||||||
OrganizationID: model.OrganizationID,
|
OrganizationID: model.OrganizationID,
|
||||||
|
OutletID: model.OutletID,
|
||||||
VendorID: model.VendorID,
|
VendorID: model.VendorID,
|
||||||
PONumber: model.PONumber,
|
PONumber: model.PONumber,
|
||||||
TransactionDate: model.TransactionDate,
|
TransactionDate: model.TransactionDate,
|
||||||
@@ -42,6 +69,8 @@ func PurchaseOrderModelToEntity(model *models.PurchaseOrder) *entities.PurchaseO
|
|||||||
Status: model.Status,
|
Status: model.Status,
|
||||||
Message: model.Message,
|
Message: model.Message,
|
||||||
TotalAmount: model.TotalAmount,
|
TotalAmount: model.TotalAmount,
|
||||||
|
TeamScope: model.TeamScope,
|
||||||
|
TeamCategoryID: model.TeamCategoryID,
|
||||||
CreatedAt: model.CreatedAt,
|
CreatedAt: model.CreatedAt,
|
||||||
UpdatedAt: model.UpdatedAt,
|
UpdatedAt: model.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -55,6 +84,7 @@ func PurchaseOrderEntityToResponse(entity *entities.PurchaseOrder) *models.Purch
|
|||||||
response := &models.PurchaseOrderResponse{
|
response := &models.PurchaseOrderResponse{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
OrganizationID: entity.OrganizationID,
|
OrganizationID: entity.OrganizationID,
|
||||||
|
OutletID: entity.OutletID,
|
||||||
VendorID: entity.VendorID,
|
VendorID: entity.VendorID,
|
||||||
PONumber: entity.PONumber,
|
PONumber: entity.PONumber,
|
||||||
TransactionDate: entity.TransactionDate,
|
TransactionDate: entity.TransactionDate,
|
||||||
@@ -63,8 +93,11 @@ func PurchaseOrderEntityToResponse(entity *entities.PurchaseOrder) *models.Purch
|
|||||||
Status: entity.Status,
|
Status: entity.Status,
|
||||||
Message: entity.Message,
|
Message: entity.Message,
|
||||||
TotalAmount: entity.TotalAmount,
|
TotalAmount: entity.TotalAmount,
|
||||||
|
TeamScope: entity.TeamScope,
|
||||||
|
TeamCategoryID: entity.TeamCategoryID,
|
||||||
CreatedAt: entity.CreatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
Team: purchaseTeamFromEntity(entity),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map vendor if present
|
// Map vendor if present
|
||||||
@@ -91,15 +124,16 @@ func PurchaseOrderItemEntityToModel(entity *entities.PurchaseOrderItem) *models.
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &models.PurchaseOrderItem{
|
return &models.PurchaseOrderItem{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
PurchaseOrderID: entity.PurchaseOrderID,
|
PurchaseOrderID: entity.PurchaseOrderID,
|
||||||
IngredientID: entity.IngredientID,
|
IngredientID: entity.IngredientID,
|
||||||
Description: entity.Description,
|
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||||
Quantity: entity.Quantity,
|
Description: entity.Description,
|
||||||
UnitID: entity.UnitID,
|
Quantity: entity.Quantity,
|
||||||
Amount: entity.Amount,
|
UnitID: entity.UnitID,
|
||||||
CreatedAt: entity.CreatedAt,
|
Amount: entity.Amount,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,15 +143,16 @@ func PurchaseOrderItemModelToEntity(model *models.PurchaseOrderItem) *entities.P
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &entities.PurchaseOrderItem{
|
return &entities.PurchaseOrderItem{
|
||||||
ID: model.ID,
|
ID: model.ID,
|
||||||
PurchaseOrderID: model.PurchaseOrderID,
|
PurchaseOrderID: model.PurchaseOrderID,
|
||||||
IngredientID: model.IngredientID,
|
IngredientID: model.IngredientID,
|
||||||
Description: model.Description,
|
PurchaseCategoryID: model.PurchaseCategoryID,
|
||||||
Quantity: model.Quantity,
|
Description: model.Description,
|
||||||
UnitID: model.UnitID,
|
Quantity: model.Quantity,
|
||||||
Amount: model.Amount,
|
UnitID: model.UnitID,
|
||||||
CreatedAt: model.CreatedAt,
|
Amount: model.Amount,
|
||||||
UpdatedAt: model.UpdatedAt,
|
CreatedAt: model.CreatedAt,
|
||||||
|
UpdatedAt: model.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,15 +162,16 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
response := &models.PurchaseOrderItemResponse{
|
response := &models.PurchaseOrderItemResponse{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
PurchaseOrderID: entity.PurchaseOrderID,
|
PurchaseOrderID: entity.PurchaseOrderID,
|
||||||
IngredientID: entity.IngredientID,
|
IngredientID: entity.IngredientID,
|
||||||
Description: entity.Description,
|
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||||
Quantity: entity.Quantity,
|
Description: entity.Description,
|
||||||
UnitID: entity.UnitID,
|
Quantity: entity.Quantity,
|
||||||
Amount: entity.Amount,
|
UnitID: entity.UnitID,
|
||||||
CreatedAt: entity.CreatedAt,
|
Amount: entity.Amount,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map ingredient if present
|
// Map ingredient if present
|
||||||
@@ -146,6 +182,10 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if entity.PurchaseCategory != nil {
|
||||||
|
response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory)
|
||||||
|
}
|
||||||
|
|
||||||
// Map unit if present
|
// Map unit if present
|
||||||
if entity.Unit != nil {
|
if entity.Unit != nil {
|
||||||
response.Unit = &models.UnitResponse{
|
response.Unit = &models.UnitResponse{
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UserDeviceEntityToModel(entity *entities.UserDevice) *models.UserDevice {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.UserDevice{
|
||||||
|
ID: entity.ID,
|
||||||
|
UserID: entity.UserID,
|
||||||
|
DeviceID: entity.DeviceID,
|
||||||
|
DeviceName: entity.DeviceName,
|
||||||
|
DeviceType: entity.DeviceType,
|
||||||
|
Platform: entity.Platform,
|
||||||
|
FCMToken: entity.FCMToken,
|
||||||
|
AppVersion: entity.AppVersion,
|
||||||
|
OsVersion: entity.OsVersion,
|
||||||
|
IPAddress: entity.IPAddress,
|
||||||
|
LastActiveAt: entity.LastActiveAt,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserDeviceEntityToResponse(entity *entities.UserDevice) *models.UserDeviceResponse {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.UserDeviceResponse{
|
||||||
|
ID: entity.ID,
|
||||||
|
UserID: entity.UserID,
|
||||||
|
DeviceID: entity.DeviceID,
|
||||||
|
DeviceName: entity.DeviceName,
|
||||||
|
DeviceType: entity.DeviceType,
|
||||||
|
Platform: entity.Platform,
|
||||||
|
FCMToken: entity.FCMToken,
|
||||||
|
AppVersion: entity.AppVersion,
|
||||||
|
OsVersion: entity.OsVersion,
|
||||||
|
IPAddress: entity.IPAddress,
|
||||||
|
LastActiveAt: entity.LastActiveAt,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserDeviceEntitiesToResponses(entities []*entities.UserDevice) []*models.UserDeviceResponse {
|
||||||
|
if entities == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
responses := make([]*models.UserDeviceResponse, len(entities))
|
||||||
|
for i, entity := range entities {
|
||||||
|
responses[i] = UserDeviceEntityToResponse(entity)
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"apskel-pos-be/internal/service"
|
"apskel-pos-be/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthMiddleware struct {
|
type AuthMiddleware struct {
|
||||||
@@ -45,9 +46,13 @@ func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
|
|||||||
setKeyInContext(c, appcontext.OrganizationIDKey, userResponse.OrganizationID.String())
|
setKeyInContext(c, appcontext.OrganizationIDKey, userResponse.OrganizationID.String())
|
||||||
setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String())
|
setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String())
|
||||||
|
|
||||||
if userResponse.Role != "superadmin" {
|
// Always override OutletID from token to prevent header injection.
|
||||||
setKeyInContext(c, appcontext.OutletIDKey, userResponse.OutletID.String())
|
// Set empty string if user has no outlet, so PopulateContext header value is ignored.
|
||||||
|
outletIDStr := ""
|
||||||
|
if userResponse.OutletID != nil && *userResponse.OutletID != uuid.Nil {
|
||||||
|
outletIDStr = userResponse.OutletID.String()
|
||||||
}
|
}
|
||||||
|
setKeyInContext(c, appcontext.OutletIDKey, outletIDStr)
|
||||||
|
|
||||||
logger.FromContext(c.Request.Context()).Infof("AuthMiddleware::RequireAuth -> User authenticated: %s", userResponse.Email)
|
logger.FromContext(c.Request.Context()).Infof("AuthMiddleware::RequireAuth -> User authenticated: %s", userResponse.Email)
|
||||||
c.Next()
|
c.Next()
|
||||||
@@ -77,7 +82,11 @@ func (m *AuthMiddleware) RequireRole(allowedRoles ...string) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *AuthMiddleware) RequireAdminOrManager() gin.HandlerFunc {
|
func (m *AuthMiddleware) RequireAdminOrManager() gin.HandlerFunc {
|
||||||
return m.RequireRole("superadmin", "admin", "manager")
|
return m.RequireRole("superadmin", "admin", "manager", "owner", "purchasing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthMiddleware) RequireAdminOrManagerOrPurchasing() gin.HandlerFunc {
|
||||||
|
return m.RequireRole("superadmin", "admin", "manager", "owner", "purchasing")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AuthMiddleware) RequireAdmin() gin.HandlerFunc {
|
func (m *AuthMiddleware) RequireAdmin() gin.HandlerFunc {
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IdempotencyKeyHeader = "X-Idempotency-Key"
|
||||||
|
idempotencyTTL = 24 * time.Hour
|
||||||
|
idempotencyPrefix = "idempotency:"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cachedResponse struct {
|
||||||
|
StatusCode int `json:"status_code"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IdempotencyMiddleware returns a Gin middleware that ensures idempotent processing
|
||||||
|
// for mutating operations. Client must send X-Idempotency-Key header.
|
||||||
|
func IdempotencyMiddleware(redisClient *redis.Client) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
key := c.GetHeader(IdempotencyKeyHeader)
|
||||||
|
if key == "" {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"errors": []gin.H{
|
||||||
|
{
|
||||||
|
"code": "missing_idempotency_key",
|
||||||
|
"entity": "IdempotencyMiddleware",
|
||||||
|
"cause": "X-Idempotency-Key header is required",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
redisKey := fmt.Sprintf("%s%s", idempotencyPrefix, key)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
fmt.Printf("[DEBUG] IdempotencyMiddleware: key=%s redisKey=%s\n", key, redisKey)
|
||||||
|
|
||||||
|
// Check if key already exists (request was already processed)
|
||||||
|
cached, err := redisClient.Get(ctx, redisKey).Result()
|
||||||
|
if err == nil {
|
||||||
|
// Key exists — return cached response
|
||||||
|
fmt.Printf("[DEBUG] IdempotencyMiddleware: cache HIT for key=%s\n", key)
|
||||||
|
var resp cachedResponse
|
||||||
|
if err := json.Unmarshal([]byte(cached), &resp); err == nil {
|
||||||
|
for k, v := range resp.Headers {
|
||||||
|
c.Writer.Header().Set(k, v)
|
||||||
|
}
|
||||||
|
c.Writer.Header().Set("X-Idempotent-Replay", "true")
|
||||||
|
c.Data(resp.StatusCode, "application/json", []byte(resp.Body))
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[DEBUG] IdempotencyMiddleware: cache MISS for key=%s err=%v\n", key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark key as in-progress to prevent concurrent duplicates
|
||||||
|
set, err := redisClient.SetNX(ctx, redisKey, "processing", idempotencyTTL).Result()
|
||||||
|
if err != nil {
|
||||||
|
// Redis error — proceed without idempotency (fail open)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !set {
|
||||||
|
// Another request with the same key is being processed
|
||||||
|
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"errors": []gin.H{
|
||||||
|
{
|
||||||
|
"code": "request_in_progress",
|
||||||
|
"entity": "IdempotencyMiddleware",
|
||||||
|
"cause": "A request with this idempotency key is already being processed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture response using a custom writer
|
||||||
|
writer := &responseCapture{
|
||||||
|
ResponseWriter: c.Writer,
|
||||||
|
body: &bytes.Buffer{},
|
||||||
|
}
|
||||||
|
c.Writer = writer
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
// After handler completes, cache the response only if successful (2xx)
|
||||||
|
statusCode := writer.Status()
|
||||||
|
if statusCode >= 200 && statusCode < 300 {
|
||||||
|
resp := cachedResponse{
|
||||||
|
StatusCode: statusCode,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": writer.Header().Get("Content-Type"),
|
||||||
|
},
|
||||||
|
Body: writer.body.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
respJSON, err := json.Marshal(resp)
|
||||||
|
if err == nil {
|
||||||
|
redisClient.Set(ctx, redisKey, string(respJSON), idempotencyTTL)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove the in-progress key so the client can retry with the same key
|
||||||
|
redisClient.Del(ctx, redisKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// responseCapture wraps gin.ResponseWriter to capture the response body
|
||||||
|
type responseCapture struct {
|
||||||
|
gin.ResponseWriter
|
||||||
|
body *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseCapture) Write(b []byte) (int, error) {
|
||||||
|
w.body.Write(b)
|
||||||
|
return w.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseCapture) WriteString(s string) (int, error) {
|
||||||
|
w.body.WriteString(s)
|
||||||
|
return w.ResponseWriter.WriteString(s)
|
||||||
|
}
|
||||||
@@ -25,12 +25,12 @@ type AccountResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateAccountRequest struct {
|
type CreateAccountRequest struct {
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
Number string `json:"number" validate:"required,min=1,max=50"`
|
Number string `json:"number" validate:"required,min=1,max=50"`
|
||||||
AccountType string `json:"account_type" validate:"required,oneof=cash wallet bank credit debit asset liability equity revenue expense"`
|
AccountType string `json:"account_type" validate:"required,oneof=cash wallet bank credit debit asset liability equity revenue expense"`
|
||||||
OpeningBalance float64 `json:"opening_balance"`
|
OpeningBalance float64 `json:"opening_balance"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateAccountRequest struct {
|
type UpdateAccountRequest struct {
|
||||||
|
|||||||
+426
-19
@@ -1,8 +1,12 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +23,7 @@ type PaymentMethodAnalyticsRequest struct {
|
|||||||
type PaymentMethodAnalyticsResponse struct {
|
type PaymentMethodAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
GroupBy string `json:"group_by"`
|
GroupBy string `json:"group_by"`
|
||||||
@@ -58,6 +63,7 @@ type SalesAnalyticsRequest struct {
|
|||||||
type SalesAnalyticsResponse struct {
|
type SalesAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
GroupBy string `json:"group_by"`
|
GroupBy string `json:"group_by"`
|
||||||
@@ -87,6 +93,118 @@ type SalesAnalyticsData struct {
|
|||||||
NetSales float64 `json:"net_sales"`
|
NetSales float64 `json:"net_sales"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PurchasingAnalyticsRequest represents the request for purchasing analytics
|
||||||
|
type PurchasingAnalyticsRequest struct {
|
||||||
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
|
OutletID *uuid.UUID `validate:"omitempty"`
|
||||||
|
// Team is the raw value the team picker sends: a parent category id,
|
||||||
|
// "central" for Pusat, "none" for purchases with no team, or empty for all.
|
||||||
|
Team string
|
||||||
|
DateFrom time.Time `validate:"required"`
|
||||||
|
DateTo time.Time `validate:"required"`
|
||||||
|
GroupBy string `validate:"omitempty,oneof=day hour week month"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePurchaseTeamFilter turns the team value the picker sends into the scope and
|
||||||
|
// category the purchasing queries filter on. An empty value spans every team; an
|
||||||
|
// unknown one is an error rather than a report that quietly ignores the filter.
|
||||||
|
func ParsePurchaseTeamFilter(team string) (*entities.PurchaseTeamFilter, error) {
|
||||||
|
switch team {
|
||||||
|
case "":
|
||||||
|
return nil, nil
|
||||||
|
case constants.PurchaseTeamScopeCentral, constants.PurchaseTeamNone:
|
||||||
|
return &entities.PurchaseTeamFilter{Scope: team}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryID, err := uuid.Parse(team)
|
||||||
|
if err != nil || categoryID == uuid.Nil {
|
||||||
|
return nil, fmt.Errorf("team must be one of: central, none, or a category id")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entities.PurchaseTeamFilter{
|
||||||
|
Scope: constants.PurchaseTeamScopeCategory,
|
||||||
|
CategoryID: &categoryID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchasingAnalyticsResponse represents the response for purchasing analytics
|
||||||
|
type PurchasingAnalyticsResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
|
Team string `json:"team,omitempty"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
GroupBy string `json:"group_by"`
|
||||||
|
Summary PurchasingSummary `json:"summary"`
|
||||||
|
Data []PurchasingAnalyticsData `json:"data"`
|
||||||
|
IngredientData []PurchasingIngredientData `json:"ingredient_data"`
|
||||||
|
VendorData []PurchasingVendorData `json:"vendor_data"`
|
||||||
|
TeamData []PurchasingTeamData `json:"team_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchasingTeamData represents purchasing analytics for a single team
|
||||||
|
type PurchasingTeamData struct {
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
CategoryID *uuid.UUID `json:"category_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
|
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Percentage float64 `json:"percentage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchasingSummary represents the summary of purchasing analytics
|
||||||
|
type PurchasingSummary struct {
|
||||||
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||||
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
TotalQuantity float64 `json:"total_quantity"`
|
||||||
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||||
|
TotalIngredients int64 `json:"total_ingredients"`
|
||||||
|
TotalVendors int64 `json:"total_vendors"`
|
||||||
|
TotalTeams int64 `json:"total_teams"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchasingAnalyticsData represents purchasing analytics by time period
|
||||||
|
type PurchasingAnalyticsData struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Purchases float64 `json:"purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Ingredients int64 `json:"ingredients"`
|
||||||
|
Vendors int64 `json:"vendors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchasingIngredientData represents purchasing analytics for an ingredient
|
||||||
|
type PurchasingIngredientData struct {
|
||||||
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
|
IngredientName string `json:"ingredient_name"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
AverageUnitCost float64 `json:"average_unit_cost"`
|
||||||
|
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchasingVendorData represents purchasing analytics for a vendor
|
||||||
|
type PurchasingVendorData struct {
|
||||||
|
VendorID *uuid.UUID `json:"vendor_id"`
|
||||||
|
VendorName string `json:"vendor_name"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
PurchaseOrderCount int64 `json:"purchase_order_count"`
|
||||||
|
IngredientCount int64 `json:"ingredient_count"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
}
|
||||||
|
|
||||||
// ProductAnalyticsRequest represents the request for product analytics
|
// ProductAnalyticsRequest represents the request for product analytics
|
||||||
type ProductAnalyticsRequest struct {
|
type ProductAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID `validate:"required"`
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
@@ -100,6 +218,7 @@ type ProductAnalyticsRequest struct {
|
|||||||
type ProductAnalyticsResponse struct {
|
type ProductAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
Data []ProductAnalyticsData `json:"data"`
|
Data []ProductAnalyticsData `json:"data"`
|
||||||
@@ -109,6 +228,7 @@ type ProductAnalyticsData struct {
|
|||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
ProductSku string `json:"product_sku"`
|
ProductSku string `json:"product_sku"`
|
||||||
|
ProductPrice float64 `json:"product_price"`
|
||||||
CategoryID uuid.UUID `json:"category_id"`
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
CategoryName string `json:"category_name"`
|
CategoryName string `json:"category_name"`
|
||||||
CategoryOrder int `json:"category_order"`
|
CategoryOrder int `json:"category_order"`
|
||||||
@@ -136,6 +256,7 @@ type ProductAnalyticsPerCategoryRequest struct {
|
|||||||
type ProductAnalyticsPerCategoryResponse struct {
|
type ProductAnalyticsPerCategoryResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
Data []ProductAnalyticsPerCategoryData `json:"data"`
|
Data []ProductAnalyticsPerCategoryData `json:"data"`
|
||||||
@@ -153,6 +274,135 @@ type ProductAnalyticsPerCategoryData struct {
|
|||||||
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProductAnalyticsPerParentCategoryRequest represents the request for product analytics per parent category
|
||||||
|
type ProductAnalyticsPerParentCategoryRequest struct {
|
||||||
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
|
OutletID *uuid.UUID `validate:"omitempty"`
|
||||||
|
DateFrom time.Time `validate:"required"`
|
||||||
|
DateTo time.Time `validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProductAnalyticsPerParentCategoryResponse represents the response for product analytics per parent category
|
||||||
|
type ProductAnalyticsPerParentCategoryResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
Data []ProductAnalyticsPerParentCategoryData `json:"data"`
|
||||||
|
Budget BudgetCutOff `json:"budget"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductAnalyticsPerParentCategoryData struct {
|
||||||
|
ParentCategoryID uuid.UUID `json:"parent_category_id"`
|
||||||
|
ParentCategoryName string `json:"parent_category_name"`
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
TotalQuantity int64 `json:"total_quantity"`
|
||||||
|
CategoryCount int64 `json:"category_count"`
|
||||||
|
ProductCount int64 `json:"product_count"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||||
|
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||||
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentCategoryAnalyticsDetailRequest represents the request for the drill-down of one parent category
|
||||||
|
type ParentCategoryAnalyticsDetailRequest struct {
|
||||||
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
|
ParentCategoryID uuid.UUID `validate:"required"`
|
||||||
|
OutletID *uuid.UUID `validate:"omitempty"`
|
||||||
|
DateFrom time.Time `validate:"required"`
|
||||||
|
DateTo time.Time `validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentCategoryAnalyticsDetailResponse represents the drill-down of one parent category
|
||||||
|
type ParentCategoryAnalyticsDetailResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
ParentCategoryID uuid.UUID `json:"parent_category_id"`
|
||||||
|
ParentCategoryName string `json:"parent_category_name"`
|
||||||
|
Summary ParentCategoryAnalyticsDetailSummary `json:"summary"`
|
||||||
|
Categories []ParentCategoryAnalyticsDetailData `json:"categories"`
|
||||||
|
Budget BudgetCutOff `json:"budget"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParentCategoryAnalyticsDetailSummary struct {
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
TotalQuantity int64 `json:"total_quantity"`
|
||||||
|
CategoryCount int64 `json:"category_count"`
|
||||||
|
ProductCount int64 `json:"product_count"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||||
|
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||||
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParentCategoryAnalyticsDetailData struct {
|
||||||
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
TotalQuantity int64 `json:"total_quantity"`
|
||||||
|
ProductCount int64 `json:"product_count"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||||
|
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||||
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
|
Products []ParentCategoryAnalyticsProductData `json:"products"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParentCategoryAnalyticsProductData struct {
|
||||||
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
ProductSku string `json:"product_sku"`
|
||||||
|
ProductPrice float64 `json:"product_price"`
|
||||||
|
QuantitySold int64 `json:"quantity_sold"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
AveragePrice float64 `json:"average_price"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
StandardHppPerUnit float64 `json:"standard_hpp_per_unit"`
|
||||||
|
StandardHppTotal float64 `json:"standard_hpp_total"`
|
||||||
|
FifoHppPerUnit float64 `json:"fifo_hpp_per_unit"`
|
||||||
|
FifoHppTotal float64 `json:"fifo_hpp_total"`
|
||||||
|
MovingAverageHppPerUnit float64 `json:"moving_average_hpp_per_unit"`
|
||||||
|
MovingAverageHppTotal float64 `json:"moving_average_hpp_total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BudgetCutOff is the Monday-to-Sunday spending limit breakdown attached to the
|
||||||
|
// parent category reports.
|
||||||
|
type BudgetCutOff struct {
|
||||||
|
Percentages BudgetPercentages `json:"percentages"`
|
||||||
|
CutOffFrom time.Time `json:"cut_off_from"`
|
||||||
|
CutOffTo time.Time `json:"cut_off_to"`
|
||||||
|
Total BudgetPeriod `json:"total"`
|
||||||
|
Weekly []BudgetPeriod `json:"weekly"`
|
||||||
|
Monthly []BudgetMonthPeriod `json:"monthly"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BudgetPercentages struct {
|
||||||
|
Purchase float64 `json:"purchase"`
|
||||||
|
Owner float64 `json:"owner"`
|
||||||
|
Team float64 `json:"team"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BudgetPeriod struct {
|
||||||
|
PeriodStart time.Time `json:"period_start"`
|
||||||
|
PeriodEnd time.Time `json:"period_end"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
LimitPurchase float64 `json:"limit_purchase"`
|
||||||
|
LimitOwner float64 `json:"limit_owner"`
|
||||||
|
LimitTeam float64 `json:"limit_team"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BudgetMonthPeriod struct {
|
||||||
|
Month string `json:"month"`
|
||||||
|
WeekCount int `json:"week_count"`
|
||||||
|
BudgetPeriod
|
||||||
|
}
|
||||||
|
|
||||||
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
||||||
type DashboardAnalyticsRequest struct {
|
type DashboardAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID `validate:"required"`
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
@@ -165,6 +415,7 @@ type DashboardAnalyticsRequest struct {
|
|||||||
type DashboardAnalyticsResponse struct {
|
type DashboardAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
Overview DashboardOverview `json:"overview"`
|
Overview DashboardOverview `json:"overview"`
|
||||||
@@ -175,15 +426,17 @@ type DashboardAnalyticsResponse struct {
|
|||||||
|
|
||||||
// DashboardOverview represents the overview data for dashboard
|
// DashboardOverview represents the overview data for dashboard
|
||||||
type DashboardOverview struct {
|
type DashboardOverview struct {
|
||||||
TotalSales float64 `json:"total_sales"`
|
TotalSales float64 `json:"total_sales"`
|
||||||
TotalOrders int64 `json:"total_orders"`
|
TotalOrders int64 `json:"total_orders"`
|
||||||
AverageOrderValue float64 `json:"average_order_value"`
|
AverageOrderValue float64 `json:"average_order_value"`
|
||||||
TotalCustomers int64 `json:"total_customers"`
|
TotalCustomers int64 `json:"total_customers"`
|
||||||
VoidedOrders int64 `json:"voided_orders"`
|
VoidedOrders int64 `json:"voided_orders"`
|
||||||
RefundedOrders int64 `json:"refunded_orders"`
|
RefundedOrders int64 `json:"refunded_orders"`
|
||||||
|
TotalItemSold int64 `json:"total_item_sold"`
|
||||||
|
TotalLowStock int64 `json:"total_low_stock"`
|
||||||
|
TotalProductActive int64 `json:"total_product_active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossAnalyticsRequest represents the request for profit and loss analytics
|
|
||||||
type ProfitLossAnalyticsRequest struct {
|
type ProfitLossAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID `validate:"required"`
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
OutletID *uuid.UUID `validate:"omitempty"`
|
OutletID *uuid.UUID `validate:"omitempty"`
|
||||||
@@ -192,19 +445,39 @@ type ProfitLossAnalyticsRequest struct {
|
|||||||
GroupBy string `validate:"omitempty,oneof=day hour week month"`
|
GroupBy string `validate:"omitempty,oneof=day hour week month"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossAnalyticsResponse represents the response for profit and loss analytics
|
|
||||||
type ProfitLossAnalyticsResponse struct {
|
type ProfitLossAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
GroupBy string `json:"group_by"`
|
DateTo time.Time `json:"date_to"`
|
||||||
Summary ProfitLossSummary `json:"summary"`
|
GroupBy string `json:"group_by"`
|
||||||
Data []ProfitLossData `json:"data"`
|
Summary ProfitLossSummary `json:"summary"`
|
||||||
ProductData []ProductProfitData `json:"product_data"`
|
Data []ProfitLossData `json:"data"`
|
||||||
|
ProductData []ProductProfitData `json:"product_data"`
|
||||||
|
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
||||||
|
Purchasing ProfitLossPurchasing `json:"purchasing"`
|
||||||
|
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
||||||
|
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfitLossPurchasing struct {
|
||||||
|
TodayTotal float64 `json:"today_total"`
|
||||||
|
MtdTotal float64 `json:"mtd_total"`
|
||||||
|
TodayRawMaterial float64 `json:"today_raw_material"`
|
||||||
|
MtdRawMaterial float64 `json:"mtd_raw_material"`
|
||||||
|
TodayExpense float64 `json:"today_expense"`
|
||||||
|
MtdExpense float64 `json:"mtd_expense"`
|
||||||
|
Items []ProfitLossPurchasingItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfitLossPurchasingItem struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Item string `json:"item"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Nominal float64 `json:"nominal"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossSummary represents the summary of profit and loss analytics
|
|
||||||
type ProfitLossSummary struct {
|
type ProfitLossSummary struct {
|
||||||
TotalRevenue float64 `json:"total_revenue"`
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
TotalCost float64 `json:"total_cost"`
|
TotalCost float64 `json:"total_cost"`
|
||||||
@@ -219,7 +492,6 @@ type ProfitLossSummary struct {
|
|||||||
ProfitabilityRatio float64 `json:"profitability_ratio"`
|
ProfitabilityRatio float64 `json:"profitability_ratio"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossData represents individual profit and loss data point by time period
|
|
||||||
type ProfitLossData struct {
|
type ProfitLossData struct {
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Revenue float64 `json:"revenue"`
|
Revenue float64 `json:"revenue"`
|
||||||
@@ -233,7 +505,6 @@ type ProfitLossData struct {
|
|||||||
Orders int64 `json:"orders"`
|
Orders int64 `json:"orders"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductProfitData represents profit data for individual products
|
|
||||||
type ProductProfitData struct {
|
type ProductProfitData struct {
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
@@ -248,3 +519,139 @@ type ProductProfitData struct {
|
|||||||
AverageCost float64 `json:"average_cost"`
|
AverageCost float64 `json:"average_cost"`
|
||||||
ProfitPerUnit float64 `json:"profit_per_unit"`
|
ProfitPerUnit float64 `json:"profit_per_unit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProfitLossSummaryRow struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
IsBold bool `json:"is_bold"`
|
||||||
|
TodayNominal float64 `json:"today_nominal"`
|
||||||
|
TodayPct float64 `json:"today_pct"`
|
||||||
|
MtdNominal float64 `json:"mtd_nominal"`
|
||||||
|
MtdPct float64 `json:"mtd_pct"`
|
||||||
|
SubItems []ProfitLossSummaryRow `json:"sub_items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperationalExpenseItem struct {
|
||||||
|
Item string `json:"item"`
|
||||||
|
Nominal float64 `json:"nominal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryPeriodRequest struct {
|
||||||
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
|
OutletID *uuid.UUID `validate:"omitempty"`
|
||||||
|
DateFrom time.Time `validate:"required"`
|
||||||
|
DateTo time.Time `validate:"required"`
|
||||||
|
ExcludeGajiStaffFromReimburse bool `validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryMonthlyRequest struct {
|
||||||
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
|
OutletID *uuid.UUID `validate:"omitempty"`
|
||||||
|
Month time.Time `validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryMTDRequest struct {
|
||||||
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
|
OutletID *uuid.UUID `validate:"omitempty"`
|
||||||
|
DateTo time.Time `validate:"required"`
|
||||||
|
ExcludeGajiStaffFromReimburse bool `validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryPeriodResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
|
Period ExclusiveSummaryPeriodRange `json:"period"`
|
||||||
|
Summary ExclusiveSummaryPeriodSummary `json:"summary"`
|
||||||
|
Reimburse ExclusiveSummaryReimburse `json:"reimburse"`
|
||||||
|
HPPBreakdown []ExclusiveSummaryCategoryBreakdown `json:"hpp_breakdown"`
|
||||||
|
OperationalExpenseBreakdown []ExclusiveSummaryCategoryBreakdown `json:"operational_expense_breakdown"`
|
||||||
|
DailySummary []ExclusiveSummaryDailySummary `json:"daily_summary"`
|
||||||
|
DailyTransactions []ExclusiveSummaryDailyTransaction `json:"daily_transactions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryPeriodRange struct {
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryPeriodSummary struct {
|
||||||
|
Sales float64 `json:"sales"`
|
||||||
|
HPP float64 `json:"hpp"`
|
||||||
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
|
SalaryTotal float64 `json:"salary_total"`
|
||||||
|
SalaryDW float64 `json:"salary_dw"`
|
||||||
|
SalaryStaff float64 `json:"salary_staff"`
|
||||||
|
SalaryOther float64 `json:"salary_other"`
|
||||||
|
OtherOperationalExpenses float64 `json:"other_operational_expenses"`
|
||||||
|
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
NetProfit float64 `json:"net_profit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryReimburse struct {
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
ExcludedSalaryStaff float64 `json:"excluded_salary_staff"`
|
||||||
|
TotalReimburse float64 `json:"total_reimburse"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryCategoryBreakdown struct {
|
||||||
|
CategoryCode string `json:"category_code"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Percentage float64 `json:"percentage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryDailySummary struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
TransactionCount int64 `json:"transaction_count"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryDailyTransaction struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
CategoryCode string `json:"category_code"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryMonthlyResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
OutletName *string `json:"outlet_name,omitempty"`
|
||||||
|
Month string `json:"month"`
|
||||||
|
Summary ExclusiveSummaryMonthlySummary `json:"summary"`
|
||||||
|
Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"`
|
||||||
|
BankBalance []ExclusiveSummaryBankBalance `json:"bank_balance"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryMonthlySummary struct {
|
||||||
|
TotalSales float64 `json:"total_sales"`
|
||||||
|
HPP float64 `json:"hpp"`
|
||||||
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
|
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
NetProfit float64 `json:"net_profit"`
|
||||||
|
NetProfitMargin float64 `json:"net_profit_margin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryMonthlyPeriod struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
Sales float64 `json:"sales"`
|
||||||
|
HPP float64 `json:"hpp"`
|
||||||
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
|
GrossMargin float64 `json:"gross_margin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExclusiveSummaryBankBalance struct {
|
||||||
|
Bank string `json:"bank"`
|
||||||
|
OpeningBalance *float64 `json:"opening_balance"`
|
||||||
|
IncomingMutation *float64 `json:"incoming_mutation"`
|
||||||
|
OutgoingMutation *float64 `json:"outgoing_mutation"`
|
||||||
|
ClosingBalance *float64 `json:"closing_balance"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import (
|
|||||||
type Category struct {
|
type Category struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
|
OutletID *uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
Description *string
|
Description *string
|
||||||
ImageURL *string
|
ImageURL *string
|
||||||
Order int
|
Order int
|
||||||
IsActive bool
|
IsActive bool
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
@@ -20,27 +21,34 @@ type Category struct {
|
|||||||
|
|
||||||
type CreateCategoryRequest struct {
|
type CreateCategoryRequest struct {
|
||||||
OrganizationID uuid.UUID `validate:"required"`
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
Name string `validate:"required,min=1,max=255"`
|
OutletID *uuid.UUID
|
||||||
Description *string `validate:"omitempty,max=1000"`
|
ParentID *uuid.UUID
|
||||||
ImageURL *string `validate:"omitempty,url"`
|
Name string `validate:"required,min=1,max=255"`
|
||||||
Order int `validate:"min=0"`
|
Description *string `validate:"omitempty,max=1000"`
|
||||||
|
ImageURL *string `validate:"omitempty,url"`
|
||||||
|
Order int `validate:"min=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateCategoryRequest struct {
|
type UpdateCategoryRequest struct {
|
||||||
Name *string `validate:"omitempty,min=1,max=255"`
|
Name *string `validate:"omitempty,min=1,max=255"`
|
||||||
Description *string `validate:"omitempty,max=1000"`
|
Description *string `validate:"omitempty,max=1000"`
|
||||||
ImageURL *string `validate:"omitempty,url"`
|
ImageURL *string `validate:"omitempty,url"`
|
||||||
Order *int `validate:"omitempty,min=0"`
|
OutletID *uuid.UUID
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Order *int `validate:"omitempty,min=0"`
|
||||||
IsActive *bool
|
IsActive *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type CategoryResponse struct {
|
type CategoryResponse struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
|
OutletID *uuid.UUID
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
ParentName *string
|
||||||
Name string
|
Name string
|
||||||
Description *string
|
Description *string
|
||||||
ImageURL *string
|
ImageURL *string
|
||||||
Order int
|
Order int
|
||||||
IsActive bool
|
IsActive bool
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
|||||||
+11
-11
@@ -23,17 +23,17 @@ type UpdateCustomerRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CustomerResponse struct {
|
type CustomerResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Email *string `json:"email,omitempty"`
|
Email *string `json:"email,omitempty"`
|
||||||
Phone *string `json:"phone,omitempty"`
|
Phone *string `json:"phone,omitempty"`
|
||||||
Address *string `json:"address,omitempty"`
|
Address *string `json:"address,omitempty"`
|
||||||
IsDefault bool `json:"is_default"`
|
IsDefault bool `json:"is_default"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
Metadata entities.Metadata `json:"metadata"`
|
Metadata entities.Metadata `json:"metadata"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListCustomersQuery represents query parameters for listing customers
|
// ListCustomersQuery represents query parameters for listing customers
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Expense struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID uuid.UUID `json:"outlet_id"`
|
||||||
|
Receiver string `json:"receiver"`
|
||||||
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
|
CodeNumber string `json:"code_number"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
Reserved1 *string `json:"reserved1"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseItem struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
ExpenseID uuid.UUID `json:"expense_id"`
|
||||||
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
|
Item string `json:"item"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID uuid.UUID `json:"outlet_id"`
|
||||||
|
Receiver string `json:"receiver"`
|
||||||
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
|
CodeNumber string `json:"code_number"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
Reserved1 *string `json:"reserved1"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Items []ExpenseItemResponse `json:"items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseItemResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
ExpenseID uuid.UUID `json:"expense_id"`
|
||||||
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
|
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
||||||
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
|
PurchaseCategoryName string `json:"purchase_category_name,omitempty"`
|
||||||
|
PurchaseCategoryType string `json:"purchase_category_type,omitempty"`
|
||||||
|
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||||
|
Item string `json:"item"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateExpenseRequest struct {
|
||||||
|
Receiver string `json:"receiver"`
|
||||||
|
TransactionDate string `json:"transaction_date"`
|
||||||
|
CodeNumber string `json:"code_number"`
|
||||||
|
OutletID string `json:"outlet_id"`
|
||||||
|
Status *string `json:"status,omitempty"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
Items []CreateExpenseItemRequest `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateExpenseItemRequest struct {
|
||||||
|
ChartOfAccountID string `json:"chart_of_account_id"`
|
||||||
|
PurchaseCategoryID string `json:"purchase_category_id"`
|
||||||
|
Item string `json:"item"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateExpenseRequest struct {
|
||||||
|
Receiver *string `json:"receiver,omitempty"`
|
||||||
|
TransactionDate *string `json:"transaction_date,omitempty"`
|
||||||
|
CodeNumber *string `json:"code_number,omitempty"`
|
||||||
|
OutletID *string `json:"outlet_id,omitempty"`
|
||||||
|
Status *string `json:"status,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Tax *float64 `json:"tax,omitempty"`
|
||||||
|
Total *float64 `json:"total,omitempty"`
|
||||||
|
Reserved1 *string `json:"reserved1,omitempty"`
|
||||||
|
Items []UpdateExpenseItemRequest `json:"items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateExpenseItemRequest struct {
|
||||||
|
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
||||||
|
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"`
|
||||||
|
Item *string `json:"item,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Amount *float64 `json:"amount,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListExpenseRequest struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Search string `json:"search,omitempty"`
|
||||||
|
OutletID string `json:"outlet_id,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
StartDate string `json:"start_date,omitempty"`
|
||||||
|
EndDate string `json:"end_date,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListExpenseResponse struct {
|
||||||
|
Expenses []*ExpenseResponse `json:"expenses"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsRequest struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
GroupBy string `json:"group_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
GroupBy string `json:"group_by"`
|
||||||
|
Summary ExpenseAnalyticsSummary `json:"summary"`
|
||||||
|
Data []ExpenseAnalyticsData `json:"data"`
|
||||||
|
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
||||||
|
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData `json:"chart_of_account_data"`
|
||||||
|
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsSummary struct {
|
||||||
|
TotalExpenses float64 `json:"total_expenses"`
|
||||||
|
TotalExpenseCount int64 `json:"total_expense_count"`
|
||||||
|
TotalTax float64 `json:"total_tax"`
|
||||||
|
AverageExpenseValue float64 `json:"average_expense_value"`
|
||||||
|
TotalCategories int64 `json:"total_categories"`
|
||||||
|
TotalItems int64 `json:"total_items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsData struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Expenses float64 `json:"expenses"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
Items int64 `json:"items"`
|
||||||
|
Categories int64 `json:"categories"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsCategoryData struct {
|
||||||
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
|
PurchaseCategoryName string `json:"purchase_category_name"`
|
||||||
|
PurchaseCategoryType string `json:"purchase_category_type"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
ItemCount int64 `json:"item_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsChartOfAccountData struct {
|
||||||
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
|
ChartOfAccountName string `json:"chart_of_account_name"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
ItemCount int64 `json:"item_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsItemData struct {
|
||||||
|
Item string `json:"item"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
ItemCount int64 `json:"item_count"`
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ type Ingredient struct {
|
|||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
UnitID *uuid.UUID `json:"unit_id"`
|
||||||
Cost float64 `json:"cost"`
|
Cost float64 `json:"cost"`
|
||||||
Stock float64 `json:"stock"`
|
Stock float64 `json:"stock"`
|
||||||
IsSemiFinished bool `json:"is_semi_finished"`
|
IsSemiFinished bool `json:"is_semi_finished"`
|
||||||
@@ -29,7 +29,7 @@ type CreateIngredientRequest struct {
|
|||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
UnitID *uuid.UUID `json:"unit_id" validate:"omitempty"`
|
||||||
Cost float64 `json:"cost" validate:"min=0"`
|
Cost float64 `json:"cost" validate:"min=0"`
|
||||||
Stock float64 `json:"stock" validate:"min=0"`
|
Stock float64 `json:"stock" validate:"min=0"`
|
||||||
IsSemiFinished bool `json:"is_semi_finished"`
|
IsSemiFinished bool `json:"is_semi_finished"`
|
||||||
@@ -48,7 +48,7 @@ type CompositionItemRequest struct {
|
|||||||
type UpdateIngredientRequest struct {
|
type UpdateIngredientRequest struct {
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
UnitID *uuid.UUID `json:"unit_id" validate:"omitempty"`
|
||||||
Cost float64 `json:"cost" validate:"min=0"`
|
Cost float64 `json:"cost" validate:"min=0"`
|
||||||
Stock float64 `json:"stock" validate:"min=0"`
|
Stock float64 `json:"stock" validate:"min=0"`
|
||||||
IsSemiFinished bool `json:"is_semi_finished"`
|
IsSemiFinished bool `json:"is_semi_finished"`
|
||||||
@@ -61,7 +61,7 @@ type IngredientResponse struct {
|
|||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
UnitID *uuid.UUID `json:"unit_id"`
|
||||||
Cost float64 `json:"cost"`
|
Cost float64 `json:"cost"`
|
||||||
Stock float64 `json:"stock"`
|
Stock float64 `json:"stock"`
|
||||||
IsSemiFinished bool `json:"is_semi_finished"`
|
IsSemiFinished bool `json:"is_semi_finished"`
|
||||||
|
|||||||
@@ -97,8 +97,7 @@ type ListIngredientUnitConvertersResponse struct {
|
|||||||
type IngredientUnitsResponse struct {
|
type IngredientUnitsResponse struct {
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
IngredientName string `json:"ingredient_name"`
|
IngredientName string `json:"ingredient_name"`
|
||||||
BaseUnitID uuid.UUID `json:"base_unit_id"`
|
BaseUnitID *uuid.UUID `json:"base_unit_id"`
|
||||||
BaseUnitName string `json:"base_unit_name"`
|
BaseUnitName string `json:"base_unit_name"`
|
||||||
Units []*UnitResponse `json:"units"`
|
Units []*UnitResponse `json:"units"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- Request models ----
|
||||||
|
|
||||||
|
type SendNotificationRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Priority entities.NotificationPriority `json:"priority"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
ActionURL string `json:"action_url"`
|
||||||
|
NotifiableType string `json:"notifiable_type"`
|
||||||
|
NotifiableID *uuid.UUID `json:"notifiable_id"`
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
ReceiverIDs []uuid.UUID `json:"receiver_ids"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at"`
|
||||||
|
CreatedBy *uuid.UUID `json:"created_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BroadcastNotificationRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Priority entities.NotificationPriority `json:"priority"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
ActionURL string `json:"action_url"`
|
||||||
|
NotifiableType string `json:"notifiable_type"`
|
||||||
|
NotifiableID *uuid.UUID `json:"notifiable_id"`
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at"`
|
||||||
|
CreatedBy *uuid.UUID `json:"created_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarkNotificationReadRequest struct {
|
||||||
|
NotificationReceiverID uuid.UUID `json:"notification_receiver_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListNotificationsRequest struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
IsRead *bool `json:"is_read"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Response models ----
|
||||||
|
|
||||||
|
type NotificationResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Priority entities.NotificationPriority `json:"priority"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
ActionURL string `json:"action_url"`
|
||||||
|
NotifiableType string `json:"notifiable_type"`
|
||||||
|
NotifiableID *uuid.UUID `json:"notifiable_id"`
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
ScheduledAt *time.Time `json:"scheduled_at"`
|
||||||
|
SentAt *time.Time `json:"sent_at"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at"`
|
||||||
|
CreatedBy *uuid.UUID `json:"created_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationReceiverResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
NotificationID uuid.UUID `json:"notification_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
IsRead bool `json:"is_read"`
|
||||||
|
ReadAt *time.Time `json:"read_at"`
|
||||||
|
IsDeleted bool `json:"is_deleted"`
|
||||||
|
DeletedAt *time.Time `json:"deleted_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Notification *NotificationResponse `json:"notification,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationDeliveryResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
NotificationReceiverID uuid.UUID `json:"notification_receiver_id"`
|
||||||
|
UserDeviceID uuid.UUID `json:"user_device_id"`
|
||||||
|
Channel entities.NotificationChannel `json:"channel"`
|
||||||
|
DeliveryStatus entities.NotificationDeliveryStatus `json:"delivery_status"`
|
||||||
|
Provider entities.NotificationProvider `json:"provider"`
|
||||||
|
ProviderMessageID string `json:"provider_message_id"`
|
||||||
|
SentAt *time.Time `json:"sent_at"`
|
||||||
|
DeliveredAt *time.Time `json:"delivered_at"`
|
||||||
|
FailedAt *time.Time `json:"failed_at"`
|
||||||
|
FailureReason string `json:"failure_reason"`
|
||||||
|
RetryCount int `json:"retry_count"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListNotificationsResponse struct {
|
||||||
|
Notifications []*NotificationReceiverResponse `json:"notifications"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
UnreadCount int `json:"unread_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
@@ -188,6 +188,8 @@ type OrderItemResponse struct {
|
|||||||
ProductName string
|
ProductName string
|
||||||
ProductVariantID *uuid.UUID
|
ProductVariantID *uuid.UUID
|
||||||
ProductVariantName *string
|
ProductVariantName *string
|
||||||
|
CategoryID *uuid.UUID
|
||||||
|
CategoryName *string
|
||||||
Quantity int
|
Quantity int
|
||||||
UnitPrice float64
|
UnitPrice float64
|
||||||
TotalPrice float64
|
TotalPrice float64
|
||||||
@@ -207,6 +209,7 @@ type OrderItemResponse struct {
|
|||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
PrinterType string
|
PrinterType string
|
||||||
|
PrintToChecker bool
|
||||||
PaidQuantity int
|
PaidQuantity int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ type UpdateOrderIngredientTransactionRequest struct {
|
|||||||
GrossQty *float64 `json:"gross_qty,omitempty" validate:"omitempty,gt=0"`
|
GrossQty *float64 `json:"gross_qty,omitempty" validate:"omitempty,gt=0"`
|
||||||
NetQty *float64 `json:"net_qty,omitempty" validate:"omitempty,gt=0"`
|
NetQty *float64 `json:"net_qty,omitempty" validate:"omitempty,gt=0"`
|
||||||
WasteQty *float64 `json:"waste_qty,omitempty" validate:"min=0"`
|
WasteQty *float64 `json:"waste_qty,omitempty" validate:"min=0"`
|
||||||
Unit *string `json:"unit,omitempty" validate:"omitempty,max=50"`
|
Unit *string `json:"unit,omitempty" validate:"omitempty,max=50"`
|
||||||
TransactionDate *time.Time `json:"transaction_date,omitempty"`
|
TransactionDate *time.Time `json:"transaction_date,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderIngredientTransactionResponse struct {
|
type OrderIngredientTransactionResponse struct {
|
||||||
@@ -98,11 +98,11 @@ type ListOrderIngredientTransactionsRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OrderIngredientTransactionSummary struct {
|
type OrderIngredientTransactionSummary struct {
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
IngredientName string `json:"ingredient_name"`
|
IngredientName string `json:"ingredient_name"`
|
||||||
TotalGrossQty float64 `json:"total_gross_qty"`
|
TotalGrossQty float64 `json:"total_gross_qty"`
|
||||||
TotalNetQty float64 `json:"total_net_qty"`
|
TotalNetQty float64 `json:"total_net_qty"`
|
||||||
TotalWasteQty float64 `json:"total_waste_qty"`
|
TotalWasteQty float64 `json:"total_waste_qty"`
|
||||||
WastePercentage float64 `json:"waste_percentage"`
|
WastePercentage float64 `json:"waste_percentage"`
|
||||||
Unit string `json:"unit"`
|
Unit string `json:"unit"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type ProductVariant struct {
|
|||||||
|
|
||||||
type CreateProductRequest struct {
|
type CreateProductRequest struct {
|
||||||
OrganizationID uuid.UUID `validate:"required"`
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
|
OutletID uuid.UUID `validate:"omitempty"` // If set, upsert product_outlet_prices on create
|
||||||
CategoryID uuid.UUID `validate:"required"`
|
CategoryID uuid.UUID `validate:"required"`
|
||||||
SKU *string `validate:"omitempty,max=100"`
|
SKU *string `validate:"omitempty,max=100"`
|
||||||
Name string `validate:"required,min=1,max=255"`
|
Name string `validate:"required,min=1,max=255"`
|
||||||
@@ -49,6 +50,7 @@ type CreateProductRequest struct {
|
|||||||
BusinessType constants.BusinessType `validate:"required"`
|
BusinessType constants.BusinessType `validate:"required"`
|
||||||
ImageURL *string `validate:"omitempty,max=500"`
|
ImageURL *string `validate:"omitempty,max=500"`
|
||||||
PrinterType *string `validate:"omitempty,max=50"`
|
PrinterType *string `validate:"omitempty,max=50"`
|
||||||
|
PrintToChecker *bool `validate:"omitempty"`
|
||||||
UnitID *uuid.UUID `validate:"omitempty"`
|
UnitID *uuid.UUID `validate:"omitempty"`
|
||||||
HasIngredients bool `validate:"omitempty"`
|
HasIngredients bool `validate:"omitempty"`
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
@@ -60,6 +62,7 @@ type CreateProductRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateProductRequest struct {
|
type UpdateProductRequest struct {
|
||||||
|
OutletID uuid.UUID `validate:"omitempty"` // If set, upsert product_outlet_prices on update
|
||||||
CategoryID *uuid.UUID `validate:"omitempty"`
|
CategoryID *uuid.UUID `validate:"omitempty"`
|
||||||
SKU *string `validate:"omitempty,max=100"`
|
SKU *string `validate:"omitempty,max=100"`
|
||||||
Name *string `validate:"omitempty,min=1,max=255"`
|
Name *string `validate:"omitempty,min=1,max=255"`
|
||||||
@@ -68,6 +71,7 @@ type UpdateProductRequest struct {
|
|||||||
Cost *float64 `validate:"omitempty,min=0"`
|
Cost *float64 `validate:"omitempty,min=0"`
|
||||||
ImageURL *string `validate:"omitempty,max=500"`
|
ImageURL *string `validate:"omitempty,max=500"`
|
||||||
PrinterType *string `validate:"omitempty,max=50"`
|
PrinterType *string `validate:"omitempty,max=50"`
|
||||||
|
PrintToChecker *bool `validate:"omitempty"`
|
||||||
UnitID *uuid.UUID `validate:"omitempty"`
|
UnitID *uuid.UUID `validate:"omitempty"`
|
||||||
HasIngredients *bool `validate:"omitempty"`
|
HasIngredients *bool `validate:"omitempty"`
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
@@ -100,10 +104,13 @@ type ProductResponse struct {
|
|||||||
Name string
|
Name string
|
||||||
Description *string
|
Description *string
|
||||||
Price float64
|
Price float64
|
||||||
|
OutletPrice *float64 // outlet-specific price, nil if not set
|
||||||
|
OutletPrices []OutletPrice // all outlet prices, populated when no outletID in context
|
||||||
Cost float64
|
Cost float64
|
||||||
BusinessType constants.BusinessType
|
BusinessType constants.BusinessType
|
||||||
ImageURL *string
|
ImageURL *string
|
||||||
PrinterType string
|
PrinterType string
|
||||||
|
PrintToChecker bool
|
||||||
UnitID *uuid.UUID
|
UnitID *uuid.UUID
|
||||||
HasIngredients bool
|
HasIngredients bool
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
@@ -113,6 +120,13 @@ type ProductResponse struct {
|
|||||||
Variants []ProductVariantResponse
|
Variants []ProductVariantResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OutletPrice struct {
|
||||||
|
OutletID uuid.UUID
|
||||||
|
OutletName string
|
||||||
|
Price float64
|
||||||
|
PrintToChecker bool
|
||||||
|
}
|
||||||
|
|
||||||
type ProductVariantResponse struct {
|
type ProductVariantResponse struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
ProductID uuid.UUID
|
ProductID uuid.UUID
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProductIngredient struct {
|
type ProductIngredient struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
WastePercentage float64 `json:"waste_percentage"`
|
WastePercentage float64 `json:"waste_percentage"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Product *Product `json:"product,omitempty"`
|
Product *Product `json:"product,omitempty"`
|
||||||
@@ -37,15 +37,15 @@ type UpdateProductIngredientRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProductIngredientResponse struct {
|
type ProductIngredientResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
WastePercentage float64 `json:"waste_percentage"`
|
WastePercentage float64 `json:"waste_percentage"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Product *Product `json:"product,omitempty"`
|
Product *Product `json:"product,omitempty"`
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductOutletPrice struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
ProductID uuid.UUID
|
||||||
|
OutletID uuid.UUID
|
||||||
|
Price float64
|
||||||
|
PrintToChecker bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateProductOutletPriceRequest struct {
|
||||||
|
ProductID uuid.UUID `validate:"required"`
|
||||||
|
OutletID uuid.UUID `validate:"required"`
|
||||||
|
Price float64 `validate:"required,min=0"`
|
||||||
|
PrintToChecker bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProductOutletPriceRequest struct {
|
||||||
|
Price *float64 `validate:"required,min=0"`
|
||||||
|
PrintToChecker *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductOutletPriceResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
|
OutletID uuid.UUID `json:"outlet_id"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
@@ -56,4 +56,4 @@ type ProductRecipeResponse struct {
|
|||||||
Product *Product `json:"product,omitempty"`
|
Product *Product `json:"product,omitempty"`
|
||||||
ProductVariant *ProductVariant `json:"product_variant,omitempty"`
|
ProductVariant *ProductVariant `json:"product_variant,omitempty"`
|
||||||
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryResponse struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
PresetID *uuid.UUID
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Code string
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
SortOrder int
|
||||||
|
IsSystem bool
|
||||||
|
IsActive bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePurchaseCategoryRequest struct {
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Code *string
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
SortOrder int
|
||||||
|
IsActive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePurchaseCategoryRequest struct {
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Code *string
|
||||||
|
Name *string
|
||||||
|
Type *string
|
||||||
|
SortOrder *int
|
||||||
|
IsActive *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPurchaseCategoriesRequest struct {
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Type string
|
||||||
|
Search string
|
||||||
|
IsActive *bool
|
||||||
|
Page int
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
@@ -7,30 +7,42 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseOrder struct {
|
type PurchaseOrder struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
VendorID uuid.UUID `json:"vendor_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
PONumber string `json:"po_number"`
|
VendorID *uuid.UUID `json:"vendor_id"`
|
||||||
TransactionDate time.Time `json:"transaction_date"`
|
PONumber string `json:"po_number"`
|
||||||
DueDate time.Time `json:"due_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
Reference *string `json:"reference"`
|
DueDate *time.Time `json:"due_date"`
|
||||||
Status string `json:"status"`
|
Reference *string `json:"reference"`
|
||||||
Message *string `json:"message"`
|
Status string `json:"status"`
|
||||||
TotalAmount float64 `json:"total_amount"`
|
Message *string `json:"message"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
TotalAmount float64 `json:"total_amount"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
TeamScope *string `json:"team_scope"`
|
||||||
|
TeamCategoryID *uuid.UUID `json:"team_category_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchaseTeam is one entry of the team picker: either a parent category or Pusat.
|
||||||
|
// Pusat carries no CategoryID because it has no category of its own.
|
||||||
|
type PurchaseTeam struct {
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
CategoryID *uuid.UUID `json:"category_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderItem struct {
|
type PurchaseOrderItem struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID *uuid.UUID `json:"ingredient_id"`
|
||||||
Description *string `json:"description"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Description *string `json:"description"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
Quantity *float64 `json:"quantity"`
|
||||||
Amount float64 `json:"amount"`
|
UnitID *uuid.UUID `json:"unit_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Amount float64 `json:"amount"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderAttachment struct {
|
type PurchaseOrderAttachment struct {
|
||||||
@@ -43,33 +55,39 @@ type PurchaseOrderAttachment struct {
|
|||||||
type PurchaseOrderResponse struct {
|
type PurchaseOrderResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
VendorID uuid.UUID `json:"vendor_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
VendorID *uuid.UUID `json:"vendor_id"`
|
||||||
PONumber string `json:"po_number"`
|
PONumber string `json:"po_number"`
|
||||||
TransactionDate time.Time `json:"transaction_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
DueDate time.Time `json:"due_date"`
|
DueDate *time.Time `json:"due_date"`
|
||||||
Reference *string `json:"reference"`
|
Reference *string `json:"reference"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message *string `json:"message"`
|
Message *string `json:"message"`
|
||||||
TotalAmount float64 `json:"total_amount"`
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
TeamScope *string `json:"team_scope"`
|
||||||
|
TeamCategoryID *uuid.UUID `json:"team_category_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Team *PurchaseTeam `json:"team,omitempty"`
|
||||||
Vendor *VendorResponse `json:"vendor,omitempty"`
|
Vendor *VendorResponse `json:"vendor,omitempty"`
|
||||||
Items []PurchaseOrderItemResponse `json:"items,omitempty"`
|
Items []PurchaseOrderItemResponse `json:"items,omitempty"`
|
||||||
Attachments []PurchaseOrderAttachmentResponse `json:"attachments,omitempty"`
|
Attachments []PurchaseOrderAttachmentResponse `json:"attachments,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderItemResponse struct {
|
type PurchaseOrderItemResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID *uuid.UUID `json:"ingredient_id"`
|
||||||
Description *string `json:"description"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Description *string `json:"description"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
Quantity *float64 `json:"quantity"`
|
||||||
Amount float64 `json:"amount"`
|
UnitID *uuid.UUID `json:"unit_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Amount float64 `json:"amount"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Unit *UnitResponse `json:"unit,omitempty"`
|
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||||
|
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||||
|
Unit *UnitResponse `json:"unit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderAttachmentResponse struct {
|
type PurchaseOrderAttachmentResponse struct {
|
||||||
@@ -81,23 +99,27 @@ type PurchaseOrderAttachmentResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreatePurchaseOrderRequest struct {
|
type CreatePurchaseOrderRequest struct {
|
||||||
VendorID uuid.UUID `json:"vendor_id"`
|
VendorID *uuid.UUID `json:"vendor_id,omitempty"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
PONumber string `json:"po_number"`
|
PONumber string `json:"po_number"`
|
||||||
TransactionDate time.Time `json:"transaction_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
DueDate time.Time `json:"due_date"`
|
DueDate *time.Time `json:"due_date,omitempty"`
|
||||||
Reference *string `json:"reference,omitempty"`
|
Reference *string `json:"reference,omitempty"`
|
||||||
Status *string `json:"status,omitempty"`
|
Status *string `json:"status,omitempty"`
|
||||||
Message *string `json:"message,omitempty"`
|
Message *string `json:"message,omitempty"`
|
||||||
|
TeamScope *string `json:"team_scope,omitempty"`
|
||||||
|
TeamCategoryID *uuid.UUID `json:"team_category_id,omitempty"`
|
||||||
Items []CreatePurchaseOrderItemRequest `json:"items"`
|
Items []CreatePurchaseOrderItemRequest `json:"items"`
|
||||||
AttachmentFileIDs []uuid.UUID `json:"attachment_file_ids,omitempty"`
|
AttachmentFileIDs []uuid.UUID `json:"attachment_file_ids,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreatePurchaseOrderItemRequest struct {
|
type CreatePurchaseOrderItemRequest struct {
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Description *string `json:"description,omitempty"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
Quantity *float64 `json:"quantity,omitempty"`
|
||||||
Amount float64 `json:"amount"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePurchaseOrderRequest struct {
|
type UpdatePurchaseOrderRequest struct {
|
||||||
@@ -108,27 +130,37 @@ type UpdatePurchaseOrderRequest struct {
|
|||||||
Reference *string `json:"reference,omitempty"`
|
Reference *string `json:"reference,omitempty"`
|
||||||
Status *string `json:"status,omitempty"`
|
Status *string `json:"status,omitempty"`
|
||||||
Message *string `json:"message,omitempty"`
|
Message *string `json:"message,omitempty"`
|
||||||
|
TeamScope *string `json:"team_scope,omitempty"`
|
||||||
|
TeamCategoryID *uuid.UUID `json:"team_category_id,omitempty"`
|
||||||
Items []UpdatePurchaseOrderItemRequest `json:"items,omitempty"`
|
Items []UpdatePurchaseOrderItemRequest `json:"items,omitempty"`
|
||||||
AttachmentFileIDs []uuid.UUID `json:"attachment_file_ids,omitempty"`
|
AttachmentFileIDs []uuid.UUID `json:"attachment_file_ids,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePurchaseOrderItemRequest struct {
|
type UpdatePurchaseOrderItemRequest struct {
|
||||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty"`
|
||||||
Quantity *float64 `json:"quantity,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
Quantity *float64 `json:"quantity,omitempty"`
|
||||||
Amount *float64 `json:"amount,omitempty"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||||
|
Amount *float64 `json:"amount,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListPurchaseOrdersRequest struct {
|
type ListPurchaseOrdersRequest struct {
|
||||||
Page int `json:"page" validate:"min=1"`
|
Page int `json:"page" validate:"min=1"`
|
||||||
Limit int `json:"limit" validate:"min=1,max=100"`
|
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||||
Search string `json:"search,omitempty"`
|
Search string `json:"search,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
VendorID *uuid.UUID `json:"vendor_id,omitempty"`
|
VendorID *uuid.UUID `json:"vendor_id,omitempty"`
|
||||||
StartDate *time.Time `json:"start_date,omitempty"`
|
Team string `json:"team,omitempty"`
|
||||||
EndDate *time.Time `json:"end_date,omitempty"`
|
TeamScope string `json:"team_scope,omitempty"`
|
||||||
|
TeamCategoryID *uuid.UUID `json:"team_category_id,omitempty"`
|
||||||
|
StartDate *time.Time `json:"start_date,omitempty"`
|
||||||
|
EndDate *time.Time `json:"end_date,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPurchaseTeamsResponse struct {
|
||||||
|
Teams []PurchaseTeam `json:"teams"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListPurchaseOrdersResponse struct {
|
type ListPurchaseOrdersResponse struct {
|
||||||
|
|||||||
@@ -63,10 +63,12 @@ type UserResponse struct {
|
|||||||
|
|
||||||
func (u *User) HasPermission(requiredRole constants.UserRole) bool {
|
func (u *User) HasPermission(requiredRole constants.UserRole) bool {
|
||||||
roleHierarchy := map[constants.UserRole]int{
|
roleHierarchy := map[constants.UserRole]int{
|
||||||
constants.RoleWaiter: 1,
|
constants.RoleWaiter: 1,
|
||||||
constants.RoleCashier: 2,
|
constants.RoleCashier: 2,
|
||||||
constants.RoleManager: 3,
|
constants.RolePurchasing: 3,
|
||||||
constants.RoleAdmin: 4,
|
constants.RoleManager: 4,
|
||||||
|
constants.RoleAdmin: 5,
|
||||||
|
constants.RoleOwner: 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
userLevel := roleHierarchy[u.Role]
|
userLevel := roleHierarchy[u.Role]
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserDevice struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
|
DeviceName string `json:"device_name"`
|
||||||
|
DeviceType entities.DeviceType `json:"device_type"`
|
||||||
|
Platform entities.DevicePlatform `json:"platform"`
|
||||||
|
FCMToken string `json:"fcm_token"`
|
||||||
|
AppVersion string `json:"app_version"`
|
||||||
|
OsVersion string `json:"os_version"`
|
||||||
|
IPAddress string `json:"ip_address"`
|
||||||
|
LastActiveAt *time.Time `json:"last_active_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserDeviceResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
|
DeviceName string `json:"device_name"`
|
||||||
|
DeviceType entities.DeviceType `json:"device_type"`
|
||||||
|
Platform entities.DevicePlatform `json:"platform"`
|
||||||
|
FCMToken string `json:"fcm_token"`
|
||||||
|
AppVersion string `json:"app_version"`
|
||||||
|
OsVersion string `json:"os_version"`
|
||||||
|
IPAddress string `json:"ip_address"`
|
||||||
|
LastActiveAt *time.Time `json:"last_active_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterUserDeviceRequest struct {
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
|
DeviceName string `json:"device_name"`
|
||||||
|
DeviceType entities.DeviceType `json:"device_type"`
|
||||||
|
Platform entities.DevicePlatform `json:"platform"`
|
||||||
|
FCMToken string `json:"fcm_token"`
|
||||||
|
AppVersion string `json:"app_version"`
|
||||||
|
OsVersion string `json:"os_version"`
|
||||||
|
IPAddress string `json:"ip_address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUserDeviceRequest struct {
|
||||||
|
DeviceName string `json:"device_name"`
|
||||||
|
DeviceType entities.DeviceType `json:"device_type"`
|
||||||
|
Platform entities.DevicePlatform `json:"platform"`
|
||||||
|
FCMToken string `json:"fcm_token"`
|
||||||
|
AppVersion string `json:"app_version"`
|
||||||
|
OsVersion string `json:"os_version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListUserDevicesRequest struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
UserID string `json:"user_id,omitempty"`
|
||||||
|
Platform string `json:"platform,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListUserDevicesResponse struct {
|
||||||
|
Devices []*UserDeviceResponse `json:"devices"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user