feat: add basic app
This commit is contained in:
parent
d4c1af4831
commit
c94098afef
13 changed files with 1850 additions and 0 deletions
35
cmd/api/main.go
Normal file
35
cmd/api/main.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/faker"
|
||||
"gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/server"
|
||||
"gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/training"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := log.New(os.Stdout, "backoffice-api: ", log.LstdFlags)
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
||||
defer cancel()
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
logger.Fatalf("error: %v\n", err)
|
||||
}
|
||||
|
||||
trainingRepo := training.NewInMemoryRepository()
|
||||
f := faker.NewFaker(trainingRepo)
|
||||
err = f.GenerateFakeData()
|
||||
if err != nil {
|
||||
logger.Fatalf("error: %v\n", err)
|
||||
}
|
||||
|
||||
s := server.NewServer(hostname, logger, trainingRepo)
|
||||
if err := s.Run(ctx); err != nil {
|
||||
logger.Fatalf("error: %v\n", err)
|
||||
}
|
||||
}
|
||||
29
go.mod
29
go.mod
|
|
@ -1,3 +1,32 @@
|
|||
module gitlab.mareshq.com/hq/backoffice/backoffice-api
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/getkin/kin-openapi v0.127.0
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
github.com/jackc/pgx/v5 v5.7.1
|
||||
github.com/oapi-codegen/nethttp-middleware v1.0.2
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/invopop/yaml v0.3.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
74
go.sum
74
go.sum
|
|
@ -0,0 +1,74 @@
|
|||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
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/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY=
|
||||
github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
|
||||
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
||||
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/oapi-codegen/nethttp-middleware v1.0.2 h1:A5tfAcKJhWIbIPnlQH+l/DtfVE1i5TFgPlQAiW+l1vQ=
|
||||
github.com/oapi-codegen/nethttp-middleware v1.0.2/go.mod h1:DfDalonSO+eRQ3RTb8kYoWZByCCPFRxm9WKq1UbY0E4=
|
||||
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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=
|
||||
13
internal/currency/currency.go
Normal file
13
internal/currency/currency.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package currency
|
||||
|
||||
type Currency string
|
||||
|
||||
const (
|
||||
USD Currency = "USD"
|
||||
EUR Currency = "EUR"
|
||||
CZK Currency = "CZK"
|
||||
)
|
||||
|
||||
var (
|
||||
SupportedCurrencies = []Currency{USD, EUR, CZK}
|
||||
)
|
||||
71
internal/faker/faker.go
Normal file
71
internal/faker/faker.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package faker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/shopspring/decimal"
|
||||
"gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/currency"
|
||||
"gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/training"
|
||||
)
|
||||
|
||||
type Faker struct {
|
||||
trainingRepository training.Repository
|
||||
}
|
||||
|
||||
func NewFaker(trainingRepository training.Repository) *Faker {
|
||||
return &Faker{
|
||||
trainingRepository: trainingRepository,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Faker) GenerateFakeData() error {
|
||||
var t *training.Training
|
||||
|
||||
oneDayPricing := []training.Price{
|
||||
{
|
||||
Currency: currency.CZK,
|
||||
Amount: decimal.NewFromInt(5900),
|
||||
Type: training.OpenTrainingPriceType,
|
||||
},
|
||||
{
|
||||
Currency: currency.CZK,
|
||||
Amount: decimal.NewFromInt(24000),
|
||||
Type: training.CorporateTrainingPriceType,
|
||||
},
|
||||
}
|
||||
|
||||
twoDayPricing := []training.Price{
|
||||
{
|
||||
Currency: currency.CZK,
|
||||
Amount: decimal.NewFromInt(9900),
|
||||
Type: training.OpenTrainingPriceType,
|
||||
},
|
||||
{
|
||||
Currency: currency.CZK,
|
||||
Amount: decimal.NewFromInt(44000),
|
||||
Type: training.CorporateTrainingPriceType,
|
||||
},
|
||||
}
|
||||
|
||||
t = training.NewTraining("Kubernetes v1", 1, nil)
|
||||
|
||||
err := f.trainingRepository.Create(context.Background(), t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t = training.NewTraining("Kubernetes", 2, twoDayPricing)
|
||||
|
||||
err = f.trainingRepository.Create(context.Background(), t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t = training.NewTraining("Terraform", 1, oneDayPricing)
|
||||
|
||||
err = f.trainingRepository.Create(context.Background(), t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1009
internal/server/api.gen.go
Normal file
1009
internal/server/api.gen.go
Normal file
File diff suppressed because it is too large
Load diff
114
internal/server/server.go
Normal file
114
internal/server/server.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
oapimiddleware "github.com/oapi-codegen/nethttp-middleware"
|
||||
"gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/training"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
logger *log.Logger
|
||||
hostname string
|
||||
|
||||
trainingRepository training.Repository
|
||||
|
||||
router *chi.Mux
|
||||
srv *http.Server
|
||||
tls *TLS
|
||||
}
|
||||
|
||||
type TLS struct {
|
||||
CertFile string
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
func NewServer(hostname string, logger *log.Logger, trainingRepository training.Repository) *Server {
|
||||
return &Server{
|
||||
logger: logger,
|
||||
hostname: hostname,
|
||||
trainingRepository: trainingRepository,
|
||||
router: chi.NewRouter(),
|
||||
tls: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func NewServerWithTLS(hostname string, tls *TLS, logger *log.Logger, trainingRepository training.Repository) *Server {
|
||||
s := NewServer(hostname, logger, trainingRepository)
|
||||
s.tls = tls
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) Run(ctx context.Context) error {
|
||||
swagger, err := GetSwagger()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear out the servers array in the swagger spec, that skips validating
|
||||
// that server names match. We don't know how this thing will be run.
|
||||
swagger.Servers = nil
|
||||
|
||||
s.router.Use(middleware.Logger)
|
||||
// middleware.Recoverer recovers from panics, logs the panic (and a stack trace),
|
||||
s.router.Use(middleware.Recoverer)
|
||||
// we trust headers, since we are running on Kubernetes with Ingress Controller (Ingress-NGINX)
|
||||
// and behind an L4 load balancer (on Hetzner Cloud)
|
||||
s.router.Use(middleware.RealIP)
|
||||
// middleware.RequestID generates a request ID and adds it to request context
|
||||
// if the request has an `X-Request-ID` header, it will use that as the request ID
|
||||
s.router.Use(middleware.RequestID)
|
||||
s.router.Use(middleware.Timeout(10 * time.Second))
|
||||
s.router.Use(oapimiddleware.OapiRequestValidator(swagger))
|
||||
|
||||
// create handler
|
||||
h := NewStrictHandler(s, nil)
|
||||
|
||||
// register endpoints
|
||||
HandlerFromMux(h, s.router)
|
||||
|
||||
s.srv = &http.Server{
|
||||
Addr: ":8080", // TODO: make port configurable
|
||||
Handler: s.router,
|
||||
}
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
if s.tls != nil {
|
||||
s.logger.Printf("Starting HTTPS server on: %s\n", s.srv.Addr)
|
||||
err = s.srv.ListenAndServeTLS(s.tls.CertFile, s.tls.KeyFile)
|
||||
} else {
|
||||
s.logger.Printf("Starting HTTP server on: %s\n", s.srv.Addr)
|
||||
err = s.srv.ListenAndServe()
|
||||
}
|
||||
// suppress the error if the server was closed gracefully
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.logger.Printf("error: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// wait for the context to be done
|
||||
// context done means the server is shutting down
|
||||
<-ctx.Done()
|
||||
|
||||
// TODO: make graceful shutdown period configurable
|
||||
timeout := 10 * time.Second
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
s.logger.Printf("Shutting down server in %s seconds\n", timeout.String())
|
||||
err = s.srv.Shutdown(timeoutCtx)
|
||||
// suppress the error if the server was closed gracefully
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Println("Server shutdown successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
167
internal/server/training.go
Normal file
167
internal/server/training.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/shopspring/decimal"
|
||||
"gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/training"
|
||||
)
|
||||
|
||||
func (s *Server) CreateTraining(ctx context.Context, req CreateTrainingRequestObject) (CreateTrainingResponseObject, error) {
|
||||
|
||||
pricing := make([]training.Price, len(req.Body.Training.Pricing))
|
||||
for i, p := range req.Body.Training.Pricing {
|
||||
amount, err := decimal.NewFromString(p.Amount)
|
||||
if err != nil {
|
||||
return CreateTraining400ApplicationProblemPlusJSONResponse{
|
||||
InvalidInputErrorApplicationProblemPlusJSONResponse{
|
||||
Title: "Invalid amount",
|
||||
Detail: err.Error(),
|
||||
Instance: s.hostname,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
pricing[i] = training.Price{
|
||||
Currency: p.Currency,
|
||||
Amount: amount,
|
||||
Type: training.PriceType(p.Type),
|
||||
}
|
||||
}
|
||||
|
||||
newTr := training.NewTraining(req.Body.Training.Name, req.Body.Training.Days, pricing)
|
||||
|
||||
err := s.trainingRepository.Create(ctx, newTr)
|
||||
if err != nil {
|
||||
// returning response and nil as error
|
||||
// since we suppress the error in code here and return an error response instead
|
||||
return CreateTraining500ApplicationProblemPlusJSONResponse{
|
||||
InternalErrorApplicationProblemPlusJSONResponse{
|
||||
Title: "Internal Server Error",
|
||||
Detail: err.Error(),
|
||||
Instance: s.hostname,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
tr := trainingToAPITraining(newTr)
|
||||
return CreateTraining201JSONResponse{
|
||||
CreateTrainingResponseJSONResponse(tr),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetTrainingByID(ctx context.Context, req GetTrainingByIDRequestObject) (GetTrainingByIDResponseObject, error) {
|
||||
tr, err := s.trainingRepository.FindByID(ctx, req.TrainingID)
|
||||
if err != nil {
|
||||
return GetTrainingByID404ApplicationProblemPlusJSONResponse{
|
||||
// returning response and nil as error
|
||||
// since we suppress the error in code here and return an error response instead
|
||||
NotFoundErrorApplicationProblemPlusJSONResponse{
|
||||
Title: "Not Found Training by ID",
|
||||
Detail: err.Error(),
|
||||
Instance: s.hostname,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
trAPI := trainingToAPITraining(tr)
|
||||
return GetTrainingByID200JSONResponse{
|
||||
GetTrainingResponseJSONResponse(trAPI),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListTrainings(ctx context.Context, _ ListTrainingsRequestObject) (ListTrainingsResponseObject, error) {
|
||||
trs, err := s.trainingRepository.FindAll(ctx)
|
||||
if err != nil {
|
||||
return ListTrainings500ApplicationProblemPlusJSONResponse{
|
||||
// returning response and nil as error
|
||||
// since we suppress the error in code here and return an error response instead
|
||||
InternalErrorApplicationProblemPlusJSONResponse{
|
||||
Title: "Internal Server Error",
|
||||
Detail: err.Error(),
|
||||
Instance: s.hostname,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var trAPIs []Training
|
||||
for _, tr := range trs {
|
||||
trAPIs = append(trAPIs, trainingToAPITraining(&tr))
|
||||
}
|
||||
|
||||
return ListTrainings200JSONResponse{
|
||||
ListTrainingsResponseJSONResponse{
|
||||
Trainings: &trAPIs,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) PublishTraining(ctx context.Context, req PublishTrainingRequestObject) (PublishTrainingResponseObject, error) {
|
||||
err := s.trainingRepository.Publish(ctx, req.TrainingID)
|
||||
if err != nil {
|
||||
return PublishTraining500ApplicationProblemPlusJSONResponse{
|
||||
// returning response and nil as error
|
||||
// since we suppress the error in code here and return an error response instead
|
||||
InternalErrorApplicationProblemPlusJSONResponse{
|
||||
Title: "Internal Server Error",
|
||||
Detail: err.Error(),
|
||||
Instance: s.hostname,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return PublishTraining204Response{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) UnpublishTraining(ctx context.Context, req UnpublishTrainingRequestObject) (UnpublishTrainingResponseObject, error) {
|
||||
err := s.trainingRepository.Unpublish(ctx, req.TrainingID)
|
||||
if err != nil {
|
||||
return UnpublishTraining500ApplicationProblemPlusJSONResponse{
|
||||
// returning response and nil as error
|
||||
// since we suppress the error in code here and return an error response instead
|
||||
InternalErrorApplicationProblemPlusJSONResponse{
|
||||
Title: "Internal Server Error",
|
||||
Detail: err.Error(),
|
||||
Instance: s.hostname,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return UnpublishTraining204Response{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) RetireTraining(ctx context.Context, req RetireTrainingRequestObject) (RetireTrainingResponseObject, error) {
|
||||
err := s.trainingRepository.Retire(ctx, req.TrainingID)
|
||||
if err != nil {
|
||||
return RetireTraining500ApplicationProblemPlusJSONResponse{
|
||||
// returning response and nil as error
|
||||
// since we suppress the error in code here and return an error response instead
|
||||
InternalErrorApplicationProblemPlusJSONResponse{
|
||||
Title: "Internal Server Error",
|
||||
Detail: err.Error(),
|
||||
Instance: s.hostname,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return RetireTraining204Response{}, nil
|
||||
}
|
||||
|
||||
func trainingToAPITraining(t *training.Training) Training {
|
||||
tPricing := t.Pricing()
|
||||
pricing := make([]TrainingPrice, len(tPricing))
|
||||
for i, p := range tPricing {
|
||||
pricing[i] = TrainingPrice{
|
||||
Currency: p.Currency,
|
||||
Amount: p.Amount.String(),
|
||||
Type: TrainingPriceType(p.Type),
|
||||
}
|
||||
}
|
||||
return Training{
|
||||
Id: t.ID(),
|
||||
Name: t.Name(),
|
||||
Days: t.Days(),
|
||||
Published: t.Published(),
|
||||
Retired: t.Retired(),
|
||||
Pricing: pricing,
|
||||
}
|
||||
}
|
||||
7
internal/training/errors.go
Normal file
7
internal/training/errors.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package training
|
||||
|
||||
type ErrNotFound struct{}
|
||||
|
||||
func (ErrNotFound) Error() string {
|
||||
return "not found"
|
||||
}
|
||||
105
internal/training/inmemory_repository.go
Normal file
105
internal/training/inmemory_repository.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package training
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type InMemoryRepository struct {
|
||||
m sync.Mutex
|
||||
nextID ID
|
||||
|
||||
trainings map[ID]*Training
|
||||
}
|
||||
|
||||
type InMemoryPricingRepository struct{}
|
||||
|
||||
func NewInMemoryRepository() *InMemoryRepository {
|
||||
return &InMemoryRepository{
|
||||
m: sync.Mutex{},
|
||||
nextID: 1,
|
||||
trainings: make(map[ID]*Training),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) FindAll(_ context.Context) ([]Training, error) {
|
||||
r.m.Lock()
|
||||
defer r.m.Unlock()
|
||||
|
||||
var trainings []Training
|
||||
for _, t := range r.trainings {
|
||||
trainings = append(trainings, *t)
|
||||
}
|
||||
|
||||
return trainings, nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) FindByID(_ context.Context, id ID) (*Training, error) {
|
||||
r.m.Lock()
|
||||
defer r.m.Unlock()
|
||||
|
||||
t, ok := r.trainings[id]
|
||||
if !ok {
|
||||
return nil, &ErrNotFound{}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) Create(_ context.Context, training *Training) error {
|
||||
r.m.Lock()
|
||||
defer r.m.Unlock()
|
||||
|
||||
training.id = r.nextID
|
||||
r.nextID++
|
||||
r.trainings[training.id] = training
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) Update(_ context.Context, training *Training) error {
|
||||
r.m.Lock()
|
||||
defer r.m.Unlock()
|
||||
|
||||
if _, ok := r.trainings[training.id]; !ok {
|
||||
return &ErrNotFound{}
|
||||
}
|
||||
|
||||
r.trainings[training.id] = training
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) Publish(_ context.Context, id ID) error {
|
||||
r.m.Lock()
|
||||
defer r.m.Unlock()
|
||||
|
||||
t, ok := r.trainings[id]
|
||||
if !ok {
|
||||
return &ErrNotFound{}
|
||||
}
|
||||
t.published = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) Unpublish(_ context.Context, id ID) error {
|
||||
r.m.Lock()
|
||||
defer r.m.Unlock()
|
||||
|
||||
t, ok := r.trainings[id]
|
||||
if !ok {
|
||||
return &ErrNotFound{}
|
||||
}
|
||||
t.published = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) Retire(_ context.Context, id ID) error {
|
||||
r.m.Lock()
|
||||
defer r.m.Unlock()
|
||||
|
||||
t, ok := r.trainings[id]
|
||||
if !ok {
|
||||
return &ErrNotFound{}
|
||||
}
|
||||
t.retired = true
|
||||
return nil
|
||||
}
|
||||
76
internal/training/model.go
Normal file
76
internal/training/model.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package training
|
||||
|
||||
import (
|
||||
"github.com/shopspring/decimal"
|
||||
"gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/currency"
|
||||
)
|
||||
|
||||
type ID = int
|
||||
|
||||
type Training struct {
|
||||
id ID // unique and auto-incrementing
|
||||
published bool
|
||||
retired bool
|
||||
|
||||
days int8
|
||||
|
||||
// In the future, this should be localized
|
||||
name string
|
||||
pricing []Price `db:"-"`
|
||||
}
|
||||
|
||||
type Price struct {
|
||||
Amount decimal.Decimal
|
||||
Currency currency.Currency
|
||||
Type PriceType
|
||||
}
|
||||
|
||||
type PriceType string
|
||||
|
||||
const (
|
||||
OpenTrainingPriceType PriceType = "OPEN"
|
||||
CorporateTrainingPriceType PriceType = "CORPORATE"
|
||||
)
|
||||
|
||||
func NewTraining(name string, days int8, pricing []Price) *Training {
|
||||
t := &Training{
|
||||
// metadata
|
||||
published: false,
|
||||
retired: false,
|
||||
|
||||
// data
|
||||
name: name,
|
||||
days: days,
|
||||
pricing: make([]Price, 0),
|
||||
}
|
||||
|
||||
if pricing != nil {
|
||||
t.pricing = pricing
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Training) ID() ID {
|
||||
return t.id
|
||||
}
|
||||
|
||||
func (t *Training) Days() int8 {
|
||||
return t.days
|
||||
}
|
||||
|
||||
func (t *Training) Published() bool {
|
||||
return t.published
|
||||
}
|
||||
|
||||
func (t *Training) Retired() bool {
|
||||
return t.retired
|
||||
}
|
||||
|
||||
func (t *Training) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *Training) Pricing() []Price {
|
||||
return t.pricing
|
||||
}
|
||||
129
internal/training/postgres_repository.go
Normal file
129
internal/training/postgres_repository.go
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
package training
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"log"
|
||||
)
|
||||
|
||||
type PostgresRepository struct {
|
||||
pg *pgxpool.Pool
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewPostgresRepository(logger *log.Logger, pg *pgxpool.Pool) *PostgresRepository {
|
||||
return &PostgresRepository{
|
||||
pg: pg,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PostgresRepository) Create(ctx context.Context, training *Training) error {
|
||||
tx, txErr := r.pg.Begin(ctx)
|
||||
if txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
|
||||
queryErr := tx.QueryRow(ctx, `
|
||||
INSERT INTO training.trainings (name, days, published)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`, training.name, training.days, training.published).Scan(&training.id)
|
||||
if queryErr != nil {
|
||||
return queryErr
|
||||
}
|
||||
|
||||
// TODO: insert pricing using the transaction (tx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PostgresRepository) Update(ctx context.Context, training *Training) error {
|
||||
_, err := r.pg.Exec(ctx, `
|
||||
UPDATE training.trainings
|
||||
SET title = $2
|
||||
WHERE id = $1
|
||||
`, training.ID, training.name)
|
||||
if err != nil {
|
||||
r.logger.Printf("error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PostgresRepository) FindByID(ctx context.Context, id ID) (*Training, error) {
|
||||
var t Training
|
||||
err := r.pg.QueryRow(ctx, `
|
||||
SELECT id, title, published, retired
|
||||
FROM training.trainings
|
||||
WHERE id = $1
|
||||
`, id).Scan(&t.id, &t.name, &t.published, &t.retired)
|
||||
if err != nil {
|
||||
r.logger.Printf("error: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *PostgresRepository) FindAll(ctx context.Context) ([]Training, error) {
|
||||
rows, err := r.pg.Query(ctx, `
|
||||
SELECT id, title, published, retired
|
||||
FROM training.trainings
|
||||
`)
|
||||
if err != nil {
|
||||
r.logger.Printf("error: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var trainings []Training
|
||||
for rows.Next() {
|
||||
var t Training
|
||||
err := rows.Scan(&t.id, &t.name, &t.published, &t.retired)
|
||||
if err != nil {
|
||||
r.logger.Printf("error: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
trainings = append(trainings, t)
|
||||
}
|
||||
return trainings, nil
|
||||
}
|
||||
|
||||
func (r *PostgresRepository) Publish(ctx context.Context, id ID) error {
|
||||
_, err := r.pg.Exec(ctx, `
|
||||
UPDATE training.trainings
|
||||
SET published = true
|
||||
WHERE id = $1
|
||||
`, id)
|
||||
if err != nil {
|
||||
r.logger.Printf("error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PostgresRepository) Unpublish(ctx context.Context, id ID) error {
|
||||
_, err := r.pg.Exec(ctx, `
|
||||
UPDATE training.trainings
|
||||
SET published = false
|
||||
WHERE id = $1
|
||||
`, id)
|
||||
if err != nil {
|
||||
r.logger.Printf("error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PostgresRepository) Retire(ctx context.Context, id ID) error {
|
||||
_, err := r.pg.Exec(ctx, `
|
||||
UPDATE training.trainings
|
||||
SET retired = true
|
||||
WHERE id = $1
|
||||
`, id)
|
||||
if err != nil {
|
||||
r.logger.Printf("error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
21
internal/training/repository.go
Normal file
21
internal/training/repository.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package training
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
FindAll(ctx context.Context) ([]Training, error)
|
||||
FindByID(ctx context.Context, id ID) (*Training, error)
|
||||
Create(ctx context.Context, training *Training) error
|
||||
Update(ctx context.Context, training *Training) error
|
||||
Publish(ctx context.Context, id ID) error
|
||||
Unpublish(ctx context.Context, id ID) error
|
||||
Retire(ctx context.Context, id ID) error
|
||||
}
|
||||
|
||||
type PricingRepository interface {
|
||||
UpdateForCurrency(ctx context.Context, trainingID ID, currency string, price float64) error
|
||||
AddCurrency(ctx context.Context, trainingID ID, currency string, price float64) error
|
||||
RemoveCurrency(ctx context.Context, trainingID ID, currency string) error
|
||||
}
|
||||
Reference in a new issue