- 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
281 lines
6.6 KiB
Go
281 lines
6.6 KiB
Go
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
|
|
}
|