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,111 @@
# Documentação do Projeto RRBEC (Gestão Raul)
Esta documentação fornece uma visão geral técnica e funcional do sistema **Gestão Raul**, abrangendo sua estrutura, regras de negócio, modelagem de banco de dados, APIs e mecanismos de autenticação.
---
## 1. Estrutura do Projeto
O projeto é baseado no framework **Django** (Python) e segue o padrão de arquitetura de aplicações modulares.
### Diretórios Principais
- `gestaoRaul/`: Configurações centrais do Django (settings, urls, wsgi).
- `products/`: Gestão de produtos, categorias e unidades de medida.
- `clients/`: Cadastro de clientes e controle de débitos ("Fiados").
- `mesas/`: Gestão das mesas físicas do estabelecimento.
- `comandas/`: Núcleo do sistema. Gerencia o consumo, estoque e fluxo de vendas.
- `orders/`: Controle da fila de produção (cozinha/bar).
- `payments/`: Registros de pagamentos e cálculos de taxas.
- `typePay/`: Tipos de pagamentos aceitos (Crédito, Débito, Pix, Fiado, etc.).
- `login/`: Lógica de autenticação e customização de tokens JWT.
- `templates/`: Arquivos HTML para a interface web (utiliza Vanilla CSS e JS).
- `media/`: Armazenamento de imagens de produtos.
---
## 2. Regras de Negócio
O sistema foi projetado para gerenciar o fluxo operacional de um restaurante/bar de ponta a ponta.
### Fluxo de Comanda e Vendas
1. **Abertura**: Uma comanda é vinculada a uma mesa e, opcionalmente, a um cliente. O status inicial é `OPEN`.
2. **Lançamento de Itens**: Ao adicionar um produto a uma comanda, o sistema automaticamente:
- Deduz a quantidade do produto do estoque.
- Se o produto for composto (combo/receita), deduz os componentes individuais.
- Se o produto tiver a flag `cuisine=True`, gera um pedido (`Order`) na fila da cozinha.
3. **Gestão de Estoque**: Todas as entradas e saídas são registradas na tabela de `StockMovement` para auditoria.
4. **Fechamento**: Ao realizar o pagamento (total ou parcial), a comanda pode ser encerrada (`CLOSED`).
5. **Taxa de Serviço**: O sistema calcula automaticamente uma taxa de 10% sobre o consumo total.
### Fila de Produção (Cozinha)
Os pedidos passam por estados cronometrados:
- `Queue` (Em espera) -> `Preparing` (Preparando) -> `Finished` (Pronto) -> `Delivered` (Entregue).
- Também é possível cancelar um pedido, o que gera um estorno automático para o estoque.
### Sistema de "Fiado" (Débitos de Clientes)
- Clientes podem ter comandas marcadas com o tipo de pagamento "FIADO".
- O sistema mantém um registro do débito acumulado do cliente.
- Existem endpoints específicos para listar e quitar múltiplos débitos de uma vez.
---
## 3. Modelagem do Banco de Dados
O banco de dados utiliza a seguinte estrutura relacional (simplificada):
### Principais Entidades
- **Mesa**: `id`, `name`, `active`.
- **Client**: `id`, `name`, `debt`, `contact`, `active`.
- **Product**: `id`, `name`, `price`, `quantity`, `category_id`, `cuisine` (boolean), `active`.
- **ProductComponent**: (Tabela intermediária de composição) Liga um `Product` a outros `Products` com uma `quantity_required`.
- **Comanda**: `id`, `mesa_id`, `client_id`, `user_id`, `status` (OPEN/CLOSED/FIADO), `dt_open`, `dt_close`.
- **ProductComanda**: `id`, `comanda_id`, `product_id`, `data_time`, `applicant`.
- **Order (Pedido)**: `id`, `productComanda_id`, `obs`, `queue`, `preparing`, `finished`, `delivered`, `canceled`.
- **Payments**: `id`, `comanda_id`, `type_pay_id`, `value`, `client_id`, `datetime`.
---
## 4. API e Autenticação
A API é construída com **Django REST Framework (DRF)** e segue os princípios REST.
### Autenticação
O sistema utiliza **JWT (JSON Web Token)** para proteger os endpoints.
1. **Obter Token**: `POST /api/v1/token/`
- Body: `{"username": "...", "password": "..."}`
- Retorno: `access` e `refresh` tokens, além de dados do usuário (ID, nome, grupos).
2. **Atualizar Token**: `POST /api/v1/token/refresh/`
- Body: `{"refresh": "..."}`
3. **Uso**: Deve-se enviar o token no cabeçalho: `Authorization: Bearer <seu_token>`.
### Endpoints Principais (`/api/v1/`)
| Endpoint | Método | Descrição |
| :--- | :--- | :--- |
| `comandas/` | GET/POST | Lista ou cria novas comandas. |
| `comandas/{id}/pagar/` | POST | Registra pagamento e fecha a comanda. |
| `comandas/{id}/apagar/` | POST | Limpa todos os itens e fecha a comanda (estorna estoque). |
| `items-comanda/` | POST/DELETE | Adiciona ou remove itens de uma comanda individualmente. |
| `orders/` | GET/PATCH | Lista pedidos da cozinha ou edita observações. |
| `orders/{id}/preparing/` | POST | Inicia o preparo do pedido. |
| `orders/{id}/finish/` | POST | Marca como pronto para entrega. |
| `clients/{id}/fiados/` | GET | Lista todas as comandas pendentes (fiado) de um cliente. |
| `clients/pagar_fiados/`| POST | Quita uma lista de IDs de comandas fiadas. |
| `products/` | GET | Lista produtos ativos e estoque. |
### Lógica de Negócio Integrada na API
- **POST `items-comanda/`**: Ao postar um item, a API automaticamente executa `StockMovement.subTransactionStock` e cria um `Order` se necessário.
- **DELETE `items-comanda/{id}/`**: Ao deletar, a API executa `StockMovement.sumTransactionStock` para devolver o item ao estoque.
---
## 5. Tecnologias Utilizadas
- **Backend**: Django 5.1, Django REST Framework, SimpleJWT.
- **Frontend**: HTML5, Vanilla CSS, JavaScript.
- **Service Worker**: Configurado para suporte a PWA (Progressive Web App).
- **Banco de Dados**: SQLite (Desenvolvimento) / PostgreSQL ou MySQL (Produção).
- **Middleware**: WhiteNoise (arquivos estáticos), CORS Headers.
---
*Atualizado em: 24 de Março de 2026*

135
GUIA_API.md Normal file
View File

@@ -0,0 +1,135 @@
# Guia de Teste da API com Postman
Este documento explica como testar os endpoints da API do sistema **Gestão Raul** utilizando o Postman.
---
## 1. Passo 1: Obter o Token de Acesso (Login)
Como a API é protegida, você precisa primeiro de um token de autorização.
1. Abra o **Postman** e crie uma nova aba de requisição.
2. Mude o método para **POST**.
3. Coloque a URL: `http://localhost:8000/api/v1/token/`
4. Vá na aba **Body**, selecione a opção **raw** e escolha **JSON** no menu à direita.
5. Insira suas credenciais no formato abaixo:
```json
{
"username": "seu_usuario_django",
"password": "sua_senha_django"
}
```
6. Clique em **Send**.
7. No resultado (JSON), copie o código que aparece no campo `"access"`.
---
## 2. Passo 2: Acessar os Endpoints da API
Agora que você tem o token, pode acessar qualquer endpoint protegido (ex: Pedidos).
1. Crie uma nova aba de requisição no Postman.
2. Mude o método para **GET**.
3. Coloque a URL do endpoint que deseja testar, por exemplo:
`http://localhost:8000/api/v1/orders/`
4. Vá na aba **Authorization** (fica logo abaixo da URL).
5. No campo **Type**, selecione **Bearer Token**.
6. No campo **Token** (à direita), cole o código `"access"` que você copiou no Passo 1.
7. Clique em **Send**.
Você deverá ver a lista de dados em formato JSON.
---
## Dicas Rápidas
- **Erro 401 Unauthorized:** Significa que o token não foi enviado corretamente ou expirou. Obtenha um novo no Passo 1.
- **CORS:** O backend está configurado para aceitar requisições de outras origens (CORS), permitindo que seu frontend se conecte normalmente.
- **Endpoints Disponíveis:**
- `/api/v1/orders/` (Pedidos)
- `/api/v1/products/` (Produtos)
- `/api/v1/clients/` (Clientes)
- `/api/v1/mesas/` (Mesas)
- `/api/v1/comandas/` (Comandas)
- `/api/v1/items-comanda/` (Itens de Comanda)
- `/api/v1/categories/` (Categorias)
- `/api/v1/payment-types/` (Tipos de Pagamento)
- `/api/v1/payments/` (Registro de Pagamentos)
---
## 3. Ações Especiais (Custom Actions)
### Apagar/Limpar Comanda Inteira
Esta ação exclui todos os itens vinculados à comanda e muda o seu status para `CLOSED`.
- **Método:** `POST`
- **URL:** `http://localhost:8000/api/v1/comandas/{ID_DA_COMANDA}/apagar/`
- **Auth:** Bearer Token necessário.
- **Body:** Vazio.
### Apagar um Item Individual da Comanda
Esta ação exclui um único item e devolve a quantidade correspondente ao estoque.
- **Método:** `DELETE`
- **URL:** `http://localhost:8000/api/v1/items-comanda/{ID_DO_ITEM}/`
- **Auth:** Bearer Token necessário.
- **Body:** Vazio.
### Pagar e Fechar Comanda
Esta ação registra um pagamento e fecha a comanda automaticamente.
- **Método:** `POST`
- **URL:** `http://localhost:8000/api/v1/comandas/{ID_DA_COMANDA}/pagar/`
- **Auth:** Bearer Token necessário.
- **Body (JSON):**
```json
{
"value": 50.00,
"type_pay": 1, // ID do tipo de pagamento
"client": null, // ID do cliente (opcional)
"description": "Pagamento total" // Opcional
}
```
### Listar Fiados de um Cliente
Retorna todas as comandas com status `FIADO` vinculadas a um cliente específico.
- **Método:** `GET`
- **URL:** `http://localhost:8000/api/v1/clients/{ID_DO_CLIENTE}/fiados/`
- **Auth:** Bearer Token necessário.
- **Body:** Vazio.
### Pagar Múltiplos Fiados
Recebe uma lista de IDs de comandas com status `FIADO`, gera os pagamentos e fecha todas.
- **Método:** `POST`
- **URL:** `http://localhost:8000/api/v1/clients/pagar_fiados/`
- **Auth:** Bearer Token necessário.
- **Body (JSON):**
```json
{
"ids": [10, 11, 15] // IDs das comandas que deseja pagar
}
```
---
## 4. Gerenciamento de Pedidos (Orders)
### Editar Observação do Pedido
Altera as observações de um pedido específico.
- **Método:** `PATCH`
- **URL:** `http://localhost:8000/api/v1/orders/{ID_DO_PEDIDO}/`
- **Auth:** Bearer Token necessário.
- **Body (JSON):**
```json
{
"obs": "Nova observação detalhada"
}
```
### Atualizar Status do Pedido
Ações para movimentar o fluxo da cozinha/atendimento.
- **Método:** `POST`
- **URLs:**
- `.../api/v1/orders/{ID_DO_PEDIDO}/preparing/` (Iniciar Preparo)
- `.../api/v1/orders/{ID_DO_PEDIDO}/finish/` (Marcar como Pronto)
- `.../api/v1/orders/{ID_DO_PEDIDO}/deliver/` (Marcar como Entregue)
- `.../api/v1/orders/{ID_DO_PEDIDO}/cancel/` (Cancelar Pedido)
- **Auth:** Bearer Token necessário.
- **Body:** Vazio.

54
SYNC_GO_MIDDLEWARE.md Normal file
View File

@@ -0,0 +1,54 @@
# Guia de Sincronização: Django <-> Middleware Go
Este documento descreve o funcionamento do sistema de **ChangeLog** implementado no Django para permitir que o middleware em Go mantenha uma cópia local (local-first) dos dados de forma eficiente.
## 1. Funcionamento Técnico
### Rastreamento de Mudanças (Django Signals)
Foi criada uma aplicação chamada `sync` que utiliza **Django Signals**. Sempre que um dos modelos abaixo é criado, editado ou excluído, uma entrada é gerada automaticamente na tabela `ChangeLog`:
- `Product`
- `Comanda`
- `ProductComanda`
- `Order`
- `Client`
- `Categories`
- `Mesa`
- `Payments`
### Tabela de Log (`ChangeLog`)
Cada registro no log contém:
- `id`: Identificador sequencial da mudança.
- `model_name`: Nome do modelo (ex: "Product").
- `object_id`: ID do objeto que mudou.
- `action`: "SAVE" (para criação/edição) ou "DELETE".
- `timestamp`: Quando a mudança ocorreu.
## 2. API de Sincronização
O middleware Go deve consumir o seguinte endpoint:
**Endpoint:** `GET /api/v1/sync/`
### Parâmetros:
- `since_id` (opcional): Retorna apenas mudanças com ID maior que este valor.
### Exemplo de Fluxo no Go:
1. **Estado Inicial**: O Go armazena o `last_sync_id` (começando em 0).
2. **Polling**: De tempos em tempos (ex: a cada 5 segundos), o Go chama:
`GET http://seu-servidor/api/v1/sync/?since_id=100` (supondo que o último ID processado foi 100).
3. **Processamento**:
- O Django retorna uma lista de mudanças (ex: IDs 101, 102, 103).
- Para cada mudança `SAVE` no log, o Go deve fazer um `GET` no endpoint específico do modelo para buscar os dados atualizados:
- Se `model_name == "Product"`, buscar em `GET /api/v1/products/{object_id}/`.
- Para cada mudança `DELETE`, o Go deve remover o item correspondente do seu banco de dados local.
4. **Atualização**: O Go atualiza seu `last_sync_id` para o maior ID recebido (neste caso, 103).
## 3. Vantagens
- **Performance**: O Go não precisa baixar todos os produtos/pedidos toda vez. Ele só baixa o que realmente mudou.
- **Detecção de Deleção**: O sistema informa explicitamente o que foi apagado no Django.
- **Resiliência**: Se a conexão cair, ao reconectar, o Go apenas retoma a partir do último ID que ele conhece, garantindo que nenhuma mudança seja perdida.
---
*Configurado em: 28 de Março de 2026*

188
Servidor Golang.md Normal file
View File

@@ -0,0 +1,188 @@
# Servidor Golang
eu tenho uma aplicação de API em django rodando em uma VPS (aplicação de gestão de bar). tenho um app lado client para desktop feito com electron. eu predento criar uma especie de midware em go para criar um db local junto com o cliente e o go sicronizase com o django. para ser mais especifico, o go criaria um servidor local onde o cliente desktop acessaria o db local e modificaria, outros dispositivos mobile fariam o mesmo, e o go ficasse constantemente sicronizando com o django. como voce me recomendaria implementar essa ideia?
## O Gemini disse
Essa é uma arquitetura clássica de **Edge Computing** ou **Local-first**, muito comum em sistemas de PDV (Ponto de Venda), onde o bar não pode parar se a internet cair.
O Go é a escolha perfeita para isso devido ao baixo consumo de memória e à facilidade de compilar um executável único que roda no Windows, Linux ou Mac.
Aqui está a recomendação de arquitetura para o seu "Middleware Go":
---
## 1. Stack Recomendada para o Middleware
Para manter o sistema leve e rápido na VPS e no cliente:
* **Banco de Dados Local:SQLite**. É um arquivo único, não precisa de instalação de servidor (como Postgres) e o Go lida perfeitamente com ele usando a lib `modernc.org/sqlite` (que não precisa de CGO).
* **API Local (Go):Gin** ou **Echo**. Eles servirão os endpoints que o Electron e os celulares vão consumir.
* **Sincronização:Gorm** (para facilitar a manipulação dos dados) e o pacote nativo `net/http` para falar com o Django.
---
## 2. Desenho da Arquitetura
1. **Electron/Mobile:** Fazem requisições `POST/GET` para `http://localhost:8080` (o Middleware Go).
2. **Go (Middleware):** * Salva a transação imediatamente no **SQLite** local (Garante a venda).
* Retorna "Sucesso" para o cliente (Baixa latência).
* Em uma **Goroutine** (segundo plano), tenta enviar os dados para o **Django**.
3. **Django (Cloud):** Recebe os dados, valida e retorna um "OK". O Go marca aquela linha no SQLite como `sincronizado = true`.
---
## 3. Estratégia de Sincronização (O "Pulo do Gato")
Para evitar conflitos e perda de dados, sua tabela no SQLite precisa de três campos extras:
* `uid`: Um UUID gerado no Go (não use IDs sequenciais de banco, ou o Django vai se confundir).
* `sincronizado`: Um booleano (`true`/`false`).
* `atualizado_em`: Um timestamp para saber qual versão é a mais nova.
## Exemplo de Fluxo no Go:
**Go**
```
// 1. O PDV envia um pedido
// 2. Go salva no SQLite:
db.Create(&Pedido{ID: "uuid-123", Total: 50.0, Sincronizado: false})
// 3. Uma Goroutine roda a cada 30 segundos:
func syncer() {
for {
var pedidos []Pedido
// Busca o que ainda não subiu para o Django
db.Where("sincronizado = ?", false).Find(&pedidos)
for _, p := range pedidos {
err := enviarParaDjango(p)
if err == nil {
db.Model(&p).Update("sincronizado", true)
}
}
time.Sleep(30 * time.Second)
}
}
```
---
## 4. Como implementar passo a passo
## Passo 1: O Banco Local
Use o SQLite para que o dono do bar não precise instalar nada. O Go cria o arquivo `.db` automaticamente se ele não existir.
## Passo 2: Service Discovery (Como o Mobile acha o Go?)
Como o IP do computador onde o Go está rodando pode mudar, você tem duas opções:
1. **IP Fixo:** Configurar o computador do bar com um IP estático na rede local.
2. **mDNS (ZeroConf):** Use uma lib em Go como `grandcat/zeroconf`. O Go "anuncia" na rede: *&quot;Eu sou o servidor do Bar&quot;* . O app mobile detecta isso automaticamente sem digitar IP.
## Passo 3: Autenticação Dupla
O seu middleware em Go deve ter uma `API_KEY`.
* O **Electron** envia a chave para o **Go**.
* O **Go** usa as credenciais de sistema para falar com o **Django**.
---
## 5. Vantagens dessa abordagem
* **Offline First:** Se a internet do bar cair, o Electron continua funcionando 100%. Quando a internet volta, o Go sobe tudo em lote (*batch*).
* **Velocidade:** O Electron não espera o sinal ir até a nuvem e voltar para imprimir um pedido. É instantâneo.
* **Segurança:** Você pode criptografar o arquivo SQLite local.
---
##
Para um projeto que mistura banco de dados local, API e sincronização em segundo plano, o **Design Pattern** mais indicado é uma versão simplificada da **Clean Architecture** (ou Hexagonal).
O objetivo é separar a **Lógica de Negócio** (regras do bar) da **Infraestrutura** (SQLite, API Django, Servidor Gin). Assim, se você quiser trocar o SQLite por um Postgres no futuro, ou o Gin pelo Fiber, o coração do app não muda.
---
## 1. Estrutura de Pastas Sugerida
Uma estrutura padrão em Go para projetos de médio porte segue este esqueleto:
**Plaintext**
```
meu-middleware-go/
├── cmd/
│ └── server/
│ └── main.go # Ponto de entrada (inicializa tudo)
├── internal/ # Código privado (não pode ser importado por outros projetos)
│ ├── api/ # Handlers HTTP (Rotas do Gin)
│ ├── database/ # Conexão e migrações do SQLite
│ ├── models/ # Estruturas de dados (Pedidos, Itens)
│ ├── repository/ # Consultas ao SQLite (CRUD)
│ ├── service/ # Lógica de negócio e regras de sincronização
│ └── sync/ # O "Worker" que fala com o Django
├── go.mod # Gerenciador de pacotes
└── go.sum # Checksum das dependências
```
---
## 2. Divisão de Responsabilidades (Design Pattern)
Aqui aplicamos o padrão **Repository/Service**:
## **Models (internal/models)**
Define como os dados são. No seu caso, o diferencial é ter campos de controle de sincronização.
**Go**
```
type Pedido struct {
ID string `gorm:"primaryKey"`
ValorTotal float64
Sincronizado bool `gorm:"default:false"`
UpdatedAt time.Time
}
```
## **Repository (internal/repository)**
É o único que "fala" com o SQLite. Ele não sabe o que é Django ou JSON, ele só sabe salvar e buscar no banco local.
* *Função exemplo:*`SavePedido(p models.Pedido)`, `GetNaoSincronizados()`.
## **Service (internal/service)**
Aqui mora a inteligência. O Service chama o Repository para salvar localmente e, ao mesmo tempo, pode disparar um evento para o módulo de sincronização.
* *Responsabilidade:* Validar se o estoque permite a venda, calcular impostos locais.
## **Sync Worker (internal/sync)**
Este é um processo separado (usando **Goroutines**). Ele roda em um loop infinito (ex: a cada 30 segundos), pergunta ao Repository quais pedidos não foram sincronizados e tenta enviar para o Django via `http.Post`.
---
## 3. Por que essa estrutura?
1. **Independência:** O `cmd/main.go` apenas "monta o quebra-cabeça". Se você precisar rodar apenas o sincronizador sem a API, é fácil separar.
2. **Testabilidade:** Você consegue testar a lógica de cálculo do bar (Service) sem precisar ligar o banco de dados de verdade.
3. **Segurança de Dados:** Ao separar o `sync`, você garante que se o Django estiver fora do ar, a API local (que o Electron usa) continue respondendo rápido, pois ela só interage com o `repository` local.
---
## 4. O Fluxo de Dados
1. **Electron** envia JSON → **API (Gin)** .
2. **API** chama **Service**.
3. **Service** manda **Repository** salvar no **SQLite**.
4. **Sync Worker** (em background) lê o **SQLite** e envia para o **Django**.
5. **Django** responde OK → **Sync Worker** manda **Repository** marcar como `Sincronizado: true`.

8
rrbec_server/.env Normal file
View File

@@ -0,0 +1,8 @@
# Server Configuration
PORT=8080
DB_NAME=rrbec.db
# Django Cloud Configuration
DJANGO_BASE_URL=http://127.0.0.1:8000
DJANGO_MASTER_USER=welton89
DJANGO_MASTER_PASSWORD=pindoba10

View File

@@ -0,0 +1,86 @@
# RRBEC Middleware - Guia de Integração (Electron/Desktop)
Este guia documenta como a aplicação Electron deve se comunicar com o servidor middleware local em Go.
## 1. Configurações Base
- **URL Base**: `http://localhost:8080/api/v1`
- **Porta Default**: `8080` (configurável no arquivo `.env` do servidor).
- **Formato**: Todas as requisições e respostas utilizam `application/json`.
## 2. Autenticação (SimpleAuth)
O servidor não utiliza tokens JWT complexos localmente. A autenticação funciona assim:
1. Faça login em `/login` enviando `username` e `password`.
2. O servidor retornará um objeto de usuário. Capture o valor do campo `id` (inteiro).
3. Envie esse valor no cabeçalho HTTP `X-User-ID` em todas as rotas marcadas como **[PROTEGIDO]**.
---
## 3. Endpoints da API
### [PÚBLICO] Login
**POST** `/login`
- **Body**: `{ "username": "seu_usuario", "password": "sua_senha" }`
- **Retorno**: Objeto User completo.
### [PÚBLICO] Listar Mesas
**GET** `/mesas`
- **Retorno**: Array de objetos mesas com `id`, `uuid`, `name`, `active`.
### [PÚBLICO] Listar Produtos/Estoque
**GET** `/products`
- **Retorno**: Array de produtos com preços e quantidade em estoque.
### [PÚBLICO] Listar Categorias
**GET** `/categories`
### [PÚBLICO] Listar Clientes
**GET** `/clients`
### [PÚBLICO] Listar Pedidos (Cozinha/Orders)
**GET** `/orders`
### [PÚBLICO] Listar Tipos de Pagamento
**GET** `/payment-types`
### [PÚBLICO] Listar Pagamentos Realizados
**GET** `/payments`
### [PÚBLICO] Ver Comanda por ID
**GET** `/comandas/:id` (Ex: `/api/v1/comandas/9`)
- **Retorno**: Detalhes da comanda.
---
## 4. Comandas e Itens (Ações)
### [PROTEGIDO] Abrir Nova Comanda
**POST** `/comandas`
- **Headers**: `X-User-ID: <id_do_usuario>`
- **Body**: `{ "mesa_id": 1, "client_id": null }`
### [PROTEGIDO] Lançar Pedido (Adicionar Item)
**POST** `/items-comanda`
- **Headers**: `X-User-ID: <id_do_usuario>`
- **Body**: `{ "comanda_id": 9, "product_id": 50, "applicant": "Nome do Garçom" }`
### [PROTEGIDO] Deletar Item Individual
**DELETE** `/items-comanda/:id`
- **Headers**: `X-User-ID: <id_do_usuario>`
### [PROTEGIDO] Limpar e Fechar Comanda (Apagar Inteira)
**POST** `/comandas/:id/apagar`
- **Headers**: `X-User-ID: <id_do_usuario>`
- **Efeito**: Remove todos os itens da comanda e muda o status para `CLOSED`.
### [PROTEGIDO] Pagar e Fechar Comanda
**POST** `/comandas/:id/pagar`
- **Headers**: `X-User-ID: <id_do_usuario>`
- **Body**:
```json
{
"value": 50.00,
"type_pay_id": 1,
"client_id": null
}
```
- **Efeito**: Registra o pagamento localmente e fecha a comanda.

View File

@@ -0,0 +1,78 @@
package main
import (
"log"
"rrbec_server/internal/api"
"rrbec_server/internal/config"
"rrbec_server/internal/database"
"rrbec_server/internal/repository"
"rrbec_server/internal/service"
"rrbec_server/internal/sync"
"github.com/gin-gonic/gin"
)
func main() {
// 1. Load Configuration
config.LoadConfig()
// 2. Initialize Database
db := database.InitDB(config.AppConfig.DBName)
// 3. Initialize Dependency Injection
repo := repository.NewRepository(db)
svc := service.NewService(repo)
handler := api.NewHandler(svc)
// 4. Start Sync Worker
syncer := sync.NewSyncer(repo, config.AppConfig.DjangoBaseURL, config.AppConfig.DjangoMasterUser, config.AppConfig.DjangoMasterPassword)
syncer.Start()
// 4. Setup Gin Router
r := gin.Default()
// API Routes
v1 := r.Group("/api/v1")
{
// Public routes
v1.POST("/login", handler.Login)
v1.GET("/products", handler.GetProducts)
v1.GET("/mesas", handler.GetMesas)
v1.GET("/comandas", handler.GetComandas)
v1.GET("/comandas/:id", handler.GetComandaByID)
v1.GET("/categories", handler.GetCategories)
v1.GET("/payment-types", handler.GetTypePayments)
v1.GET("/clients", handler.GetClients)
v1.GET("/orders", handler.GetOrders)
v1.GET("/payments", handler.GetPayments)
// Protected routes
protected := v1.Group("/")
protected.Use(api.SimpleAuthMiddleware())
{
protected.POST("/products", handler.CreateProduct)
protected.PATCH("/products/:id", handler.UpdateProduct)
protected.POST("/categories", handler.CreateCategory)
protected.PATCH("/categories/:id", handler.UpdateCategory)
protected.POST("/comandas", handler.CreateComanda)
protected.POST("/items-comanda", handler.AddItemToComanda)
protected.DELETE("/items-comanda/:id", handler.DeleteItemFromComanda)
protected.POST("/comandas/:id/apagar", handler.ClearComanda)
protected.POST("/comandas/:id/pagar", handler.PagarComanda)
protected.PATCH("/comandas/:id", handler.UpdateComanda)
protected.POST("/orders", handler.CreateOrder)
protected.PATCH("/orders/:id", handler.UpdateOrder)
protected.POST("/orders/:id/preparing", handler.SetOrderPreparing)
protected.POST("/orders/:id/finish", handler.SetOrderFinished)
protected.POST("/orders/:id/deliver", handler.SetOrderDelivered)
protected.POST("/orders/:id/cancel", handler.SetOrderCanceled)
protected.GET("/me", handler.GetCurrentUser)
}
}
// 6. Run Server
log.Printf("Local server running on http://localhost:%s", config.AppConfig.Port)
if err := r.Run(":" + config.AppConfig.Port); err != nil {
log.Fatalf("failed to run server: %v", err)
}
}

53
rrbec_server/go.mod Normal file
View File

@@ -0,0 +1,53 @@
module rrbec_server
go 1.25.0
require (
github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.49.0
gorm.io/gorm v1.31.1
)
require (
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.25.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.47.0 // indirect
)

147
rrbec_server/go.sum Normal file
View File

@@ -0,0 +1,147 @@
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

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

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

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

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

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

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

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
}

View File

@@ -0,0 +1,871 @@
package sync
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"rrbec_server/internal/models"
"rrbec_server/internal/repository"
"github.com/google/uuid"
)
type Syncer struct {
repo *repository.Repository
djangoURL string
masterUser string
masterPass string
accessToken string
refreshToken string
}
func NewSyncer(repo *repository.Repository, djangoURL, masterUser, masterPass string) *Syncer {
return &Syncer{
repo: repo,
djangoURL: djangoURL,
masterUser: masterUser,
masterPass: masterPass,
}
}
func (s *Syncer) Start() {
go func() {
// 1. Login to Django
token, err := s.LoginToDjango()
if err != nil {
log.Printf("Initial Django login failed: %v", err)
} else {
s.masterPass = token // Use token for future requests
// 2. Load Last Sync ID
lastID := s.repo.GetLastSyncID()
if lastID == 0 {
log.Println("Database is empty or fresh. Starting initial sync...")
s.InitialSync()
}
}
// 3. Sync Loop (Bidirectional)
for {
log.Println("Sync cycle started...")
s.processChangeLog() // Cloud -> Local
s.pushLocalChanges() // Local -> Cloud
time.Sleep(5 * time.Second)
}
}()
}
func (s *Syncer) ensureValidToken() {
if s.accessToken == "" {
token, err := s.LoginToDjango()
if err != nil {
log.Printf("Failed to login: %v", err)
return
}
s.accessToken = token
}
}
func (s *Syncer) ensureValidTokenWithRefresh() {
s.ensureValidToken()
if s.refreshToken == "" {
return
}
resp := s.fetchFromDjangoRaw("products")
if s.IsTokenExpired(resp) {
if err := s.RefreshAccessToken(); err != nil {
log.Printf("Token refresh failed, trying full login: %v", err)
s.accessToken = ""
s.ensureValidToken()
}
}
}
func (s *Syncer) LoginToDjango() (string, error) {
url := fmt.Sprintf("%s/api/v1/token/", s.djangoURL)
loginData := map[string]string{
"username": s.masterUser,
"password": s.masterPass,
}
jsonData, _ := json.Marshal(loginData)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("login failed with status: %d", resp.StatusCode)
}
var result struct {
Access string `json:"access"`
Refresh string `json:"refresh"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
s.accessToken = result.Access
s.refreshToken = result.Refresh
return result.Access, nil
}
func (s *Syncer) RefreshAccessToken() error {
url := fmt.Sprintf("%s/api/v1/token/refresh/", s.djangoURL)
refreshData := map[string]string{
"refresh": s.refreshToken,
}
jsonData, _ := json.Marshal(refreshData)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
log.Printf("Token refresh failed: %s", string(body))
return fmt.Errorf("refresh failed with status: %d", resp.StatusCode)
}
var result struct {
Access string `json:"access"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
s.accessToken = result.Access
log.Println("Token refreshed successfully")
return nil
}
func (s *Syncer) IsTokenExpired(responseBody []byte) bool {
var errResp struct {
Code string `json:"code"`
}
if json.Unmarshal(responseBody, &errResp) == nil {
return errResp.Code == "token_not_valid" || strings.Contains(string(responseBody), "token_not_valid")
}
return false
}
func (s *Syncer) InitialSync() {
// Sync Products
var products []models.Product
s.fetchFromDjango("products", &products)
for _, p := range products {
p.Sincronizado = true
if p.UUID == "" {
p.UUID = uuid.New().String()
}
s.repo.SaveProduct(&p)
}
// Sync Clients
var clients []models.Client
s.fetchFromDjango("clients", &clients)
for _, c := range clients {
c.Sincronizado = true
if c.UUID == "" {
c.UUID = uuid.New().String()
}
s.repo.SaveClient(&c)
}
// Sync Mesas
var mesas []models.Mesa
s.fetchFromDjango("mesas", &mesas)
for _, m := range mesas {
m.Sincronizado = true
if m.UUID == "" {
m.UUID = uuid.New().String()
}
s.repo.SaveMesa(&m)
}
// Sync Users
var users []models.User
s.fetchFromDjango("users", &users)
for _, u := range users {
u.Sincronizado = true
if u.UUID == "" {
u.UUID = uuid.New().String()
}
s.repo.SaveUser(&u)
}
// Sync Categories
var categories []models.Category
s.fetchFromDjango("categories", &categories)
for _, cat := range categories {
cat.Sincronizado = true
if cat.UUID == "" {
cat.UUID = uuid.New().String()
}
s.repo.SaveCategory(&cat)
}
// Sync Payment Types
var paymentTypes []models.TypePay
s.fetchFromDjango("payment-types", &paymentTypes)
for _, tp := range paymentTypes {
tp.Sincronizado = true
if tp.UUID == "" {
tp.UUID = uuid.New().String()
}
s.repo.SaveTypePay(&tp)
}
// Sync Comandas
var comandas []models.Comanda
s.fetchFromDjango("comandas", &comandas)
for _, co := range comandas {
co.Sincronizado = true
if co.UUID == "" {
co.UUID = uuid.New().String()
}
s.repo.SaveComanda(&co)
// Sync nested items from the serializer
for _, item := range co.Items {
item.Sincronizado = true
if item.UUID == "" {
item.UUID = uuid.New().String()
}
s.repo.SaveProductComanda(&item)
}
}
// Sync Comanda Items
var items []models.ProductComanda
s.fetchFromDjango("items-comanda", &items)
for _, it := range items {
it.Sincronizado = true
if it.UUID == "" {
it.UUID = uuid.New().String()
}
s.repo.SaveProductComanda(&it)
}
// Sync Orders
var orders []models.Order
s.fetchFromDjango("orders", &orders)
for _, o := range orders {
o.Sincronizado = true
if o.UUID == "" {
o.UUID = uuid.New().String()
}
s.repo.SaveOrder(&o)
}
// Sync Payments
var payments []models.Payment
s.fetchFromDjango("payments", &payments)
for _, pay := range payments {
pay.Sincronizado = true
if pay.UUID == "" {
pay.UUID = uuid.New().String()
}
s.repo.SavePayment(&pay)
}
log.Println("Initial sync completed successfully.")
}
func (s *Syncer) fetchFromDjango(endpoint string, target interface{}) {
endpoint = strings.Trim(endpoint, "/")
url := ""
if strings.Contains(endpoint, "?") {
url = fmt.Sprintf("%s/api/v1/%s", s.djangoURL, endpoint)
} else {
url = fmt.Sprintf("%s/api/v1/%s/", s.djangoURL, endpoint)
}
token := s.accessToken
if token == "" {
token = s.masterPass
}
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Printf("Failed to fetch %s: %v", endpoint, err)
return
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
if err := json.Unmarshal(bodyBytes, target); err != nil {
var paginated struct {
Results json.RawMessage `json:"results"`
}
if err2 := json.Unmarshal(bodyBytes, &paginated); err2 == nil && paginated.Results != nil {
if err3 := json.Unmarshal(paginated.Results, target); err3 != nil {
log.Printf("Failed to decode paginated %s: %v", endpoint, err3)
}
} else {
log.Printf("Failed to decode %s: %v", endpoint, err)
}
}
log.Printf("Synchronized %s from cloud", endpoint)
}
func (s *Syncer) fetchFromDjangoRaw(endpoint string) []byte {
endpoint = strings.Trim(endpoint, "/")
url := ""
if strings.Contains(endpoint, "?") {
url = fmt.Sprintf("%s/api/v1/%s", s.djangoURL, endpoint)
} else {
url = fmt.Sprintf("%s/api/v1/%s/", s.djangoURL, endpoint)
}
token := s.accessToken
if token == "" {
token = s.masterPass
}
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Printf("Failed to fetch %s: %v", endpoint, err)
return nil
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
if s.IsTokenExpired(bodyBytes) {
log.Printf("Token expired, refreshing...")
if err := s.RefreshAccessToken(); err != nil {
log.Printf("Token refresh failed: %v", err)
} else {
return s.fetchFromDjangoRaw(endpoint)
}
}
log.Printf("DEBUG: Raw response for %s: %s", endpoint, string(bodyBytes))
return bodyBytes
}
func (s *Syncer) processChangeLog() {
lastID := s.repo.GetLastSyncID()
url := fmt.Sprintf("sync?since_id=%d", lastID)
var changes []models.ChangeLog
respBody := s.fetchFromDjangoRaw(url)
if respBody != nil {
if err := json.Unmarshal(respBody, &changes); err != nil {
log.Printf("ERROR: Failed to parse changelog: %v", err)
return
}
}
log.Printf("DEBUG: Received %d changes from Django", len(changes))
if len(changes) == 0 {
return
}
maxID := lastID
for _, change := range changes {
log.Printf("Processing change %s for %s (ID: %d)", change.Action, change.ModelName, change.ObjectID)
if change.Action == "SAVE" {
s.fetchAndSaveObject(change.ModelName, change.ObjectID)
} else if change.Action == "DELETE" {
s.repo.DeleteByID(change.ModelName, change.ObjectID)
}
if change.ID > maxID {
maxID = change.ID
}
}
s.repo.SaveLastSyncID(maxID)
}
func (s *Syncer) fetchAndSaveObject(modelName string, id uint) {
endpoint := ""
switch modelName {
case "Product":
endpoint = "products"
case "Comanda":
endpoint = "comandas"
case "ProductComanda":
endpoint = "items-comanda"
case "Order":
endpoint = "orders"
case "Client":
endpoint = "clients"
case "Categories":
endpoint = "categories"
case "Mesa":
endpoint = "mesas"
case "Payments":
endpoint = "payments"
default:
return
}
url := fmt.Sprintf("%s/%d", endpoint, id)
// Create temporary instance based on modelName
switch modelName {
case "Comanda":
var obj models.Comanda
s.fetchFromDjango(url, &obj)
if obj.UUID == "" {
var local models.Comanda
if err := s.repo.GetComandaToSync(obj.ID, &local); err == nil {
obj.UUID = local.UUID
}
if obj.UUID == "" {
obj.UUID = uuid.New().String()
}
}
obj.Sincronizado = true
s.repo.SaveComanda(&obj)
case "Product":
var obj models.Product
s.fetchFromDjango(url, &obj)
if obj.UUID == "" {
var local models.Product
if err := s.repo.GetProductToSync(obj.ID, &local); err == nil {
obj.UUID = local.UUID
}
if obj.UUID == "" {
obj.UUID = uuid.New().String()
}
}
obj.Sincronizado = true
s.repo.SaveProduct(&obj)
case "ProductComanda":
var obj models.ProductComanda
s.fetchFromDjango(url, &obj)
if obj.UUID == "" {
var local models.ProductComanda
if err := s.repo.GetItemToSync(obj.ID, &local); err == nil {
obj.UUID = local.UUID
}
if obj.UUID == "" {
obj.UUID = uuid.New().String()
}
}
obj.Sincronizado = true
s.repo.SaveProductComanda(&obj)
case "Order":
var obj models.Order
s.fetchFromDjango(url, &obj)
if obj.UUID == "" {
var local models.Order
if err := s.repo.GetOrderToSync(obj.ID, &local); err == nil {
obj.UUID = local.UUID
}
if obj.UUID == "" {
obj.UUID = uuid.New().String()
}
}
obj.Sincronizado = true
s.repo.SaveOrder(&obj)
case "Client":
var obj models.Client
s.fetchFromDjango(url, &obj)
if obj.UUID == "" {
obj.UUID = uuid.New().String()
}
obj.Sincronizado = true
s.repo.SaveClient(&obj)
case "Categories":
var obj models.Category
s.fetchFromDjango(url, &obj)
if obj.UUID == "" {
obj.UUID = uuid.New().String()
}
obj.Sincronizado = true
s.repo.SaveCategory(&obj)
case "Mesa":
var obj models.Mesa
s.fetchFromDjango(url, &obj)
if obj.UUID == "" {
obj.UUID = uuid.New().String()
}
obj.Sincronizado = true
s.repo.SaveMesa(&obj)
case "Payments":
var obj models.Payment
s.fetchFromDjango(url, &obj)
if obj.UUID == "" {
var local models.Payment
if err := s.repo.GetPaymentToSync(obj.ID, &local); err == nil {
obj.UUID = local.UUID
}
if obj.UUID == "" {
obj.UUID = uuid.New().String()
}
}
obj.Sincronizado = true
s.repo.SavePayment(&obj)
}
}
func (s *Syncer) pushLocalChanges() {
// 1. Sync Comandas
comandas, err := s.repo.GetUnsyncedComandas()
if err == nil && len(comandas) > 0 {
for _, c := range comandas {
if c.MesaID == 0 {
log.Printf("SKIP: Comanda %d has mesa 0. Correct data required.", c.ID)
continue
}
// Capture local ID before we might change it
oldID := c.ID
comandaName := c.Name
if comandaName == "" {
comandaName = fmt.Sprintf("Comanda %d", oldID)
}
payload := map[string]interface{}{
"mesa": c.MesaID,
"client": c.ClientID,
"user": c.UserID,
"status": c.Status,
"name": comandaName,
"uuid": c.UUID,
}
// Try PATCH first
endpoint := fmt.Sprintf("comandas/%d", c.ID)
respBody, err := s.sendRequest("PATCH", endpoint, payload)
if err != nil && strings.Contains(err.Error(), "status: 404") {
respBody, err = s.sendRequest("POST", "comandas", payload)
}
if err == nil {
var created models.Comanda
json.Unmarshal(respBody, &created)
// CRITICAL: Update local ID and children safely
if created.ID != 0 && created.ID != oldID {
s.repo.SafeChangeID("comandas", oldID, created.ID)
// Update items that were pointing to the old ID
s.repo.UpdateFK("product_comandas", "comanda_id", oldID, created.ID)
s.repo.UpdateFK("orders", "id_comanda", oldID, created.ID)
s.repo.UpdateFK("payments", "comanda_id", oldID, created.ID)
}
s.repo.MarkAsSynced(&models.Comanda{}, created.ID)
log.Printf("SUCCESS: Pushed Comanda %d (now %d) to cloud", oldID, created.ID)
} else {
log.Printf("ERROR pushing Comanda %d: %v", oldID, err)
}
}
}
// 2. Sync Items
items, err := s.repo.GetUnsyncedItems()
if err == nil && len(items) > 0 {
for _, it := range items {
oldID := it.ID
payload := map[string]interface{}{
"comanda": it.ComandaID,
"product": it.ProductID,
"applicant": it.Applicant,
"uuid": it.UUID,
}
endpoint := fmt.Sprintf("items-comanda/%d", it.ID)
respBody, err := s.sendRequest("PATCH", endpoint, payload)
if err != nil && strings.Contains(err.Error(), "status: 404") {
respBody, err = s.sendRequest("POST", "items-comanda", payload)
}
if err == nil {
var created models.ProductComanda
json.Unmarshal(respBody, &created)
if created.ID != 0 && created.ID != oldID {
s.repo.SafeChangeID("product_comandas", oldID, created.ID)
s.repo.UpdateFK("orders", "product_comanda_id", oldID, created.ID)
}
s.repo.MarkAsSynced(&models.ProductComanda{}, created.ID)
log.Printf("SUCCESS: Pushed Item %d (now %d) to cloud", oldID, created.ID)
} else {
log.Printf("ERROR pushing Item %d: %v", oldID, err)
}
}
}
// 3. Sync Orders
orders, err := s.repo.GetUnsyncedOrders()
if err == nil && len(orders) > 0 {
for _, o := range orders {
oldID := o.ID
payload := map[string]interface{}{
"productComanda": o.ProductComandaID,
"id_product": o.ProductID,
"id_comanda": o.ComandaID,
"obs": o.Obs,
"uuid": o.UUID,
}
// Optional: mapping timestamps if they exist
if !o.Queue.IsZero() {
payload["queue"] = o.Queue
}
if o.Preparing != nil {
payload["preparing"] = o.Preparing
}
if o.Finished != nil {
payload["finished"] = o.Finished
}
if o.Delivered != nil {
payload["delivered"] = o.Delivered
}
if o.Canceled != nil {
payload["canceled"] = o.Canceled
}
endpoint := fmt.Sprintf("orders/%d", o.ID)
respBody, err := s.sendRequest("PATCH", endpoint, payload)
if err != nil && strings.Contains(err.Error(), "status: 404") {
respBody, err = s.sendRequest("POST", "orders", payload)
}
if err == nil {
var created models.Order
json.Unmarshal(respBody, &created)
if created.ID != 0 && created.ID != oldID {
s.repo.SafeChangeID("orders", oldID, created.ID)
}
s.repo.MarkAsSynced(&models.Order{}, created.ID)
log.Printf("SUCCESS: Pushed Order %d (now %d) to cloud", oldID, created.ID)
} else {
log.Printf("ERROR pushing Order %d: %v", oldID, err)
}
}
}
// 4. Sync Payments
payments, err := s.repo.GetUnsyncedPayments()
if err == nil && len(payments) > 0 {
for _, p := range payments {
if p.TypePayID == 0 {
log.Printf("SKIP: Payment %d has invalid type_pay 0.", p.ID)
s.repo.MarkAsSynced(&models.Payment{}, p.ID)
continue
}
oldID := p.ID
payload := map[string]interface{}{
"value": p.Value,
"type_pay": p.TypePayID,
"comanda": p.ComandaID,
"description": p.Description,
"datetime": p.DateTime,
"uuid": p.UUID,
}
if p.ClientID != nil {
payload["client"] = *p.ClientID
}
endpoint := fmt.Sprintf("payments/%d", p.ID)
respBody, err := s.sendRequest("PATCH", endpoint, payload)
if err != nil && strings.Contains(err.Error(), "status: 404") {
respBody, err = s.sendRequest("POST", "payments", payload)
}
if err == nil {
var created models.Payment
json.Unmarshal(respBody, &created)
if created.ID != 0 && created.ID != oldID {
s.repo.SafeChangeID("payments", oldID, created.ID)
}
s.repo.MarkAsSynced(&models.Payment{}, created.ID)
log.Printf("SUCCESS: Pushed Payment %d (now %d) to cloud", oldID, created.ID)
} else {
log.Printf("ERROR pushing Payment %d: %v", oldID, err)
}
}
}
// 5. Sync Products
products, err := s.repo.GetUnsyncedProducts()
if err == nil && len(products) > 0 {
for _, p := range products {
oldID := p.ID
payload := map[string]interface{}{
"name": p.Name,
"description": p.Description,
"image": p.Image,
"price": p.Price,
"quantity": p.Quantity,
"category": p.CategoryID,
"cuisine": p.Cuisine,
"active": p.Active,
"uuid": p.UUID,
}
if p.UnitOfMeasure != nil {
payload["unit_of_measure"] = *p.UnitOfMeasure
}
endpoint := fmt.Sprintf("products/%d", p.ID)
respBody, err := s.sendMultipartRequest("PATCH", endpoint, payload)
if err != nil && strings.Contains(err.Error(), "status: 404") {
respBody, err = s.sendMultipartRequest("POST", "products", payload)
}
if err == nil {
var created models.Product
json.Unmarshal(respBody, &created)
if created.ID != 0 && created.ID != oldID {
s.repo.SafeChangeID("products", oldID, created.ID)
}
s.repo.MarkAsSynced(&models.Product{}, created.ID)
log.Printf("SUCCESS: Pushed Product %d (now %d) to cloud", oldID, created.ID)
} else {
log.Printf("ERROR pushing Product %d: %v", oldID, err)
}
}
}
}
func (s *Syncer) sendRequest(method, endpoint string, data interface{}) ([]byte, error) {
url := fmt.Sprintf("%s/api/v1/%s/", s.djangoURL, endpoint)
jsonData, _ := json.Marshal(data)
token := s.accessToken
if token == "" {
token = s.masterPass
}
req, _ := http.NewRequest(method, url, bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 401 || strings.Contains(string(respBody), "token_not_valid") {
log.Printf("Token expired during %s %s, refreshing...", method, endpoint)
if err := s.RefreshAccessToken(); err != nil {
return nil, fmt.Errorf("token refresh failed: %w", err)
}
return s.sendRequest(method, endpoint, data)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return respBody, nil
}
return nil, fmt.Errorf("django returned status: %d | Response: %s", resp.StatusCode, string(respBody))
}
func (s *Syncer) sendMultipartRequest(method, endpoint string, data map[string]interface{}) ([]byte, error) {
urlStr := fmt.Sprintf("%s/api/v1/%s/", s.djangoURL, endpoint)
var b bytes.Buffer
w := multipart.NewWriter(&b)
for key, val := range data {
if key == "image" {
imageStr, ok := val.(string)
if !ok || imageStr == "" {
continue // Skip empty image
}
// If it's an existing URL, don't re-upload it
if strings.HasPrefix(imageStr, "http") || strings.HasPrefix(imageStr, "/media/") || strings.HasPrefix(imageStr, "/images/") {
continue
}
// Base64
if strings.HasPrefix(imageStr, "data:image") {
parts := strings.Split(imageStr, ",")
if len(parts) == 2 {
decoded, err := base64.StdEncoding.DecodeString(parts[1])
if err == nil {
fw, _ := w.CreateFormFile(key, "upload.jpg")
fw.Write(decoded)
}
}
continue
}
// Local file
fileContent, err := os.ReadFile(imageStr)
if err == nil {
fw, _ := w.CreateFormFile(key, filepath.Base(imageStr))
fw.Write(fileContent)
}
} else {
// standard text field
w.WriteField(key, fmt.Sprintf("%v", val))
}
}
w.Close()
token := s.accessToken
if token == "" {
token = s.masterPass
}
req, _ := http.NewRequest(method, urlStr, &b)
req.Header.Set("Content-Type", w.FormDataContentType())
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 401 || strings.Contains(string(respBody), "token_not_valid") {
log.Printf("Token expired during %s %s, refreshing...", method, urlStr)
if err := s.RefreshAccessToken(); err != nil {
return nil, fmt.Errorf("token refresh failed: %w", err)
}
return s.sendMultipartRequest(method, endpoint, data)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return respBody, nil
}
return nil, fmt.Errorf("django returned status: %d | Response: %s", resp.StatusCode, string(respBody))
}
func (s *Syncer) syncUsers() {
// Optional: logic for background sync of users if needed
}

BIN
rrbec_server/main Executable file

Binary file not shown.

BIN
rrbec_server/rrbec.db Normal file

Binary file not shown.

BIN
rrbec_server/server Executable file

Binary file not shown.