diff --git a/apps/api/cmd/api/main.go b/apps/api/cmd/api/main.go index 9fed47ef..d9d8b2e9 100644 --- a/apps/api/cmd/api/main.go +++ b/apps/api/cmd/api/main.go @@ -63,6 +63,7 @@ func main() { eventInterestRepo := repository.NewEventInterestRepository(database) eventRepo := repository.NewEventRespository(database) applicationRepo := repository.NewApplicationRepository(database) + campaignRepo := repository.NewCampaignRepository(database) // Injections into services authService := services.NewAuthService(userRepo, accountRepo, sessionRepo, txm, client, logger, &cfg.Auth) @@ -70,10 +71,11 @@ func main() { eventInterestService := services.NewEventInterestService(eventInterestRepo, logger) eventService := services.NewEventService(eventRepo, userRepo, r2Client, &cfg.CoreBuckets, logger) emailService := services.NewEmailService(taskQueueClient, logger) + campaignService := services.NewCampaignService(campaignRepo, logger) applicationService := services.NewApplicationService(applicationRepo, eventService, txm, r2Client, &cfg.CoreBuckets, logger) // Injections into handlers - apiHandlers := handlers.NewHandlers(authService, userService, eventInterestService, eventService, emailService, applicationService, cfg, logger) + apiHandlers := handlers.NewHandlers(authService, userService, eventInterestService, eventService, emailService, applicationService, campaignService, cfg, logger) api := api.NewAPI(&logger, apiHandlers, mw) diff --git a/apps/api/internal/api/api.go b/apps/api/internal/api/api.go index 45d3a27d..841b56ee 100644 --- a/apps/api/internal/api/api.go +++ b/apps/api/internal/api/api.go @@ -127,6 +127,17 @@ func (api *API) setupRoutes(mw *mw.Middleware) { api.Router.Route("/email", func(r chi.Router) { r.Post("/queue", api.Handlers.Email.QueueEmail) }) + + // Campaign routes + api.Router.Route("/campaigns", func(r chi.Router) { + r.Use(ensureEventAdmin) + + r.Post("/", api.Handlers.Campaign.CreateCampaign) + + r.Route("/{campaignId}", func(r chi.Router) { + + }) + }) // Protected test routes api.Router.Route("/protected", func(r chi.Router) { diff --git a/apps/api/internal/api/handlers/campaigns.go b/apps/api/internal/api/handlers/campaigns.go new file mode 100644 index 00000000..f80ebfd6 --- /dev/null +++ b/apps/api/internal/api/handlers/campaigns.go @@ -0,0 +1,72 @@ +package handlers + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/go-playground/validator/v10" + "github.com/google/uuid" + "github.com/rs/zerolog" + res "github.com/swamphacks/core/apps/api/internal/api/response" + "github.com/swamphacks/core/apps/api/internal/config" + "github.com/swamphacks/core/apps/api/internal/db/sqlc" + "github.com/swamphacks/core/apps/api/internal/services" +) + +type CampaignHandler struct { + campaignService *services.CampaignService + cfg *config.Config + logger zerolog.Logger +} + +func NewCampaignHandler(campaignService *services.CampaignService, logger zerolog.Logger) *CampaignHandler { + return &CampaignHandler{ + campaignService: campaignService, + logger: logger.With().Str("handler", "CampaignHandler").Str("component", "campaigns").Logger(), + } +} + +type CreateCampaignFields struct { + EventID uuid.UUID `json:"event_id" validate:"required"` + Title string `json:"title" validate:"required"` + Description *string `json:"description"` + RecipientRoles *[]string `json:"recipient_roles"` + CreatedBy uuid.UUID `json:"created_by" validate:"required"` +} + +func (h *CampaignHandler) CreateCampaign(w http.ResponseWriter, r *http.Request) { + + // Parse JSON body + var req CreateCampaignFields + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&req); err != nil { + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Could not parse request body")) + return + } + + validate := validator.New() + if err := validate.Struct(req); err != nil { + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", err.Error())) + } + + params := sqlc.CreateCampaignParams{ + EventID: req.EventID, + Title: req.Title, + Description: req.Description, + RecipientRoles: req.RecipientRoles, + CreatedBy: req.CreatedBy, + } + + campaign, err := h.campaignService.CreateCampaign(r.Context(), params) + if err != nil { + if errors.Is(err, services.ErrFailedToCreateCampaign) { + res.SendError(w, http.StatusInternalServerError, res.NewError("creation_error", "Failed to create campaign")) + } else { + res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) + } + } + + res.Send(w, http.StatusCreated, campaign) +} diff --git a/apps/api/internal/api/handlers/handlers.go b/apps/api/internal/api/handlers/handlers.go index f47290b5..7cade831 100644 --- a/apps/api/internal/api/handlers/handlers.go +++ b/apps/api/internal/api/handlers/handlers.go @@ -13,6 +13,7 @@ type Handlers struct { Event *EventHandler Email *EmailHandler Application *ApplicationHandler + Campaign *CampaignHandler } func NewHandlers( @@ -22,6 +23,7 @@ func NewHandlers( eventService *services.EventService, emailService *services.EmailService, appService *services.ApplicationService, + campaignService *services.CampaignService, cfg *config.Config, logger zerolog.Logger, ) *Handlers { @@ -31,6 +33,7 @@ func NewHandlers( EventInterest: NewEventInterestHandler(eventInterestService, cfg, logger), Event: NewEventHandler(eventService, cfg, logger), Email: NewEmailHandler(emailService, logger), + Campaign: NewCampaignHandler(campaignService, logger), Application: NewApplicationHandler(appService), } } diff --git a/apps/api/internal/db/migrations/20250923161349_campaign_schema.sql b/apps/api/internal/db/migrations/20250923161349_campaign_schema.sql new file mode 100644 index 00000000..c1904607 --- /dev/null +++ b/apps/api/internal/db/migrations/20250923161349_campaign_schema.sql @@ -0,0 +1,94 @@ +-- +goose Up +-- +goose StatementBegin + +CREATE TABLE campaign_email_templates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT CONSTRAINT title_charlim CHECK (char_length(title) <= 200), + html TEXT, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE campaign_email_links ( + -- May be able to simplify the primary key being used here + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + role event_role_type NOT NULL, + hit_count INT DEFAULT 0, + unsubscribed_count INT DEFAULT 0, + + -- Example endpoint: GET localhost/?redirect=&role= + api_endpoint TEXT NOT NULL, + redirect_to TEXT NOT NULL, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE campaign_emails ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + recipient_addresses TEXT[], + recipient_roles TEXT[] NOT NULL, + send_from TEXT NOT NULL, + subject TEXT CONSTRAINT subject_charlim CHECK (char_length(subject) < 78), -- From SMTP spec. + body TEXT, -- Char limit determined by mail clients. + + -- Metadata + template UUID NOT NULL REFERENCES campaign_email_templates(id), + send_on TIMESTAMPTZ NOT NULL, + links UUID[], + created_by UUID NOT NULL REFERENCES auth.users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE campaigns ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, + title TEXT CONSTRAINT title_charlim CHECK (char_length(title) <= 200) NOT NULL, + description TEXT CONSTRAINT description_charlim CHECK (char_length(description) <= 1000), + recipient_roles event_role_type[] NOT NULL, + recipient_addresses TEXT[], + emails UUID[], + + created_by UUID NOT NULL REFERENCES auth.users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TRIGGER set_updated_at_campaign_email_templates +BEFORE UPDATE ON campaign_email_templates +FOR EACH ROW +EXECUTE FUNCTION update_modified_column(); + +CREATE TRIGGER set_updated_at_campaign_email_links +BEFORE UPDATE ON campaign_email_links +FOR EACH ROW +EXECUTE FUNCTION update_modified_column(); + +CREATE TRIGGER set_updated_at_campaign_emails +BEFORE UPDATE ON campaign_emails +FOR EACH ROW +EXECUTE FUNCTION update_modified_column(); + +CREATE TRIGGER set_updated_at_campaigns +BEFORE UPDATE ON campaigns +FOR EACH ROW +EXECUTE FUNCTION update_modified_column(); + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +DROP TRIGGER IF EXISTS set_updated_at_campaigns ON campaigns; +DROP TRIGGER IF EXISTS set_updated_at_campaign_emails ON campaign_emails; +DROP TRIGGER IF EXISTS set_updated_at_campaign_email_links ON campaign_email_links; +DROP TRIGGER IF EXISTS set_updated_at_campaign_email_templates ON campaign_email_templates; + +DROP TABLE IF EXISTS campaigns; +DROP TABLE IF EXISTS campaign_emails; +DROP TABLE IF EXISTS campaign_email_links; +DROP TABLE IF EXISTS campaign_email_templates; + +-- +goose StatementEnd \ No newline at end of file diff --git a/apps/api/internal/db/queries/campaigns.sql b/apps/api/internal/db/queries/campaigns.sql new file mode 100644 index 00000000..c2f70802 --- /dev/null +++ b/apps/api/internal/db/queries/campaigns.sql @@ -0,0 +1,33 @@ +-- name: CreateCampaign :one +INSERT INTO campaigns ( + event_id, + title, description, + recipient_roles, + created_by +) VALUES ( + @event_id, @title, + coalesce(sqlc.narg(description), null), + coalesce(sqlc.narg(recipient_roles), null), + @created_by +) +RETURNING *; + +-- name: GetCampaignById :one +SELECT * FROM campaigns +WHERE id = $1; + +-- name: UpdateCampaignById :exec +UPDATE campaigns +SET + title = CASE WHEN @title_do_update::boolean THEN @title ELSE title END, + description = CASE WHEN @description_do_update::boolean THEN @description ELSE description END, + recipient_roles = CASE WHEN @recipient_roles::boolean THEN @recipient_roles ELSE recipient_roles END, + recipient_addresses = CASE WHEN @recipient_addresses::boolean THEN @recipient_addresses ELSE recipient_addresses END, + emails = CASE WHEN @emails::boolean THEN @emails ELSE emails END +WHERE + id = @id::uuid +returning *; + +-- name: DeleteCampaignById :execrows +DELETE FROM campaigns +WHERE id = $1; \ No newline at end of file diff --git a/apps/api/internal/db/repository/campaigns.go b/apps/api/internal/db/repository/campaigns.go new file mode 100644 index 00000000..d855fe48 --- /dev/null +++ b/apps/api/internal/db/repository/campaigns.go @@ -0,0 +1,68 @@ +package repository + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/swamphacks/core/apps/api/internal/db" + "github.com/swamphacks/core/apps/api/internal/db/sqlc" +) + +var ( + ErrCampaignNotFound = errors.New("campaign not found") + ErrNoCampaignsDeleted = errors.New("no campsigns deleted") + ErrMultipleCampaignsDeleted = errors.New("multiple campaigns affected by delete query while only expecting one to delete one") +) + +type CampaignRepository struct { + db *db.DB +} + +func NewCampaignRepository(db *db.DB) *CampaignRepository { + return &CampaignRepository{ + db: db, + } +} + +func (r *CampaignRepository) CreateCampaign(ctx context.Context, params sqlc.CreateCampaignParams) (*sqlc.Campaign, error) { + campaign, err := r.db.Query.CreateCampaign(ctx, params) + if err != nil { + return nil, err + } + + return &campaign, nil +} + +func (r *CampaignRepository) GetCampaignByID(ctx context.Context, id uuid.UUID) (*sqlc.Campaign, error) { + campaign, err := r.db.Query.GetCampaignById(ctx, id) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrCampaignNotFound + } else if err != nil { + return nil, err + } + + return &campaign, nil +} + +func (r *CampaignRepository) UpdateCampaignById(ctx context.Context, params sqlc.UpdateCampaignByIdParams) error { + err := r.db.Query.UpdateCampaignById(ctx, params) + if errors.Is(err, pgx.ErrNoRows) { + return ErrCampaignNotFound + } + return err +} + +func (r *CampaignRepository) DeleteCampaignById(ctx context.Context, id uuid.UUID) error { + affectedRows, err := r.db.Query.DeleteCampaignById(ctx, id) + if errors.Is(err, pgx.ErrNoRows) { + return ErrCampaignNotFound + } + if affectedRows == 0 { + return ErrNoCampaignsDeleted + } else if affectedRows > 1 { + return ErrMultipleCampaignsDeleted + } + return err +} diff --git a/apps/api/internal/db/sqlc/accounts.sql.go b/apps/api/internal/db/sqlc/accounts.sql.go index 628f20b2..b1c94d40 100644 --- a/apps/api/internal/db/sqlc/accounts.sql.go +++ b/apps/api/internal/db/sqlc/accounts.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: accounts.sql package sqlc diff --git a/apps/api/internal/db/sqlc/applications.sql.go b/apps/api/internal/db/sqlc/applications.sql.go index 43fe3cb4..8d94d9c1 100644 --- a/apps/api/internal/db/sqlc/applications.sql.go +++ b/apps/api/internal/db/sqlc/applications.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: applications.sql package sqlc diff --git a/apps/api/internal/db/sqlc/campaigns.sql.go b/apps/api/internal/db/sqlc/campaigns.sql.go new file mode 100644 index 00000000..45db8880 --- /dev/null +++ b/apps/api/internal/db/sqlc/campaigns.sql.go @@ -0,0 +1,133 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: campaigns.sql + +package sqlc + +import ( + "context" + + "github.com/google/uuid" +) + +const createCampaign = `-- name: CreateCampaign :one +INSERT INTO campaigns ( + event_id, + title, description, + recipient_roles, + created_by +) VALUES ( + $1, $2, + coalesce($3, null), + coalesce($4, null), + $5 +) +RETURNING id, event_id, title, description, recipient_roles, recipient_addresses, emails, created_by, created_at, updated_at +` + +type CreateCampaignParams struct { + EventID uuid.UUID `json:"event_id"` + Title string `json:"title"` + Description interface{} `json:"description"` + RecipientRoles interface{} `json:"recipient_roles"` + CreatedBy uuid.UUID `json:"created_by"` +} + +func (q *Queries) CreateCampaign(ctx context.Context, arg CreateCampaignParams) (Campaign, error) { + row := q.db.QueryRow(ctx, createCampaign, + arg.EventID, + arg.Title, + arg.Description, + arg.RecipientRoles, + arg.CreatedBy, + ) + var i Campaign + err := row.Scan( + &i.ID, + &i.EventID, + &i.Title, + &i.Description, + &i.RecipientRoles, + &i.RecipientAddresses, + &i.Emails, + &i.CreatedBy, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteCampaignById = `-- name: DeleteCampaignById :execrows +DELETE FROM campaigns +WHERE id = $1 +` + +func (q *Queries) DeleteCampaignById(ctx context.Context, id uuid.UUID) (int64, error) { + result, err := q.db.Exec(ctx, deleteCampaignById, id) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const getCampaignById = `-- name: GetCampaignById :one +SELECT id, event_id, title, description, recipient_roles, recipient_addresses, emails, created_by, created_at, updated_at FROM campaigns +WHERE id = $1 +` + +func (q *Queries) GetCampaignById(ctx context.Context, id uuid.UUID) (Campaign, error) { + row := q.db.QueryRow(ctx, getCampaignById, id) + var i Campaign + err := row.Scan( + &i.ID, + &i.EventID, + &i.Title, + &i.Description, + &i.RecipientRoles, + &i.RecipientAddresses, + &i.Emails, + &i.CreatedBy, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateCampaignById = `-- name: UpdateCampaignById :exec +UPDATE campaigns +SET + title = CASE WHEN $1::boolean THEN $2 ELSE title END, + description = CASE WHEN $3::boolean THEN $4 ELSE description END, + recipient_roles = CASE WHEN $5::boolean THEN $5 ELSE recipient_roles END, + recipient_addresses = CASE WHEN $6::boolean THEN $6 ELSE recipient_addresses END, + emails = CASE WHEN $7::boolean THEN $7 ELSE emails END +WHERE + id = $8::uuid +returning id, event_id, title, description, recipient_roles, recipient_addresses, emails, created_by, created_at, updated_at +` + +type UpdateCampaignByIdParams struct { + TitleDoUpdate bool `json:"title_do_update"` + Title string `json:"title"` + DescriptionDoUpdate bool `json:"description_do_update"` + Description *string `json:"description"` + RecipientRoles bool `json:"recipient_roles"` + RecipientAddresses bool `json:"recipient_addresses"` + Emails bool `json:"emails"` + ID uuid.UUID `json:"id"` +} + +func (q *Queries) UpdateCampaignById(ctx context.Context, arg UpdateCampaignByIdParams) error { + _, err := q.db.Exec(ctx, updateCampaignById, + arg.TitleDoUpdate, + arg.Title, + arg.DescriptionDoUpdate, + arg.Description, + arg.RecipientRoles, + arg.RecipientAddresses, + arg.Emails, + arg.ID, + ) + return err +} diff --git a/apps/api/internal/db/sqlc/db.go b/apps/api/internal/db/sqlc/db.go index 27251088..7a565074 100644 --- a/apps/api/internal/db/sqlc/db.go +++ b/apps/api/internal/db/sqlc/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package sqlc diff --git a/apps/api/internal/db/sqlc/event_interest_submissions.sql.go b/apps/api/internal/db/sqlc/event_interest_submissions.sql.go index 27239b85..bb73843d 100644 --- a/apps/api/internal/db/sqlc/event_interest_submissions.sql.go +++ b/apps/api/internal/db/sqlc/event_interest_submissions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: event_interest_submissions.sql package sqlc diff --git a/apps/api/internal/db/sqlc/event_roles.sql.go b/apps/api/internal/db/sqlc/event_roles.sql.go index 75ed670e..667f45ea 100644 --- a/apps/api/internal/db/sqlc/event_roles.sql.go +++ b/apps/api/internal/db/sqlc/event_roles.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: event_roles.sql package sqlc diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go index b712008b..7b61c55a 100644 --- a/apps/api/internal/db/sqlc/events.sql.go +++ b/apps/api/internal/db/sqlc/events.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: events.sql package sqlc diff --git a/apps/api/internal/db/sqlc/models.go b/apps/api/internal/db/sqlc/models.go index ac91f6c2..dd878a87 100644 --- a/apps/api/internal/db/sqlc/models.go +++ b/apps/api/internal/db/sqlc/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package sqlc @@ -196,6 +196,53 @@ type AuthUser struct { EmailConsent bool `json:"email_consent"` } +type Campaign struct { + ID uuid.UUID `json:"id"` + EventID uuid.UUID `json:"event_id"` + Title string `json:"title"` + Description *string `json:"description"` + RecipientRoles []EventRoleType `json:"recipient_roles"` + RecipientAddresses []string `json:"recipient_addresses"` + Emails []uuid.UUID `json:"emails"` + CreatedBy uuid.UUID `json:"created_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CampaignEmail struct { + ID uuid.UUID `json:"id"` + RecipientAddresses []string `json:"recipient_addresses"` + RecipientRoles []string `json:"recipient_roles"` + SendFrom string `json:"send_from"` + Subject *string `json:"subject"` + Body *string `json:"body"` + Template uuid.UUID `json:"template"` + SendOn time.Time `json:"send_on"` + Links []uuid.UUID `json:"links"` + CreatedBy uuid.UUID `json:"created_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CampaignEmailLink struct { + ID uuid.UUID `json:"id"` + Role EventRoleType `json:"role"` + HitCount *int32 `json:"hit_count"` + UnsubscribedCount *int32 `json:"unsubscribed_count"` + ApiEndpoint string `json:"api_endpoint"` + RedirectTo string `json:"redirect_to"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CampaignEmailTemplate struct { + ID uuid.UUID `json:"id"` + Title *string `json:"title"` + Html *string `json:"html"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + type Event struct { ID uuid.UUID `json:"id"` Name string `json:"name"` diff --git a/apps/api/internal/db/sqlc/querier.go b/apps/api/internal/db/sqlc/querier.go index e7806f69..32f83ce0 100644 --- a/apps/api/internal/db/sqlc/querier.go +++ b/apps/api/internal/db/sqlc/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package sqlc @@ -18,11 +18,13 @@ type Querier interface { AssignRole(ctx context.Context, arg AssignRoleParams) error CreateAccount(ctx context.Context, arg CreateAccountParams) (AuthAccount, error) CreateApplication(ctx context.Context, arg CreateApplicationParams) (Application, error) + CreateCampaign(ctx context.Context, arg CreateCampaignParams) (Campaign, error) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) CreateSession(ctx context.Context, arg CreateSessionParams) (AuthSession, error) CreateUser(ctx context.Context, arg CreateUserParams) (AuthUser, error) DeleteAccount(ctx context.Context, arg DeleteAccountParams) error DeleteApplication(ctx context.Context, arg DeleteApplicationParams) error + DeleteCampaignById(ctx context.Context, id uuid.UUID) (int64, error) // execrows returns affect row count on top of an error DeleteEventById(ctx context.Context, id uuid.UUID) (int64, error) DeleteExpiredSession(ctx context.Context) error @@ -32,6 +34,7 @@ type Querier interface { GetApplicationByUserAndEventID(ctx context.Context, arg GetApplicationByUserAndEventIDParams) (Application, error) GetByProviderAndAccountID(ctx context.Context, arg GetByProviderAndAccountIDParams) (AuthAccount, error) GetByUserID(ctx context.Context, userID uuid.UUID) ([]AuthAccount, error) + GetCampaignById(ctx context.Context, id uuid.UUID) (Campaign, error) GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) GetEventRoleByIds(ctx context.Context, arg GetEventRoleByIdsParams) (EventRole, error) GetEventStaff(ctx context.Context, eventID uuid.UUID) ([]GetEventStaffRow, error) @@ -44,6 +47,7 @@ type Querier interface { InvalidateSessionByID(ctx context.Context, id uuid.UUID) error TouchSession(ctx context.Context, arg TouchSessionParams) error UpdateApplication(ctx context.Context, arg UpdateApplicationParams) error + UpdateCampaignById(ctx context.Context, arg UpdateCampaignByIdParams) error UpdateEventById(ctx context.Context, arg UpdateEventByIdParams) error UpdateSessionExpiration(ctx context.Context, arg UpdateSessionExpirationParams) error UpdateTokens(ctx context.Context, arg UpdateTokensParams) error diff --git a/apps/api/internal/db/sqlc/sessions.sql.go b/apps/api/internal/db/sqlc/sessions.sql.go index c95d1c82..abfb10aa 100644 --- a/apps/api/internal/db/sqlc/sessions.sql.go +++ b/apps/api/internal/db/sqlc/sessions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: sessions.sql package sqlc diff --git a/apps/api/internal/db/sqlc/users.sql.go b/apps/api/internal/db/sqlc/users.sql.go index d4d69c9a..969ff3e6 100644 --- a/apps/api/internal/db/sqlc/users.sql.go +++ b/apps/api/internal/db/sqlc/users.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: users.sql package sqlc diff --git a/apps/api/internal/services/campaigns.go b/apps/api/internal/services/campaigns.go new file mode 100644 index 00000000..ad1292da --- /dev/null +++ b/apps/api/internal/services/campaigns.go @@ -0,0 +1,94 @@ +package services + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/db/repository" + "github.com/swamphacks/core/apps/api/internal/db/sqlc" +) + +var ( + ErrFailedToCreateCampaign = errors.New("failed to create campaign") + ErrCampaignNotFound = errors.New("campaign not found") + ErrFailedToGetCampaign = errors.New("failed to get campaign") + ErrFailedToUpdateCampaign = errors.New("failed to update campaign") + ErrFailedToDeleteCampaign = errors.New("failed to get campaign") +) + +type CampaignService struct { + campaignRepo *repository.CampaignRepository + logger zerolog.Logger +} + +func NewCampaignService(campaignRepo *repository.CampaignRepository, logger zerolog.Logger) *CampaignService { + return &CampaignService{ + campaignRepo: campaignRepo, + logger: logger.With().Str("service", "CampaignService").Str("component", "campaign").Logger(), + } +} + +func (s *CampaignService) CreateCampaign(ctx context.Context, params sqlc.CreateCampaignParams) (*sqlc.Campaign, error) { + event, err := s.campaignRepo.CreateCampaign(ctx, params) + if err != nil { + if errors.Is(err, repository.ErrCampaignNotFound) { + s.logger.Err(err).Msg(repository.ErrCampaignNotFound.Error()) + } else { + s.logger.Err(err).Msg(repository.ErrUnknown.Error()) + } + return nil, ErrFailedToCreateCampaign + } + + return event, nil +} + +func (s *CampaignService) GetCampaignByID(ctx context.Context, campaignId uuid.UUID) (*sqlc.Campaign, error) { + campaign, err := s.campaignRepo.GetCampaignByID(ctx, campaignId) + if err != nil { + if err == repository.ErrCampaignNotFound { + s.logger.Err(err).Msg(repository.ErrCampaignNotFound.Error()) + } else { + s.logger.Err(err).Msg(repository.ErrUnknown.Error()) + } + return nil, ErrFailedToGetCampaign + } + + return campaign, nil +} + +func (s *CampaignService) UpdateCampaignById(ctx context.Context, params sqlc.UpdateCampaignByIdParams) (*sqlc.Campaign, error) { + err := s.campaignRepo.UpdateCampaignById(ctx, params) + if err != nil { + if errors.Is(err, repository.ErrCampaignNotFound) { + s.logger.Err(err).Msg(repository.ErrCampaignNotFound.Error()) + } else { + s.logger.Err(err).Msg(repository.ErrUnknown.Error()) + } + return nil, ErrFailedToUpdateCampaign + } + + event, err := s.campaignRepo.GetCampaignByID(ctx, params.ID) + + return event, err +} + +func (s *CampaignService) DeleteCampaignById(ctx context.Context, id uuid.UUID) error { + err := s.campaignRepo.DeleteCampaignById(ctx, id) + if err != nil { + switch err { + case repository.ErrCampaignNotFound: + s.logger.Err(err).Msg(repository.ErrCampaignNotFound.Error()) + case repository.ErrNoCampaignsDeleted: + s.logger.Err(err).Msg(repository.ErrCampaignNotFound.Error()) + case repository.ErrMultipleCampaignsDeleted: + s.logger.Err(err).Msg(repository.ErrMultipleCampaignsDeleted.Error()) + default: + s.logger.Err(err).Msg(repository.ErrUnknown.Error()) + } + return ErrFailedToDeleteCampaign + } + + return err +}