feat: RRBEC Local Server - Go backend with Django sync
- Implement local-first architecture with SQLite - Add bidirectional sync with Django via ChangeLog - JWT authentication with auto-refresh token - REST API for products, orders, commands, payments - Stock management with automatic deduction
This commit is contained in:
8
rrbec_server/.env
Normal file
8
rrbec_server/.env
Normal file
@@ -0,0 +1,8 @@
|
||||
# Server Configuration
|
||||
PORT=8080
|
||||
DB_NAME=rrbec.db
|
||||
|
||||
# Django Cloud Configuration
|
||||
DJANGO_BASE_URL=http://127.0.0.1:8000
|
||||
DJANGO_MASTER_USER=welton89
|
||||
DJANGO_MASTER_PASSWORD=pindoba10
|
||||
86
rrbec_server/ELECTRON_INTEGRATION.md
Normal file
86
rrbec_server/ELECTRON_INTEGRATION.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# RRBEC Middleware - Guia de Integração (Electron/Desktop)
|
||||
|
||||
Este guia documenta como a aplicação Electron deve se comunicar com o servidor middleware local em Go.
|
||||
|
||||
## 1. Configurações Base
|
||||
- **URL Base**: `http://localhost:8080/api/v1`
|
||||
- **Porta Default**: `8080` (configurável no arquivo `.env` do servidor).
|
||||
- **Formato**: Todas as requisições e respostas utilizam `application/json`.
|
||||
|
||||
## 2. Autenticação (SimpleAuth)
|
||||
O servidor não utiliza tokens JWT complexos localmente. A autenticação funciona assim:
|
||||
1. Faça login em `/login` enviando `username` e `password`.
|
||||
2. O servidor retornará um objeto de usuário. Capture o valor do campo `id` (inteiro).
|
||||
3. Envie esse valor no cabeçalho HTTP `X-User-ID` em todas as rotas marcadas como **[PROTEGIDO]**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Endpoints da API
|
||||
|
||||
### [PÚBLICO] Login
|
||||
**POST** `/login`
|
||||
- **Body**: `{ "username": "seu_usuario", "password": "sua_senha" }`
|
||||
- **Retorno**: Objeto User completo.
|
||||
|
||||
### [PÚBLICO] Listar Mesas
|
||||
**GET** `/mesas`
|
||||
- **Retorno**: Array de objetos mesas com `id`, `uuid`, `name`, `active`.
|
||||
|
||||
### [PÚBLICO] Listar Produtos/Estoque
|
||||
**GET** `/products`
|
||||
- **Retorno**: Array de produtos com preços e quantidade em estoque.
|
||||
|
||||
### [PÚBLICO] Listar Categorias
|
||||
**GET** `/categories`
|
||||
|
||||
### [PÚBLICO] Listar Clientes
|
||||
**GET** `/clients`
|
||||
|
||||
### [PÚBLICO] Listar Pedidos (Cozinha/Orders)
|
||||
**GET** `/orders`
|
||||
|
||||
### [PÚBLICO] Listar Tipos de Pagamento
|
||||
**GET** `/payment-types`
|
||||
|
||||
### [PÚBLICO] Listar Pagamentos Realizados
|
||||
**GET** `/payments`
|
||||
|
||||
### [PÚBLICO] Ver Comanda por ID
|
||||
**GET** `/comandas/:id` (Ex: `/api/v1/comandas/9`)
|
||||
- **Retorno**: Detalhes da comanda.
|
||||
|
||||
---
|
||||
|
||||
## 4. Comandas e Itens (Ações)
|
||||
|
||||
### [PROTEGIDO] Abrir Nova Comanda
|
||||
**POST** `/comandas`
|
||||
- **Headers**: `X-User-ID: <id_do_usuario>`
|
||||
- **Body**: `{ "mesa_id": 1, "client_id": null }`
|
||||
|
||||
### [PROTEGIDO] Lançar Pedido (Adicionar Item)
|
||||
**POST** `/items-comanda`
|
||||
- **Headers**: `X-User-ID: <id_do_usuario>`
|
||||
- **Body**: `{ "comanda_id": 9, "product_id": 50, "applicant": "Nome do Garçom" }`
|
||||
|
||||
### [PROTEGIDO] Deletar Item Individual
|
||||
**DELETE** `/items-comanda/:id`
|
||||
- **Headers**: `X-User-ID: <id_do_usuario>`
|
||||
|
||||
### [PROTEGIDO] Limpar e Fechar Comanda (Apagar Inteira)
|
||||
**POST** `/comandas/:id/apagar`
|
||||
- **Headers**: `X-User-ID: <id_do_usuario>`
|
||||
- **Efeito**: Remove todos os itens da comanda e muda o status para `CLOSED`.
|
||||
|
||||
### [PROTEGIDO] Pagar e Fechar Comanda
|
||||
**POST** `/comandas/:id/pagar`
|
||||
- **Headers**: `X-User-ID: <id_do_usuario>`
|
||||
- **Body**:
|
||||
```json
|
||||
{
|
||||
"value": 50.00,
|
||||
"type_pay_id": 1,
|
||||
"client_id": null
|
||||
}
|
||||
```
|
||||
- **Efeito**: Registra o pagamento localmente e fecha a comanda.
|
||||
78
rrbec_server/cmd/server/main.go
Normal file
78
rrbec_server/cmd/server/main.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"rrbec_server/internal/api"
|
||||
"rrbec_server/internal/config"
|
||||
"rrbec_server/internal/database"
|
||||
"rrbec_server/internal/repository"
|
||||
"rrbec_server/internal/service"
|
||||
"rrbec_server/internal/sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. Load Configuration
|
||||
config.LoadConfig()
|
||||
|
||||
// 2. Initialize Database
|
||||
db := database.InitDB(config.AppConfig.DBName)
|
||||
|
||||
// 3. Initialize Dependency Injection
|
||||
repo := repository.NewRepository(db)
|
||||
svc := service.NewService(repo)
|
||||
handler := api.NewHandler(svc)
|
||||
|
||||
// 4. Start Sync Worker
|
||||
syncer := sync.NewSyncer(repo, config.AppConfig.DjangoBaseURL, config.AppConfig.DjangoMasterUser, config.AppConfig.DjangoMasterPassword)
|
||||
syncer.Start()
|
||||
|
||||
// 4. Setup Gin Router
|
||||
r := gin.Default()
|
||||
|
||||
// API Routes
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
// Public routes
|
||||
v1.POST("/login", handler.Login)
|
||||
v1.GET("/products", handler.GetProducts)
|
||||
v1.GET("/mesas", handler.GetMesas)
|
||||
v1.GET("/comandas", handler.GetComandas)
|
||||
v1.GET("/comandas/:id", handler.GetComandaByID)
|
||||
v1.GET("/categories", handler.GetCategories)
|
||||
v1.GET("/payment-types", handler.GetTypePayments)
|
||||
v1.GET("/clients", handler.GetClients)
|
||||
v1.GET("/orders", handler.GetOrders)
|
||||
v1.GET("/payments", handler.GetPayments)
|
||||
|
||||
// Protected routes
|
||||
protected := v1.Group("/")
|
||||
protected.Use(api.SimpleAuthMiddleware())
|
||||
{
|
||||
protected.POST("/products", handler.CreateProduct)
|
||||
protected.PATCH("/products/:id", handler.UpdateProduct)
|
||||
protected.POST("/categories", handler.CreateCategory)
|
||||
protected.PATCH("/categories/:id", handler.UpdateCategory)
|
||||
protected.POST("/comandas", handler.CreateComanda)
|
||||
protected.POST("/items-comanda", handler.AddItemToComanda)
|
||||
protected.DELETE("/items-comanda/:id", handler.DeleteItemFromComanda)
|
||||
protected.POST("/comandas/:id/apagar", handler.ClearComanda)
|
||||
protected.POST("/comandas/:id/pagar", handler.PagarComanda)
|
||||
protected.PATCH("/comandas/:id", handler.UpdateComanda)
|
||||
protected.POST("/orders", handler.CreateOrder)
|
||||
protected.PATCH("/orders/:id", handler.UpdateOrder)
|
||||
protected.POST("/orders/:id/preparing", handler.SetOrderPreparing)
|
||||
protected.POST("/orders/:id/finish", handler.SetOrderFinished)
|
||||
protected.POST("/orders/:id/deliver", handler.SetOrderDelivered)
|
||||
protected.POST("/orders/:id/cancel", handler.SetOrderCanceled)
|
||||
protected.GET("/me", handler.GetCurrentUser)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Run Server
|
||||
log.Printf("Local server running on http://localhost:%s", config.AppConfig.Port)
|
||||
if err := r.Run(":" + config.AppConfig.Port); err != nil {
|
||||
log.Fatalf("failed to run server: %v", err)
|
||||
}
|
||||
}
|
||||
53
rrbec_server/go.mod
Normal file
53
rrbec_server/go.mod
Normal file
@@ -0,0 +1,53 @@
|
||||
module rrbec_server
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
golang.org/x/crypto v0.49.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
golang.org/x/arch v0.25.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
modernc.org/libc v1.70.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.47.0 // indirect
|
||||
)
|
||||
147
rrbec_server/go.sum
Normal file
147
rrbec_server/go.sum
Normal file
@@ -0,0 +1,147 @@
|
||||
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
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=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
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/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
|
||||
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
||||
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
|
||||
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
457
rrbec_server/internal/api/handlers.go
Normal file
457
rrbec_server/internal/api/handlers.go
Normal file
@@ -0,0 +1,457 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"rrbec_server/internal/models"
|
||||
"rrbec_server/internal/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
svc *service.Service
|
||||
}
|
||||
|
||||
func NewHandler(svc *service.Service) *Handler {
|
||||
return &Handler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *Handler) GetProducts(c *gin.Context) {
|
||||
products, err := h.svc.GetProducts()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, products)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateProduct(c *gin.Context) {
|
||||
var product models.Product
|
||||
if err := c.ShouldBindJSON(&product); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.CreateProduct(&product); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"message": "Product created successfully", "product": product})
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateProduct(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id format"})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.UpdateProduct(uint(id), updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Product updated successfully"})
|
||||
}
|
||||
|
||||
func (h *Handler) GetMesas(c *gin.Context) {
|
||||
mesas, err := h.svc.GetMesas()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, mesas)
|
||||
}
|
||||
|
||||
func (h *Handler) GetCategories(c *gin.Context) {
|
||||
categories, err := h.svc.GetCategories()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, categories)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateCategory(c *gin.Context) {
|
||||
var cat models.Category
|
||||
if err := c.ShouldBindJSON(&cat); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.CreateCategory(&cat); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, cat)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateCategory(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id format"})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.UpdateCategory(uint(id), updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Category updated successfully"})
|
||||
}
|
||||
|
||||
func (h *Handler) GetTypePayments(c *gin.Context) {
|
||||
types, err := h.svc.GetTypePayments()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, types)
|
||||
}
|
||||
|
||||
func (h *Handler) GetClients(c *gin.Context) {
|
||||
clients, err := h.svc.GetClients()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, clients)
|
||||
}
|
||||
|
||||
func (h *Handler) GetOrders(c *gin.Context) {
|
||||
orders, err := h.svc.GetOrders()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, orders)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateOrder(c *gin.Context) {
|
||||
var order models.Order
|
||||
if err := c.ShouldBindJSON(&order); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.CreateOrder(&order); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, order)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateOrder(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id format"})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.UpdateOrder(uint(id), updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Order updated successfully"})
|
||||
}
|
||||
|
||||
func (h *Handler) SetOrderPreparing(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id format"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.SetOrderPreparing(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Order is now preparing"})
|
||||
}
|
||||
|
||||
func (h *Handler) SetOrderFinished(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id format"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.SetOrderFinished(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Order finished"})
|
||||
}
|
||||
|
||||
func (h *Handler) SetOrderDelivered(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id format"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.SetOrderDelivered(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Order delivered"})
|
||||
}
|
||||
|
||||
func (h *Handler) SetOrderCanceled(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id format"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.SetOrderCanceled(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Order canceled"})
|
||||
}
|
||||
|
||||
func (h *Handler) GetPayments(c *gin.Context) {
|
||||
payments, err := h.svc.GetPayments()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, payments)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateComanda(c *gin.Context) {
|
||||
var comanda models.Comanda
|
||||
if err := c.ShouldBindJSON(&comanda); err != nil {
|
||||
log.Printf("DEBUG: CreateComanda bind error: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("DEBUG: CreateComanda received: MesaID=%d, UserID=%d, ClientID=%v", comanda.MesaID, comanda.UserID, comanda.ClientID)
|
||||
|
||||
if comanda.MesaID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Mesa ID is required for sync consistency"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.CreateComanda(&comanda); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, comanda)
|
||||
}
|
||||
|
||||
func (h *Handler) AddItemToComanda(c *gin.Context) {
|
||||
var item models.ProductComanda
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
log.Printf("DEBUG: AddItem bind error: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("DEBUG: AddItem received: ComandaID=%d, ProductID=%d", item.ComandaID, item.ProductID)
|
||||
|
||||
if item.ComandaID == 0 || item.ProductID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ComandaID and ProductID are required for sync"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.AddItemToComandaRaw(&item); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteItemFromComanda(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id format"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.DeleteItem(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Item deleted successfully"})
|
||||
}
|
||||
|
||||
func (h *Handler) ClearComanda(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id format"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.ClearComanda(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Comanda cleared and closed"})
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateComanda(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id format"})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.UpdateComanda(uint(id), updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Comanda updated successfully"})
|
||||
}
|
||||
|
||||
func (h *Handler) PagarComanda(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id format"})
|
||||
return
|
||||
}
|
||||
|
||||
var rawData map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&rawData); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("DEBUG: PagarComanda RAW JSON: %v", rawData)
|
||||
|
||||
var payment models.Payment
|
||||
if v, ok := rawData["value"].(float64); ok {
|
||||
payment.Value = models.Price(v)
|
||||
}
|
||||
if v, ok := rawData["type_pay"].(float64); ok {
|
||||
payment.TypePayID = uint(v)
|
||||
} else if v, ok := rawData["id_type_pay"].(float64); ok {
|
||||
payment.TypePayID = uint(v)
|
||||
}
|
||||
if v, ok := rawData["client"].(float64); ok {
|
||||
clientId := uint(v)
|
||||
payment.ClientID = &clientId
|
||||
} else if v, ok := rawData["client_id"].(float64); ok {
|
||||
clientId := uint(v)
|
||||
payment.ClientID = &clientId
|
||||
}
|
||||
if v, ok := rawData["description"].(string); ok {
|
||||
payment.Description = v
|
||||
}
|
||||
|
||||
status := "CLOSED"
|
||||
if v, ok := rawData["status"].(string); ok {
|
||||
status = v
|
||||
}
|
||||
|
||||
if err := h.svc.PagarComanda(uint(id), &payment, status); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Payment registered and comanda updated to " + status})
|
||||
}
|
||||
|
||||
func (h *Handler) GetComandas(c *gin.Context) {
|
||||
comandas, err := h.svc.GetComandas()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, comandas)
|
||||
}
|
||||
|
||||
func (h *Handler) GetComandaByID(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id format"})
|
||||
return
|
||||
}
|
||||
|
||||
comanda, err := h.svc.GetComandaByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "comanda not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, comanda)
|
||||
}
|
||||
|
||||
func (h *Handler) Login(c *gin.Context) {
|
||||
var input struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.svc.Login(input.Username, input.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Login successful",
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) GetCurrentUser(c *gin.Context) {
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"user_id": userID})
|
||||
}
|
||||
24
rrbec_server/internal/api/middleware.go
Normal file
24
rrbec_server/internal/api/middleware.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SimpleAuthMiddleware checks for a "User-ID" header for simplicity as requested.
|
||||
// This can be expanded to use JWT or Cookies if needed.
|
||||
func SimpleAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID := c.GetHeader("X-User-ID")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Map the user ID to the context for later use
|
||||
c.Set("userID", userID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
40
rrbec_server/internal/config/config.go
Normal file
40
rrbec_server/internal/config/config.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port string
|
||||
DBName string
|
||||
DjangoBaseURL string
|
||||
DjangoMasterUser string
|
||||
DjangoMasterPassword string
|
||||
}
|
||||
|
||||
var AppConfig *Config
|
||||
|
||||
func LoadConfig() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Println("No .env file found, using defaults")
|
||||
}
|
||||
|
||||
AppConfig = &Config{
|
||||
Port: getEnv("PORT", "8080"),
|
||||
DBName: getEnv("DB_NAME", "rrbec.db"),
|
||||
DjangoBaseURL: getEnv("DJANGO_BASE_URL", ""),
|
||||
DjangoMasterUser: getEnv("DJANGO_MASTER_USER", ""),
|
||||
DjangoMasterPassword: getEnv("DJANGO_MASTER_PASSWORD", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
40
rrbec_server/internal/database/database.go
Normal file
40
rrbec_server/internal/database/database.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"log"
|
||||
"rrbec_server/internal/models"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func InitDB(dataSourceName string) *gorm.DB {
|
||||
var err error
|
||||
DB, err = gorm.Open(sqlite.Open(dataSourceName), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to connect database: %v", err)
|
||||
}
|
||||
|
||||
// Auto Migration
|
||||
err = DB.AutoMigrate(
|
||||
&models.Mesa{},
|
||||
&models.Client{},
|
||||
&models.Category{},
|
||||
&models.Product{},
|
||||
&models.ProductComponent{},
|
||||
&models.Comanda{},
|
||||
&models.ProductComanda{},
|
||||
&models.Order{},
|
||||
&models.TypePay{},
|
||||
&models.Payment{},
|
||||
&models.User{},
|
||||
&models.SyncState{},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to migrate database: %v", err)
|
||||
}
|
||||
|
||||
return DB
|
||||
}
|
||||
177
rrbec_server/internal/models/models.go
Normal file
177
rrbec_server/internal/models/models.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Custom type to handle Price as string or float from JSON
|
||||
type Price float64
|
||||
|
||||
func (p *Price) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
s := string(data)
|
||||
// Remove quotes if it's a string
|
||||
if s[0] == '"' {
|
||||
s = s[1 : len(s)-1]
|
||||
}
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*p = Price(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Base model with UUID and sync fields
|
||||
type Base struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UUID string `gorm:"uniqueIndex" json:"uuid"`
|
||||
Sincronizado bool `gorm:"default:false" json:"sincronizado"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (b *Base) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if b.UUID == "" {
|
||||
b.UUID = uuid.New().String()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Mesa struct {
|
||||
Base
|
||||
Name string `json:"name"`
|
||||
Location string `json:"location"`
|
||||
Active bool `gorm:"default:false" json:"active"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Base
|
||||
Name string `json:"name"`
|
||||
Debt Price `gorm:"default:0" json:"debt"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Active bool `gorm:"default:true" json:"active"`
|
||||
Contact string `json:"contact"`
|
||||
}
|
||||
|
||||
type Category struct {
|
||||
Base
|
||||
Name string `json:"name"`
|
||||
Active bool `gorm:"default:true" json:"active"`
|
||||
}
|
||||
|
||||
type Product struct {
|
||||
Base
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"` // URL or Path
|
||||
Price Price `json:"price"`
|
||||
Quantity int `gorm:"default:0" json:"quantity"`
|
||||
CategoryID uint `gorm:"column:category" json:"category"`
|
||||
Cuisine bool `gorm:"default:false" json:"cuisine"`
|
||||
Active bool `gorm:"default:true" json:"active"`
|
||||
UnitOfMeasure *uint `gorm:"column:unit_of_measure" json:"unit_of_measure"`
|
||||
}
|
||||
|
||||
type ProductComponent struct {
|
||||
Base
|
||||
CompositeProductID uint `json:"composite_product"`
|
||||
ComponentProductID uint `json:"component_product"`
|
||||
QuantityRequired float64 `json:"quantity_required"`
|
||||
}
|
||||
|
||||
type Comanda struct {
|
||||
Base
|
||||
MesaID uint `json:"mesa"`
|
||||
UserID uint `json:"user"`
|
||||
TypePayID *uint `json:"type_pay"`
|
||||
ClientID *uint `json:"client"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"` // OPEN, CLOSED, FIADO
|
||||
DtOpen time.Time `json:"dt_open"`
|
||||
DtClose *time.Time `json:"dt_close"`
|
||||
|
||||
// Nested items from Django (not stored in coma nda table)
|
||||
Items []ProductComanda `gorm:"-" json:"items"`
|
||||
}
|
||||
|
||||
type ProductComanda struct {
|
||||
Base
|
||||
ComandaID uint `json:"comanda"`
|
||||
ProductID uint `json:"product"`
|
||||
ProductName string `gorm:"-" json:"product_name"` // Read-only
|
||||
ProductPrice Price `gorm:"-" json:"product_price"` // Read-only
|
||||
DateTime time.Time `json:"data_time"`
|
||||
Applicant string `json:"applicant"`
|
||||
Obs string `gorm:"-" json:"obs"` // From Order
|
||||
}
|
||||
|
||||
type Order struct {
|
||||
Base
|
||||
ProductComandaID *uint `json:"productComanda"`
|
||||
ProductID uint `json:"id_product"`
|
||||
ComandaID uint `json:"id_comanda"`
|
||||
Obs string `json:"obs"`
|
||||
Queue time.Time `json:"queue"`
|
||||
Preparing *time.Time `json:"preparing"`
|
||||
Finished *time.Time `json:"finished"`
|
||||
Delivered *time.Time `json:"delivered"`
|
||||
Canceled *time.Time `json:"canceled"`
|
||||
}
|
||||
|
||||
func (o *Order) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
// Call Base.BeforeCreate to generate UUID
|
||||
if err := o.Base.BeforeCreate(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.Queue.IsZero() {
|
||||
o.Queue = time.Now()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type TypePay struct {
|
||||
Base
|
||||
Name string `json:"name"`
|
||||
Active bool `gorm:"default:true" json:"active"`
|
||||
}
|
||||
|
||||
type Payment struct {
|
||||
Base
|
||||
Value Price `json:"value"`
|
||||
TypePayID uint `json:"type_pay"`
|
||||
ComandaID uint `json:"comanda"`
|
||||
ClientID *uint `json:"client"`
|
||||
Description string `json:"description"`
|
||||
DateTime time.Time `json:"datetime"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Base
|
||||
Username string `gorm:"uniqueIndex" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
IsStaff bool `json:"is_staff"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type SyncState struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
LastSyncID int `gorm:"default:0"`
|
||||
}
|
||||
|
||||
type ChangeLog struct {
|
||||
ID int `json:"id"`
|
||||
ModelName string `json:"model_name"`
|
||||
ObjectID uint `json:"object_id"`
|
||||
Action string `json:"action"` // SAVE or DELETE
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
410
rrbec_server/internal/repository/repository.go
Normal file
410
rrbec_server/internal/repository/repository.go
Normal file
@@ -0,0 +1,410 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
"rrbec_server/internal/models"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db}
|
||||
}
|
||||
|
||||
// Mesa
|
||||
func (r *Repository) GetMesas() ([]models.Mesa, error) {
|
||||
var mesas []models.Mesa
|
||||
err := r.db.Find(&mesas).Error
|
||||
return mesas, err
|
||||
}
|
||||
|
||||
func (r *Repository) CreateMesa(mesa *models.Mesa) error {
|
||||
return r.db.Create(mesa).Error
|
||||
}
|
||||
|
||||
func (r *Repository) SaveMesa(mesa *models.Mesa) error {
|
||||
return r.db.Save(mesa).Error
|
||||
}
|
||||
|
||||
// Client
|
||||
func (r *Repository) GetClients() ([]models.Client, error) {
|
||||
var clients []models.Client
|
||||
err := r.db.Find(&clients).Error
|
||||
return clients, err
|
||||
}
|
||||
|
||||
func (r *Repository) CreateClient(client *models.Client) error {
|
||||
return r.db.Create(client).Error
|
||||
}
|
||||
|
||||
func (r *Repository) SaveClient(client *models.Client) error {
|
||||
return r.db.Save(client).Error
|
||||
}
|
||||
|
||||
// Product
|
||||
func (r *Repository) GetProducts() ([]models.Product, error) {
|
||||
var products []models.Product
|
||||
err := r.db.Find(&products).Error
|
||||
return products, err
|
||||
}
|
||||
|
||||
func (r *Repository) SaveProduct(product *models.Product) error {
|
||||
return r.db.Save(product).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetProductByID(id uint) (*models.Product, error) {
|
||||
var product models.Product
|
||||
err := r.db.First(&product, id).Error
|
||||
return &product, err
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateProductFields(id uint, updates map[string]interface{}) error {
|
||||
updates["sincronizado"] = false
|
||||
return r.db.Model(&models.Product{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (r *Repository) DeductStock(productID uint, quantity int) error {
|
||||
return r.db.Model(&models.Product{}).Where("id = ?", productID).
|
||||
Update("quantity", gorm.Expr("quantity - ?", quantity)).Error
|
||||
}
|
||||
|
||||
func (r *Repository) RestoreStock(productID uint, quantity int) error {
|
||||
return r.db.Model(&models.Product{}).Where("id = ?", productID).
|
||||
Update("quantity", gorm.Expr("quantity + ?", quantity)).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetProductComponents(productID uint) ([]models.ProductComponent, error) {
|
||||
var components []models.ProductComponent
|
||||
err := r.db.Where("composite_product = ?", productID).Find(&components).Error
|
||||
return components, err
|
||||
}
|
||||
|
||||
func (r *Repository) CreateProduct(product *models.Product) error {
|
||||
return r.db.Create(product).Error
|
||||
}
|
||||
|
||||
// Comanda
|
||||
func (r *Repository) GetComandas() ([]models.Comanda, error) {
|
||||
var comandas []models.Comanda
|
||||
if err := r.db.Find(&comandas).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For each comanda, load its items
|
||||
for i := range comandas {
|
||||
var items []models.ProductComanda
|
||||
r.db.Where("comanda_id = ?", comandas[i].ID).Find(&items)
|
||||
comandas[i].Items = items
|
||||
}
|
||||
|
||||
return comandas, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateComanda(comanda *models.Comanda) error {
|
||||
return r.db.Create(comanda).Error
|
||||
}
|
||||
|
||||
func (r *Repository) SaveComanda(comanda *models.Comanda) error {
|
||||
return r.db.Save(comanda).Error
|
||||
}
|
||||
|
||||
func (r *Repository) SaveCategory(cat *models.Category) error {
|
||||
return r.db.Save(cat).Error
|
||||
}
|
||||
|
||||
func (r *Repository) CreateCategory(cat *models.Category) error {
|
||||
return r.db.Create(cat).Error
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateCategoryFields(id uint, updates map[string]interface{}) error {
|
||||
return r.db.Model(&models.Category{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (r *Repository) SaveTypePay(tp *models.TypePay) error {
|
||||
return r.db.Save(tp).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetCategories() ([]models.Category, error) {
|
||||
var categories []models.Category
|
||||
err := r.db.Find(&categories).Error
|
||||
return categories, err
|
||||
}
|
||||
|
||||
func (r *Repository) GetTypePayments() ([]models.TypePay, error) {
|
||||
var types []models.TypePay
|
||||
err := r.db.Find(&types).Error
|
||||
return types, err
|
||||
}
|
||||
|
||||
func (r *Repository) GetComandaByID(id uint) (*models.Comanda, error) {
|
||||
var comanda models.Comanda
|
||||
if err := r.db.First(&comanda, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var items []models.ProductComanda
|
||||
r.db.Where("comanda_id = ?", comanda.ID).Find(&items)
|
||||
comanda.Items = items
|
||||
|
||||
return &comanda, nil
|
||||
}
|
||||
|
||||
// Items
|
||||
func (r *Repository) AddItemToComanda(item *models.ProductComanda) error {
|
||||
return r.db.Create(item).Error
|
||||
}
|
||||
|
||||
func (r *Repository) SaveProductComanda(item *models.ProductComanda) error {
|
||||
return r.db.Save(item).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetItemsByComanda(comandaID uint) ([]models.ProductComanda, error) {
|
||||
var items []models.ProductComanda
|
||||
err := r.db.Where("comanda_id = ?", comandaID).Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteItem(itemID uint) error {
|
||||
return r.db.Delete(&models.ProductComanda{}, itemID).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetOrders() ([]models.Order, error) {
|
||||
var orders []models.Order
|
||||
err := r.db.Find(&orders).Error
|
||||
return orders, err
|
||||
}
|
||||
|
||||
func (r *Repository) SaveOrder(order *models.Order) error {
|
||||
return r.db.Save(order).Error
|
||||
}
|
||||
|
||||
func (r *Repository) CreateOrder(order *models.Order) error {
|
||||
return r.db.Create(order).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetOrderByPC(pcID uint) (*models.Order, error) {
|
||||
var order models.Order
|
||||
err := r.db.Where("product_comanda_id = ?", pcID).First(&order).Error
|
||||
return &order, err
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateOrder(order *models.Order) error {
|
||||
return r.db.Save(order).Error
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateOrderFields(id uint, updates map[string]interface{}) error {
|
||||
mappedUpdates := make(map[string]interface{})
|
||||
for k, v := range updates {
|
||||
switch k {
|
||||
case "productComanda":
|
||||
mappedUpdates["product_comanda_id"] = v
|
||||
case "id_product":
|
||||
mappedUpdates["product_id"] = v
|
||||
case "id_comanda":
|
||||
mappedUpdates["comanda_id"] = v
|
||||
default:
|
||||
mappedUpdates[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
mappedUpdates["sincronizado"] = false
|
||||
|
||||
return r.db.Model(&models.Order{}).Where("id = ?", id).Updates(mappedUpdates).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetPayments() ([]models.Payment, error) {
|
||||
var payments []models.Payment
|
||||
err := r.db.Find(&payments).Error
|
||||
return payments, err
|
||||
}
|
||||
|
||||
func (r *Repository) CreatePayment(payment *models.Payment) error {
|
||||
return r.db.Create(payment).Error
|
||||
}
|
||||
|
||||
func (r *Repository) SavePayment(payment *models.Payment) error {
|
||||
return r.db.Save(payment).Error
|
||||
}
|
||||
|
||||
// Sync
|
||||
func (r *Repository) GetUnsynced() (map[string][]interface{}, error) {
|
||||
// This is a simplified helper to find anything not synced.
|
||||
// In a real app, you'd iterate per table.
|
||||
return nil, nil // Placeholder for sync worker logic
|
||||
}
|
||||
|
||||
func (r *Repository) MarkAsSynced(model interface{}, id uint) error {
|
||||
return r.db.Model(model).Where("id = ?", id).Update("sincronizado", true).Error
|
||||
}
|
||||
|
||||
func (r *Repository) SafeChangeID(tableName string, oldID, newID uint) error {
|
||||
if oldID == newID {
|
||||
return nil
|
||||
}
|
||||
|
||||
var count int64
|
||||
r.db.Table(tableName).Where("id = ?", newID).Count(&count)
|
||||
if count > 0 {
|
||||
squatterID := newID + 1000000
|
||||
for {
|
||||
var sCount int64
|
||||
r.db.Table(tableName).Where("id = ?", squatterID).Count(&sCount)
|
||||
if sCount == 0 {
|
||||
break
|
||||
}
|
||||
squatterID++
|
||||
}
|
||||
|
||||
log.Printf("COLLISION: ID %d in %s taken! Moving squatter to %d", newID, tableName, squatterID)
|
||||
r.db.Exec(fmt.Sprintf("UPDATE %s SET id = ? WHERE id = ?", tableName), squatterID, newID)
|
||||
|
||||
if tableName == "comandas" {
|
||||
r.UpdateFK("product_comandas", "comanda_id", newID, squatterID)
|
||||
r.UpdateFK("orders", "id_comanda", newID, squatterID)
|
||||
r.UpdateFK("payments", "comanda_id", newID, squatterID)
|
||||
} else if tableName == "product_comandas" {
|
||||
r.UpdateFK("orders", "product_comanda_id", newID, squatterID)
|
||||
}
|
||||
}
|
||||
|
||||
return r.UpdateID(tableName, oldID, newID)
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateID(tableName string, oldID, newID uint) error {
|
||||
return r.db.Exec(fmt.Sprintf("UPDATE %s SET id = ? WHERE id = ?", tableName), newID, oldID).Error
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateFK(tableName, fkColumn string, oldID, newID uint) error {
|
||||
return r.db.Exec(fmt.Sprintf("UPDATE %s SET %s = ? WHERE %s = ?", tableName, fkColumn, fkColumn), newID, oldID).Error
|
||||
}
|
||||
|
||||
// User
|
||||
func (r *Repository) GetUserByUsername(username string) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.Where("username = ?", username).First(&user).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (r *Repository) SaveUser(user *models.User) error {
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateComandaStatus(id uint, status string) error {
|
||||
return r.db.Model(&models.Comanda{}).Where("id = ?", id).Updates(map[string]interface{}{"status": status, "sincronizado": false}).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ClearComandaItems(comandaID uint) error {
|
||||
return r.db.Where("comanda_id = ?", comandaID).Delete(&models.ProductComanda{}).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetLastSyncID() int {
|
||||
var state models.SyncState
|
||||
r.db.FirstOrCreate(&state)
|
||||
return state.LastSyncID
|
||||
}
|
||||
|
||||
func (r *Repository) SaveLastSyncID(id int) {
|
||||
r.db.Model(&models.SyncState{}).Where("id = ?", 1).Update("last_sync_id", id)
|
||||
}
|
||||
|
||||
func (r *Repository) GetUnsyncedComandas() ([]models.Comanda, error) {
|
||||
var results []models.Comanda
|
||||
err := r.db.Where("sincronizado = ?", false).Find(&results).Error
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *Repository) GetUnsyncedItems() ([]models.ProductComanda, error) {
|
||||
var results []models.ProductComanda
|
||||
err := r.db.Where("sincronizado = ?", false).Find(&results).Error
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *Repository) GetUnsyncedOrders() ([]models.Order, error) {
|
||||
var results []models.Order
|
||||
err := r.db.Where("sincronizado = ?", false).Find(&results).Error
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *Repository) GetUnsyncedPayments() ([]models.Payment, error) {
|
||||
var results []models.Payment
|
||||
err := r.db.Where("sincronizado = ?", false).Find(&results).Error
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *Repository) GetUnsyncedProducts() ([]models.Product, error) {
|
||||
var results []models.Product
|
||||
err := r.db.Where("sincronizado = ?", false).Find(&results).Error
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *Repository) GetComandaToSync(id uint, dest *models.Comanda) error {
|
||||
return r.db.First(dest, id).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetItemToSync(id uint, dest *models.ProductComanda) error {
|
||||
return r.db.First(dest, id).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetPaymentToSync(id uint, dest *models.Payment) error {
|
||||
return r.db.First(dest, id).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetProductToSync(id uint, dest *models.Product) error {
|
||||
return r.db.First(dest, id).Error
|
||||
}
|
||||
|
||||
func (r *Repository) GetOrderToSync(id uint, dest *models.Order) error {
|
||||
return r.db.First(dest, id).Error
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteByID(modelName string, id uint) error {
|
||||
var model interface{}
|
||||
switch modelName {
|
||||
case "Product":
|
||||
model = &models.Product{}
|
||||
case "Comanda":
|
||||
model = &models.Comanda{}
|
||||
case "ProductComanda":
|
||||
model = &models.ProductComanda{}
|
||||
case "Order":
|
||||
model = &models.Order{}
|
||||
case "Client":
|
||||
model = &models.Client{}
|
||||
case "Categories":
|
||||
model = &models.Category{}
|
||||
case "Mesa":
|
||||
model = &models.Mesa{}
|
||||
case "Payments":
|
||||
model = &models.Payment{}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return r.db.Delete(model, id).Error
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateComandaFields(id uint, updates map[string]interface{}) error {
|
||||
mappedUpdates := make(map[string]interface{})
|
||||
for k, v := range updates {
|
||||
switch k {
|
||||
case "mesa":
|
||||
mappedUpdates["mesa_id"] = v
|
||||
case "user":
|
||||
mappedUpdates["user_id"] = v
|
||||
case "client":
|
||||
mappedUpdates["client_id"] = v
|
||||
case "type_pay":
|
||||
mappedUpdates["type_pay_id"] = v
|
||||
default:
|
||||
mappedUpdates[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as unsynced so the background worker pushes the update
|
||||
mappedUpdates["sincronizado"] = false
|
||||
|
||||
return r.db.Model(&models.Comanda{}).Where("id = ?", id).Updates(mappedUpdates).Error
|
||||
}
|
||||
280
rrbec_server/internal/service/service.go
Normal file
280
rrbec_server/internal/service/service.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"log"
|
||||
"rrbec_server/internal/models"
|
||||
"rrbec_server/internal/repository"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *repository.Repository
|
||||
}
|
||||
|
||||
func NewService(repo *repository.Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
// Comanda Logic
|
||||
func (s *Service) CreateComanda(comanda *models.Comanda) error {
|
||||
comanda.DtOpen = time.Now()
|
||||
comanda.Status = "OPEN"
|
||||
return s.repo.CreateComanda(comanda)
|
||||
}
|
||||
|
||||
// Item Addition and Stock Logic
|
||||
func (s *Service) AddItemToComanda(comandaID, productID uint, applicant string) (*models.ProductComanda, error) {
|
||||
comanda, err := s.repo.GetComandaByID(comandaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if comanda.Status != "OPEN" {
|
||||
return nil, errors.New("cannot add items to a closed or fiado comanda")
|
||||
}
|
||||
|
||||
product, err := s.repo.GetProductByID(productID)
|
||||
if err != nil {
|
||||
return nil, errors.New("product not found")
|
||||
}
|
||||
|
||||
if product.Quantity <= 0 {
|
||||
return nil, errors.New("product out of stock")
|
||||
}
|
||||
|
||||
item := &models.ProductComanda{
|
||||
ComandaID: comandaID,
|
||||
ProductID: productID,
|
||||
ProductName: product.Name,
|
||||
ProductPrice: product.Price,
|
||||
DateTime: time.Now(),
|
||||
Applicant: applicant,
|
||||
}
|
||||
err = s.repo.AddItemToComanda(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.deductProductStock(productID, 1); err != nil {
|
||||
log.Printf("Warning: failed to deduct stock for product %d: %v", productID, err)
|
||||
}
|
||||
|
||||
if product.Cuisine {
|
||||
order := &models.Order{
|
||||
ProductComandaID: &item.ID,
|
||||
ProductID: productID,
|
||||
ComandaID: comandaID,
|
||||
Queue: time.Now(),
|
||||
}
|
||||
s.repo.CreateOrder(order)
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *Service) deductProductStock(productID uint, quantity int) error {
|
||||
components, err := s.repo.GetProductComponents(productID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(components) > 0 {
|
||||
for _, comp := range components {
|
||||
if err := s.repo.DeductStock(comp.ComponentProductID, int(comp.QuantityRequired)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.repo.DeductStock(productID, quantity)
|
||||
}
|
||||
|
||||
func (s *Service) AddItemToComandaRaw(item *models.ProductComanda) error {
|
||||
comanda, err := s.repo.GetComandaByID(item.ComandaID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if comanda.Status != "OPEN" {
|
||||
return errors.New("cannot add items to a closed or fiado comanda")
|
||||
}
|
||||
|
||||
product, err := s.repo.GetProductByID(item.ProductID)
|
||||
if err != nil {
|
||||
return errors.New("product not found")
|
||||
}
|
||||
|
||||
item.ProductName = product.Name
|
||||
item.ProductPrice = product.Price
|
||||
item.DateTime = time.Now()
|
||||
|
||||
return s.repo.AddItemToComanda(item)
|
||||
}
|
||||
|
||||
// Sync Logic Placeholder
|
||||
func (s *Service) SyncWithCloud() error {
|
||||
// Task for the background worker
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetProducts() ([]models.Product, error) {
|
||||
return s.repo.GetProducts()
|
||||
}
|
||||
|
||||
func (s *Service) CreateProduct(product *models.Product) error {
|
||||
return s.repo.CreateProduct(product)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateProduct(id uint, updates map[string]interface{}) error {
|
||||
return s.repo.UpdateProductFields(id, updates)
|
||||
}
|
||||
|
||||
func (s *Service) GetMesas() ([]models.Mesa, error) {
|
||||
return s.repo.GetMesas()
|
||||
}
|
||||
|
||||
func (s *Service) GetComandas() ([]models.Comanda, error) {
|
||||
return s.repo.GetComandas()
|
||||
}
|
||||
|
||||
func (s *Service) GetComandaByID(id uint) (*models.Comanda, error) {
|
||||
return s.repo.GetComandaByID(id)
|
||||
}
|
||||
|
||||
func (s *Service) GetCategories() ([]models.Category, error) {
|
||||
return s.repo.GetCategories()
|
||||
}
|
||||
|
||||
func (s *Service) CreateCategory(cat *models.Category) error {
|
||||
return s.repo.CreateCategory(cat)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateCategory(id uint, updates map[string]interface{}) error {
|
||||
return s.repo.UpdateCategoryFields(id, updates)
|
||||
}
|
||||
|
||||
func (s *Service) GetTypePayments() ([]models.TypePay, error) {
|
||||
return s.repo.GetTypePayments()
|
||||
}
|
||||
|
||||
func (s *Service) GetClients() ([]models.Client, error) {
|
||||
return s.repo.GetClients()
|
||||
}
|
||||
|
||||
func (s *Service) GetOrders() ([]models.Order, error) {
|
||||
return s.repo.GetOrders()
|
||||
}
|
||||
|
||||
func (s *Service) CreateOrder(order *models.Order) error {
|
||||
order.Queue = time.Now()
|
||||
return s.repo.CreateOrder(order)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateOrder(id uint, updates map[string]interface{}) error {
|
||||
return s.repo.UpdateOrderFields(id, updates)
|
||||
}
|
||||
|
||||
func (s *Service) SetOrderPreparing(id uint) error {
|
||||
now := time.Now()
|
||||
return s.repo.UpdateOrderFields(id, map[string]interface{}{"preparing": &now})
|
||||
}
|
||||
|
||||
func (s *Service) SetOrderFinished(id uint) error {
|
||||
now := time.Now()
|
||||
return s.repo.UpdateOrderFields(id, map[string]interface{}{"finished": &now})
|
||||
}
|
||||
|
||||
func (s *Service) SetOrderDelivered(id uint) error {
|
||||
now := time.Now()
|
||||
return s.repo.UpdateOrderFields(id, map[string]interface{}{"delivered": &now})
|
||||
}
|
||||
|
||||
func (s *Service) SetOrderCanceled(id uint) error {
|
||||
now := time.Now()
|
||||
return s.repo.UpdateOrderFields(id, map[string]interface{}{"canceled": &now})
|
||||
}
|
||||
|
||||
func (s *Service) GetPayments() ([]models.Payment, error) {
|
||||
return s.repo.GetPayments()
|
||||
}
|
||||
|
||||
func (s *Service) DeleteItem(itemID uint) error {
|
||||
// 1. Check if there's an associated Order (Kitchen)
|
||||
order, err := s.repo.GetOrderByPC(itemID)
|
||||
if err == nil && order != nil {
|
||||
// 2. Mark as Canceled
|
||||
now := time.Now()
|
||||
order.Canceled = &now
|
||||
s.repo.UpdateOrder(order)
|
||||
}
|
||||
|
||||
// 3. Delete the item
|
||||
return s.repo.DeleteItem(itemID)
|
||||
}
|
||||
|
||||
func (s *Service) ClearComanda(id uint) error {
|
||||
if err := s.repo.ClearComandaItems(id); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.repo.UpdateComandaStatus(id, "CLOSED")
|
||||
}
|
||||
|
||||
func (s *Service) PagarComanda(id uint, payment *models.Payment, status string) error {
|
||||
payment.ComandaID = id
|
||||
payment.DateTime = time.Now()
|
||||
|
||||
if err := s.repo.CreatePayment(payment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if status == "" {
|
||||
status = "CLOSED"
|
||||
}
|
||||
return s.repo.UpdateComandaStatus(id, status)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateComanda(id uint, updates map[string]interface{}) error {
|
||||
return s.repo.UpdateComandaFields(id, updates)
|
||||
}
|
||||
|
||||
// User Auth
|
||||
func (s *Service) Login(username, password string) (*models.User, error) {
|
||||
user, err := s.repo.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
if s.CheckDjangoPassword(password, user.Password) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
func (s *Service) CheckDjangoPassword(password, hash string) bool {
|
||||
parts := strings.Split(hash, "$")
|
||||
if len(parts) != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
algorithm := parts[0]
|
||||
if algorithm != "pbkdf2_sha256" {
|
||||
return false
|
||||
}
|
||||
|
||||
var iterations int
|
||||
fmt.Sscanf(parts[1], "%d", &iterations)
|
||||
salt := parts[2]
|
||||
djangoHash := parts[3]
|
||||
|
||||
// PBKDF2 with SHA256
|
||||
dk := pbkdf2.Key([]byte(password), []byte(salt), iterations, 32, sha256.New)
|
||||
encoded := base64.StdEncoding.EncodeToString(dk)
|
||||
|
||||
return encoded == djangoHash
|
||||
}
|
||||
871
rrbec_server/internal/sync/syncer.go
Normal file
871
rrbec_server/internal/sync/syncer.go
Normal file
@@ -0,0 +1,871 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"rrbec_server/internal/models"
|
||||
"rrbec_server/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Syncer struct {
|
||||
repo *repository.Repository
|
||||
djangoURL string
|
||||
masterUser string
|
||||
masterPass string
|
||||
accessToken string
|
||||
refreshToken string
|
||||
}
|
||||
|
||||
func NewSyncer(repo *repository.Repository, djangoURL, masterUser, masterPass string) *Syncer {
|
||||
return &Syncer{
|
||||
repo: repo,
|
||||
djangoURL: djangoURL,
|
||||
masterUser: masterUser,
|
||||
masterPass: masterPass,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Syncer) Start() {
|
||||
go func() {
|
||||
// 1. Login to Django
|
||||
token, err := s.LoginToDjango()
|
||||
if err != nil {
|
||||
log.Printf("Initial Django login failed: %v", err)
|
||||
} else {
|
||||
s.masterPass = token // Use token for future requests
|
||||
|
||||
// 2. Load Last Sync ID
|
||||
lastID := s.repo.GetLastSyncID()
|
||||
if lastID == 0 {
|
||||
log.Println("Database is empty or fresh. Starting initial sync...")
|
||||
s.InitialSync()
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Sync Loop (Bidirectional)
|
||||
for {
|
||||
log.Println("Sync cycle started...")
|
||||
s.processChangeLog() // Cloud -> Local
|
||||
s.pushLocalChanges() // Local -> Cloud
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Syncer) ensureValidToken() {
|
||||
if s.accessToken == "" {
|
||||
token, err := s.LoginToDjango()
|
||||
if err != nil {
|
||||
log.Printf("Failed to login: %v", err)
|
||||
return
|
||||
}
|
||||
s.accessToken = token
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Syncer) ensureValidTokenWithRefresh() {
|
||||
s.ensureValidToken()
|
||||
if s.refreshToken == "" {
|
||||
return
|
||||
}
|
||||
|
||||
resp := s.fetchFromDjangoRaw("products")
|
||||
if s.IsTokenExpired(resp) {
|
||||
if err := s.RefreshAccessToken(); err != nil {
|
||||
log.Printf("Token refresh failed, trying full login: %v", err)
|
||||
s.accessToken = ""
|
||||
s.ensureValidToken()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Syncer) LoginToDjango() (string, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/token/", s.djangoURL)
|
||||
loginData := map[string]string{
|
||||
"username": s.masterUser,
|
||||
"password": s.masterPass,
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(loginData)
|
||||
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("login failed with status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Access string `json:"access"`
|
||||
Refresh string `json:"refresh"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
s.accessToken = result.Access
|
||||
s.refreshToken = result.Refresh
|
||||
|
||||
return result.Access, nil
|
||||
}
|
||||
|
||||
func (s *Syncer) RefreshAccessToken() error {
|
||||
url := fmt.Sprintf("%s/api/v1/token/refresh/", s.djangoURL)
|
||||
refreshData := map[string]string{
|
||||
"refresh": s.refreshToken,
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(refreshData)
|
||||
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("Token refresh failed: %s", string(body))
|
||||
return fmt.Errorf("refresh failed with status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Access string `json:"access"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.accessToken = result.Access
|
||||
log.Println("Token refreshed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Syncer) IsTokenExpired(responseBody []byte) bool {
|
||||
var errResp struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if json.Unmarshal(responseBody, &errResp) == nil {
|
||||
return errResp.Code == "token_not_valid" || strings.Contains(string(responseBody), "token_not_valid")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Syncer) InitialSync() {
|
||||
// Sync Products
|
||||
var products []models.Product
|
||||
s.fetchFromDjango("products", &products)
|
||||
for _, p := range products {
|
||||
p.Sincronizado = true
|
||||
if p.UUID == "" {
|
||||
p.UUID = uuid.New().String()
|
||||
}
|
||||
s.repo.SaveProduct(&p)
|
||||
}
|
||||
|
||||
// Sync Clients
|
||||
var clients []models.Client
|
||||
s.fetchFromDjango("clients", &clients)
|
||||
for _, c := range clients {
|
||||
c.Sincronizado = true
|
||||
if c.UUID == "" {
|
||||
c.UUID = uuid.New().String()
|
||||
}
|
||||
s.repo.SaveClient(&c)
|
||||
}
|
||||
|
||||
// Sync Mesas
|
||||
var mesas []models.Mesa
|
||||
s.fetchFromDjango("mesas", &mesas)
|
||||
for _, m := range mesas {
|
||||
m.Sincronizado = true
|
||||
if m.UUID == "" {
|
||||
m.UUID = uuid.New().String()
|
||||
}
|
||||
s.repo.SaveMesa(&m)
|
||||
}
|
||||
|
||||
// Sync Users
|
||||
var users []models.User
|
||||
s.fetchFromDjango("users", &users)
|
||||
for _, u := range users {
|
||||
u.Sincronizado = true
|
||||
if u.UUID == "" {
|
||||
u.UUID = uuid.New().String()
|
||||
}
|
||||
s.repo.SaveUser(&u)
|
||||
}
|
||||
|
||||
// Sync Categories
|
||||
var categories []models.Category
|
||||
s.fetchFromDjango("categories", &categories)
|
||||
for _, cat := range categories {
|
||||
cat.Sincronizado = true
|
||||
if cat.UUID == "" {
|
||||
cat.UUID = uuid.New().String()
|
||||
}
|
||||
s.repo.SaveCategory(&cat)
|
||||
}
|
||||
|
||||
// Sync Payment Types
|
||||
var paymentTypes []models.TypePay
|
||||
s.fetchFromDjango("payment-types", &paymentTypes)
|
||||
for _, tp := range paymentTypes {
|
||||
tp.Sincronizado = true
|
||||
if tp.UUID == "" {
|
||||
tp.UUID = uuid.New().String()
|
||||
}
|
||||
s.repo.SaveTypePay(&tp)
|
||||
}
|
||||
|
||||
// Sync Comandas
|
||||
var comandas []models.Comanda
|
||||
s.fetchFromDjango("comandas", &comandas)
|
||||
for _, co := range comandas {
|
||||
co.Sincronizado = true
|
||||
if co.UUID == "" {
|
||||
co.UUID = uuid.New().String()
|
||||
}
|
||||
s.repo.SaveComanda(&co)
|
||||
|
||||
// Sync nested items from the serializer
|
||||
for _, item := range co.Items {
|
||||
item.Sincronizado = true
|
||||
if item.UUID == "" {
|
||||
item.UUID = uuid.New().String()
|
||||
}
|
||||
s.repo.SaveProductComanda(&item)
|
||||
}
|
||||
}
|
||||
|
||||
// Sync Comanda Items
|
||||
var items []models.ProductComanda
|
||||
s.fetchFromDjango("items-comanda", &items)
|
||||
for _, it := range items {
|
||||
it.Sincronizado = true
|
||||
if it.UUID == "" {
|
||||
it.UUID = uuid.New().String()
|
||||
}
|
||||
s.repo.SaveProductComanda(&it)
|
||||
}
|
||||
|
||||
// Sync Orders
|
||||
var orders []models.Order
|
||||
s.fetchFromDjango("orders", &orders)
|
||||
for _, o := range orders {
|
||||
o.Sincronizado = true
|
||||
if o.UUID == "" {
|
||||
o.UUID = uuid.New().String()
|
||||
}
|
||||
s.repo.SaveOrder(&o)
|
||||
}
|
||||
|
||||
// Sync Payments
|
||||
var payments []models.Payment
|
||||
s.fetchFromDjango("payments", &payments)
|
||||
for _, pay := range payments {
|
||||
pay.Sincronizado = true
|
||||
if pay.UUID == "" {
|
||||
pay.UUID = uuid.New().String()
|
||||
}
|
||||
s.repo.SavePayment(&pay)
|
||||
}
|
||||
|
||||
log.Println("Initial sync completed successfully.")
|
||||
}
|
||||
|
||||
func (s *Syncer) fetchFromDjango(endpoint string, target interface{}) {
|
||||
endpoint = strings.Trim(endpoint, "/")
|
||||
|
||||
url := ""
|
||||
if strings.Contains(endpoint, "?") {
|
||||
url = fmt.Sprintf("%s/api/v1/%s", s.djangoURL, endpoint)
|
||||
} else {
|
||||
url = fmt.Sprintf("%s/api/v1/%s/", s.djangoURL, endpoint)
|
||||
}
|
||||
|
||||
token := s.accessToken
|
||||
if token == "" {
|
||||
token = s.masterPass
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch %s: %v", endpoint, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if err := json.Unmarshal(bodyBytes, target); err != nil {
|
||||
var paginated struct {
|
||||
Results json.RawMessage `json:"results"`
|
||||
}
|
||||
if err2 := json.Unmarshal(bodyBytes, &paginated); err2 == nil && paginated.Results != nil {
|
||||
if err3 := json.Unmarshal(paginated.Results, target); err3 != nil {
|
||||
log.Printf("Failed to decode paginated %s: %v", endpoint, err3)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Failed to decode %s: %v", endpoint, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Synchronized %s from cloud", endpoint)
|
||||
}
|
||||
|
||||
func (s *Syncer) fetchFromDjangoRaw(endpoint string) []byte {
|
||||
endpoint = strings.Trim(endpoint, "/")
|
||||
|
||||
url := ""
|
||||
if strings.Contains(endpoint, "?") {
|
||||
url = fmt.Sprintf("%s/api/v1/%s", s.djangoURL, endpoint)
|
||||
} else {
|
||||
url = fmt.Sprintf("%s/api/v1/%s/", s.djangoURL, endpoint)
|
||||
}
|
||||
|
||||
token := s.accessToken
|
||||
if token == "" {
|
||||
token = s.masterPass
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch %s: %v", endpoint, err)
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if s.IsTokenExpired(bodyBytes) {
|
||||
log.Printf("Token expired, refreshing...")
|
||||
if err := s.RefreshAccessToken(); err != nil {
|
||||
log.Printf("Token refresh failed: %v", err)
|
||||
} else {
|
||||
return s.fetchFromDjangoRaw(endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("DEBUG: Raw response for %s: %s", endpoint, string(bodyBytes))
|
||||
return bodyBytes
|
||||
}
|
||||
|
||||
func (s *Syncer) processChangeLog() {
|
||||
lastID := s.repo.GetLastSyncID()
|
||||
url := fmt.Sprintf("sync?since_id=%d", lastID)
|
||||
|
||||
var changes []models.ChangeLog
|
||||
respBody := s.fetchFromDjangoRaw(url)
|
||||
if respBody != nil {
|
||||
if err := json.Unmarshal(respBody, &changes); err != nil {
|
||||
log.Printf("ERROR: Failed to parse changelog: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("DEBUG: Received %d changes from Django", len(changes))
|
||||
|
||||
if len(changes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
maxID := lastID
|
||||
for _, change := range changes {
|
||||
log.Printf("Processing change %s for %s (ID: %d)", change.Action, change.ModelName, change.ObjectID)
|
||||
|
||||
if change.Action == "SAVE" {
|
||||
s.fetchAndSaveObject(change.ModelName, change.ObjectID)
|
||||
} else if change.Action == "DELETE" {
|
||||
s.repo.DeleteByID(change.ModelName, change.ObjectID)
|
||||
}
|
||||
|
||||
if change.ID > maxID {
|
||||
maxID = change.ID
|
||||
}
|
||||
}
|
||||
|
||||
s.repo.SaveLastSyncID(maxID)
|
||||
}
|
||||
|
||||
func (s *Syncer) fetchAndSaveObject(modelName string, id uint) {
|
||||
endpoint := ""
|
||||
switch modelName {
|
||||
case "Product":
|
||||
endpoint = "products"
|
||||
case "Comanda":
|
||||
endpoint = "comandas"
|
||||
case "ProductComanda":
|
||||
endpoint = "items-comanda"
|
||||
case "Order":
|
||||
endpoint = "orders"
|
||||
case "Client":
|
||||
endpoint = "clients"
|
||||
case "Categories":
|
||||
endpoint = "categories"
|
||||
case "Mesa":
|
||||
endpoint = "mesas"
|
||||
case "Payments":
|
||||
endpoint = "payments"
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%d", endpoint, id)
|
||||
|
||||
// Create temporary instance based on modelName
|
||||
switch modelName {
|
||||
case "Comanda":
|
||||
var obj models.Comanda
|
||||
s.fetchFromDjango(url, &obj)
|
||||
if obj.UUID == "" {
|
||||
var local models.Comanda
|
||||
if err := s.repo.GetComandaToSync(obj.ID, &local); err == nil {
|
||||
obj.UUID = local.UUID
|
||||
}
|
||||
if obj.UUID == "" {
|
||||
obj.UUID = uuid.New().String()
|
||||
}
|
||||
}
|
||||
obj.Sincronizado = true
|
||||
s.repo.SaveComanda(&obj)
|
||||
case "Product":
|
||||
var obj models.Product
|
||||
s.fetchFromDjango(url, &obj)
|
||||
if obj.UUID == "" {
|
||||
var local models.Product
|
||||
if err := s.repo.GetProductToSync(obj.ID, &local); err == nil {
|
||||
obj.UUID = local.UUID
|
||||
}
|
||||
if obj.UUID == "" {
|
||||
obj.UUID = uuid.New().String()
|
||||
}
|
||||
}
|
||||
obj.Sincronizado = true
|
||||
s.repo.SaveProduct(&obj)
|
||||
case "ProductComanda":
|
||||
var obj models.ProductComanda
|
||||
s.fetchFromDjango(url, &obj)
|
||||
if obj.UUID == "" {
|
||||
var local models.ProductComanda
|
||||
if err := s.repo.GetItemToSync(obj.ID, &local); err == nil {
|
||||
obj.UUID = local.UUID
|
||||
}
|
||||
if obj.UUID == "" {
|
||||
obj.UUID = uuid.New().String()
|
||||
}
|
||||
}
|
||||
obj.Sincronizado = true
|
||||
s.repo.SaveProductComanda(&obj)
|
||||
case "Order":
|
||||
var obj models.Order
|
||||
s.fetchFromDjango(url, &obj)
|
||||
if obj.UUID == "" {
|
||||
var local models.Order
|
||||
if err := s.repo.GetOrderToSync(obj.ID, &local); err == nil {
|
||||
obj.UUID = local.UUID
|
||||
}
|
||||
if obj.UUID == "" {
|
||||
obj.UUID = uuid.New().String()
|
||||
}
|
||||
}
|
||||
obj.Sincronizado = true
|
||||
s.repo.SaveOrder(&obj)
|
||||
case "Client":
|
||||
var obj models.Client
|
||||
s.fetchFromDjango(url, &obj)
|
||||
if obj.UUID == "" {
|
||||
obj.UUID = uuid.New().String()
|
||||
}
|
||||
obj.Sincronizado = true
|
||||
s.repo.SaveClient(&obj)
|
||||
case "Categories":
|
||||
var obj models.Category
|
||||
s.fetchFromDjango(url, &obj)
|
||||
if obj.UUID == "" {
|
||||
obj.UUID = uuid.New().String()
|
||||
}
|
||||
obj.Sincronizado = true
|
||||
s.repo.SaveCategory(&obj)
|
||||
case "Mesa":
|
||||
var obj models.Mesa
|
||||
s.fetchFromDjango(url, &obj)
|
||||
if obj.UUID == "" {
|
||||
obj.UUID = uuid.New().String()
|
||||
}
|
||||
obj.Sincronizado = true
|
||||
s.repo.SaveMesa(&obj)
|
||||
case "Payments":
|
||||
var obj models.Payment
|
||||
s.fetchFromDjango(url, &obj)
|
||||
if obj.UUID == "" {
|
||||
var local models.Payment
|
||||
if err := s.repo.GetPaymentToSync(obj.ID, &local); err == nil {
|
||||
obj.UUID = local.UUID
|
||||
}
|
||||
if obj.UUID == "" {
|
||||
obj.UUID = uuid.New().String()
|
||||
}
|
||||
}
|
||||
obj.Sincronizado = true
|
||||
s.repo.SavePayment(&obj)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Syncer) pushLocalChanges() {
|
||||
// 1. Sync Comandas
|
||||
comandas, err := s.repo.GetUnsyncedComandas()
|
||||
if err == nil && len(comandas) > 0 {
|
||||
for _, c := range comandas {
|
||||
if c.MesaID == 0 {
|
||||
log.Printf("SKIP: Comanda %d has mesa 0. Correct data required.", c.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Capture local ID before we might change it
|
||||
oldID := c.ID
|
||||
comandaName := c.Name
|
||||
if comandaName == "" {
|
||||
comandaName = fmt.Sprintf("Comanda %d", oldID)
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"mesa": c.MesaID,
|
||||
"client": c.ClientID,
|
||||
"user": c.UserID,
|
||||
"status": c.Status,
|
||||
"name": comandaName,
|
||||
"uuid": c.UUID,
|
||||
}
|
||||
|
||||
// Try PATCH first
|
||||
endpoint := fmt.Sprintf("comandas/%d", c.ID)
|
||||
respBody, err := s.sendRequest("PATCH", endpoint, payload)
|
||||
if err != nil && strings.Contains(err.Error(), "status: 404") {
|
||||
respBody, err = s.sendRequest("POST", "comandas", payload)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
var created models.Comanda
|
||||
json.Unmarshal(respBody, &created)
|
||||
|
||||
// CRITICAL: Update local ID and children safely
|
||||
if created.ID != 0 && created.ID != oldID {
|
||||
s.repo.SafeChangeID("comandas", oldID, created.ID)
|
||||
// Update items that were pointing to the old ID
|
||||
s.repo.UpdateFK("product_comandas", "comanda_id", oldID, created.ID)
|
||||
s.repo.UpdateFK("orders", "id_comanda", oldID, created.ID)
|
||||
s.repo.UpdateFK("payments", "comanda_id", oldID, created.ID)
|
||||
}
|
||||
|
||||
s.repo.MarkAsSynced(&models.Comanda{}, created.ID)
|
||||
log.Printf("SUCCESS: Pushed Comanda %d (now %d) to cloud", oldID, created.ID)
|
||||
} else {
|
||||
log.Printf("ERROR pushing Comanda %d: %v", oldID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Sync Items
|
||||
items, err := s.repo.GetUnsyncedItems()
|
||||
if err == nil && len(items) > 0 {
|
||||
for _, it := range items {
|
||||
oldID := it.ID
|
||||
payload := map[string]interface{}{
|
||||
"comanda": it.ComandaID,
|
||||
"product": it.ProductID,
|
||||
"applicant": it.Applicant,
|
||||
"uuid": it.UUID,
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("items-comanda/%d", it.ID)
|
||||
respBody, err := s.sendRequest("PATCH", endpoint, payload)
|
||||
if err != nil && strings.Contains(err.Error(), "status: 404") {
|
||||
respBody, err = s.sendRequest("POST", "items-comanda", payload)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
var created models.ProductComanda
|
||||
json.Unmarshal(respBody, &created)
|
||||
if created.ID != 0 && created.ID != oldID {
|
||||
s.repo.SafeChangeID("product_comandas", oldID, created.ID)
|
||||
s.repo.UpdateFK("orders", "product_comanda_id", oldID, created.ID)
|
||||
}
|
||||
s.repo.MarkAsSynced(&models.ProductComanda{}, created.ID)
|
||||
log.Printf("SUCCESS: Pushed Item %d (now %d) to cloud", oldID, created.ID)
|
||||
} else {
|
||||
log.Printf("ERROR pushing Item %d: %v", oldID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Sync Orders
|
||||
orders, err := s.repo.GetUnsyncedOrders()
|
||||
if err == nil && len(orders) > 0 {
|
||||
for _, o := range orders {
|
||||
oldID := o.ID
|
||||
payload := map[string]interface{}{
|
||||
"productComanda": o.ProductComandaID,
|
||||
"id_product": o.ProductID,
|
||||
"id_comanda": o.ComandaID,
|
||||
"obs": o.Obs,
|
||||
"uuid": o.UUID,
|
||||
}
|
||||
|
||||
// Optional: mapping timestamps if they exist
|
||||
if !o.Queue.IsZero() {
|
||||
payload["queue"] = o.Queue
|
||||
}
|
||||
if o.Preparing != nil {
|
||||
payload["preparing"] = o.Preparing
|
||||
}
|
||||
if o.Finished != nil {
|
||||
payload["finished"] = o.Finished
|
||||
}
|
||||
if o.Delivered != nil {
|
||||
payload["delivered"] = o.Delivered
|
||||
}
|
||||
if o.Canceled != nil {
|
||||
payload["canceled"] = o.Canceled
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("orders/%d", o.ID)
|
||||
respBody, err := s.sendRequest("PATCH", endpoint, payload)
|
||||
if err != nil && strings.Contains(err.Error(), "status: 404") {
|
||||
respBody, err = s.sendRequest("POST", "orders", payload)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
var created models.Order
|
||||
json.Unmarshal(respBody, &created)
|
||||
if created.ID != 0 && created.ID != oldID {
|
||||
s.repo.SafeChangeID("orders", oldID, created.ID)
|
||||
}
|
||||
s.repo.MarkAsSynced(&models.Order{}, created.ID)
|
||||
log.Printf("SUCCESS: Pushed Order %d (now %d) to cloud", oldID, created.ID)
|
||||
} else {
|
||||
log.Printf("ERROR pushing Order %d: %v", oldID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Sync Payments
|
||||
payments, err := s.repo.GetUnsyncedPayments()
|
||||
if err == nil && len(payments) > 0 {
|
||||
for _, p := range payments {
|
||||
if p.TypePayID == 0 {
|
||||
log.Printf("SKIP: Payment %d has invalid type_pay 0.", p.ID)
|
||||
s.repo.MarkAsSynced(&models.Payment{}, p.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
oldID := p.ID
|
||||
payload := map[string]interface{}{
|
||||
"value": p.Value,
|
||||
"type_pay": p.TypePayID,
|
||||
"comanda": p.ComandaID,
|
||||
"description": p.Description,
|
||||
"datetime": p.DateTime,
|
||||
"uuid": p.UUID,
|
||||
}
|
||||
if p.ClientID != nil {
|
||||
payload["client"] = *p.ClientID
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("payments/%d", p.ID)
|
||||
respBody, err := s.sendRequest("PATCH", endpoint, payload)
|
||||
if err != nil && strings.Contains(err.Error(), "status: 404") {
|
||||
respBody, err = s.sendRequest("POST", "payments", payload)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
var created models.Payment
|
||||
json.Unmarshal(respBody, &created)
|
||||
if created.ID != 0 && created.ID != oldID {
|
||||
s.repo.SafeChangeID("payments", oldID, created.ID)
|
||||
}
|
||||
s.repo.MarkAsSynced(&models.Payment{}, created.ID)
|
||||
log.Printf("SUCCESS: Pushed Payment %d (now %d) to cloud", oldID, created.ID)
|
||||
} else {
|
||||
log.Printf("ERROR pushing Payment %d: %v", oldID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Sync Products
|
||||
products, err := s.repo.GetUnsyncedProducts()
|
||||
if err == nil && len(products) > 0 {
|
||||
for _, p := range products {
|
||||
oldID := p.ID
|
||||
payload := map[string]interface{}{
|
||||
"name": p.Name,
|
||||
"description": p.Description,
|
||||
"image": p.Image,
|
||||
"price": p.Price,
|
||||
"quantity": p.Quantity,
|
||||
"category": p.CategoryID,
|
||||
"cuisine": p.Cuisine,
|
||||
"active": p.Active,
|
||||
"uuid": p.UUID,
|
||||
}
|
||||
if p.UnitOfMeasure != nil {
|
||||
payload["unit_of_measure"] = *p.UnitOfMeasure
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("products/%d", p.ID)
|
||||
respBody, err := s.sendMultipartRequest("PATCH", endpoint, payload)
|
||||
if err != nil && strings.Contains(err.Error(), "status: 404") {
|
||||
respBody, err = s.sendMultipartRequest("POST", "products", payload)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
var created models.Product
|
||||
json.Unmarshal(respBody, &created)
|
||||
if created.ID != 0 && created.ID != oldID {
|
||||
s.repo.SafeChangeID("products", oldID, created.ID)
|
||||
}
|
||||
s.repo.MarkAsSynced(&models.Product{}, created.ID)
|
||||
log.Printf("SUCCESS: Pushed Product %d (now %d) to cloud", oldID, created.ID)
|
||||
} else {
|
||||
log.Printf("ERROR pushing Product %d: %v", oldID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Syncer) sendRequest(method, endpoint string, data interface{}) ([]byte, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/%s/", s.djangoURL, endpoint)
|
||||
jsonData, _ := json.Marshal(data)
|
||||
|
||||
token := s.accessToken
|
||||
if token == "" {
|
||||
token = s.masterPass
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest(method, url, bytes.NewBuffer(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode == 401 || strings.Contains(string(respBody), "token_not_valid") {
|
||||
log.Printf("Token expired during %s %s, refreshing...", method, endpoint)
|
||||
if err := s.RefreshAccessToken(); err != nil {
|
||||
return nil, fmt.Errorf("token refresh failed: %w", err)
|
||||
}
|
||||
return s.sendRequest(method, endpoint, data)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
return respBody, nil
|
||||
}
|
||||
return nil, fmt.Errorf("django returned status: %d | Response: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
func (s *Syncer) sendMultipartRequest(method, endpoint string, data map[string]interface{}) ([]byte, error) {
|
||||
urlStr := fmt.Sprintf("%s/api/v1/%s/", s.djangoURL, endpoint)
|
||||
var b bytes.Buffer
|
||||
w := multipart.NewWriter(&b)
|
||||
|
||||
for key, val := range data {
|
||||
if key == "image" {
|
||||
imageStr, ok := val.(string)
|
||||
if !ok || imageStr == "" {
|
||||
continue // Skip empty image
|
||||
}
|
||||
|
||||
// If it's an existing URL, don't re-upload it
|
||||
if strings.HasPrefix(imageStr, "http") || strings.HasPrefix(imageStr, "/media/") || strings.HasPrefix(imageStr, "/images/") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Base64
|
||||
if strings.HasPrefix(imageStr, "data:image") {
|
||||
parts := strings.Split(imageStr, ",")
|
||||
if len(parts) == 2 {
|
||||
decoded, err := base64.StdEncoding.DecodeString(parts[1])
|
||||
if err == nil {
|
||||
fw, _ := w.CreateFormFile(key, "upload.jpg")
|
||||
fw.Write(decoded)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Local file
|
||||
fileContent, err := os.ReadFile(imageStr)
|
||||
if err == nil {
|
||||
fw, _ := w.CreateFormFile(key, filepath.Base(imageStr))
|
||||
fw.Write(fileContent)
|
||||
}
|
||||
} else {
|
||||
// standard text field
|
||||
w.WriteField(key, fmt.Sprintf("%v", val))
|
||||
}
|
||||
}
|
||||
w.Close()
|
||||
|
||||
token := s.accessToken
|
||||
if token == "" {
|
||||
token = s.masterPass
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest(method, urlStr, &b)
|
||||
req.Header.Set("Content-Type", w.FormDataContentType())
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode == 401 || strings.Contains(string(respBody), "token_not_valid") {
|
||||
log.Printf("Token expired during %s %s, refreshing...", method, urlStr)
|
||||
if err := s.RefreshAccessToken(); err != nil {
|
||||
return nil, fmt.Errorf("token refresh failed: %w", err)
|
||||
}
|
||||
return s.sendMultipartRequest(method, endpoint, data)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
return respBody, nil
|
||||
}
|
||||
return nil, fmt.Errorf("django returned status: %d | Response: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
func (s *Syncer) syncUsers() {
|
||||
// Optional: logic for background sync of users if needed
|
||||
}
|
||||
BIN
rrbec_server/main
Executable file
BIN
rrbec_server/main
Executable file
Binary file not shown.
BIN
rrbec_server/rrbec.db
Normal file
BIN
rrbec_server/rrbec.db
Normal file
Binary file not shown.
BIN
rrbec_server/server
Executable file
BIN
rrbec_server/server
Executable file
Binary file not shown.
Reference in New Issue
Block a user