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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user