Multi-Tenant - 07 - Exemplo concreto em Go
Vamos fechar a stack da NFS-e então: service + handler HTTP com chi e multi-tenant em /tenants/{tenantID}/nfse.
Vou assumir:
-
Já existem:
internal/nfse/domain.gointernal/nfse/repository_sqlx.go- Middleware global que injeta
tenantIDe Tx no contexto (DBTxMiddleware) pkg/httpcontextcomTenantIDFromContext.
Abaixo vão só os arquivos novos/ajustados.
internal/nfse/service.go
package nfse
import "context"
type Service struct {
repo Repository
}
func NewService(repo Repository) *Service {
return &Service{repo: repo}
}
func (s *Service) CriarNfseCompleta(ctx context.Context, tenantID int64, cab Nfse, itens []NfseItem) (*NfseCompleta, error) {
cab.TenantID = tenantID
for i := range itens {
itens[i].TenantID = tenantID
}
if err := s.repo.CriarNfse(ctx, &cab, itens); err != nil {
return nil, err
}
return &NfseCompleta{
Cabecalho: cab,
Itens: itens,
}, nil
}
func (s *Service) ObterNfseCompleta(ctx context.Context, tenantID, id int64) (*NfseCompleta, error) {
return s.repo.ObterPorID(ctx, tenantID, id)
}
func (s *Service) ListarPorCompetencia(ctx context.Context, tenantID int64, competencia string, limit, offset int) ([]Nfse, error) {
return s.repo.ListarPorCompetencia(ctx, tenantID, competencia, limit, offset)
}
internal/nfse/handler_http.go
package nfse
import (
"encoding/json"
"net/http"
"strconv"
"fsgo/pkg/httpcontext"
"github.com/go-chi/chi/v5"
)
// ---------------------------------------------------------------------
// DTOs (requests)
// ---------------------------------------------------------------------
type criarNfseItemRequest struct {
NumeroItem int `json:"numero_item"`
CodigoServico string `json:"codigo_servico"`
Descricao string `json:"descricao"`
Quantidade float64 `json:"quantidade"`
ValorUnitario float64 `json:"valor_unitario"`
ValorTotal float64 `json:"valor_total"`
AliquotaIss float64 `json:"aliquota_iss"`
ValorIss float64 `json:"valor_iss"`
CodigoTribMunicipio *string `json:"codigo_tributacao_municipio,omitempty"`
ItemListaServico *string `json:"item_lista_servico,omitempty"`
}
type criarNfseRequest struct {
NumeroNfse int64 `json:"numero_nfse"`
Serie string `json:"serie"`
Ambiente string `json:"ambiente"`
Situacao string `json:"situacao"`
PrestadorID int64 `json:"prestador_id"`
TomadorID int64 `json:"tomador_id"`
DataEmissao string `json:"data_emissao"` // ISO8601
Competencia string `json:"competencia"` // "YYYY-MM-DD"
ValorServicos float64 `json:"valor_servicos"`
ValorDeducoes float64 `json:"valor_deducoes"`
ValorPis float64 `json:"valor_pis"`
ValorCofins float64 `json:"valor_cofins"`
ValorInss float64 `json:"valor_inss"`
ValorIr float64 `json:"valor_ir"`
ValorCsll float64 `json:"valor_csll"`
ValorIss float64 `json:"valor_iss"`
ValorIssRetido float64 `json:"valor_iss_retido"`
IssRetido bool `json:"iss_retido"`
CodigoMunicipioPrest string `json:"codigo_municipio_prestacao"`
CodigoMunicipioTomador *string `json:"codigo_municipio_tomador,omitempty"`
CodigoTribMunicipio string `json:"codigo_tributacao_municipio"`
ItemListaServico string `json:"item_lista_servico"`
Discriminacao string `json:"discriminacao"`
// opcionalmente você pode mandar XML/protocolo já preenchidos
ProtocoloEnvio *string `json:"protocolo_envio,omitempty"`
DataEnvio *string `json:"data_envio,omitempty"` // ISO
CodigoMensagemRetorno *string `json:"codigo_mensagem_retorno,omitempty"`
MensagemRetorno *string `json:"mensagem_retorno,omitempty"`
XmlEnvio *string `json:"xml_envio,omitempty"`
XmlRetorno *string `json:"xml_retorno,omitempty"`
Itens []criarNfseItemRequest `json:"itens"`
}
type listarNfseResponse struct {
Notas []Nfse `json:"notas"`
}
// ---------------------------------------------------------------------
// Handler
// ---------------------------------------------------------------------
type Handler struct {
svc *Service
}
func NewHandler(svc *Service) *Handler {
return &Handler{svc: svc}
}
// Monta as rotas de NFS-e
func (h *Handler) Routes(r chi.Router) {
r.Route("/tenants/{tenantID}", func(r chi.Router) {
// Aqui assumo que o middleware DBTxMiddleware já setou tenantID no contexto
r.Route("/nfse", func(r chi.Router) {
r.Get("/", h.ListarPorCompetencia) // GET /tenants/{tenantID}/nfse?competencia=YYYY-MM-DD
r.Post("/", h.CriarNfse) // POST /tenants/{tenantID}/nfse
r.Get("/{id}", h.ObterNfse) // GET /tenants/{tenantID}/nfse/{id}
})
})
}
// ---------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------
func (h *Handler) getTenantID(r *http.Request) (int64, error) {
id, ok := httpcontext.TenantIDFromContext(r.Context())
if !ok {
// fallback: tenta ler da URL se não tiver no contexto
tenantStr := chi.URLParam(r, "tenantID")
return strconv.ParseInt(tenantStr, 10, 64)
}
return id, nil
}
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
// ---------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------
// POST /tenants/{tenantID}/nfse
func (h *Handler) CriarNfse(w http.ResponseWriter, r *http.Request) {
tenantID, err := h.getTenantID(r)
if err != nil || tenantID <= 0 {
http.Error(w, "tenant_id inválido", http.StatusBadRequest)
return
}
var req criarNfseRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "JSON inválido", http.StatusBadRequest)
return
}
// Parse de datas
dataEmissao, err := time.Parse(time.RFC3339, req.DataEmissao)
if err != nil {
http.Error(w, "data_emissao inválida (use RFC3339)", http.StatusBadRequest)
return
}
competencia, err := time.Parse("2006-01-02", req.Competencia)
if err != nil {
http.Error(w, "competencia inválida (use YYYY-MM-DD)", http.StatusBadRequest)
return
}
nf := Nfse{
TenantID: tenantID,
NumeroNfse: req.NumeroNfse,
Serie: req.Serie,
Ambiente: req.Ambiente,
Situacao: req.Situacao,
PrestadorID: req.PrestadorID,
TomadorID: req.TomadorID,
DataEmissao: dataEmissao,
Competencia: competencia,
ValorServicos: req.ValorServicos,
ValorDeducoes: req.ValorDeducoes,
ValorPis: req.ValorPis,
ValorCofins: req.ValorCofins,
ValorInss: req.ValorInss,
ValorIr: req.ValorIr,
ValorCsll: req.ValorCsll,
ValorIss: req.ValorIss,
ValorIssRetido: req.ValorIssRetido,
IssRetido: req.IssRetido,
CodigoMunicipioPrest: req.CodigoMunicipioPrest,
CodigoMunicipioTomador: req.CodigoMunicipioTomador,
CodigoTribMunicipio: req.CodigoTribMunicipio,
ItemListaServico: req.ItemListaServico,
Discriminacao: req.Discriminacao,
ProtocoloEnvio: req.ProtocoloEnvio,
CodigoMensagemRetorno: req.CodigoMensagemRetorno,
MensagemRetorno: req.MensagemRetorno,
XmlEnvio: req.XmlEnvio,
XmlRetorno: req.XmlRetorno,
}
if req.DataEnvio != nil {
if t, err := time.Parse(time.RFC3339, *req.DataEnvio); err == nil {
nf.DataEnvio = &t
}
}
var itens []NfseItem
for _, it := range req.Itens {
item := NfseItem{
TenantID: tenantID,
NumeroItem: it.NumeroItem,
CodigoServico: it.CodigoServico,
Descricao: it.Descricao,
Quantidade: it.Quantidade,
ValorUnitario: it.ValorUnitario,
ValorTotal: it.ValorTotal,
AliquotaIss: it.AliquotaIss,
ValorIss: it.ValorIss,
CodigoTribMunicipio: it.CodigoTribMunicipio,
ItemListaServico: it.ItemListaServico,
}
itens = append(itens, item)
}
nfCompleta, err := h.svc.CriarNfseCompleta(r.Context(), tenantID, nf, itens)
if err != nil {
http.Error(w, "erro ao criar NFS-e: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusCreated, nfCompleta)
}
// GET /tenants/{tenantID}/nfse/{id}
func (h *Handler) ObterNfse(w http.ResponseWriter, r *http.Request) {
tenantID, err := h.getTenantID(r)
if err != nil || tenantID <= 0 {
http.Error(w, "tenant_id inválido", http.StatusBadRequest)
return
}
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "id inválido", http.StatusBadRequest)
return
}
nf, err := h.svc.ObterNfseCompleta(r.Context(), tenantID, id)
if err != nil {
http.Error(w, "erro ao buscar NFS-e: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, nf)
}
// GET /tenants/{tenantID}/nfse?competencia=YYYY-MM-DD
func (h *Handler) ListarPorCompetencia(w http.ResponseWriter, r *http.Request) {
tenantID, err := h.getTenantID(r)
if err != nil || tenantID <= 0 {
http.Error(w, "tenant_id inválido", http.StatusBadRequest)
return
}
comp := r.URL.Query().Get("competencia")
if comp == "" {
http.Error(w, "parâmetro competencia é obrigatório (YYYY-MM-DD)", http.StatusBadRequest)
return
}
// para simplificar, limit/offset fixos
notas, err := h.svc.ListarPorCompetencia(r.Context(), tenantID, comp, 100, 0)
if err != nil {
http.Error(w, "erro ao listar NFS-e: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, listarNfseResponse{Notas: notas})
}
No
main.go, é só fazer o wiring igual ao contextoidentity:
// ...
nfseRepo := nfse.NewRepository(db)
nfseSvc := nfse.NewService(nfseRepo)
nfseHandler := nfse.NewHandler(nfseSvc)
r := chi.NewRouter()
r.Use(DBTxMiddleware(db))
nfseHandler.Routes(r)
// outros handlers...
// ...