Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
577 changes: 570 additions & 7 deletions api/docs/docs.go

Large diffs are not rendered by default.

9,115 changes: 5,055 additions & 4,060 deletions api/docs/swagger.json

Large diffs are not rendered by default.

2,572 changes: 1,456 additions & 1,116 deletions api/docs/swagger.yaml

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions api/pkg/di/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func NewContainer(projectID string, version string) (container *Container) {
container.RegisterHeartbeatListeners()

container.RegisterUserRoutes()
container.RegisterSendScheduleRoutes()
container.RegisterUserListeners()

container.RegisterPhoneRoutes()
Expand Down Expand Up @@ -361,6 +362,10 @@ ALTER TABLE discords ADD CONSTRAINT IF NOT EXISTS uni_discords_server_id CHECK (
container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.User{})))
}

if err = db.AutoMigrate(&entities.SendSchedule{}); err != nil {
container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.SendSchedule{})))
}

if err = db.AutoMigrate(&entities.Phone{}); err != nil {
container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Phone{})))
}
Expand Down Expand Up @@ -750,6 +755,46 @@ func (container *Container) PhoneRepository() (repository repositories.PhoneRepo
)
}

// SendScheduleRepository creates a new instance of repositories.SendScheduleRepository
func (container *Container) SendScheduleRepository() repositories.SendScheduleRepository {
container.logger.Debug("creating GORM repositories.SendScheduleRepository")
return repositories.NewGormSendScheduleRepository(
container.Logger(),
container.Tracer(),
container.DB(),
)
}

// SendScheduleService creates a new instance of services.SendScheduleService
func (container *Container) SendScheduleService() *services.SendScheduleService {
container.logger.Debug("creating services.SendScheduleService")
return services.NewSendScheduleService(
container.Logger(),
container.Tracer(),
container.SendScheduleRepository(),
)
}

// SendScheduleHandlerValidator creates a new instance of validators.SendScheduleHandlerValidator
func (container *Container) SendScheduleHandlerValidator() *validators.SendScheduleHandlerValidator {
container.logger.Debug("creating validators.SendScheduleHandlerValidator")
return validators.NewSendScheduleHandlerValidator(
container.Logger(),
container.Tracer(),
)
}

// SendScheduleHandler creates a new instance of handlers.SendScheduleHandler
func (container *Container) SendScheduleHandler() *handlers.SendScheduleHandler {
container.logger.Debug("creating handlers.SendScheduleHandler")
return handlers.NewSendScheduleHandler(
container.Logger(),
container.Tracer(),
container.SendScheduleHandlerValidator(),
container.SendScheduleService(),
)
}

// BillingUsageRepository creates a new instance of repositories.BillingUsageRepository
func (container *Container) BillingUsageRepository() (repository repositories.BillingUsageRepository) {
container.logger.Debug("creating GORM repositories.BillingUsageRepository")
Expand Down Expand Up @@ -1453,6 +1498,7 @@ func (container *Container) NotificationService() (service *services.PhoneNotifi
container.FirebaseMessagingClient(),
container.PhoneRepository(),
container.PhoneNotificationRepository(),
container.SendScheduleRepository(),
container.EventDispatcher(),
)
}
Expand Down Expand Up @@ -1508,6 +1554,12 @@ func (container *Container) RegisterUserRoutes() {
container.UserHandler().RegisterRoutes(container.App(), container.AuthenticatedMiddleware())
}

// RegisterSendScheduleRoutes registers routes for the /send-schedules prefix
func (container *Container) RegisterSendScheduleRoutes() {
container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.SendScheduleHandler{}))
container.SendScheduleHandler().RegisterRoutes(container.App(), container.AuthenticatedMiddleware())
}

// RegisterEventRoutes registers routes for the /events prefix
func (container *Container) RegisterEventRoutes() {
container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.EventsHandler{}))
Expand Down
14 changes: 8 additions & 6 deletions api/pkg/entities/phone.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import (

// Phone represents an android phone which has installed the http sms app
type Phone struct {
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
FcmToken *string `json:"fcm_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." validate:"optional"`
PhoneNumber string `json:"phone_number" example:"+18005550199"`
MessagesPerMinute uint `json:"messages_per_minute" example:"1"`
SIM SIM `json:"sim" gorm:"default:SIM1"`
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
FcmToken *string `json:"fcm_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." validate:"optional"`
PhoneNumber string `json:"phone_number" example:"+18005550199"`
MessagesPerMinute uint `json:"messages_per_minute" example:"1"`
SIM SIM `json:"sim" gorm:"default:SIM1"`
ScheduleID *uuid.UUID `json:"schedule_id" gorm:"type:uuid" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
Schedule *SendSchedule `json:"-" gorm:"foreignKey:ScheduleID;constraint:OnDelete:SET NULL"`
// MaxSendAttempts determines how many times to retry sending an SMS message
MaxSendAttempts uint `json:"max_send_attempts" example:"2"`

Expand Down
26 changes: 26 additions & 0 deletions api/pkg/entities/send_schedule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package entities

import (
"time"

"github.com/google/uuid"
)

// SendScheduleWindow represents a single availability window for a day of the week.
type SendScheduleWindow struct {
DayOfWeek int `json:"day_of_week" example:"1"`
StartMinute int `json:"start_minute" example:"540"`
EndMinute int `json:"end_minute" example:"1020"`
}

// SendSchedule controls when a phone is allowed to send outgoing SMS messages.
type SendSchedule struct {
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
Name string `json:"name" example:"Business Hours"`
Timezone string `json:"timezone" example:"Europe/Tallinn"`
IsActive bool `json:"is_active" gorm:"default:true" example:"true"`
Windows []SendScheduleWindow `json:"windows" gorm:"type:jsonb;serializer:json"`
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
}
187 changes: 187 additions & 0 deletions api/pkg/handlers/send_schedule_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package handlers

import (
"fmt"

"github.com/NdoleStudio/httpsms/pkg/requests"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/palantir/stacktrace"
)

type SendScheduleHandler struct {
handler
logger telemetry.Logger
tracer telemetry.Tracer
validator *validators.SendScheduleHandlerValidator
service *services.SendScheduleService
}

func NewSendScheduleHandler(logger telemetry.Logger, tracer telemetry.Tracer, validator *validators.SendScheduleHandlerValidator, service *services.SendScheduleService) *SendScheduleHandler {
return &SendScheduleHandler{logger: logger.WithService(fmt.Sprintf("%T", &SendScheduleHandler{})), tracer: tracer, validator: validator, service: service}
}

func (h *SendScheduleHandler) RegisterRoutes(router fiber.Router, middlewares ...fiber.Handler) {
router.Get("/v1/send-schedules", h.computeRoute(middlewares, h.Index)...)
router.Post("/v1/send-schedules", h.computeRoute(middlewares, h.Store)...)
router.Get("/v1/send-schedules/:scheduleID", h.computeRoute(middlewares, h.Show)...)
router.Put("/v1/send-schedules/:scheduleID", h.computeRoute(middlewares, h.Update)...)
router.Delete("/v1/send-schedules/:scheduleID", h.computeRoute(middlewares, h.Delete)...)
}

// Index godoc
// @Summary List send schedules
// @Description Lists the send schedules owned by the authenticated user.
// @Security ApiKeyAuth
// @Tags Send Schedules
// @Produce json
// @Success 200 {object} responses.SendSchedulesResponse
// @Failure 401 {object} responses.Unauthorized
// @Failure 500 {object} responses.InternalServerError
// @Router /send-schedules [get]
func (h *SendScheduleHandler) Index(c *fiber.Ctx) error {
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
defer span.End()
schedules, err := h.service.Index(ctx, h.userIDFomContext(c))
if err != nil {
ctxLogger.Error(stacktrace.Propagate(err, "cannot list send schedules"))
return h.responseInternalServerError(c)
}
return h.responseOK(c, "send schedules fetched successfully", schedules)
}

// Show godoc
// @Summary Show send schedule
// @Description Loads a single send schedule owned by the authenticated user.
// @Security ApiKeyAuth
// @Tags Send Schedules
// @Produce json
// @Param scheduleID path string true "Schedule ID"
// @Success 200 {object} responses.SendScheduleResponse
// @Failure 401 {object} responses.Unauthorized
// @Failure 404 {object} responses.NotFound
// @Failure 500 {object} responses.InternalServerError
// @Router /send-schedules/{scheduleID} [get]
func (h *SendScheduleHandler) Show(c *fiber.Ctx) error {
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
defer span.End()
scheduleID, err := uuid.Parse(c.Params("scheduleID"))
if err != nil {
return h.responseBadRequest(c, err)
}
schedule, err := h.service.Load(ctx, h.userIDFomContext(c), scheduleID)
if err != nil {
ctxLogger.Error(stacktrace.Propagate(err, "cannot load send schedule"))
if stacktrace.GetCode(err) == 404 {
return h.responseNotFound(c, err.Error())
}
return h.responseInternalServerError(c)
}
return h.responseOK(c, "send schedule fetched successfully", schedule)
}

// Store godoc
// @Summary Create send schedule
// @Description Creates a send schedule for the authenticated user.
// @Security ApiKeyAuth
// @Tags Send Schedules
// @Accept json
// @Produce json
// @Param payload body requests.SendScheduleStore true "Payload of new send schedule."
// @Success 201 {object} responses.SendScheduleResponse
// @Failure 400 {object} responses.BadRequest
// @Failure 401 {object} responses.Unauthorized
// @Failure 422 {object} responses.UnprocessableEntity
// @Failure 500 {object} responses.InternalServerError
// @Router /send-schedules [post]
func (h *SendScheduleHandler) Store(c *fiber.Ctx) error {
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
defer span.End()
var request requests.SendScheduleStore
if err := c.BodyParser(&request); err != nil {
return h.responseBadRequest(c, err)
}
request = request.Sanitize()
if errors := h.validator.ValidateStore(ctx, request); len(errors) != 0 {
ctxLogger.Warn(stacktrace.NewError(fmt.Sprintf("validation errors [%s], while storing send schedule [%+#v]", spew.Sdump(errors), request)))
return h.responseUnprocessableEntity(c, errors, "validation errors while saving send schedule")
}
schedule, err := h.service.Store(ctx, request.ToParams(h.userFromContext(c)))
if err != nil {
ctxLogger.Error(stacktrace.Propagate(err, "cannot create send schedule"))
return h.responseInternalServerError(c)
}
return h.responseCreated(c, "send schedule created successfully", schedule)
}

// Update godoc
// @Summary Update send schedule
// @Description Updates a send schedule owned by the authenticated user.
// @Security ApiKeyAuth
// @Tags Send Schedules
// @Accept json
// @Produce json
// @Param scheduleID path string true "Schedule ID"
// @Param payload body requests.SendScheduleStore true "Payload of updated send schedule."
// @Success 200 {object} responses.SendScheduleResponse
// @Failure 400 {object} responses.BadRequest
// @Failure 401 {object} responses.Unauthorized
// @Failure 404 {object} responses.NotFound
// @Failure 422 {object} responses.UnprocessableEntity
// @Failure 500 {object} responses.InternalServerError
// @Router /send-schedules/{scheduleID} [put]
func (h *SendScheduleHandler) Update(c *fiber.Ctx) error {
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
defer span.End()
scheduleID, err := uuid.Parse(c.Params("scheduleID"))
if err != nil {
return h.responseBadRequest(c, err)
}
var request requests.SendScheduleStore
if err = c.BodyParser(&request); err != nil {
return h.responseBadRequest(c, err)
}
request = request.Sanitize()
if errors := h.validator.ValidateStore(ctx, request); len(errors) != 0 {
return h.responseUnprocessableEntity(c, errors, "validation errors while updating send schedule")
}
schedule, err := h.service.Update(ctx, h.userIDFomContext(c), scheduleID, request.ToParams(h.userFromContext(c)))
if err != nil {
ctxLogger.Error(stacktrace.Propagate(err, "cannot update send schedule"))
if stacktrace.GetCode(err) == 404 {
return h.responseNotFound(c, err.Error())
}
return h.responseInternalServerError(c)
}
return h.responseOK(c, "send schedule updated successfully", schedule)
}

// Delete godoc
// @Summary Delete send schedule
// @Description Deletes a send schedule owned by the authenticated user.
// @Security ApiKeyAuth
// @Tags Send Schedules
// @Produce json
// @Param scheduleID path string true "Schedule ID"
// @Success 204 {object} responses.NoContent
// @Failure 400 {object} responses.BadRequest
// @Failure 401 {object} responses.Unauthorized
// @Failure 500 {object} responses.InternalServerError
// @Router /send-schedules/{scheduleID} [delete]
func (h *SendScheduleHandler) Delete(c *fiber.Ctx) error {
ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
defer span.End()
scheduleID, err := uuid.Parse(c.Params("scheduleID"))
if err != nil {
return h.responseBadRequest(c, err)
}
if err = h.service.Delete(ctx, h.userIDFomContext(c), scheduleID); err != nil {
ctxLogger.Error(stacktrace.Propagate(err, "cannot delete send schedule"))
return h.responseInternalServerError(c)
}
return h.responseNoContent(c, "send schedule deleted successfully")
}
Loading