Compare commits

..
133 Commits
Author SHA1 Message Date
Efril 4f7e774043 feat(products): sort by 2026-08-11 20:47:38 +07:00
Efril a7c2d6cbb3 feat(category): update list filter category 2026-08-06 21:21:41 +07:00
Efril b9ac97178f feat: profit sharing 2026-08-05 19:28:38 +07:00
Efril 2b80c92caa feat: deployment 2026-07-09 22:43:01 +07:00
Efril f7dd0bd5e8 config: update port 2026-07-09 22:19:53 +07:00
Efril 1533914e4d config: prod and staging 2026-07-09 22:11:03 +07:00
Efril bfce4b865b update purchase date 2026-07-02 12:49:28 +07:00
Efril 581e4a5453 update purchase date 2026-07-02 12:23:12 +07:00
Efril 9b0fc9a63b feat: updat analytic profit loss add purchasing 2026-06-24 00:11:04 +07:00
Efril 793919cf10 feat: add outlet name at analytic response and new overview dashboard 2026-06-23 22:18:16 +07:00
aefril 25024c210a Merge pull request 'fix: product price' (#21) from feature/exclusive-summary into main
Reviewed-on: #21
2026-06-22 06:34:02 +00:00
efrilm 3977370079 fix: product price 2026-06-22 13:33:25 +07:00
Efril 37bcb90ab0 feat: ensure all role 2026-06-19 13:58:36 +07:00
Efril e345aeee97 feat: new users role 2026-06-19 13:31:33 +07:00
Efril 486d94335b Merge branch 'main' of https://gits.altru.id/apksel-dev/apskel-pos-backend 2026-06-18 19:55:04 +07:00
aefril 7d5acb33e8 Merge pull request 'Update profit-loss' (#20) from feature/exclusive-summary into main
Reviewed-on: #20
2026-06-18 12:53:23 +00:00
ryan 2138b44c53 Update profit-loss 2026-06-18 19:51:43 +07:00
Efril 503fb5734f fix: migration 82 2026-06-18 16:34:37 +07:00
aefril ac06a4bbe9 Merge pull request 'feature/exclusive-summary' (#19) from feature/exclusive-summary into main
Reviewed-on: #19
2026-06-18 09:12:13 +00:00
ryan 87540fa1b7 Update exclusive summary 2026-06-18 15:44:15 +07:00
ryan 66d4c9f0af Update purchase order with outlet id 2026-06-18 15:27:20 +07:00
ryan 55119b3e91 Add MTD 2026-06-18 14:19:45 +07:00
aefril 67a5c076e7 Merge pull request 'Make vendor nullable' (#18) from feature/exclusive-summary into main
Reviewed-on: #18
2026-06-18 04:12:08 +00:00
ryan c1d859ebdd Make vendor nullable 2026-06-18 11:04:59 +07:00
aefril 7a2060efdc Merge pull request 'Fix total_amount to use base price' (#17) from feature/exclusive-summary into main
Reviewed-on: #17
2026-06-18 03:18:04 +00:00
ryan 2ad9e2f85f Fix total_amount to use base price 2026-06-18 09:52:16 +07:00
aefril a8d62bc5e8 Merge pull request 'feature/exclusive-summary' (#16) from feature/exclusive-summary into main
Reviewed-on: #16
2026-06-18 01:11:14 +00:00
ryan 8816e4addc Merge branch 'feature/expense-revisi' into feature/exclusive-summary 2026-06-17 18:36:46 +07:00
ryan 2921631ac3 Revert "Revert purchase order"
This reverts commit 657a201fc0.
2026-06-17 18:31:10 +07:00
ryan 0db838e2c4 Fix bank 2026-06-17 18:26:40 +07:00
ryan 4b6cbb69c1 Add exclusive-summary 2026-06-17 18:17:08 +07:00
aefril 9e0ba0ce56 Merge pull request 'feature/exclusive-summary' (#15) from feature/exclusive-summary into main
Reviewed-on: #15
2026-06-17 09:26:24 +00:00
ryan 6c19876a47 Fix issue 2026-06-15 17:44:25 +07:00
ryan b2db56f855 Add excusive-summary endpoint 2026-06-15 17:01:47 +07:00
ryan 8c4d9c69d0 Update analytic to support new categories change 2026-06-15 14:17:48 +07:00
ryan 657a201fc0 Revert purchase order 2026-06-15 13:52:08 +07:00
Efril 7a7ac25dcf fix conflict 2026-06-11 16:47:03 +07:00
Efril d0a548f44e merger 2026-06-11 16:43:53 +07:00
ryan f4172fcea7 Fix purchase analytic by outlet 2026-06-11 16:27:50 +07:00
ryan d5216e7994 Fix purchase analytic zero for group by today 2026-06-11 15:57:19 +07:00
ryan 1718c5adab Fix expense to be nullable without raw material. 2026-06-10 14:25:23 +07:00
ryan d0c090a657 Update purchase analytics 2026-06-10 13:18:49 +07:00
ryan c3db919531 Update expense for product category (non-inventory type) 2026-06-10 12:42:53 +07:00
ryan e09feff36d Update purchase for product category (inventory type) 2026-06-09 15:59:34 +07:00
ryan e7dd9660da Merge remote-tracking branch 'origin' into feature/expense
# Conflicts:
#	internal/router/router.go
2026-06-09 13:25:09 +07:00
Efril c57620beeb feat: update product analytic 2026-06-08 19:32:30 +07:00
ryan 29aeb58fc0 Fix formatting 2026-06-08 12:30:39 +07:00
ryan 69d8c8ce5e Add category table 2026-06-08 12:29:59 +07:00
aefril 6e3fc43d86 Merge pull request 'feat: implement idempotency key for critical API endpoints' (#14) from feature/activity-logs into main
Reviewed-on: #14
2026-06-03 17:51:48 +00:00
Efril 021ec152e9 feat: implement idempotency key for critical API endpoints 2026-06-04 00:49:45 +07:00
Efril ea9dceb333 fix: prevent race condition on order subtotal calculation 2026-06-03 23:59:15 +07:00
Efril afa1aa5b75 Merge branch 'main' of https://gits.altru.id/apksel-dev/apskel-pos-backend 2026-06-03 22:02:14 +07:00
Efril 328336ea5a fix log error and omset tracker scheduled 2026-06-03 22:01:58 +07:00
ryan 094e8b2a47 Add expense analytics 2026-06-03 14:56:27 +07:00
ryan b90a3cde4a Fix coa summary zero 2026-06-03 13:00:50 +07:00
ryan 7c8c7fb7db update expense coa 2026-06-02 16:32:20 +07:00
aefril 343aa25230 Merge pull request 'feature/expense' (#13) from feature/expense into main
Reviewed-on: #13
2026-06-01 17:20:28 +00:00
ryan 47fa21d739 reinstate profit loss overview 2026-06-01 13:13:40 +07:00
ryan dc13bb5f93 update due date and range date 2026-05-29 18:24:14 +07:00
ryan d26f5c5354 add status to expense 2026-05-29 15:44:59 +07:00
ryan 1b7bec4f81 update expense item name 2026-05-29 13:25:38 +07:00
Efril f7399fd0e7 Merge branch 'main' of https://gits.altru.id/apksel-dev/apskel-pos-backend into feature/expense 2026-05-29 12:34:34 +07:00
aefril cd61ad0eb9 Merge pull request 'feature/print-checker' (#12) from feature/print-checker into main
Reviewed-on: #12
2026-05-28 08:31:30 +00:00
Efril 84222fc7f4 update 2026-05-28 15:30:18 +07:00
Efril 23ac572e3f add print_to_checker at product outlet 2026-05-28 13:49:57 +07:00
Efril 66a8126da0 expense filter by outlet and date range 2026-05-28 11:52:16 +07:00
ryan a55a3f4ee2 add expense_name 2026-05-26 15:25:47 +07:00
ryan 024d9ee637 Update profit-loss 2026-05-26 14:59:56 +07:00
Efril 957c1ae53d update order response 2026-05-25 20:28:24 +07:00
ryan b8be29e110 Add item_expense 2026-05-25 16:19:36 +07:00
ryan da87d659df Add expense CRUD 2026-05-25 14:59:40 +07:00
Efril d0378b5ac4 update category 2026-05-21 23:05:25 +07:00
Efril 91960f0e57 categories add outlet id 2026-05-21 21:27:57 +07:00
Efril 72f67cb519 create or update product assign to product outlet 2026-05-21 21:20:54 +07:00
aefril 35c4cf2f2f Merge pull request 'add purchasing in analytics endpoint' (#11) from feature/purchasing into main
Reviewed-on: #11
2026-05-19 15:53:59 +00:00
aefril c9ef90f5ea Merge pull request 'feature/outlet-table' (#10) from feature/outlet-table into main
Reviewed-on: #10
2026-05-19 15:53:16 +00:00
ryan 35e7152abb add purchasing in analytics endpoint 2026-05-19 14:45:26 +07:00
Efril d9b51a7616 update ordedr list 2026-05-14 16:17:28 +07:00
ryan b27e40b531 fix filter order by outlet id 2026-05-14 15:57:46 +07:00
ryan 44aca7641f Revert "add filter order by outlet id"
This reverts commit a89ff00d94.
2026-05-14 15:28:45 +07:00
ryan a89ff00d94 add filter order by outlet id 2026-05-14 15:15:32 +07:00
ryan 227f11359c Revert "add list order by outlet id"
This reverts commit 7a737d7f83.
2026-05-14 14:46:32 +07:00
ryan 7a737d7f83 add list order by outlet id 2026-05-14 14:41:50 +07:00
ryan 312ea94e62 fix scheduler counting void and refund 2026-05-14 14:22:07 +07:00
aefril 6d735c20cb Merge pull request 'fix pointer' (#9) from feature/outlet-table into main
Reviewed-on: #9
2026-05-14 06:54:51 +00:00
Efril cb8a830345 fix pointer 2026-05-14 13:54:15 +07:00
aefril 9c143a43aa Merge pull request 'table and order grouping by outlet' (#8) from feature/outlet-table into main
Reviewed-on: #8
2026-05-13 18:40:34 +00:00
Efril 222cadd8df table and order grouping by outlet 2026-05-14 01:40:17 +07:00
aefril cad4e6c816 Merge pull request 'feature/outlet-table' (#7) from feature/outlet-table into main
Reviewed-on: #7
2026-05-13 18:22:44 +00:00
Efril 50d633ee3a fix products 2026-05-14 01:19:45 +07:00
Efril 21fa21d089 get products all 2026-05-14 00:15:28 +07:00
ryan 5f379faf17 change product list to retrieve its data from product outlets 2026-05-13 23:15:09 +07:00
ryan 3b62504798 fix 2026-05-13 22:30:55 +07:00
ryan 4130cb66df refactor and add outlet product table 2026-05-13 21:58:54 +07:00
aefril 30dff17272 Merge pull request 'self-order+notification' (#6) from self-order+notification into main
Reviewed-on: #6
2026-05-13 07:27:05 +00:00
efrilm fa037b4d2a fix request outlet id at analytic 2026-05-13 14:23:27 +07:00
ryan d38a770ec5 Add omset milestone scheduler with owner role and revenue tracking 2026-05-13 09:48:17 +07:00
ryan 015292e830 Refactor: extract outlet ID filtering to helper method 2026-05-12 21:50:53 +07:00
efrilm f8c732f0ff update dockerfile 2026-05-12 18:46:12 +07:00
aefril e92c487815 Merge pull request 'self-order+notification' (#5) from self-order+notification into main
Reviewed-on: #5
2026-05-12 11:41:03 +00:00
ryan c573b23d76 Add outlet_id to use context or request 2026-05-12 18:32:36 +07:00
Efril f73a5d533c add notif at create order 2026-05-10 23:36:22 +07:00
Efril 4ea8e32a8e Merge branch 'self-order+notification' of https://gits.altru.id/apksel-dev/apskel-pos-backend into self-order+notification 2026-05-10 23:10:03 +07:00
Efril 06d79046d0 fix order and self order response 2026-05-10 23:07:57 +07:00
ryan 8eb19c57ba Change self-order response 2026-05-10 21:34:45 +07:00
Efril f123de7233 Revert "change order response at self order"
This reverts commit 7ba776555e.
2026-05-10 21:31:17 +07:00
Efril 7ba776555e change order response at self order 2026-05-10 21:19:37 +07:00
Efril bccf02b5f7 rename session_id 2026-05-10 19:56:34 +07:00
Efril c24a8a8c13 rename organization_id, add customer_name and order_type at order self order 2026-05-10 19:15:50 +07:00
ryan 6064ef8fde Update QR token generation 2026-05-10 14:52:02 +07:00
Efril 1834dd0b19 update url qrcode table 2026-05-10 14:13:20 +07:00
Efril 9f653eef37 fix migration number 2026-05-10 13:30:40 +07:00
Efril ddaf6df436 migration notification 2026-05-10 12:35:44 +07:00
ryan 0708ce816e Update Redis host 2026-05-10 12:34:36 +07:00
ryan 2c34578a98 Merge remote-tracking branch 'origin/feature/notification' into self-order+notification
# Conflicts:
#	go.mod
#	go.sum
#	internal/app/app.go
#	internal/router/router.go
2026-05-10 12:23:16 +07:00
Efril 9d71b339b5 notification 2026-05-10 10:57:38 +07:00
Efril bbd6666299 user devices 2026-05-10 10:42:09 +07:00
Efril 23b6293502 fcm setup 2026-05-09 12:24:44 +07:00
ryan 07b186c986 Barcode generation with Boombuler 2026-05-09 01:01:25 +07:00
ryan 4cc563f6f1 Add self-order/orders/:sessionId 2026-05-09 00:12:00 +07:00
ryan e7c4681102 Add outlet_id to self-order/categories 2026-05-08 23:56:06 +07:00
ryan f957b07d23 Change self-order/menu from POST to GET 2026-05-08 23:18:52 +07:00
ryan 3c103b7692 Token and session implementation with Redis 2026-05-08 18:41:14 +07:00
ryan fe57aab3b4 Fix categories with table 2026-05-08 14:19:40 +07:00
ryan 3721fb3cd7 List categories for self-order 2026-05-06 11:56:11 +07:00
ryan 2d6df8e4c6 Self Order - BE 2026-05-05 21:07:03 +07:00
aefril 2c76962959 Merge pull request 'ingredient composition' (#2) from feature/ingredient-composition into main
Reviewed-on: #2
2026-04-27 14:22:22 +00:00
Efril a7022dd4c1 ingredient composition 2026-04-27 21:17:12 +07:00
Efril 3542104050 fix sales 2026-04-27 13:24:16 +07:00
Efril eb95459578 fix if order amount 0 2026-04-26 23:40:01 +07:00
Efril 1a5ddd2b34 update 2026-04-26 23:25:54 +07:00
Efril 0aa280462c fix cors 2026-04-26 23:14:11 +07:00
aefril 421475006b Merge pull request 'hpp' (#1) from hpp into main
Reviewed-on: #1
2026-04-26 16:08:02 +00:00
255 changed files with 18344 additions and 1854 deletions
+7
View File
@@ -6,3 +6,10 @@ config/env/*
!.env
vendor
# Firebase service account credentials
infra/firebase-service-account.json
# Config files containing secrets (manage manually on each server)
# infra/production.yaml
# infra/staging.yaml
+1
View File
@@ -0,0 +1 @@
{}
+2 -2
View File
@@ -1,5 +1,5 @@
# 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
WORKDIR /src
COPY go.mod go.sum ./
@@ -13,7 +13,7 @@ RUN apt-get update && apt-get install -y ca-certificates tzdata curl && rm -rf /
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=build /out/app /app
ENV TZ=Asia/Jakarta
EXPOSE 3300
EXPOSE 4000
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -fsS http://localhost:3300/health || exit 1
USER appuser
+33 -7
View File
@@ -1,9 +1,21 @@
#PROJECT_NAME = "enaklo-pos-backend"
DB_USERNAME :=apskel
DB_PASSWORD :=7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk
DB_HOST :=62.72.45.250
DB_PORT :=5433
DB_NAME :=apskel_pos
# ─── Environment (default: staging) ──────────────────────────────────────────
ENV ?= staging
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
@@ -16,15 +28,19 @@ endif
.SILENT: help
help:
@echo
@echo "Usage: make [command]"
@echo "Usage: make [command] [ENV=staging|production]"
@echo
@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
@echo " build-http Build http server"
@echo
@echo " migration-create name={name} Create migration"
@echo " migration-up Up migrations"
@echo " migration-up ENV=production Up migrations (production DB)"
@echo " migration-down Down last migration"
@echo
@echo " docker-up Up docker services"
@@ -83,6 +99,12 @@ migration-up:
migration-down:
@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
seeder-create:
@migrate create -ext sql -dir ./seeders -seq $(name)
@@ -108,7 +130,11 @@ fmt:
@go fmt ./...
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
+7 -2
View File
@@ -12,13 +12,18 @@ func main() {
cfg := config.LoadConfig()
logger.Setup(cfg.LogLevel(), cfg.LogFormat())
db, err := db.NewPostgres(cfg.Database)
pg, err := db.NewPostgres(cfg.Database)
if err != nil {
log.Fatal(err)
}
redisClient, err := db.NewRedisClient(cfg.Redis)
if err != nil {
log.Fatal(err)
}
logger.NonContext.Info("helloworld")
application := app.NewApp(db)
application := app.NewApp(pg, redisClient)
if err := application.Initialize(cfg); err != nil {
log.Fatalf("Failed to initialize application: %v", err)
+8 -1
View File
@@ -12,13 +12,14 @@ import (
const (
YAML_PATH = "infra/%s"
ENV_MODE = "ENV_MODE"
DEFAULT_ENV_MODE = "development"
DEFAULT_ENV_MODE = "staging"
)
var (
validEnvMode = map[string]struct{}{
"local": {},
"development": {},
"staging": {},
"production": {},
}
)
@@ -26,10 +27,12 @@ var (
type Config struct {
Server Server `mapstructure:"server"`
Database Database `mapstructure:"postgresql"`
Redis Redis `mapstructure:"redis"`
Jwt Jwt `mapstructure:"jwt"`
Log Log `mapstructure:"log"`
S3Config S3Config `mapstructure:"s3"`
Fonnte Fonnte `mapstructure:"fonnte"`
FCM FCM `mapstructure:"fcm"`
}
var (
@@ -94,3 +97,7 @@ func (c *Config) LogFormat() string {
func (c *Config) GetFonnte() *Fonnte {
return &c.Fonnte
}
func (c *Config) GetFCM() *FCM {
return &c.FCM
}
+14
View File
@@ -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
}
+55
View File
@@ -0,0 +1,55 @@
package config
import (
"fmt"
"time"
)
type Redis struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
DialTimeout string `mapstructure:"dial_timeout"`
ReadTimeout string `mapstructure:"read_timeout"`
WriteTimeout string `mapstructure:"write_timeout"`
PoolSize int `mapstructure:"pool_size"`
MinIdleConnections int `mapstructure:"min_idle_connections"`
}
func (r Redis) Addr() string {
return fmt.Sprintf("%s:%d", r.Host, r.Port)
}
func (r Redis) ParseDialTimeout() time.Duration {
if r.DialTimeout == "" {
return 5 * time.Second
}
d, err := time.ParseDuration(r.DialTimeout)
if err != nil {
return 5 * time.Second
}
return d
}
func (r Redis) ParseReadTimeout() time.Duration {
if r.ReadTimeout == "" {
return 3 * time.Second
}
d, err := time.ParseDuration(r.ReadTimeout)
if err != nil {
return 3 * time.Second
}
return d
}
func (r Redis) ParseWriteTimeout() time.Duration {
if r.WriteTimeout == "" {
return 3 * time.Second
}
d, err := time.ParseDuration(r.WriteTimeout)
if err != nil {
return 3 * time.Second
}
return d
}
+4 -3
View File
@@ -1,7 +1,8 @@
package config
type Server struct {
Port string `mapstructure:"port"`
BaseUrl string `mapstructure:"common-url"`
LocalUrl string `mapstructure:"local-url"`
Port string `mapstructure:"port"`
BaseUrl string `mapstructure:"common-url"`
LocalUrl string `mapstructure:"local-url"`
SelfOrderUrl string `mapstructure:"self-order-url"`
}
+46 -8
View File
@@ -2,23 +2,61 @@
set -euo pipefail
APP_NAME="apskel-pos"
PORT="3300"
# ─── 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..."
git pull
echo "🐳 Building Docker image (production target)..."
docker build --target production -t $APP_NAME:latest .
echo "🐳 Building Docker image ($ENV_MODE)..."
docker build --target production -t "$IMAGE_NAME" .
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..."
docker run -d --name $APP_NAME \
-p $PORT:$PORT \
docker run -d --name "$CONTAINER_NAME" \
-p "$PORT:4000" \
-e TZ=Asia/Jakarta \
-e ENV_MODE="$ENV_MODE" \
-v "$(pwd)/infra":/infra: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"
+63 -14
View File
@@ -1,28 +1,54 @@
module apskel-pos-be
go 1.21
go 1.24
require (
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.17.0
github.com/google/uuid v1.1.2
github.com/google/uuid v1.6.0
github.com/lib/pq v1.2.0
github.com/spf13/viper v1.16.0
gopkg.in/yaml.v3 v3.0.1
)
require (
cel.dev/expr v0.23.1 // indirect
cloud.google.com/go v0.121.0 // indirect
cloud.google.com/go/auth v0.16.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/firestore v1.18.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
cloud.google.com/go/storage v1.53.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
@@ -31,7 +57,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -39,34 +65,57 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/atomic v1.10.0 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.20.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect
google.golang.org/grpc v1.72.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
firebase.google.com/go/v4 v4.19.0
github.com/aws/aws-sdk-go v1.55.7
github.com/boombuler/barcode v1.1.0
github.com/golang-jwt/jwt/v5 v5.2.3
github.com/redis/go-redis/v9 v9.19.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
github.com/stretchr/testify v1.10.0
go.uber.org/zap v1.21.0
golang.org/x/crypto v0.28.0
golang.org/x/crypto v0.40.0
google.golang.org/api v0.231.0
gorm.io/driver/postgres v1.5.0
gorm.io/gorm v1.30.0
)
+158 -24
View File
@@ -1,3 +1,5 @@
cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -17,14 +19,32 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg=
cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q=
cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -35,18 +55,42 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw=
cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
firebase.google.com/go/v4 v4.19.0 h1:f5NMlC2YHFsncz00c2+ecBr+ZYlRMhKIhj1z8Iz0lD8=
firebase.google.com/go/v4 v4.19.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
@@ -61,6 +105,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -70,7 +116,17 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
@@ -84,6 +140,13 @@ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SU
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -94,6 +157,9 @@ github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -121,6 +187,9 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -132,12 +201,16 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -149,10 +222,17 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -181,8 +261,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -215,17 +295,21 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
@@ -234,10 +318,13 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -247,8 +334,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
@@ -261,17 +349,42 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA=
go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
@@ -289,8 +402,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -361,8 +474,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -372,6 +485,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -385,6 +500,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -428,8 +545,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -441,12 +558,15 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -519,6 +639,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q=
google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -526,6 +648,8 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -562,6 +686,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 h1:IqsN8hx+lWLqlN+Sc3DoMy/watjofWiU8sRFgQ8fhKM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -578,6 +708,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -588,8 +720,10 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -1,6 +1,7 @@
server:
base-url:
local-url:
self-order-url:
port: 4000
jwt:
@@ -8,7 +9,7 @@ jwt:
expires-ttl: 144000
secret: "5Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
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"
customer:
expires-ttl: 7776000
@@ -20,26 +21,41 @@ postgresql:
driver: postgres
db: apskel_pos
username: apskel
password: '7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk'
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: 194.233.78.1
port: 6379
password: "CmICdmnX1EZPhVBYzQPEGw==U"
db: 0
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:'
host_url: "https://sin1.contabostorage.com/fda98c2228f246f29a7e466b86b3b9e7:"
log:
log_format: 'json'
log_level: 'debug'
log_format: "json"
log_level: "info"
fonnte:
api_url: "https://api.fonnte.com/send"
token: "bADQrf9NTXfLZQCK2wGg"
timeout: 30
timeout: 30
fcm:
credentials_file: "infra/firebase-service-account.json"
project_id: "apskel-pos-v2"
+61
View File
@@ -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"
+133 -15
View File
@@ -20,30 +20,54 @@ import (
"apskel-pos-be/internal/service"
"apskel-pos-be/internal/validator"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type App struct {
server *http.Server
db *gorm.DB
router *router.Router
shutdown chan os.Signal
server *http.Server
db *gorm.DB
redisClient *redis.Client
router *router.Router
shutdown chan os.Signal
omsetScheduler *service.OmsetMilestoneScheduler
}
func NewApp(db *gorm.DB) *App {
func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
return &App{
db: db,
shutdown: make(chan os.Signal, 1),
db: db,
redisClient: redisClient,
shutdown: make(chan os.Signal, 1),
}
}
func (a *App) Initialize(cfg *config.Config) error {
repos := a.initRepositories()
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)
validators := a.initValidators()
middleware := a.initMiddleware(services, cfg)
healthHandler := handler.NewHealthHandler()
selfOrderHandler := handler.NewSelfOrderHandler(
services.orderService,
services.categoryService,
services.productService,
repos.tableRepo,
repos.outletRepo,
repos.userRepo,
repos.sessionRepo,
repos.orderRepo,
services.productOutletPriceService,
)
a.router = router.NewRouter(
cfg,
@@ -84,6 +108,8 @@ func (a *App) Initialize(cfg *config.Config) error {
validators.vendorValidator,
services.purchaseOrderService,
validators.purchaseOrderValidator,
services.purchaseCategoryService,
validators.purchaseCategoryValidator,
services.unitConverterService,
validators.unitConverterValidator,
services.chartOfAccountTypeService,
@@ -105,12 +131,27 @@ func (a *App) Initialize(cfg *config.Config) error {
services.customerPointsService,
services.spinGameService,
middleware.customerAuthMiddleware,
services.userDeviceService,
validators.userDeviceValidator,
services.notificationService,
validators.notificationValidator,
services.productOutletPriceService,
validators.productOutletPriceValidator,
selfOrderHandler,
services.expenseService,
validators.expenseValidator,
a.redisClient,
)
return nil
}
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()
a.server = &http.Server{
@@ -146,6 +187,9 @@ func (a *App) Start(port string) error {
}
func (a *App) Shutdown() {
if a.omsetScheduler != nil {
a.omsetScheduler.Stop()
}
close(a.shutdown)
}
@@ -170,9 +214,11 @@ type repositories struct {
tableRepo *repository.TableRepository
unitRepo *repository.UnitRepository
ingredientRepo *repository.IngredientRepository
ingredientCompositionRepo *repository.IngredientCompositionRepository
productRecipeRepo *repository.ProductRecipeRepository
vendorRepo *repository.VendorRepositoryImpl
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
purchaseCategoryRepo *repository.PurchaseCategoryRepositoryImpl
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
@@ -190,7 +236,14 @@ type repositories struct {
customerAuthRepo repository.CustomerAuthRepository
customerPointsRepo repository.CustomerPointsRepository
otpRepo repository.OtpRepository
sessionRepo repository.SessionRepository
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 {
@@ -215,9 +268,11 @@ func (a *App) initRepositories() *repositories {
tableRepo: repository.NewTableRepository(a.db),
unitRepo: repository.NewUnitRepository(a.db),
ingredientRepo: repository.NewIngredientRepository(a.db),
ingredientCompositionRepo: repository.NewIngredientCompositionRepository(a.db),
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
vendorRepo: repository.NewVendorRepositoryImpl(a.db),
purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db),
purchaseCategoryRepo: repository.NewPurchaseCategoryRepositoryImpl(a.db),
unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl),
chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db),
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
@@ -235,7 +290,14 @@ func (a *App) initRepositories() *repositories {
customerAuthRepo: repository.NewCustomerAuthRepository(a.db),
customerPointsRepo: repository.NewCustomerPointsRepository(a.db),
otpRepo: repository.NewOtpRepository(a.db),
sessionRepo: repository.NewSessionRepository(a.redisClient),
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),
}
}
@@ -259,6 +321,7 @@ type processors struct {
productRecipeProcessor *processor.ProductRecipeProcessorImpl
vendorProcessor *processor.VendorProcessorImpl
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
purchaseCategoryProcessor *processor.PurchaseCategoryProcessorImpl
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
@@ -278,6 +341,10 @@ type processors struct {
otpProcessor processor.OtpProcessor
fileClient processor.FileClient
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 {
@@ -292,20 +359,21 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
outletProcessor: processor.NewOutletProcessorImpl(repos.outletRepo),
outletSettingProcessor: processor.NewOutletSettingProcessorImpl(repos.outletSettingRepo, repos.outletRepo),
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),
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),
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo),
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo, repos.expenseRepo),
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo),
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
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.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo),
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
@@ -325,6 +393,10 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
otpProcessor: otpProcessor,
fileClient: fileClient,
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),
}
}
@@ -350,6 +422,7 @@ type services struct {
productRecipeService *service.ProductRecipeServiceImpl
vendorService *service.VendorServiceImpl
purchaseOrderService *service.PurchaseOrderServiceImpl
purchaseCategoryService service.PurchaseCategoryService
unitConverterService *service.IngredientUnitConverterServiceImpl
chartOfAccountTypeService service.ChartOfAccountTypeService
chartOfAccountService service.ChartOfAccountService
@@ -361,11 +434,15 @@ type services struct {
customerAuthService service.CustomerAuthService
customerPointsService service.CustomerPointsService
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 {
authConfig := cfg.Auth()
authService := service.NewAuthService(processors.userProcessor, authConfig)
authService := service.NewAuthService(processors.userProcessor, processors.userDeviceProcessor, authConfig)
organizationService := service.NewOrganizationService(processors.organizationProcessor)
outletService := service.NewOutletService(processors.outletProcessor)
outletSettingService := service.NewOutletSettingService(processors.outletSettingProcessor)
@@ -373,7 +450,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
productService := service.NewProductService(processors.productProcessor)
productVariantService := service.NewProductVariantService(processors.productVariantProcessor)
inventoryService := service.NewInventoryService(processors.inventoryProcessor)
orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, nil, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager) // 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)
fileService := service.NewFileServiceImpl(processors.fileProcessor)
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
@@ -385,6 +462,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor)
vendorService := service.NewVendorService(processors.vendorProcessor)
purchaseOrderService := service.NewPurchaseOrderService(processors.purchaseOrderProcessor)
purchaseCategoryService := service.NewPurchaseCategoryService(processors.purchaseCategoryProcessor)
unitConverterService := service.NewIngredientUnitConverterService(processors.unitConverterProcessor)
chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor)
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
@@ -396,9 +474,11 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
customerAuthService := service.NewCustomerAuthService(processors.customerAuthProcessor)
customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor)
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
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager)
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo, processors.notificationProcessor, repos.userRepo)
return &services{
userService: service.NewUserService(processors.userProcessor),
@@ -422,6 +502,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
productRecipeService: productRecipeService,
vendorService: vendorService,
purchaseOrderService: purchaseOrderService,
purchaseCategoryService: purchaseCategoryService,
unitConverterService: unitConverterService,
chartOfAccountTypeService: chartOfAccountTypeService,
chartOfAccountService: chartOfAccountService,
@@ -433,6 +514,10 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
customerAuthService: customerAuthService,
customerPointsService: customerPointsService,
spinGameService: spinGameService,
userDeviceService: userDeviceService,
notificationService: notificationService,
productOutletPriceService: service.NewProductOutletPriceService(processors.productOutletPriceProcessor),
expenseService: service.NewExpenseService(processors.expenseProcessor),
}
}
@@ -463,6 +548,7 @@ type validators struct {
tableValidator *validator.TableValidator
vendorValidator *validator.VendorValidatorImpl
purchaseOrderValidator *validator.PurchaseOrderValidatorImpl
purchaseCategoryValidator *validator.PurchaseCategoryValidatorImpl
unitConverterValidator *validator.IngredientUnitConverterValidatorImpl
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
@@ -472,6 +558,10 @@ type validators struct {
rewardValidator validator.RewardValidator
campaignValidator validator.CampaignValidator
customerAuthValidator validator.CustomerAuthValidator
userDeviceValidator *validator.UserDeviceValidatorImpl
notificationValidator *validator.NotificationValidatorImpl
productOutletPriceValidator *validator.ProductOutletPriceValidatorImpl
expenseValidator *validator.ExpenseValidatorImpl
}
func (a *App) initValidators() *validators {
@@ -490,6 +580,7 @@ func (a *App) initValidators() *validators {
tableValidator: validator.NewTableValidator(),
vendorValidator: validator.NewVendorValidator(),
purchaseOrderValidator: validator.NewPurchaseOrderValidator(),
purchaseCategoryValidator: validator.NewPurchaseCategoryValidator(),
unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl),
chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl),
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
@@ -499,5 +590,32 @@ func (a *App) initValidators() *validators {
rewardValidator: validator.NewRewardValidator(),
campaignValidator: validator.NewCampaignValidator(),
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,
)
}
+142
View File
@@ -0,0 +1,142 @@
package client
import (
"context"
"fmt"
firebase "firebase.google.com/go/v4"
"firebase.google.com/go/v4/messaging"
"google.golang.org/api/option"
)
type FCMConfig interface {
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 {
messaging *messaging.Client
}
func NewFCMClient(cfg FCMConfig) (FCMClient, error) {
ctx := context.Background()
opt := option.WithCredentialsFile(cfg.GetCredentialsFile())
app, err := firebase.NewApp(ctx, &firebase.Config{
ProjectID: cfg.GetProjectID(),
}, opt)
if err != nil {
return nil, fmt.Errorf("failed to initialize firebase app: %w", err)
}
msgClient, err := app.Messaging(ctx)
if err != nil {
return nil, fmt.Errorf("failed to initialize firebase messaging client: %w", err)
}
return &fcmClient{
messaging: msgClient,
}, nil
}
// SendNotification sends a push notification to a single device token.
func (f *fcmClient) SendNotification(ctx context.Context, token string, title string, body string, data map[string]string) error {
message := &messaging.Message{
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 {
return nil
}
message := &messaging.MulticastMessage{
Tokens: tokens,
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",
},
},
},
}
batchResp, err := f.messaging.SendEachForMulticast(ctx, message)
if err != nil {
return fmt.Errorf("failed to send FCM multicast notification: %w", err)
}
if batchResp.FailureCount > 0 {
return fmt.Errorf("FCM multicast: %d/%d messages failed to send", batchResp.FailureCount, len(tokens))
}
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)
}
return nil
}
+9
View File
@@ -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
)
+19 -12
View File
@@ -40,21 +40,28 @@ const (
OutletServiceEntity = "outlet_service"
VendorServiceEntity = "vendor_service"
PurchaseOrderServiceEntity = "purchase_order_service"
PurchaseCategoryServiceEntity = "purchase_category_service"
IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service"
IngredientCompositionServiceEntity = "ingredient_composition_service"
TableEntity = "table"
// Gamification entities
CustomerPointsEntity = "customer_points"
CustomerTokensEntity = "customer_tokens"
TierEntity = "tier"
GameEntity = "game"
GamePrizeEntity = "game_prize"
GamePlayEntity = "game_play"
OmsetTrackerEntity = "omset_tracker"
RewardEntity = "reward"
CampaignEntity = "campaign"
CampaignRuleEntity = "campaign_rule"
CustomerEntity = "customer"
SpinGameHandlerEntity = "spin_game_handler"
CustomerPointsEntity = "customer_points"
CustomerTokensEntity = "customer_tokens"
TierEntity = "tier"
GameEntity = "game"
GamePrizeEntity = "game_prize"
GamePlayEntity = "game_play"
OmsetTrackerEntity = "omset_tracker"
RewardEntity = "reward"
CampaignEntity = "campaign"
CampaignRuleEntity = "campaign_rule"
CustomerEntity = "customer"
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{
+28
View File
@@ -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
}
+8 -4
View File
@@ -3,10 +3,12 @@ package constants
type UserRole string
const (
RoleAdmin UserRole = "admin"
RoleManager UserRole = "manager"
RoleCashier UserRole = "cashier"
RoleWaiter UserRole = "waiter"
RoleAdmin UserRole = "admin"
RoleManager UserRole = "manager"
RoleCashier UserRole = "cashier"
RoleWaiter UserRole = "waiter"
RoleOwner UserRole = "owner"
RolePurchasing UserRole = "purchasing"
)
func GetAllUserRoles() []UserRole {
@@ -15,6 +17,8 @@ func GetAllUserRoles() []UserRole {
RoleManager,
RoleCashier,
RoleWaiter,
RoleOwner,
RolePurchasing,
}
}
+21 -21
View File
@@ -5,12 +5,12 @@ import (
)
type CreateAccountRequest struct {
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
Name string `json:"name" validate:"required,min=1,max=255"`
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"`
OpeningBalance float64 `json:"opening_balance"`
Description *string `json:"description"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
Name string `json:"name" validate:"required,min=1,max=255"`
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"`
OpeningBalance float64 `json:"opening_balance"`
Description *string `json:"description"`
}
type UpdateAccountRequest struct {
@@ -24,21 +24,21 @@ type UpdateAccountRequest struct {
}
type AccountResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
Name string `json:"name"`
Number string `json:"number"`
AccountType string `json:"account_type"`
OpeningBalance float64 `json:"opening_balance"`
CurrentBalance float64 `json:"current_balance"`
Description *string `json:"description"`
IsActive bool `json:"is_active"`
IsSystem bool `json:"is_system"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ChartOfAccount *ChartOfAccountResponse `json:"chart_of_account,omitempty"`
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
Name string `json:"name"`
Number string `json:"number"`
AccountType string `json:"account_type"`
OpeningBalance float64 `json:"opening_balance"`
CurrentBalance float64 `json:"current_balance"`
Description *string `json:"description"`
IsActive bool `json:"is_active"`
IsSystem bool `json:"is_system"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ChartOfAccount *ChartOfAccountResponse `json:"chart_of_account,omitempty"`
}
type ListAccountsRequest struct {
+398 -42
View File
@@ -7,17 +7,18 @@ import (
)
type PaymentMethodAnalyticsRequest struct {
OrganizationID uuid.UUID `form:"organization_id"`
OutletID *uuid.UUID `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"`
OrganizationID uuid.UUID `form:"organization_id"`
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"`
}
// PaymentMethodAnalyticsResponse represents the response for payment method analytics
type PaymentMethodAnalyticsResponse 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"`
GroupBy string `json:"group_by"`
@@ -45,15 +46,16 @@ type PaymentMethodAnalyticsData struct {
type SalesAnalyticsRequest struct {
OrganizationID uuid.UUID
OutletID *uuid.UUID `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"`
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 SalesAnalyticsResponse 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"`
GroupBy string `json:"group_by"`
@@ -83,19 +85,85 @@ type SalesAnalyticsData struct {
NetSales float64 `json:"net_sales"`
}
type PurchasingAnalyticsRequest struct {
OrganizationID uuid.UUID
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 PurchasingAnalyticsResponse 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"`
GroupBy string `json:"group_by"`
Summary PurchasingSummary `json:"summary"`
Data []PurchasingAnalyticsData `json:"data"`
IngredientData []PurchasingIngredientData `json:"ingredient_data"`
VendorData []PurchasingVendorData `json:"vendor_data"`
}
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"`
}
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
type ProductAnalyticsRequest struct {
OrganizationID uuid.UUID
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" validate:"required"`
Limit int `form:"limit,default=1000" validate:"min=1,max=1000"`
OutletID *string `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" validate:"required"`
Limit int `form:"limit,default=1000" validate:"min=1,max=1000"`
}
// ProductAnalyticsResponse represents the response for product analytics
type ProductAnalyticsResponse 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 []ProductAnalyticsData `json:"data"`
@@ -105,6 +173,7 @@ type ProductAnalyticsData struct {
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
ProductSku string `json:"product_sku"`
ProductPrice float64 `json:"product_price"`
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
CategoryOrder int `json:"category_order"`
@@ -123,15 +192,16 @@ type ProductAnalyticsData struct {
// ProductAnalyticsPerCategoryRequest represents the request for product analytics per category
type ProductAnalyticsPerCategoryRequest struct {
OrganizationID uuid.UUID
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" validate:"required"`
OutletID *string `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" validate:"required"`
}
// ProductAnalyticsPerCategoryResponse represents the response for product analytics per category
type ProductAnalyticsPerCategoryResponse 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 []ProductAnalyticsPerCategoryData `json:"data"`
@@ -149,18 +219,148 @@ type ProductAnalyticsPerCategoryData struct {
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
type DashboardAnalyticsRequest struct {
OrganizationID uuid.UUID
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" validate:"required"`
OutletID *string `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" validate:"required"`
}
// DashboardAnalyticsResponse represents the response for dashboard analytics
type DashboardAnalyticsResponse 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"`
Overview DashboardOverview `json:"overview"`
@@ -171,36 +371,58 @@ type DashboardAnalyticsResponse struct {
// DashboardOverview represents the overview data for dashboard
type DashboardOverview struct {
TotalSales float64 `json:"total_sales"`
TotalOrders int64 `json:"total_orders"`
AverageOrderValue float64 `json:"average_order_value"`
TotalCustomers int64 `json:"total_customers"`
VoidedOrders int64 `json:"voided_orders"`
RefundedOrders int64 `json:"refunded_orders"`
TotalSales float64 `json:"total_sales"`
TotalOrders int64 `json:"total_orders"`
AverageOrderValue float64 `json:"average_order_value"`
TotalCustomers int64 `json:"total_customers"`
VoidedOrders int64 `json:"voided_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 {
OrganizationID uuid.UUID
OutletID *uuid.UUID `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"`
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"`
}
// ProfitLossAnalyticsResponse represents the response for profit and loss analytics
type ProfitLossAnalyticsResponse 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 ProfitLossSummary `json:"summary"`
Data []ProfitLossData `json:"data"`
ProductData []ProductProfitData `json:"product_data"`
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"`
GroupBy string `json:"group_by"`
Summary ProfitLossSummary `json:"summary"`
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 {
TotalRevenue float64 `json:"total_revenue"`
TotalCost float64 `json:"total_cost"`
@@ -215,7 +437,6 @@ type ProfitLossSummary struct {
ProfitabilityRatio float64 `json:"profitability_ratio"`
}
// ProfitLossData represents individual profit and loss data point by time period
type ProfitLossData struct {
Date time.Time `json:"date"`
Revenue float64 `json:"revenue"`
@@ -229,7 +450,6 @@ type ProfitLossData struct {
Orders int64 `json:"orders"`
}
// ProductProfitData represents profit data for individual products
type ProductProfitData struct {
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
@@ -244,3 +464,139 @@ type ProductProfitData struct {
AverageCost float64 `json:"average_cost"`
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"`
}
+13 -3
View File
@@ -10,7 +10,9 @@ type CreateCategoryRequest struct {
Name string `json:"name" validate:"required,min=1,max=255"`
Description *string `json:"description,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"`
}
@@ -18,12 +20,17 @@ type UpdateCategoryRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Description *string `json:"description,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"`
}
type ListCategoriesRequest struct {
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"`
Search string `json:"search,omitempty"`
Page int `json:"page" validate:"required,min=1"`
@@ -34,10 +41,13 @@ type ListCategoriesRequest struct {
type CategoryResponse struct {
ID uuid.UUID `json:"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"`
Description *string `json:"description"`
BusinessType string `json:"business_type"`
Order int `json:"order"`
Order int `json:"order"`
Metadata map[string]interface{} `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
+161
View File
@@ -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"`
}
@@ -1,16 +0,0 @@
package contract
import (
"apskel-pos-be/internal/models"
"github.com/google/uuid"
)
type IngredientCompositionContract interface {
Create(request *models.CreateIngredientCompositionRequest, organizationID uuid.UUID) (*models.IngredientCompositionResponse, error)
GetByID(id uuid.UUID, organizationID uuid.UUID) (*models.IngredientCompositionResponse, error)
GetByParentIngredientID(parentIngredientID uuid.UUID, organizationID uuid.UUID) ([]*models.IngredientCompositionResponse, error)
GetByChildIngredientID(childIngredientID uuid.UUID, organizationID uuid.UUID) ([]*models.IngredientCompositionResponse, error)
Update(id uuid.UUID, request *models.UpdateIngredientCompositionRequest, organizationID uuid.UUID) (*models.IngredientCompositionResponse, error)
Delete(id uuid.UUID, organizationID uuid.UUID) error
}
@@ -81,4 +81,3 @@ type IngredientUnitsResponse struct {
BaseUnitName string `json:"base_unit_name"`
Units []*UnitResponse `json:"units"`
}
+6 -6
View File
@@ -26,9 +26,9 @@ type AdjustInventoryRequest 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"`
Reason string `json:"reason" validate:"required,min=1,max=255"`
Reason string `json:"reason" validate:"required,min=1,max=255"`
}
type RestockItem struct {
@@ -82,10 +82,10 @@ type InventoryAdjustmentResponse struct {
}
type RestockInventoryResponse struct {
OutletID uuid.UUID `json:"outlet_id"`
Items []RestockItemResult `json:"items"`
Reason string `json:"reason"`
RestockedAt time.Time `json:"restocked_at"`
OutletID uuid.UUID `json:"outlet_id"`
Items []RestockItemResult `json:"items"`
Reason string `json:"reason"`
RestockedAt time.Time `json:"restocked_at"`
}
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"`
}
+3
View File
@@ -98,6 +98,8 @@ type OrderItemResponse struct {
ProductName string `json:"product_name"`
ProductVariantID *uuid.UUID `json:"product_variant_id"`
ProductVariantName *string `json:"product_variant_name,omitempty"`
CategoryID *uuid.UUID `json:"category_id,omitempty"`
CategoryName *string `json:"category_name,omitempty"`
Quantity int `json:"quantity"`
UnitPrice float64 `json:"unit_price"`
TotalPrice float64 `json:"total_price"`
@@ -108,6 +110,7 @@ type OrderItemResponse struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PrinterType string `json:"printer_type"`
PrintToChecker bool `json:"print_to_checker"`
PaidQuantity int `json:"paid_quantity"`
}
+41 -34
View File
@@ -8,6 +8,7 @@ import (
type CreateProductRequest struct {
CategoryID uuid.UUID `json:"category_id" validate:"required"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
SKU *string `json:"sku,omitempty"`
Name string `json:"name" validate:"required,min=1,max=255"`
Description *string `json:"description,omitempty"`
@@ -16,28 +17,30 @@ type CreateProductRequest struct {
BusinessType *string `json:"business_type,omitempty"`
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
PrintToChecker *bool `json:"print_to_checker,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
Variants []CreateProductVariantRequest `json:"variants,omitempty"`
InitialStock *int `json:"initial_stock,omitempty" validate:"omitempty,min=0"` // Initial stock quantity for all outlets
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` // Reorder level for all outlets
CreateInventory bool `json:"create_inventory,omitempty"` // Whether to create inventory records for all outlets
InitialStock *int `json:"initial_stock,omitempty" validate:"omitempty,min=0"`
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"`
CreateInventory bool `json:"create_inventory,omitempty"`
}
type UpdateProductRequest struct {
CategoryID *uuid.UUID `json:"category_id,omitempty"`
SKU *string `json:"sku,omitempty"`
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Description *string `json:"description,omitempty"`
Price *float64 `json:"price,omitempty" validate:"omitempty,min=0"`
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
BusinessType *string `json:"business_type,omitempty"`
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
// Stock management fields
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` // Update reorder level for all existing inventory records
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
CategoryID *uuid.UUID `json:"category_id,omitempty"`
SKU *string `json:"sku,omitempty"`
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Description *string `json:"description,omitempty"`
Price *float64 `json:"price,omitempty" validate:"omitempty,min=0"`
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
BusinessType *string `json:"business_type,omitempty"`
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
PrintToChecker *bool `json:"print_to_checker,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"`
}
type CreateProductVariantRequest struct {
@@ -56,24 +59,27 @@ type UpdateProductVariantRequest struct {
}
type ProductResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
SKU *string `json:"sku"`
Name string `json:"name"`
Description *string `json:"description"`
Price float64 `json:"price"`
Cost float64 `json:"cost"`
BusinessType string `json:"business_type"`
ImageURL *string `json:"image_url"`
PrinterType string `json:"printer_type"`
Metadata map[string]interface{} `json:"metadata"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Category *CategoryResponse `json:"category,omitempty"`
Variants []ProductVariantResponse `json:"variants,omitempty"`
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
SKU *string `json:"sku"`
Name string `json:"name"`
Description *string `json:"description"`
Price float64 `json:"price"`
OutletPrice *float64 `json:"outlet_price,omitempty"`
OutletPrices []ProductOutletPriceResponse `json:"outlet_prices,omitempty"`
Cost float64 `json:"cost"`
BusinessType string `json:"business_type"`
ImageURL *string `json:"image_url"`
PrinterType string `json:"printer_type"`
PrintToChecker bool `json:"print_to_checker"`
Metadata map[string]interface{} `json:"metadata"`
IsActive bool `json:"is_active"`
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 {
@@ -89,6 +95,7 @@ type ProductVariantResponse struct {
type ListProductsRequest struct {
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
CategoryID *uuid.UUID `json:"category_id,omitempty"`
BusinessType string `json:"business_type,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"`
}
+25 -25
View File
@@ -34,34 +34,34 @@ type BulkCreateProductRecipeRequest struct {
// Response structures
type ProductRecipeResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
ProductID uuid.UUID `json:"product_id"`
VariantID *uuid.UUID `json:"variant_id"`
IngredientID uuid.UUID `json:"ingredient_id"`
Quantity float64 `json:"quantity"`
WastePercentage float64 `json:"waste_percentage"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Product *ProductResponse `json:"product,omitempty"`
ProductVariant *ProductVariantResponse `json:"product_variant,omitempty"`
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
ProductID uuid.UUID `json:"product_id"`
VariantID *uuid.UUID `json:"variant_id"`
IngredientID uuid.UUID `json:"ingredient_id"`
Quantity float64 `json:"quantity"`
WastePercentage float64 `json:"waste_percentage"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Product *ProductResponse `json:"product,omitempty"`
ProductVariant *ProductVariantResponse `json:"product_variant,omitempty"`
Ingredient *ProductRecipeIngredientResponse `json:"ingredient,omitempty"`
}
type ProductRecipeIngredientResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
Name string `json:"name"`
UnitID uuid.UUID `json:"unit_id"`
Cost float64 `json:"cost"`
Stock float64 `json:"stock"`
IsSemiFinished bool `json:"is_semi_finished"`
IsActive bool `json:"is_active"`
Metadata map[string]interface{} `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
Name string `json:"name"`
UnitID uuid.UUID `json:"unit_id"`
Cost float64 `json:"cost"`
Stock float64 `json:"stock"`
IsSemiFinished bool `json:"is_semi_finished"`
IsActive bool `json:"is_active"`
Metadata map[string]interface{} `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Unit *ProductRecipeUnitResponse `json:"unit,omitempty"`
}
@@ -71,4 +71,4 @@ type ProductRecipeUnitResponse struct {
Symbol string `json:"symbol"`
CreatedAt time.Time `json:"created_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"`
}
+33 -28
View File
@@ -7,10 +7,10 @@ import (
)
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"`
TransactionDate string `json:"transaction_date" validate:"required"` // Format: YYYY-MM-DD
DueDate string `json:"due_date" validate:"required"` // Format: YYYY-MM-DD
TransactionDate string `json:"transaction_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"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"`
Message *string `json:"message,omitempty" validate:"omitempty"`
@@ -19,18 +19,19 @@ type CreatePurchaseOrderRequest struct {
}
type CreatePurchaseOrderItemRequest struct {
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
Description *string `json:"description,omitempty" validate:"omitempty"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
UnitID uuid.UUID `json:"unit_id" validate:"required"`
Amount float64 `json:"amount" validate:"required,gte=0"`
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
PurchaseCategoryID uuid.UUID `json:"purchase_category_id" validate:"required"`
Description *string `json:"description,omitempty" validate:"omitempty"`
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
Amount float64 `json:"amount" validate:"required,gte=0"`
}
type UpdatePurchaseOrderRequest struct {
VendorID *uuid.UUID `json:"vendor_id,omitempty" validate:"omitempty"`
PONumber *string `json:"po_number,omitempty" validate:"omitempty,min=1,max=50"`
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"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"`
Message *string `json:"message,omitempty" validate:"omitempty"`
@@ -39,21 +40,23 @@ type UpdatePurchaseOrderRequest struct {
}
type UpdatePurchaseOrderItemRequest struct {
ID *uuid.UUID `json:"id,omitempty"` // For existing items
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
Description *string `json:"description,omitempty" validate:"omitempty"`
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
ID *uuid.UUID `json:"id,omitempty"` // For existing items
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty" validate:"omitempty"`
Description *string `json:"description,omitempty" validate:"omitempty"`
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
}
type PurchaseOrderResponse struct {
ID uuid.UUID `json:"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"`
TransactionDate time.Time `json:"transaction_date"`
DueDate time.Time `json:"due_date"`
DueDate *time.Time `json:"due_date"`
Reference *string `json:"reference"`
Status string `json:"status"`
Message *string `json:"message"`
@@ -66,17 +69,19 @@ type PurchaseOrderResponse struct {
}
type PurchaseOrderItemResponse struct {
ID uuid.UUID `json:"id"`
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
IngredientID uuid.UUID `json:"ingredient_id"`
Description *string `json:"description"`
Quantity float64 `json:"quantity"`
UnitID uuid.UUID `json:"unit_id"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
Unit *UnitResponse `json:"unit,omitempty"`
ID uuid.UUID `json:"id"`
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
IngredientID *uuid.UUID `json:"ingredient_id"`
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
Description *string `json:"description"`
Quantity *float64 `json:"quantity"`
UnitID *uuid.UUID `json:"unit_id"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
Unit *UnitResponse `json:"unit,omitempty"`
}
type PurchaseOrderAttachmentResponse struct {
+82
View File
@@ -0,0 +1,82 @@
package contract
import (
"github.com/google/uuid"
)
type SelfOrderTableTokenResponse struct {
SessionID string `json:"session_id"`
TableID string `json:"table_id"`
OrganizationID string `json:"organization_id"`
OutletID string `json:"outlet_id"`
TableName string `json:"table_name"`
OutletName string `json:"outlet_name"`
Status string `json:"status"`
}
type SelfOrderMenuRequest struct {
SessionID string `form:"session_id" validate:"required"`
}
type SelfOrderMenuResponse struct {
OutletName string `json:"outlet_name"`
TableName string `json:"table_name"`
Categories []SelfOrderMenuCategory `json:"categories"`
}
type SelfOrderMenuCategory struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Order int `json:"order"`
Products []SelfOrderMenuItem `json:"products"`
}
type SelfOrderMenuItem struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Price float64 `json:"price"`
ImageURL *string `json:"image_url,omitempty"`
Variants []SelfOrderMenuVariant `json:"variants,omitempty"`
}
type SelfOrderMenuVariant struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
PriceModifier float64 `json:"price_modifier"`
}
type SelfOrderCreateOrderRequest struct {
SessionID string `json:"session_id" validate:"required"`
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 {
ProductID uuid.UUID `json:"product_id" validate:"required"`
ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"`
Quantity int `json:"quantity" validate:"required,min=1"`
Notes *string `json:"notes,omitempty"`
}
type SelfOrderListCategoriesRequest struct {
OrganizationID string `form:"organization_id" validate:"required"`
OutletID string `form:"outlet_id" validate:"required"`
}
type SelfOrderCategoryItem struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Order int `json:"order"`
}
type SelfOrderListCategoriesResponse struct {
Categories []SelfOrderCategoryItem `json:"categories"`
}
type SelfOrderListOrdersResponse struct {
Orders []OrderResponse `json:"orders"`
}
+16 -9
View File
@@ -12,14 +12,14 @@ type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=1,max=255"`
Email string `json:"email" validate:"required,email"`
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"`
}
type UpdateUserRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
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"`
IsActive *bool `json:"is_active,omitempty"`
Permissions *map[string]interface{} `json:"permissions,omitempty"`
@@ -35,16 +35,23 @@ type UpdateUserOutletRequest struct {
}
type LoginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
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 {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt time.Time `json:"expires_at"`
RefreshExpiresAt time.Time `json:"refresh_expires_at"`
User UserResponse `json:"user"`
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt time.Time `json:"expires_at"`
RefreshExpiresAt time.Time `json:"refresh_expires_at"`
User UserResponse `json:"user"`
}
type UserResponse struct {
+59
View File
@@ -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"`
}
+30
View File
@@ -0,0 +1,30 @@
package db
import (
"apskel-pos-be/config"
"fmt"
"github.com/redis/go-redis/v9"
)
func NewRedisClient(c config.Redis) (*redis.Client, error) {
opts := &redis.Options{
Addr: c.Addr(),
Password: c.Password,
DB: c.DB,
DialTimeout: c.ParseDialTimeout(),
ReadTimeout: c.ParseReadTimeout(),
WriteTimeout: c.ParseWriteTimeout(),
}
if c.PoolSize > 0 {
opts.PoolSize = c.PoolSize
}
if c.MinIdleConnections > 0 {
opts.MinIdleConns = c.MinIdleConnections
}
client := redis.NewClient(opts)
fmt.Println("Successfully connected to Redis")
return client, nil
}
+196 -40
View File
@@ -27,10 +27,64 @@ type SalesAnalytics struct {
NetSales float64 `json:"net_sales"`
}
// 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"`
}
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"`
}
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 {
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
ProductSku string `json:"product_sku"`
ProductPrice float64 `json:"product_price"`
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
CategoryOrder int `json:"category_order"`
@@ -58,6 +112,39 @@ type ProductAnalyticsPerCategory struct {
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
type DashboardOverview struct {
TotalSales float64 `json:"total_sales"`
@@ -66,56 +153,125 @@ type DashboardOverview struct {
TotalCustomers int64 `json:"total_customers"`
VoidedOrders int64 `json:"voided_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 {
Summary ProfitLossSummary `json:"summary"`
Data []ProfitLossData `json:"data"`
ProductData []ProductProfitData `json:"product_data"`
Summary ProfitLossSummary
Data []ProfitLossData
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 {
TotalRevenue float64 `json:"total_revenue"`
TotalCost float64 `json:"total_cost"`
GrossProfit float64 `json:"gross_profit"`
GrossProfitMargin float64 `json:"gross_profit_margin"`
TotalTax float64 `json:"total_tax"`
TotalDiscount float64 `json:"total_discount"`
NetProfit float64 `json:"net_profit"`
NetProfitMargin float64 `json:"net_profit_margin"`
TotalOrders int64 `json:"total_orders"`
AverageProfit float64 `json:"average_profit"`
ProfitabilityRatio float64 `json:"profitability_ratio"`
TotalRevenue float64
TotalCost float64
GrossProfit float64
GrossProfitMargin float64
TotalTax float64
TotalDiscount float64
NetProfit float64
NetProfitMargin float64
TotalOrders int64
AverageProfit float64
ProfitabilityRatio float64
}
// ProfitLossData represents profit and loss data by time period
type ProfitLossData struct {
Date time.Time `json:"date"`
Revenue float64 `json:"revenue"`
Cost float64 `json:"cost"`
GrossProfit float64 `json:"gross_profit"`
GrossProfitMargin float64 `json:"gross_profit_margin"`
Tax float64 `json:"tax"`
Discount float64 `json:"discount"`
NetProfit float64 `json:"net_profit"`
NetProfitMargin float64 `json:"net_profit_margin"`
Orders int64 `json:"orders"`
Date time.Time
Revenue float64
Cost float64
GrossProfit float64
GrossProfitMargin float64
Tax float64
Discount float64
NetProfit float64
NetProfitMargin float64
Orders int64
}
// ProductProfitData represents profit data for individual products
type ProductProfitData struct {
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
QuantitySold int64 `json:"quantity_sold"`
Revenue float64 `json:"revenue"`
Cost float64 `json:"cost"`
GrossProfit float64 `json:"gross_profit"`
GrossProfitMargin float64 `json:"gross_profit_margin"`
AveragePrice float64 `json:"average_price"`
AverageCost float64 `json:"average_cost"`
ProfitPerUnit float64 `json:"profit_per_unit"`
ProductID uuid.UUID
ProductName string
CategoryID uuid.UUID
CategoryName string
QuantitySold int64
Revenue float64
Cost float64
GrossProfit float64
GrossProfitMargin float64
AveragePrice float64
AverageCost float64
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
}
+12 -9
View File
@@ -31,15 +31,18 @@ func (m *Metadata) Scan(value interface{}) error {
}
type Category 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" validate:"required"`
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
Description *string `gorm:"type:text" json:"description"`
Order int `gorm:"default:0" json:"order"`
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"`
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"`
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"`
Parent *Category `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
Description *string `gorm:"type:text" json:"description"`
Order int `gorm:"default:0" json:"order"`
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"`
Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"`
+7
View File
@@ -36,6 +36,13 @@ func GetAllEntities() []interface{} {
&CampaignRule{},
&OtpSession{},
// Analytics entities are not database tables, they are query results
&UserDevice{},
// Notification entities
&Notification{},
&NotificationReceiver{},
&NotificationDelivery{},
&ProductOutletPrice{},
&Expense{},
}
}
+90
View File
@@ -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"
}
+36
View File
@@ -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"
}
+14 -13
View File
@@ -7,17 +7,18 @@ import (
)
type Ingredient 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;index" json:"outlet_id"`
Name string `gorm:"not null;size:255" json:"name"`
UnitID uuid.UUID `gorm:"type:uuid;not null;index" json:"unit_id"`
Cost float64 `gorm:"type:decimal(10,2);default:0.00" json:"cost"`
Stock float64 `gorm:"type:decimal(10,2);default:0.00" json:"stock"`
IsSemiFinished bool `gorm:"default:false" json:"is_semi_finished"`
IsActive bool `gorm:"default:true" json:"is_active"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Unit *Unit `gorm:"foreignKey:UnitID;references:ID" json:"unit,omitempty"`
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;index" json:"outlet_id"`
Name string `gorm:"not null;size:255" json:"name"`
UnitID uuid.UUID `gorm:"type:uuid;not null;index" json:"unit_id"`
Cost float64 `gorm:"type:decimal(10,2);default:0.00" json:"cost"`
Stock float64 `gorm:"type:decimal(10,2);default:0.00" json:"stock"`
IsSemiFinished bool `gorm:"default:false" json:"is_semi_finished"`
IsActive bool `gorm:"default:true" json:"is_active"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Unit *Unit `gorm:"foreignKey:UnitID;references:ID" json:"unit,omitempty"`
Compositions []IngredientComposition `gorm:"foreignKey:ParentIngredientID;references:ID" json:"compositions,omitempty"`
}
+10 -10
View File
@@ -7,14 +7,14 @@ import (
)
type IngredientComposition struct {
ID uuid.UUID `json:"id" db:"id"`
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
ParentIngredientID uuid.UUID `json:"parent_ingredient_id" db:"parent_ingredient_id"`
ChildIngredientID uuid.UUID `json:"child_ingredient_id" db:"child_ingredient_id"`
Quantity float64 `json:"quantity" db:"quantity"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
ParentIngredient *Ingredient `json:"parent_ingredient,omitempty"`
ChildIngredient *Ingredient `json:"child_ingredient,omitempty"`
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;index" json:"outlet_id"`
ParentIngredientID uuid.UUID `gorm:"type:uuid;not null;index" json:"parent_ingredient_id"`
ChildIngredientID uuid.UUID `gorm:"type:uuid;not null;index" json:"child_ingredient_id"`
Quantity float64 `gorm:"type:decimal(10,4);not null" json:"quantity"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
ParentIngredient *Ingredient `gorm:"foreignKey:ParentIngredientID;references:ID" json:"parent_ingredient,omitempty"`
ChildIngredient *Ingredient `gorm:"foreignKey:ChildIngredientID;references:ID" json:"child_ingredient,omitempty"`
}
@@ -39,4 +39,3 @@ func (iuc *IngredientUnitConverter) BeforeCreate() error {
}
return nil
}
+29 -27
View File
@@ -36,34 +36,36 @@ const (
)
type InventoryMovement 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" 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"`
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"`
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
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"`
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
Reason *string `gorm:"size:255" json:"reason"`
Notes *string `gorm:"type:text" json:"notes"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
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"`
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"`
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"`
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
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"`
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
PurchaseOrderItemID *uuid.UUID `gorm:"type:uuid;index" json:"purchase_order_item_id"`
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
Reason *string `gorm:"size:255" json:"reason"`
Notes *string `gorm:"type:text" json:"notes"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
PurchaseOrderItem *PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderItemID" json:"purchase_order_item,omitempty"`
Order *Order `gorm:"foreignKey:OrderID" json:"order,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 {
+150
View File
@@ -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"`
// Relations
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"`
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"`
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
ProductVariant *ProductVariant `gorm:"foreignKey:ProductVariantID" json:"product_variant,omitempty"`
Ingredient Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
CreatedByUser User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"`
Ingredient Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
CreatedByUser User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"`
}
func (oit *OrderIngredientTransaction) BeforeCreate(tx *gorm.DB) error {
+8 -7
View File
@@ -26,13 +26,14 @@ type Product struct {
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
ProductRecipes []ProductRecipe `gorm:"foreignKey:ProductID" json:"product_recipes,omitempty"`
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"`
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
ProductRecipes []ProductRecipe `gorm:"foreignKey:ProductID" json:"product_recipes,omitempty"`
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,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 {
+9 -9
View File
@@ -7,15 +7,15 @@ import (
)
type ProductIngredient struct {
ID uuid.UUID `json:"id" db:"id"`
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
ProductID uuid.UUID `json:"product_id" db:"product_id"`
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
Quantity float64 `json:"quantity" db:"quantity"`
WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
ID uuid.UUID `json:"id" db:"id"`
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
ProductID uuid.UUID `json:"product_id" db:"product_id"`
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
Quantity float64 `json:"quantity" db:"quantity"`
WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
// Relations
Product *Product `json:"product,omitempty"`
+32
View File
@@ -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"
}
+1 -1
View File
@@ -34,4 +34,4 @@ func (pr *ProductRecipe) BeforeCreate(tx *gorm.DB) error {
func (ProductRecipe) TableName() string {
return "product_recipes"
}
}
+71
View File
@@ -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"
}
+28 -24
View File
@@ -9,20 +9,22 @@ import (
)
type PurchaseOrder struct {
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"`
VendorID uuid.UUID `gorm:"type:uuid;not null" json:"vendor_id" validate:"required"`
PONumber string `gorm:"not null;size:50" json:"po_number" validate:"required,min=1,max=50"`
TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date" validate:"required"`
DueDate time.Time `gorm:"type:date;not null" json:"due_date" validate:"required"`
Reference *string `gorm:"size:100" json:"reference" validate:"omitempty,max=100"`
Status string `gorm:"not null;size:20;default:'draft'" json:"status" validate:"required,oneof=draft sent approved received cancelled"`
Message *string `gorm:"type:text" json:"message" validate:"omitempty"`
TotalAmount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"total_amount"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
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"`
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id" validate:"omitempty"`
VendorID *uuid.UUID `gorm:"type:uuid" json:"vendor_id" validate:"omitempty"`
PONumber string `gorm:"not null;size:50" json:"po_number" validate:"required,min=1,max=50"`
TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date" validate:"required"`
DueDate *time.Time `gorm:"type:date" json:"due_date" validate:"omitempty"`
Reference *string `gorm:"size:100" json:"reference" validate:"omitempty,max=100"`
Status string `gorm:"not null;size:20;default:'draft'" json:"status" validate:"required,oneof=draft sent approved received cancelled"`
Message *string `gorm:"type:text" json:"message" validate:"omitempty"`
TotalAmount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"total_amount"`
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"`
Vendor *Vendor `gorm:"foreignKey:VendorID" json:"vendor,omitempty"`
Items []PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderID" json:"items,omitempty"`
Attachments []PurchaseOrderAttachment `gorm:"foreignKey:PurchaseOrderID" json:"attachments,omitempty"`
@@ -41,19 +43,21 @@ func (PurchaseOrder) TableName() string {
}
type PurchaseOrderItem struct {
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"`
IngredientID uuid.UUID `gorm:"type:uuid;not null" json:"ingredient_id" validate:"required"`
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
Quantity float64 `gorm:"type:decimal(10,3);not null" json:"quantity" validate:"required,gt=0"`
UnitID uuid.UUID `gorm:"type:uuid;not null" json:"unit_id" validate:"required"`
Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
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"`
IngredientID *uuid.UUID `gorm:"type:uuid" json:"ingredient_id" validate:"omitempty"`
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id" validate:"required"`
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
Quantity *float64 `gorm:"type:decimal(10,3)" json:"quantity" validate:"omitempty,gt=0"`
UnitID *uuid.UUID `gorm:"type:uuid" json:"unit_id" validate:"omitempty"`
Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"`
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"`
Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"`
Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,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 {
+5
View File
@@ -1,6 +1,7 @@
package entities
import (
"apskel-pos-be/internal/pkg/tabletoken"
"time"
"github.com/google/uuid"
@@ -12,6 +13,7 @@ type Table struct {
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"`
TableName string `gorm:"not null;size:100" json:"table_name" validate:"required"`
Token string `gorm:"uniqueIndex;not null;size:255" json:"token"`
StartTime *time.Time `gorm:"" json:"start_time"`
Status string `gorm:"default:'available';size:50" json:"status"`
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
@@ -33,6 +35,9 @@ func (t *Table) BeforeCreate(tx *gorm.DB) error {
if t.ID == uuid.Nil {
t.ID = uuid.New()
}
if t.Token == "" {
t.Token = tabletoken.Encode(t.ID, t.OrganizationID, t.OutletID)
}
return nil
}
+7 -5
View File
@@ -13,10 +13,12 @@ import (
type UserRole string
const (
RoleAdmin UserRole = "admin"
RoleManager UserRole = "manager"
RoleCashier UserRole = "cashier"
RoleWaiter UserRole = "waiter"
RoleAdmin UserRole = "admin"
RoleManager UserRole = "manager"
RoleCashier UserRole = "cashier"
RoleWaiter UserRole = "waiter"
RoleOwner UserRole = "owner"
RolePurchasing UserRole = "purchasing"
)
type Permissions map[string]interface{}
@@ -46,7 +48,7 @@ type User struct {
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"`
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"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
+50
View File
@@ -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"
}
+175 -5
View File
@@ -8,6 +8,7 @@ import (
"apskel-pos-be/internal/util"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
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) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
@@ -36,7 +48,7 @@ func (h *AnalyticsHandler) GetPaymentMethodAnalytics(c *gin.Context) {
}
req.OrganizationID = contextInfo.OrganizationID
req.OutletID = &contextInfo.OutletID
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
modelReq := transformer.PaymentMethodAnalyticsContractToModel(&req)
response, err := h.analyticsService.GetPaymentMethodAnalytics(ctx, modelReq)
@@ -60,7 +72,7 @@ func (h *AnalyticsHandler) GetSalesAnalytics(c *gin.Context) {
}
req.OrganizationID = contextInfo.OrganizationID
req.OutletID = &contextInfo.OutletID
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
modelReq := transformer.SalesAnalyticsContractToModel(&req)
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")
}
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) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
@@ -84,7 +120,7 @@ func (h *AnalyticsHandler) GetProductAnalytics(c *gin.Context) {
}
req.OrganizationID = contextInfo.OrganizationID
req.OutletID = &contextInfo.OutletID
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
modelReq := transformer.ProductAnalyticsContractToModel(&req)
response, err := h.analyticsService.GetProductAnalytics(ctx, modelReq)
@@ -108,7 +144,7 @@ func (h *AnalyticsHandler) GetProductAnalyticsPerCategory(c *gin.Context) {
}
req.OrganizationID = contextInfo.OrganizationID
req.OutletID = &contextInfo.OutletID
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
modelReq := transformer.ProductAnalyticsPerCategoryContractToModel(&req)
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")
}
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) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
@@ -132,7 +217,7 @@ func (h *AnalyticsHandler) GetDashboardAnalytics(c *gin.Context) {
}
req.OrganizationID = contextInfo.OrganizationID
req.OutletID = &contextInfo.OutletID
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
modelReq := transformer.DashboardAnalyticsContractToModel(&req)
response, err := h.analyticsService.GetDashboardAnalytics(ctx, modelReq)
@@ -156,6 +241,7 @@ func (h *AnalyticsHandler) GetProfitLossAnalytics(c *gin.Context) {
}
req.OrganizationID = contextInfo.OrganizationID
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
modelReq, err := transformer.ProfitLossAnalyticsContractToModel(&req)
if err != nil {
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)
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")
}
+28 -1
View File
@@ -36,7 +36,7 @@ func (h *CategoryHandler) CreateCategory(c *gin.Context) {
contextInfo := appcontext.FromGinContext(ctx)
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 {
logger.FromContext(c.Request.Context()).WithError(err).Error("CategoryHandler::CreateCategory -> request binding failed")
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
@@ -44,6 +44,11 @@ func (h *CategoryHandler) CreateCategory(c *gin.Context) {
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)
if validationError != nil {
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
@@ -149,6 +154,11 @@ func (h *CategoryHandler) ListCategories(c *gin.Context) {
OrganizationID: &contextInfo.OrganizationID,
}
// Inject outlet_id from context if user has one
if contextInfo.OutletID != uuid.Nil {
req.OutletID = &contextInfo.OutletID
}
// Parse query parameters
if pageStr := c.Query("page"); pageStr != "" {
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)
if validationError != nil {
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) {
// Parse query parameters
filters := make(map[string]interface{})
if isActive := c.Query("is_active"); isActive != "" {
if isActiveBool, err := strconv.ParseBool(isActive); err == nil {
filters["is_active"] = isActiveBool
+3 -1
View File
@@ -1,6 +1,8 @@
package handler
import (
"apskel-pos-be/internal/logger"
"fmt"
"net/http"
"time"
)
@@ -47,7 +49,7 @@ func (m *CommonMiddleware) Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
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)
}
}()
+229
View File
@@ -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")
}
+142 -62
View File
@@ -18,9 +18,7 @@ type IngredientHandler struct {
}
func NewIngredientHandler(ingredientService IngredientService) *IngredientHandler {
return &IngredientHandler{
ingredientService: ingredientService,
}
return &IngredientHandler{ingredientService: ingredientService}
}
func (h *IngredientHandler) Create(c *gin.Context) {
@@ -29,53 +27,53 @@ func (h *IngredientHandler) Create(c *gin.Context) {
var request models.CreateIngredientRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.FromContext(c.Request.Context()).WithError(err).Error("IngredientHandler::Create -> request binding failed")
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Create")
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Create -> request binding failed")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
}), "IngredientHandler::Create")
return
}
request.OrganizationID = contextInfo.OrganizationID
ingredientResponse, err := h.ingredientService.CreateIngredient(ctx, &request)
resp, err := h.ingredientService.CreateIngredient(ctx, &request)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Create -> Failed to create ingredient from service")
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Create")
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Create -> failed")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
}), "IngredientHandler::Create")
return
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::Create")
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::Create")
}
func (h *IngredientHandler) GetByID(c *gin.Context) {
ctx := c.Request.Context()
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.Parse(c.Param("id"))
if err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetByID -> Invalid ingredient ID")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetByID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID"),
}), "IngredientHandler::GetByID")
return
}
ingredientResponse, err := h.ingredientService.GetIngredientByID(ctx, id)
resp, err := h.ingredientService.GetIngredientByID(ctx, id)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetByID -> Failed to get ingredient from service")
validationResponseError := contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "Ingredient not found")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetByID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "Ingredient not found"),
}), "IngredientHandler::GetByID")
return
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::GetByID")
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::GetByID")
}
func (h *IngredientHandler) GetAll(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
// Get query parameters
pageStr := c.DefaultQuery("page", "1")
limitStr := c.DefaultQuery("limit", "10")
search := c.Query("search")
@@ -83,95 +81,177 @@ func (h *IngredientHandler) GetAll(c *gin.Context) {
page, err := strconv.Atoi(pageStr)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Invalid page parameter")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid page parameter")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid page parameter"),
}), "IngredientHandler::GetAll")
return
}
limit, err := strconv.Atoi(limitStr)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Invalid limit parameter")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid limit parameter")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid limit parameter"),
}), "IngredientHandler::GetAll")
return
}
var outletID *uuid.UUID
if outletIDStr != "" {
parsedOutletID, err := uuid.Parse(outletIDStr)
parsed, err := uuid.Parse(outletIDStr)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Invalid outlet ID")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID"),
}), "IngredientHandler::GetAll")
return
}
outletID = &parsedOutletID
outletID = &parsed
}
ingredientResponse, err := h.ingredientService.ListIngredients(ctx, contextInfo.OrganizationID, outletID, page, limit, search)
resp, err := h.ingredientService.ListIngredients(ctx, contextInfo.OrganizationID, outletID, page, limit, search)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Failed to get ingredients from service")
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, "Failed to get ingredients")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, "Failed to get ingredients"),
}), "IngredientHandler::GetAll")
return
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::GetAll")
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::GetAll")
}
func (h *IngredientHandler) Update(c *gin.Context) {
ctx := c.Request.Context()
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.Parse(c.Param("id"))
if err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Update -> Invalid ingredient ID")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Update")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID"),
}), "IngredientHandler::Update")
return
}
var request models.UpdateIngredientRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Update -> request binding failed")
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Update")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body"),
}), "IngredientHandler::Update")
return
}
ingredientResponse, err := h.ingredientService.UpdateIngredient(ctx, id, &request)
resp, err := h.ingredientService.UpdateIngredient(ctx, id, &request)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Update -> Failed to update ingredient from service")
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Update")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
}), "IngredientHandler::Update")
return
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::Update")
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::Update")
}
func (h *IngredientHandler) Delete(c *gin.Context) {
ctx := c.Request.Context()
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
id, err := uuid.Parse(c.Param("id"))
if err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Delete -> Invalid ingredient ID")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Delete")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID"),
}), "IngredientHandler::Delete")
return
}
err = h.ingredientService.DeleteIngredient(ctx, id)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Delete -> Failed to delete ingredient from service")
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Delete")
if err := h.ingredientService.DeleteIngredient(ctx, id); err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
}), "IngredientHandler::Delete")
return
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(map[string]interface{}{
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(map[string]string{
"message": "Ingredient deleted successfully",
}), "IngredientHandler::Delete")
}
// AddCompositions adds multiple composition items to a semi-finished ingredient.
func (h *IngredientHandler) AddCompositions(c *gin.Context) {
ctx := c.Request.Context()
id, err := uuid.Parse(c.Param("id"))
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "invalid ingredient id"),
}), "IngredientHandler::AddCompositions")
return
}
var req models.AddIngredientCompositionsRequest
if err := c.ShouldBindJSON(&req); err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
}), "IngredientHandler::AddCompositions")
return
}
resp, err := h.ingredientService.AddCompositions(ctx, id, &req)
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
}), "IngredientHandler::AddCompositions")
return
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::AddCompositions")
}
// UpdateComposition updates quantity/outlet of a single composition entry.
func (h *IngredientHandler) UpdateComposition(c *gin.Context) {
ctx := c.Request.Context()
id, err := uuid.Parse(c.Param("composition_id"))
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "invalid composition id"),
}), "IngredientHandler::UpdateComposition")
return
}
var req models.UpdateIngredientCompositionRequest
if err := c.ShouldBindJSON(&req); err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
}), "IngredientHandler::UpdateComposition")
return
}
resp, err := h.ingredientService.UpdateComposition(ctx, id, &req)
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
}), "IngredientHandler::UpdateComposition")
return
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::UpdateComposition")
}
// DeleteComposition removes a single composition entry.
func (h *IngredientHandler) DeleteComposition(c *gin.Context) {
ctx := c.Request.Context()
id, err := uuid.Parse(c.Param("composition_id"))
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "invalid composition id"),
}), "IngredientHandler::DeleteComposition")
return
}
resp, err := h.ingredientService.DeleteComposition(ctx, id)
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
}), "IngredientHandler::DeleteComposition")
return
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::DeleteComposition")
}
+3
View File
@@ -13,4 +13,7 @@ type IngredientService interface {
DeleteIngredient(ctx context.Context, id uuid.UUID) error
GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error)
ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error)
UpdateComposition(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientCompositionRequest) (*models.IngredientCompositionResponse, error)
DeleteComposition(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error)
AddCompositions(ctx context.Context, parentID uuid.UUID, req *models.AddIngredientCompositionsRequest) (*models.AddIngredientCompositionsResponse, error)
}
@@ -275,4 +275,3 @@ func (h *IngredientUnitConverterHandler) GetUnitsByIngredientID(c *gin.Context)
util.HandleResponse(c.Writer, c.Request, unitsResponse, "IngredientUnitConverterHandler::GetUnitsByIngredientID")
}
+190
View File
@@ -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")
}
+4
View File
@@ -137,6 +137,10 @@ func (h *OrderHandler) ListOrders(c *gin.Context) {
}
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)
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::ListOrders", err.Error())}), "OrderHandler::ListOrders")
+95 -2
View File
@@ -60,6 +60,7 @@ func (h *ProductHandler) CreateProduct(c *gin.Context) {
func (h *ProductHandler) UpdateProduct(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
productIDStr := c.Param("id")
productID, err := uuid.Parse(productIDStr)
@@ -85,7 +86,7 @@ func (h *ProductHandler) UpdateProduct(c *gin.Context) {
return
}
productResponse := h.productService.UpdateProduct(ctx, productID, &req)
productResponse := h.productService.UpdateProduct(ctx, contextInfo, productID, &req)
if productResponse.HasErrors() {
errorResp := productResponse.GetErrors()[0]
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) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
productIDStr := c.Param("id")
productID, err := uuid.Parse(productIDStr)
@@ -127,7 +129,7 @@ func (h *ProductHandler) GetProduct(c *gin.Context) {
return
}
productResponse := h.productService.GetProductByID(ctx, productID)
productResponse := h.productService.GetProductByID(ctx, productID, contextInfo.OutletID)
if productResponse.HasErrors() {
errorResp := productResponse.GetErrors()[0]
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 minPrice, err := strconv.ParseFloat(minPriceStr, 64); err == nil {
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")
}
+1 -1
View File
@@ -219,4 +219,4 @@ func (h *ProductRecipeHandler) BulkCreate(c *gin.Context) {
}
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")
}
+49 -1
View File
@@ -8,6 +8,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type ReportHandler struct {
@@ -19,11 +20,26 @@ func NewReportHandler(reportService service.ReportService, userService UserServi
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) {
ctx := c.Request.Context()
ci := appcontext.FromGinContext(ctx)
outletID := c.Param("outlet_id")
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 {
@@ -50,3 +66,35 @@ func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
"file_name": fileName,
}), "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")
}
+571
View File
@@ -0,0 +1,571 @@
package handler
import (
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/logger"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/pkg/tabletoken"
"apskel-pos-be/internal/processor"
"apskel-pos-be/internal/repository"
"apskel-pos-be/internal/service"
"apskel-pos-be/internal/transformer"
"apskel-pos-be/internal/util"
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type SelfOrderHandler struct {
orderService service.OrderService
categoryService service.CategoryService
productService service.ProductService
tableRepo repository.TableRepositoryInterface
outletRepo processor.OutletRepository
userRepo processor.UserRepository
sessionRepo repository.SessionRepository
orderRepo repository.OrderRepository
productOutletPriceService service.ProductOutletPriceService
}
func NewSelfOrderHandler(
orderService service.OrderService,
categoryService service.CategoryService,
productService service.ProductService,
tableRepo repository.TableRepositoryInterface,
outletRepo processor.OutletRepository,
userRepo processor.UserRepository,
sessionRepo repository.SessionRepository,
orderRepo repository.OrderRepository,
productOutletPriceService service.ProductOutletPriceService,
) *SelfOrderHandler {
return &SelfOrderHandler{
orderService: orderService,
categoryService: categoryService,
productService: productService,
tableRepo: tableRepo,
outletRepo: outletRepo,
userRepo: userRepo,
sessionRepo: sessionRepo,
orderRepo: orderRepo,
productOutletPriceService: productOutletPriceService,
}
}
func (h *SelfOrderHandler) ValidateToken(c *gin.Context) {
ctx := c.Request.Context()
token := c.Param("token")
if token == "" {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "token is required"),
}), "SelfOrderHandler::ValidateToken")
return
}
tableID, orgID, outletID, err := tabletoken.Decode(token)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ValidateToken -> invalid token")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid table token"),
}), "SelfOrderHandler::ValidateToken")
return
}
table, err := h.tableRepo.GetByID(ctx, tableID)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ValidateToken -> table not found")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "table not found"),
}), "SelfOrderHandler::ValidateToken")
return
}
if !table.IsActive {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.TableEntity, "table is not active"),
}), "SelfOrderHandler::ValidateToken")
return
}
if table.OrganizationID != orgID || table.OutletID != outletID {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.TableEntity, "token does not match table"),
}), "SelfOrderHandler::ValidateToken")
return
}
outlet, err := h.outletRepo.GetByID(ctx, table.OutletID)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ValidateToken -> outlet not found")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.NotFoundErrorCode, constants.OrderServiceEntity, "outlet not found"),
}), "SelfOrderHandler::ValidateToken")
return
}
existingSession, err := h.sessionRepo.GetActiveByTableID(ctx, table.ID)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ValidateToken -> failed to check session")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, "failed to check session"),
}), "SelfOrderHandler::ValidateToken")
return
}
var sessionStatus string
var sessionID string
if existingSession != nil {
sessionStatus = "joined_session"
sessionID = existingSession.ID
} else {
session := &models.SelfOrderSession{
TableID: table.ID,
OrganizationID: table.OrganizationID,
OutletID: table.OutletID,
}
if err := h.sessionRepo.Create(ctx, session); err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ValidateToken -> failed to create session")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, "failed to create session"),
}), "SelfOrderHandler::ValidateToken")
return
}
sessionStatus = "new_session"
sessionID = session.ID
}
resp := &contract.SelfOrderTableTokenResponse{
SessionID: sessionID,
TableID: table.ID.String(),
OrganizationID: table.OrganizationID.String(),
OutletID: table.OutletID.String(),
TableName: table.TableName,
OutletName: outlet.Name,
Status: sessionStatus,
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "SelfOrderHandler::ValidateToken")
}
func (h *SelfOrderHandler) GetMenu(c *gin.Context) {
ctx := c.Request.Context()
var req contract.SelfOrderMenuRequest
if err := c.ShouldBindQuery(&req); err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetMenu -> query binding failed")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
}), "SelfOrderHandler::GetMenu")
return
}
session, table, outlet, err := h.resolveSession(ctx, req.SessionID)
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()),
}), "SelfOrderHandler::GetMenu")
return
}
if session == nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "session not found or expired"),
}), "SelfOrderHandler::GetMenu")
return
}
isActive := true
catResp := h.categoryService.ListCategories(ctx, &contract.ListCategoriesRequest{
OrganizationID: &table.OrganizationID,
Page: 1,
Limit: 100,
})
if catResp.HasErrors() {
logger.FromContext(ctx).WithError(catResp.GetErrors()[0]).Error("SelfOrderHandler::GetMenu -> failed to list categories")
util.HandleResponse(c.Writer, c.Request, catResp, "SelfOrderHandler::GetMenu")
return
}
prodResp := h.productService.ListProducts(ctx, &contract.ListProductsRequest{
OrganizationID: &table.OrganizationID,
IsActive: &isActive,
Page: 1,
Limit: 1000,
})
if prodResp.HasErrors() {
logger.FromContext(ctx).WithError(prodResp.GetErrors()[0]).Error("SelfOrderHandler::GetMenu -> failed to list products")
util.HandleResponse(c.Writer, c.Request, prodResp, "SelfOrderHandler::GetMenu")
return
}
catList, ok := catResp.Data.(*contract.ListCategoriesResponse)
if !ok {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.CategoryServiceEntity, "unexpected categories response type"),
}), "SelfOrderHandler::GetMenu")
return
}
prodList, ok := prodResp.Data.(*contract.ListProductsResponse)
if !ok {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductServiceEntity, "unexpected products response type"),
}), "SelfOrderHandler::GetMenu")
return
}
menu := h.buildMenuResponse(ctx, outlet, table, catList.Categories, prodList.Products)
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(menu), "SelfOrderHandler::GetMenu")
}
func (h *SelfOrderHandler) buildMenuResponse(
ctx context.Context,
outlet *entities.Outlet,
table *entities.Table,
categories []contract.CategoryResponse,
products []contract.ProductResponse,
) *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)
for _, p := range products {
productMap[p.CategoryID] = append(productMap[p.CategoryID], p)
}
menuCategories := make([]contract.SelfOrderMenuCategory, 0, len(categories))
for _, cat := range categories {
menuItems := make([]contract.SelfOrderMenuItem, 0)
if prods, ok := productMap[cat.ID]; ok {
for _, p := range prods {
price := p.Price
if outletPrice, exists := outletPriceMap[p.ID]; exists {
price = outletPrice
}
item := contract.SelfOrderMenuItem{
ID: p.ID,
Name: p.Name,
Description: p.Description,
Price: price,
ImageURL: p.ImageURL,
}
for _, v := range p.Variants {
item.Variants = append(item.Variants, contract.SelfOrderMenuVariant{
ID: v.ID,
Name: v.Name,
PriceModifier: v.PriceModifier,
})
}
menuItems = append(menuItems, item)
}
}
menuCategories = append(menuCategories, contract.SelfOrderMenuCategory{
ID: cat.ID,
Name: cat.Name,
Description: cat.Description,
Order: cat.Order,
Products: menuItems,
})
}
return &contract.SelfOrderMenuResponse{
OutletName: outlet.Name,
TableName: table.TableName,
Categories: menuCategories,
}
}
func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
ctx := c.Request.Context()
var req contract.SelfOrderCreateOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> request binding failed")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
}), "SelfOrderHandler::CreateOrder")
return
}
if err := h.validateCreateOrderRequest(&req); err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()),
}), "SelfOrderHandler::CreateOrder")
return
}
session, table, _, err := h.resolveSession(ctx, req.SessionID)
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()),
}), "SelfOrderHandler::CreateOrder")
return
}
if session == nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "session not found or expired"),
}), "SelfOrderHandler::CreateOrder")
return
}
if !table.IsActive || !table.IsAvailable() {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.TableEntity, "table is not available for ordering"),
}), "SelfOrderHandler::CreateOrder")
return
}
userID, err := h.resolveOrgUser(ctx, table.OrganizationID)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> failed to resolve org user")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, "failed to create self-order"),
}), "SelfOrderHandler::CreateOrder")
return
}
orderItems := make([]models.CreateOrderItemRequest, 0, len(req.OrderItems))
for _, item := range req.OrderItems {
orderItems = append(orderItems, models.CreateOrderItemRequest{
ProductID: item.ProductID,
ProductVariantID: item.ProductVariantID,
Quantity: item.Quantity,
Notes: item.Notes,
})
}
metadata := make(map[string]interface{})
metadata["self_order"] = true
metadata["session_id"] = session.ID
metadata["customer_name"] = req.CustomerName
tableID := table.ID
modelReq := &models.CreateOrderRequest{
OutletID: table.OutletID,
UserID: userID,
TableID: &tableID,
TableNumber: &table.TableName,
OrderType: constants.OrderType(req.OrderType),
OrderItems: orderItems,
Metadata: metadata,
}
response, err := h.orderService.CreateOrder(ctx, modelReq, table.OrganizationID)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> failed to create order")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, err.Error()),
}), "SelfOrderHandler::CreateOrder")
return
}
contractResp := transformer.OrderModelToContract(response)
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "SelfOrderHandler::CreateOrder")
}
func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) {
ctx := c.Request.Context()
sessionID := c.Param("session_id")
if sessionID == "" {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "session_id is required"),
}), "SelfOrderHandler::GetOrdersBySession")
return
}
session, err := h.sessionRepo.GetByID(ctx, sessionID)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetOrdersBySession -> failed to get session")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "session not found"),
}), "SelfOrderHandler::GetOrdersBySession")
return
}
if session == nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "session not found"),
}), "SelfOrderHandler::GetOrdersBySession")
return
}
orders, err := h.orderRepo.ListBySessionID(ctx, sessionID)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetOrdersBySession -> failed to list orders")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, "failed to list orders"),
}), "SelfOrderHandler::GetOrdersBySession")
return
}
modelOrders := mappers.OrderEntitiesToResponses(orders)
contractOrders := make([]contract.OrderResponse, len(modelOrders))
for i := range modelOrders {
contractOrders[i] = *transformer.OrderModelToContract(&modelOrders[i])
}
resp := &contract.SelfOrderListOrdersResponse{
Orders: contractOrders,
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "SelfOrderHandler::GetOrdersBySession")
}
func (h *SelfOrderHandler) validateCreateOrderRequest(req *contract.SelfOrderCreateOrderRequest) error {
if req.SessionID == "" {
return fmt.Errorf("session_id is required")
}
if len(req.OrderItems) == 0 {
return fmt.Errorf("at least one order item is required")
}
for i, item := range req.OrderItems {
if item.ProductID == uuid.Nil {
return fmt.Errorf("product_id is required for item %d", i+1)
}
if item.Quantity <= 0 {
return fmt.Errorf("quantity must be greater than zero for item %d", i+1)
}
}
return nil
}
func (h *SelfOrderHandler) ListCategories(c *gin.Context) {
ctx := c.Request.Context()
var req contract.SelfOrderListCategoriesRequest
if err := c.ShouldBindQuery(&req); err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ListCategories -> query binding failed")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
}), "SelfOrderHandler::ListCategories")
return
}
if req.OrganizationID == "" {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "organization_id is required"),
}), "SelfOrderHandler::ListCategories")
return
}
if req.OutletID == "" {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "outlet_id is required"),
}), "SelfOrderHandler::ListCategories")
return
}
orgID, err := uuid.Parse(req.OrganizationID)
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid organization_id format"),
}), "SelfOrderHandler::ListCategories")
return
}
outletID, err := uuid.Parse(req.OutletID)
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid outlet_id format"),
}), "SelfOrderHandler::ListCategories")
return
}
outlet, err := h.outletRepo.GetByID(ctx, outletID)
if err != nil || outlet == nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "outlet not found"),
}), "SelfOrderHandler::ListCategories")
return
}
if outlet.OrganizationID != orgID {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "outlet does not belong to the specified organization"),
}), "SelfOrderHandler::ListCategories")
return
}
catResp := h.categoryService.ListCategories(ctx, &contract.ListCategoriesRequest{
OrganizationID: &orgID,
Page: 1,
Limit: 100,
})
if catResp.HasErrors() {
logger.FromContext(ctx).WithError(catResp.GetErrors()[0]).Error("SelfOrderHandler::ListCategories -> failed to list categories")
util.HandleResponse(c.Writer, c.Request, catResp, "SelfOrderHandler::ListCategories")
return
}
catList, ok := catResp.Data.(*contract.ListCategoriesResponse)
if !ok {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.CategoryServiceEntity, "unexpected categories response type"),
}), "SelfOrderHandler::ListCategories")
return
}
items := make([]contract.SelfOrderCategoryItem, 0, len(catList.Categories))
for _, cat := range catList.Categories {
items = append(items, contract.SelfOrderCategoryItem{
ID: cat.ID,
Name: cat.Name,
Description: cat.Description,
Order: cat.Order,
})
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(&contract.SelfOrderListCategoriesResponse{
Categories: items,
}), "SelfOrderHandler::ListCategories")
}
func (h *SelfOrderHandler) resolveSession(ctx context.Context, sessionID string) (*models.SelfOrderSession, *entities.Table, *entities.Outlet, error) {
session, err := h.sessionRepo.GetByID(ctx, sessionID)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to get session: %w", err)
}
if session == nil {
return nil, nil, nil, nil
}
if session.Status != "active" {
return nil, nil, nil, fmt.Errorf("session is no longer active")
}
table, err := h.tableRepo.GetByID(ctx, session.TableID)
if err != nil {
return nil, nil, nil, fmt.Errorf("table not found for session")
}
outlet, err := h.outletRepo.GetByID(ctx, table.OutletID)
if err != nil {
return nil, nil, nil, fmt.Errorf("outlet not found for session")
}
return session, table, outlet, nil
}
func (h *SelfOrderHandler) resolveOrgUser(ctx context.Context, organizationID uuid.UUID) (uuid.UUID, error) {
users, err := h.userRepo.GetByOrganizationID(ctx, organizationID)
if err != nil {
return uuid.Nil, fmt.Errorf("failed to get users for organization: %w", err)
}
if len(users) == 0 {
return uuid.Nil, fmt.Errorf("no users found for organization")
}
return users[0].ID, nil
}
+53 -1
View File
@@ -5,8 +5,11 @@ import (
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/logger"
"apskel-pos-be/internal/pkg/qrcode"
"apskel-pos-be/internal/util"
"apskel-pos-be/internal/validator"
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
@@ -16,12 +19,14 @@ import (
type TableHandler struct {
tableService TableService
tableValidator *validator.TableValidator
selfOrderURL string
}
func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator) *TableHandler {
func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator, selfOrderURL string) *TableHandler {
return &TableHandler{
tableService: tableService,
tableValidator: tableValidator,
selfOrderURL: selfOrderURL,
}
}
@@ -145,6 +150,11 @@ func (h *TableHandler) List(c *gin.Context) {
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 page, err := strconv.Atoi(pageStr); err == nil && page > 0 {
query.Page = page
@@ -286,3 +296,45 @@ func (h *TableHandler) GetOccupiedTables(c *gin.Context) {
util.HandleResponse(c.Writer, c.Request, response, "TableHandler::GetOccupiedTables")
}
func (h *TableHandler) GenerateQRCode(c *gin.Context) {
ctx := c.Request.Context()
id := c.Param("id")
tableID, err := uuid.Parse(id)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("TableHandler::GenerateQRCode -> Invalid table ID")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid table ID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::GenerateQRCode")
return
}
token, err := h.tableService.GetTableToken(ctx, tableID)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("TableHandler::GenerateQRCode -> table not found")
validationResponseError := contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "Table not found")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::GenerateQRCode")
return
}
selfOrderURLResult := fmt.Sprintf("%s/menu?token=%s", h.selfOrderURL, token)
size := 256
if sizeStr := c.Query("size"); sizeStr != "" {
if s, err := strconv.Atoi(sizeStr); err == nil && s > 0 && s <= 1024 {
size = s
}
}
pngBytes, err := qrcode.GeneratePNG(selfOrderURLResult, size)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("TableHandler::GenerateQRCode -> QR generation failed")
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, "Failed to generate QR code")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::GenerateQRCode")
return
}
c.Header("Content-Type", "image/png")
c.Header("Content-Disposition", fmt.Sprintf("inline; filename=\"table-%s-qr.png\"", tableID))
c.Data(http.StatusOK, "image/png", pngBytes)
}
+1
View File
@@ -17,4 +17,5 @@ type TableService interface {
ReleaseTable(ctx context.Context, tableID uuid.UUID, req *contract.ReleaseTableRequest) *contract.Response
GetAvailableTables(ctx context.Context, outletID uuid.UUID) *contract.Response
GetOccupiedTables(ctx context.Context, outletID uuid.UUID) *contract.Response
GetTableToken(ctx context.Context, tableID uuid.UUID) (string, error)
}
+215
View File
@@ -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")
}
+29 -9
View File
@@ -13,11 +13,12 @@ func CategoryEntityToModel(entity *entities.Category) *models.Category {
return &models.Category{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
Name: entity.Name,
Description: entity.Description,
ImageURL: nil, // Entity doesn't have ImageURL, model does
Order: entity.Order, // Entity doesn't have SortOrder, model does
IsActive: true, // Entity doesn't have IsActive, default to true
ImageURL: nil,
Order: entity.Order,
IsActive: true,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
@@ -32,14 +33,14 @@ func CategoryModelToEntity(model *models.Category) *entities.Category {
if model.ImageURL != nil {
metadata["image_url"] = *model.ImageURL
}
// metadata["sort_order"] = model.SortOrder
return &entities.Category{
ID: model.ID,
OrganizationID: model.OrganizationID,
OutletID: model.OutletID,
Name: model.Name,
Description: model.Description,
BusinessType: "restaurant", // Default business type
BusinessType: "restaurant",
Order: model.Order,
Metadata: metadata,
CreatedAt: model.CreatedAt,
@@ -56,14 +57,15 @@ func CreateCategoryRequestToEntity(req *models.CreateCategoryRequest) *entities.
if req.ImageURL != nil {
metadata["image_url"] = *req.ImageURL
}
// metadata["sort_order"] = req.SortOrder
return &entities.Category{
OrganizationID: req.OrganizationID,
OutletID: req.OutletID,
ParentID: req.ParentID,
Name: req.Name,
Description: req.Description,
Order: req.Order,
BusinessType: "restaurant", // Default business type
BusinessType: "restaurant",
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{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
ParentID: entity.ParentID,
ParentName: parentName,
Name: entity.Name,
Description: entity.Description,
ImageURL: imageURL,
Order: entity.Order,
IsActive: true, // Default to true since entity doesn't have this field
Order: entity.Order,
IsActive: true,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
@@ -121,6 +133,14 @@ func UpdateCategoryEntityFromRequest(entity *entities.Category, req *models.Upda
if req.Order != nil {
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 {
+135
View File
@@ -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
}
@@ -1,70 +0,0 @@
package mappers
import (
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models"
)
func MapIngredientCompositionEntityToModel(entity *entities.IngredientComposition) *models.IngredientComposition {
if entity == nil {
return nil
}
return &models.IngredientComposition{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
ParentIngredientID: entity.ParentIngredientID,
ChildIngredientID: entity.ChildIngredientID,
Quantity: entity.Quantity,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
ParentIngredient: MapIngredientEntityToModel(entity.ParentIngredient),
ChildIngredient: MapIngredientEntityToModel(entity.ChildIngredient),
}
}
func MapIngredientCompositionModelToEntity(model *models.IngredientComposition) *entities.IngredientComposition {
if model == nil {
return nil
}
return &entities.IngredientComposition{
ID: model.ID,
OrganizationID: model.OrganizationID,
OutletID: model.OutletID,
ParentIngredientID: model.ParentIngredientID,
ChildIngredientID: model.ChildIngredientID,
Quantity: model.Quantity,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
ParentIngredient: MapIngredientModelToEntity(model.ParentIngredient),
ChildIngredient: MapIngredientModelToEntity(model.ChildIngredient),
}
}
func MapIngredientCompositionEntitiesToModels(entities []*entities.IngredientComposition) []*models.IngredientComposition {
if entities == nil {
return nil
}
models := make([]*models.IngredientComposition, len(entities))
for i, entity := range entities {
models[i] = MapIngredientCompositionEntityToModel(entity)
}
return models
}
func MapIngredientCompositionModelsToEntities(models []*models.IngredientComposition) []*entities.IngredientComposition {
if models == nil {
return nil
}
entities := make([]*entities.IngredientComposition, len(models))
for i, model := range models {
entities[i] = MapIngredientCompositionModelToEntity(model)
}
return entities
}
+85
View File
@@ -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,
}
}
+22 -4
View File
@@ -82,7 +82,7 @@ func OrderEntityToResponse(order *entities.Order) *models.OrderResponse {
}
for i, item := range order.OrderItems {
resp := OrderItemEntityToResponse(&item)
resp := OrderItemEntityToResponse(&item, order.OutletID)
if resp != nil {
resp.PaidQuantity = paidQtyByOrderItem[item.ID]
response.OrderItems[i] = *resp
@@ -101,11 +101,20 @@ func OrderEntityToResponse(order *entities.Order) *models.OrderResponse {
return response
}
func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemResponse {
func OrderItemEntityToResponse(item *entities.OrderItem, outletID uuid.UUID) *models.OrderItemResponse {
if item == 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{
ID: item.ID,
OrderID: item.OrderID,
@@ -130,10 +139,19 @@ func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemRespon
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
PrinterType: item.Product.PrinterType,
PrintToChecker: printToChecker,
}
if item.Product.ID != uuid.Nil {
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 {
@@ -316,14 +334,14 @@ func OrderEntitiesToResponses(orders []*entities.Order) []models.OrderResponse {
return responses
}
func OrderItemEntitiesToResponses(items []*entities.OrderItem) []models.OrderItemResponse {
func OrderItemEntitiesToResponses(items []*entities.OrderItem, outletID uuid.UUID) []models.OrderItemResponse {
if items == nil {
return nil
}
responses := make([]models.OrderItemResponse, len(items))
for i, item := range items {
response := OrderItemEntityToResponse(item)
response := OrderItemEntityToResponse(item, outletID)
if response != nil {
responses[i] = *response
}
+3 -3
View File
@@ -45,7 +45,7 @@ func TestOrderItemEntityToResponse_WithProductNames(t *testing.T) {
}
// Act
result := OrderItemEntityToResponse(orderItem)
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
// Assert
assert.NotNil(t, result)
@@ -89,7 +89,7 @@ func TestOrderItemEntityToResponse_WithoutProductVariant(t *testing.T) {
}
// Act
result := OrderItemEntityToResponse(orderItem)
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
// Assert
assert.NotNil(t, result)
@@ -129,7 +129,7 @@ func TestOrderItemEntityToResponse_WithoutProductPreload(t *testing.T) {
}
// Act
result := OrderItemEntityToResponse(orderItem)
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
// Assert
assert.NotNil(t, result)
+22 -22
View File
@@ -11,17 +11,17 @@ func MapProductIngredientEntityToModel(entity *entities.ProductIngredient) *mode
}
return &models.ProductIngredient{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
ProductID: entity.ProductID,
IngredientID: entity.IngredientID,
Quantity: entity.Quantity,
WastePercentage: entity.WastePercentage,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
Product: ProductEntityToModel(entity.Product),
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
ProductID: entity.ProductID,
IngredientID: entity.IngredientID,
Quantity: entity.Quantity,
WastePercentage: entity.WastePercentage,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
Product: ProductEntityToModel(entity.Product),
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
}
}
@@ -31,17 +31,17 @@ func MapProductIngredientModelToEntity(model *models.ProductIngredient) *entitie
}
return &entities.ProductIngredient{
ID: model.ID,
OrganizationID: model.OrganizationID,
OutletID: model.OutletID,
ProductID: model.ProductID,
IngredientID: model.IngredientID,
Quantity: model.Quantity,
WastePercentage: model.WastePercentage,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
Product: ProductModelToEntity(model.Product),
Ingredient: MapIngredientModelToEntity(model.Ingredient),
ID: model.ID,
OrganizationID: model.OrganizationID,
OutletID: model.OutletID,
ProductID: model.ProductID,
IngredientID: model.IngredientID,
Quantity: model.Quantity,
WastePercentage: model.WastePercentage,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
Product: ProductModelToEntity(model.Product),
Ingredient: MapIngredientModelToEntity(model.Ingredient),
}
}
+1
View File
@@ -135,6 +135,7 @@ func ProductEntityToResponse(entity *entities.Product) *models.ProductResponse {
Name: entity.Name,
Description: entity.Description,
Price: entity.Price,
OutletPrice: nil, // populated by processor when outletID is available
Cost: entity.Cost,
BusinessType: constants.BusinessType(entity.BusinessType),
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
}
+37 -27
View File
@@ -13,6 +13,7 @@ func PurchaseOrderEntityToModel(entity *entities.PurchaseOrder) *models.Purchase
return &models.PurchaseOrder{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
VendorID: entity.VendorID,
PONumber: entity.PONumber,
TransactionDate: entity.TransactionDate,
@@ -34,6 +35,7 @@ func PurchaseOrderModelToEntity(model *models.PurchaseOrder) *entities.PurchaseO
return &entities.PurchaseOrder{
ID: model.ID,
OrganizationID: model.OrganizationID,
OutletID: model.OutletID,
VendorID: model.VendorID,
PONumber: model.PONumber,
TransactionDate: model.TransactionDate,
@@ -55,6 +57,7 @@ func PurchaseOrderEntityToResponse(entity *entities.PurchaseOrder) *models.Purch
response := &models.PurchaseOrderResponse{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
VendorID: entity.VendorID,
PONumber: entity.PONumber,
TransactionDate: entity.TransactionDate,
@@ -91,15 +94,16 @@ func PurchaseOrderItemEntityToModel(entity *entities.PurchaseOrderItem) *models.
}
return &models.PurchaseOrderItem{
ID: entity.ID,
PurchaseOrderID: entity.PurchaseOrderID,
IngredientID: entity.IngredientID,
Description: entity.Description,
Quantity: entity.Quantity,
UnitID: entity.UnitID,
Amount: entity.Amount,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
ID: entity.ID,
PurchaseOrderID: entity.PurchaseOrderID,
IngredientID: entity.IngredientID,
PurchaseCategoryID: entity.PurchaseCategoryID,
Description: entity.Description,
Quantity: entity.Quantity,
UnitID: entity.UnitID,
Amount: entity.Amount,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
}
@@ -109,15 +113,16 @@ func PurchaseOrderItemModelToEntity(model *models.PurchaseOrderItem) *entities.P
}
return &entities.PurchaseOrderItem{
ID: model.ID,
PurchaseOrderID: model.PurchaseOrderID,
IngredientID: model.IngredientID,
Description: model.Description,
Quantity: model.Quantity,
UnitID: model.UnitID,
Amount: model.Amount,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
ID: model.ID,
PurchaseOrderID: model.PurchaseOrderID,
IngredientID: model.IngredientID,
PurchaseCategoryID: model.PurchaseCategoryID,
Description: model.Description,
Quantity: model.Quantity,
UnitID: model.UnitID,
Amount: model.Amount,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
@@ -127,15 +132,16 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
}
response := &models.PurchaseOrderItemResponse{
ID: entity.ID,
PurchaseOrderID: entity.PurchaseOrderID,
IngredientID: entity.IngredientID,
Description: entity.Description,
Quantity: entity.Quantity,
UnitID: entity.UnitID,
Amount: entity.Amount,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
ID: entity.ID,
PurchaseOrderID: entity.PurchaseOrderID,
IngredientID: entity.IngredientID,
PurchaseCategoryID: entity.PurchaseCategoryID,
Description: entity.Description,
Quantity: entity.Quantity,
UnitID: entity.UnitID,
Amount: entity.Amount,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
// Map ingredient if present
@@ -146,6 +152,10 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
}
}
if entity.PurchaseCategory != nil {
response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory)
}
// Map unit if present
if entity.Unit != nil {
response.Unit = &models.UnitResponse{
+62
View File
@@ -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
}
+12 -3
View File
@@ -11,6 +11,7 @@ import (
"apskel-pos-be/internal/service"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type AuthMiddleware struct {
@@ -45,9 +46,13 @@ func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
setKeyInContext(c, appcontext.OrganizationIDKey, userResponse.OrganizationID.String())
setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String())
if userResponse.Role != "superadmin" {
setKeyInContext(c, appcontext.OutletIDKey, userResponse.OutletID.String())
// Always override OutletID from token to prevent header injection.
// 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)
c.Next()
@@ -77,7 +82,11 @@ func (m *AuthMiddleware) RequireRole(allowedRoles ...string) 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 {
+5 -1
View File
@@ -6,7 +6,11 @@ import (
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
origin := c.Request.Header.Get("Origin")
if origin == "" {
origin = "*"
}
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
+137
View File
@@ -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)
}
+6 -6
View File
@@ -25,12 +25,12 @@ type AccountResponse struct {
}
type CreateAccountRequest struct {
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
Name string `json:"name" validate:"required,min=1,max=255"`
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"`
OpeningBalance float64 `json:"opening_balance"`
Description *string `json:"description"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
Name string `json:"name" validate:"required,min=1,max=255"`
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"`
OpeningBalance float64 `json:"opening_balance"`
Description *string `json:"description"`
}
type UpdateAccountRequest struct {
+381 -19
View File
@@ -19,6 +19,7 @@ type PaymentMethodAnalyticsRequest struct {
type PaymentMethodAnalyticsResponse 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"`
GroupBy string `json:"group_by"`
@@ -58,6 +59,7 @@ type SalesAnalyticsRequest struct {
type SalesAnalyticsResponse 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"`
GroupBy string `json:"group_by"`
@@ -87,6 +89,77 @@ type SalesAnalyticsData struct {
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"`
DateFrom time.Time `validate:"required"`
DateTo time.Time `validate:"required"`
GroupBy string `validate:"omitempty,oneof=day hour week month"`
}
// 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"`
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"`
}
// 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"`
}
// 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
type ProductAnalyticsRequest struct {
OrganizationID uuid.UUID `validate:"required"`
@@ -100,6 +173,7 @@ type ProductAnalyticsRequest struct {
type ProductAnalyticsResponse 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 []ProductAnalyticsData `json:"data"`
@@ -109,6 +183,7 @@ type ProductAnalyticsData struct {
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
ProductSku string `json:"product_sku"`
ProductPrice float64 `json:"product_price"`
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
CategoryOrder int `json:"category_order"`
@@ -136,6 +211,7 @@ type ProductAnalyticsPerCategoryRequest struct {
type ProductAnalyticsPerCategoryResponse 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 []ProductAnalyticsPerCategoryData `json:"data"`
@@ -153,6 +229,135 @@ type ProductAnalyticsPerCategoryData struct {
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
type DashboardAnalyticsRequest struct {
OrganizationID uuid.UUID `validate:"required"`
@@ -165,6 +370,7 @@ type DashboardAnalyticsRequest struct {
type DashboardAnalyticsResponse 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"`
Overview DashboardOverview `json:"overview"`
@@ -175,15 +381,17 @@ type DashboardAnalyticsResponse struct {
// DashboardOverview represents the overview data for dashboard
type DashboardOverview struct {
TotalSales float64 `json:"total_sales"`
TotalOrders int64 `json:"total_orders"`
AverageOrderValue float64 `json:"average_order_value"`
TotalCustomers int64 `json:"total_customers"`
VoidedOrders int64 `json:"voided_orders"`
RefundedOrders int64 `json:"refunded_orders"`
TotalSales float64 `json:"total_sales"`
TotalOrders int64 `json:"total_orders"`
AverageOrderValue float64 `json:"average_order_value"`
TotalCustomers int64 `json:"total_customers"`
VoidedOrders int64 `json:"voided_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 {
OrganizationID uuid.UUID `validate:"required"`
OutletID *uuid.UUID `validate:"omitempty"`
@@ -192,19 +400,39 @@ type ProfitLossAnalyticsRequest struct {
GroupBy string `validate:"omitempty,oneof=day hour week month"`
}
// ProfitLossAnalyticsResponse represents the response for profit and loss analytics
type ProfitLossAnalyticsResponse 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 ProfitLossSummary `json:"summary"`
Data []ProfitLossData `json:"data"`
ProductData []ProductProfitData `json:"product_data"`
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"`
GroupBy string `json:"group_by"`
Summary ProfitLossSummary `json:"summary"`
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 {
TotalRevenue float64 `json:"total_revenue"`
TotalCost float64 `json:"total_cost"`
@@ -219,7 +447,6 @@ type ProfitLossSummary struct {
ProfitabilityRatio float64 `json:"profitability_ratio"`
}
// ProfitLossData represents individual profit and loss data point by time period
type ProfitLossData struct {
Date time.Time `json:"date"`
Revenue float64 `json:"revenue"`
@@ -233,7 +460,6 @@ type ProfitLossData struct {
Orders int64 `json:"orders"`
}
// ProductProfitData represents profit data for individual products
type ProductProfitData struct {
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
@@ -248,3 +474,139 @@ type ProductProfitData struct {
AverageCost float64 `json:"average_cost"`
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"`
}
+15 -7
View File
@@ -9,10 +9,11 @@ import (
type Category struct {
ID uuid.UUID
OrganizationID uuid.UUID
OutletID *uuid.UUID
Name string
Description *string
ImageURL *string
Order int
Order int
IsActive bool
CreatedAt time.Time
UpdatedAt time.Time
@@ -20,27 +21,34 @@ type Category struct {
type CreateCategoryRequest struct {
OrganizationID uuid.UUID `validate:"required"`
Name string `validate:"required,min=1,max=255"`
Description *string `validate:"omitempty,max=1000"`
ImageURL *string `validate:"omitempty,url"`
Order int `validate:"min=0"`
OutletID *uuid.UUID
ParentID *uuid.UUID
Name string `validate:"required,min=1,max=255"`
Description *string `validate:"omitempty,max=1000"`
ImageURL *string `validate:"omitempty,url"`
Order int `validate:"min=0"`
}
type UpdateCategoryRequest struct {
Name *string `validate:"omitempty,min=1,max=255"`
Description *string `validate:"omitempty,max=1000"`
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
}
type CategoryResponse struct {
ID uuid.UUID
OrganizationID uuid.UUID
OutletID *uuid.UUID
ParentID *uuid.UUID
ParentName *string
Name string
Description *string
ImageURL *string
Order int
Order int
IsActive bool
CreatedAt time.Time
UpdatedAt time.Time
+11 -11
View File
@@ -23,17 +23,17 @@ type UpdateCustomerRequest struct {
}
type CustomerResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
Name string `json:"name"`
Email *string `json:"email,omitempty"`
Phone *string `json:"phone,omitempty"`
Address *string `json:"address,omitempty"`
IsDefault bool `json:"is_default"`
IsActive bool `json:"is_active"`
Metadata entities.Metadata `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
Name string `json:"name"`
Email *string `json:"email,omitempty"`
Phone *string `json:"phone,omitempty"`
Address *string `json:"address,omitempty"`
IsDefault bool `json:"is_default"`
IsActive bool `json:"is_active"`
Metadata entities.Metadata `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ListCustomersQuery represents query parameters for listing customers
+190
View File
@@ -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"`
}
+31 -24
View File
@@ -26,15 +26,23 @@ type Ingredient struct {
}
type CreateIngredientRequest struct {
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
Name string `json:"name" validate:"required,min=1,max=255"`
UnitID uuid.UUID `json:"unit_id" validate:"required"`
Cost float64 `json:"cost" validate:"min=0"`
Stock float64 `json:"stock" validate:"min=0"`
IsSemiFinished bool `json:"is_semi_finished"`
IsActive bool `json:"is_active"`
Metadata entities.Metadata `json:"metadata"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
Name string `json:"name" validate:"required,min=1,max=255"`
UnitID uuid.UUID `json:"unit_id" validate:"required"`
Cost float64 `json:"cost" validate:"min=0"`
Stock float64 `json:"stock" validate:"min=0"`
IsSemiFinished bool `json:"is_semi_finished"`
IsActive bool `json:"is_active"`
Metadata entities.Metadata `json:"metadata"`
Compositions []CompositionItemRequest `json:"compositions,omitempty"`
}
// CompositionItemRequest is used inside create ingredient request.
type CompositionItemRequest struct {
ChildIngredientID uuid.UUID `json:"child_ingredient_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
OutletID *uuid.UUID `json:"outlet_id"`
}
type UpdateIngredientRequest struct {
@@ -49,19 +57,18 @@ type UpdateIngredientRequest struct {
}
type IngredientResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
Name string `json:"name"`
UnitID uuid.UUID `json:"unit_id"`
Cost float64 `json:"cost"`
Stock float64 `json:"stock"`
IsSemiFinished bool `json:"is_semi_finished"`
IsActive bool `json:"is_active"`
Metadata entities.Metadata `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Relations
Unit *Unit `json:"unit,omitempty"`
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
Name string `json:"name"`
UnitID uuid.UUID `json:"unit_id"`
Cost float64 `json:"cost"`
Stock float64 `json:"stock"`
IsSemiFinished bool `json:"is_semi_finished"`
IsActive bool `json:"is_active"`
Metadata entities.Metadata `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Unit *Unit `json:"unit,omitempty"`
Compositions []*IngredientCompositionResponse `json:"compositions,omitempty"`
}
+22 -33
View File
@@ -6,44 +6,33 @@ import (
"github.com/google/uuid"
)
type IngredientComposition struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
ParentIngredientID uuid.UUID `json:"parent_ingredient_id"`
ChildIngredientID uuid.UUID `json:"child_ingredient_id"`
Quantity float64 `json:"quantity"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Relations
ParentIngredient *Ingredient `json:"parent_ingredient,omitempty"`
ChildIngredient *Ingredient `json:"child_ingredient,omitempty"`
}
type CreateIngredientCompositionRequest struct {
OutletID *uuid.UUID `json:"outlet_id"`
ParentIngredientID uuid.UUID `json:"parent_ingredient_id" validate:"required"`
ChildIngredientID uuid.UUID `json:"child_ingredient_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
}
type UpdateIngredientCompositionRequest struct {
OutletID *uuid.UUID `json:"outlet_id"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
}
type IngredientCompositionResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
ParentIngredientID uuid.UUID `json:"parent_ingredient_id"`
ChildIngredientID uuid.UUID `json:"child_ingredient_id"`
Quantity float64 `json:"quantity"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uuid.UUID `json:"id"`
OutletID *uuid.UUID `json:"outlet_id"`
ChildIngredientID uuid.UUID `json:"child_ingredient_id"`
Quantity float64 `json:"quantity"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ChildIngredient *IngredientResponse `json:"child_ingredient,omitempty"`
ParentIngredient *IngredientResponse `json:"parent_ingredient,omitempty"`
}
// Relations
ParentIngredient *Ingredient `json:"parent_ingredient,omitempty"`
ChildIngredient *Ingredient `json:"child_ingredient,omitempty"`
type CompositionItem struct {
ChildIngredientID uuid.UUID `json:"child_ingredient_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
OutletID *uuid.UUID `json:"outlet_id"`
}
type AddIngredientCompositionsRequest struct {
Compositions []CompositionItem `json:"compositions" validate:"required,min=1,dive"`
}
type AddIngredientCompositionsResponse struct {
ParentIngredient *IngredientResponse `json:"parent_ingredient"`
Compositions []*IngredientCompositionResponse `json:"compositions"`
}
@@ -101,4 +101,3 @@ type IngredientUnitsResponse struct {
BaseUnitName string `json:"base_unit_name"`
Units []*UnitResponse `json:"units"`
}

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