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:
2026-04-04 17:38:40 -03:00
commit 936aad779b
20 changed files with 3159 additions and 0 deletions

View 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
}