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