326 lines
8.1 KiB
Go
326 lines
8.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
|
|
"github.com/gofrs/uuid/v5"
|
|
|
|
"github.com/saveinmed/backend-go/internal/domain"
|
|
"github.com/saveinmed/backend-go/internal/usecase"
|
|
)
|
|
|
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
|
|
|
type Handler struct {
|
|
svc *usecase.Service
|
|
}
|
|
|
|
func New(svc *usecase.Service) *Handler {
|
|
return &Handler{svc: svc}
|
|
}
|
|
|
|
// CreateCompany godoc
|
|
// @Summary Registro de empresas
|
|
// @Description Cadastra farmácia, distribuidora ou administrador com CNPJ e licença sanitária.
|
|
// @Tags Empresas
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param company body registerCompanyRequest true "Dados da empresa"
|
|
// @Success 201 {object} domain.Company
|
|
// @Router /api/companies [post]
|
|
func (h *Handler) CreateCompany(w http.ResponseWriter, r *http.Request) {
|
|
var req registerCompanyRequest
|
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
company := &domain.Company{
|
|
Role: req.Role,
|
|
CNPJ: req.CNPJ,
|
|
CorporateName: req.CorporateName,
|
|
SanitaryLicense: req.SanitaryLicense,
|
|
}
|
|
|
|
if err := h.svc.RegisterCompany(r.Context(), company); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, company)
|
|
}
|
|
|
|
// ListCompanies godoc
|
|
// @Summary Lista empresas
|
|
// @Tags Empresas
|
|
// @Produce json
|
|
// @Success 200 {array} domain.Company
|
|
// @Router /api/companies [get]
|
|
func (h *Handler) ListCompanies(w http.ResponseWriter, r *http.Request) {
|
|
companies, err := h.svc.ListCompanies(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, companies)
|
|
}
|
|
|
|
// CreateProduct godoc
|
|
// @Summary Cadastro de produto com rastreabilidade de lote
|
|
// @Tags Produtos
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param product body registerProductRequest true "Produto"
|
|
// @Success 201 {object} domain.Product
|
|
// @Router /api/products [post]
|
|
func (h *Handler) CreateProduct(w http.ResponseWriter, r *http.Request) {
|
|
var req registerProductRequest
|
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
product := &domain.Product{
|
|
SellerID: req.SellerID,
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
Batch: req.Batch,
|
|
ExpiresAt: req.ExpiresAt,
|
|
PriceCents: req.PriceCents,
|
|
Stock: req.Stock,
|
|
}
|
|
|
|
if err := h.svc.RegisterProduct(r.Context(), product); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, product)
|
|
}
|
|
|
|
// ListProducts godoc
|
|
// @Summary Lista catálogo com lote e validade
|
|
// @Tags Produtos
|
|
// @Produce json
|
|
// @Success 200 {array} domain.Product
|
|
// @Router /api/products [get]
|
|
func (h *Handler) ListProducts(w http.ResponseWriter, r *http.Request) {
|
|
products, err := h.svc.ListProducts(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, products)
|
|
}
|
|
|
|
// CreateOrder godoc
|
|
// @Summary Criação de pedido com split
|
|
// @Tags Pedidos
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param order body createOrderRequest true "Pedido"
|
|
// @Success 201 {object} domain.Order
|
|
// @Router /api/orders [post]
|
|
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
|
var req createOrderRequest
|
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
order := &domain.Order{
|
|
BuyerID: req.BuyerID,
|
|
SellerID: req.SellerID,
|
|
Items: req.Items,
|
|
}
|
|
|
|
var total int64
|
|
for _, item := range req.Items {
|
|
total += item.UnitCents * item.Quantity
|
|
}
|
|
order.TotalCents = total
|
|
|
|
if err := h.svc.CreateOrder(r.Context(), order); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, order)
|
|
}
|
|
|
|
// GetOrder godoc
|
|
// @Summary Consulta pedido
|
|
// @Tags Pedidos
|
|
// @Produce json
|
|
// @Param id path string true "Order ID"
|
|
// @Success 200 {object} domain.Order
|
|
// @Router /api/orders/{id} [get]
|
|
func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
|
|
id, err := parseUUIDFromPath(r.URL.Path)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
order, err := h.svc.GetOrder(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, order)
|
|
}
|
|
|
|
// UpdateOrderStatus godoc
|
|
// @Summary Atualiza status do pedido
|
|
// @Tags Pedidos
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "Order ID"
|
|
// @Param status body updateStatusRequest true "Novo status"
|
|
// @Success 204 ""
|
|
// @Router /api/orders/{id}/status [patch]
|
|
func (h *Handler) UpdateOrderStatus(w http.ResponseWriter, r *http.Request) {
|
|
id, err := parseUUIDFromPath(r.URL.Path)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
var req updateStatusRequest
|
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
if !isValidStatus(req.Status) {
|
|
writeError(w, http.StatusBadRequest, errors.New("invalid status"))
|
|
return
|
|
}
|
|
|
|
if err := h.svc.UpdateOrderStatus(r.Context(), id, domain.OrderStatus(req.Status)); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// CreatePaymentPreference godoc
|
|
// @Summary Cria preferência de pagamento Mercado Pago com split nativo
|
|
// @Tags Pagamentos
|
|
// @Produce json
|
|
// @Param id path string true "Order ID"
|
|
// @Success 201 {object} domain.PaymentPreference
|
|
// @Router /api/orders/{id}/payment [post]
|
|
func (h *Handler) CreatePaymentPreference(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.HasSuffix(r.URL.Path, "/payment") {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
id, err := parseUUIDFromPath(r.URL.Path)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
pref, err := h.svc.CreatePaymentPreference(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, pref)
|
|
}
|
|
|
|
type registerCompanyRequest struct {
|
|
Role string `json:"role"`
|
|
CNPJ string `json:"cnpj"`
|
|
CorporateName string `json:"corporate_name"`
|
|
SanitaryLicense string `json:"sanitary_license"`
|
|
}
|
|
|
|
type registerProductRequest struct {
|
|
SellerID uuid.UUID `json:"seller_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Batch string `json:"batch"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
PriceCents int64 `json:"price_cents"`
|
|
Stock int64 `json:"stock"`
|
|
}
|
|
|
|
type createOrderRequest struct {
|
|
BuyerID uuid.UUID `json:"buyer_id"`
|
|
SellerID uuid.UUID `json:"seller_id"`
|
|
Items []domain.OrderItem `json:"items"`
|
|
}
|
|
|
|
type updateStatusRequest struct {
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, err error) {
|
|
writeJSON(w, status, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
func decodeJSON(ctx context.Context, r *http.Request, v any) error {
|
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
|
|
dec := json.NewDecoder(r.Body)
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(v); err != nil {
|
|
return err
|
|
}
|
|
return ctx.Err()
|
|
}
|
|
|
|
func parseUUIDFromPath(path string) (uuid.UUID, error) {
|
|
parts := splitPath(path)
|
|
for i := len(parts) - 1; i >= 0; i-- {
|
|
if id, err := uuid.FromString(parts[i]); err == nil {
|
|
return id, nil
|
|
}
|
|
}
|
|
return uuid.UUID{}, errors.New("missing resource id")
|
|
}
|
|
|
|
func splitPath(p string) []string {
|
|
var parts []string
|
|
start := 0
|
|
for i := 0; i < len(p); i++ {
|
|
if p[i] == '/' {
|
|
if i > start {
|
|
parts = append(parts, p[start:i])
|
|
}
|
|
start = i + 1
|
|
}
|
|
}
|
|
if start < len(p) {
|
|
parts = append(parts, p[start:])
|
|
}
|
|
return parts
|
|
}
|
|
|
|
func isValidStatus(status string) bool {
|
|
switch domain.OrderStatus(status) {
|
|
case domain.OrderStatusPending, domain.OrderStatusPaid, domain.OrderStatusInvoiced, domain.OrderStatusDelivered:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|