From c0b70b31de71743d37c32999674fa7b736f4003e Mon Sep 17 00:00:00 2001 From: Gede Indra Adi Brata Date: Sat, 27 Apr 2024 15:11:19 +0700 Subject: [PATCH] ref(campaign) :CRUD campaign --- cmd/api/main.go | 5 ++ internal/delivery/rest/campaign.go | 117 ++++++++++++++++++++++++- internal/model/campaign.go | 9 ++ internal/repository/campaign.go | 69 ++++++++++++++- internal/repository/query.go | 42 ++++++++- internal/usecase/campaign.go | 133 ++++++++++++++++++++++++++++- internal/usecase/error.go | 1 + internal/usecase/user.go | 11 +-- 8 files changed, 369 insertions(+), 18 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index c16530b..88050e4 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -28,9 +28,12 @@ func main() { api := app.Group("/api") rest.RegisterUtilsHandler(api) + supabaseClient := conf.NewSupabaseImage(log) + userRepo := repository.NewUserRepository(db) verifRepo := repository.NewVerificationRepository(db) merchantRepo := repository.NewMerchantRepository(db) + campaignRepo := repository.NewCampaignRepository(db) authUsecase := usecase.NewAuthUsecase(userRepo, verifRepo, mailer, log) rest.RegisterAuthHandler(authUsecase, validator, api) userUsecase := usecase.NewUserUsecase(userRepo, merchantRepo, supabaseImg, log) @@ -38,6 +41,8 @@ func main() { enclosureRepo := repository.NewEnclosureRepository(db) animalUsecase := usecase.NewAnimalUsecase(userRepo, enclosureRepo) rest.RegisterAnimalHandler(animalUsecase, api) + campaignUsecase := usecase.NewCampaignUsecase(campaignRepo, log, supabaseClient) + rest.RegisterCampaignHandler(campaignUsecase, api, validator) if err := app.Listen(":" + os.Getenv("APP_PORT")); err != nil { log.Fatalf("Fail to start server: %v", err) diff --git a/internal/delivery/rest/campaign.go b/internal/delivery/rest/campaign.go index 369fc37..6f334bb 100644 --- a/internal/delivery/rest/campaign.go +++ b/internal/delivery/rest/campaign.go @@ -1,24 +1,34 @@ package rest import ( + "mime/multipart" "strconv" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" + "github.com/mirzahilmi/hackathon/internal/model" "github.com/mirzahilmi/hackathon/internal/usecase" ) type campaignHandler struct { - usecase usecase.CampaignUsecaseItf + usecase usecase.CampaignUsecaseItf + validator *validator.Validate } func RegisterCampaignHandler( usecase usecase.CampaignUsecaseItf, router fiber.Router, + validator *validator.Validate, ) { - campaignHandler := campaignHandler{usecase} + campaignHandler := campaignHandler{usecase, validator} router = router.Group("/campaigns") router.Get("", campaignHandler.FetchAll) router.Get("/:id", campaignHandler.FetchSingle) + router.Post("", campaignHandler.Create) + router.Patch("/:id", campaignHandler.Update) + router.Delete("/:id", campaignHandler.Delete) + router.Get("/_admin", campaignHandler.GetAllForAdmin) + router.Get("/_admin/:id", campaignHandler.GetByIDForAdmin) } func (h *campaignHandler) FetchAll(c *fiber.Ctx) error { @@ -40,3 +50,106 @@ func (h *campaignHandler) FetchSingle(c *fiber.Ctx) error { } return c.Status(fiber.StatusOK).JSON(campaign) } + +func (h *campaignHandler) Create(c *fiber.Ctx) error { + pict, err := c.FormFile("picture") + if err != nil { + return err + } + + reward, err := strconv.ParseUint(c.FormValue("reward"), 10, 64) + if err != nil { + return err + } + + req := model.CampaignRequest{ + Picture: pict, + Title: c.FormValue("title"), + Description: c.FormValue("description"), + Reward: int(reward), + } + + if err := h.validator.Struct(&req); err != nil { + return err + } + + err = h.usecase.Create(c.Context(), req) + if err != nil { + return err + } + return c.Status(fiber.StatusCreated).JSON(fiber.Map{"message": "Campaign created successfully"}) +} + +func (h *campaignHandler) Update(c *fiber.Ctx) error { + id, err := strconv.ParseInt(c.Params("id"), 10, 64) + if err != nil { + return err + } + + var pict *multipart.FileHeader + pict, err = c.FormFile("picture") + if err != nil { + if err.Error() == "there is no uploaded file associated with the given key" { + pict = nil + } else { + return err + } + } + + var reward uint64 + if c.FormValue("reward") == "" { + reward = 0 + } else { + reward, err = strconv.ParseUint(c.FormValue("reward"), 10, 64) + if err != nil { + return err + } + } + + req := model.CampaignRequest{ + Picture: pict, + Title: c.FormValue("title"), + Description: c.FormValue("description"), + Reward: int(reward), + } + + err = h.usecase.Update(c.Context(), req, id) + if err != nil { + return err + } + return c.Status(fiber.StatusCreated).JSON(fiber.Map{"message": "Campaign updated successfully"}) + +} + +func (h *campaignHandler) Delete(c *fiber.Ctx) error { + id, err := strconv.ParseInt(c.Params("id"), 10, 64) + if err != nil { + return err + } + + err = h.usecase.Delete(c.Context(), id) + if err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Campaign deleted successfully"}) +} + +func (h *campaignHandler) GetAllForAdmin(c *fiber.Ctx) error { + campaigns, err := h.usecase.GetAll(c.Context()) + if err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(campaigns) +} + +func (h *campaignHandler) GetByIDForAdmin(c *fiber.Ctx) error { + id, err := strconv.ParseInt(c.Params("id"), 10, 64) + if err != nil { + return err + } + campaign, err := h.usecase.GetByID(c.Context(), id) + if err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(campaign) +} diff --git a/internal/model/campaign.go b/internal/model/campaign.go index d0c2b67..0c88436 100644 --- a/internal/model/campaign.go +++ b/internal/model/campaign.go @@ -1,5 +1,7 @@ package model +import "mime/multipart" + type Campaign struct { ID int64 `db:"ID" json:"id,omitempty"` Picture string `db:"Picture" json:"picture"` @@ -9,3 +11,10 @@ type Campaign struct { Submitted bool `db:"Submitted" json:"submitted"` Submission string `db:"Submission" json:"submission,omitempty"` } + +type CampaignRequest struct { + Picture *multipart.FileHeader `form:"picture" validate:"required"` + Title string `form:"title" validate:"required"` + Description string `form:"description" validate:"required"` + Reward int `form:"reward" validate:"required"` +} diff --git a/internal/repository/campaign.go b/internal/repository/campaign.go index 546d080..8a84e6e 100644 --- a/internal/repository/campaign.go +++ b/internal/repository/campaign.go @@ -3,6 +3,7 @@ package repository import ( "context" + "github.com/gofiber/fiber/v2" "github.com/jmoiron/sqlx" "github.com/mirzahilmi/hackathon/internal/model" ) @@ -21,8 +22,13 @@ func NewCampaignRepository(db *sqlx.DB) CampaignRepositoryItf { type CampaignQueryerItf interface { txCompat - GetAll(ctx context.Context, userID int64) ([]model.Campaign, error) + GetAllByUserID(ctx context.Context, userID int64) ([]model.Campaign, error) GetWithSubmission(ctx context.Context, id, userID int64) (model.Campaign, error) + Create(ctx context.Context, campaign model.Campaign) error + Update(ctx context.Context, campaign model.Campaign) error + Delete(ctx context.Context, id int64) error + GetByID(ctx context.Context, id int64) (model.Campaign, error) + GetAll(ctx context.Context) ([]model.Campaign, error) } type campaignQueryer struct { @@ -63,9 +69,9 @@ func (q *campaignQueryer) Ext() sqlx.ExtContext { return q.ext } -func (q *campaignQueryer) GetAll(ctx context.Context, userID int64) ([]model.Campaign, error) { +func (q *campaignQueryer) GetAllByUserID(ctx context.Context, userID int64) ([]model.Campaign, error) { var campaigns []model.Campaign - if err := sqlx.SelectContext(ctx, q.ext, &campaigns, qGetAllCampaign, userID); err != nil { + if err := sqlx.SelectContext(ctx, q.ext, &campaigns, qGetAllCampaignByUserID, userID); err != nil { return nil, err } return campaigns, nil @@ -78,3 +84,60 @@ func (q *campaignQueryer) GetWithSubmission(ctx context.Context, id, userID int6 } return campaign, nil } + +func (q *campaignQueryer) Create(ctx context.Context, campaign model.Campaign) error { + query, args, err := sqlx.Named(qCreateCampaign, fiber.Map{ + "Picture": campaign.Picture, + "Title": campaign.Title, + "Description": campaign.Description, + "Reward": campaign.Reward, + }) + if err != nil { + return err + } + _, err = q.ext.ExecContext(ctx, query, args...) + if err != nil { + return err + } + return err +} + +func (q *campaignQueryer) Update(ctx context.Context, campaign model.Campaign) error { + query, args, err := sqlx.Named(qUpdateCampaign, fiber.Map{ + "ID": campaign.ID, + "Picture": campaign.Picture, + "Title": campaign.Title, + "Description": campaign.Description, + "Reward": campaign.Reward, + }) + if err != nil { + return err + } + _, err = q.ext.ExecContext(ctx, query, args...) + return err +} + +func (q *campaignQueryer) Delete(ctx context.Context, id int64) error { + _, err := q.ext.ExecContext(ctx, qDeleteCampaign, id) + if err != nil { + return err + } + + return nil +} + +func (q *campaignQueryer) GetByID(ctx context.Context, id int64) (model.Campaign, error) { + var campaign model.Campaign + if err := sqlx.GetContext(ctx, q.ext, &campaign, qGetCampaignByID, id); err != nil { + return model.Campaign{}, err + } + return campaign, nil +} + +func (q *campaignQueryer) GetAll(ctx context.Context) ([]model.Campaign, error) { + var campaigns []model.Campaign + if err := sqlx.SelectContext(ctx, q.ext, &campaigns, qGetAllCampaign); err != nil { + return nil, err + } + return campaigns, nil +} diff --git a/internal/repository/query.go b/internal/repository/query.go index 9c73e2b..0a4ce0e 100644 --- a/internal/repository/query.go +++ b/internal/repository/query.go @@ -91,7 +91,7 @@ const ( ` // Campaign - qGetAllCampaign = ` + qGetAllCampaignByUserID = ` SELECT Campaigns.ID, Campaigns.Picture, @@ -118,6 +118,46 @@ const ( WHERE Campaigns.ID = ? AND CampaignSubmissions.UserID = ? LIMIT 1; ` + qCreateCampaign = ` + INSERT INTO Campaigns + (Picture, Title, Description, Reward) + VALUE + (:Picture, :Title, :Description, :Reward); + ` + qUpdateCampaign = ` + UPDATE + Campaigns + SET + Picture = :Picture, + Title = :Title, + Description = :Description, + Reward = :Reward + WHERE ID = :ID; + ` + qDeleteCampaign = ` + DELETE FROM Campaigns + WHERE ID = ?; + ` + qGetCampaignByID = ` + SELECT + ID, + Picture, + Title, + Description, + Reward + FROM Campaigns + WHERE ID = ? + LIMIT 1; + ` + qGetAllCampaign = ` + SELECT + ID, + Picture, + Title, + Description, + Reward + FROM Campaigns + ` // Exchanges qCreateExchange = ` diff --git a/internal/usecase/campaign.go b/internal/usecase/campaign.go index 697419b..3920add 100644 --- a/internal/usecase/campaign.go +++ b/internal/usecase/campaign.go @@ -2,27 +2,38 @@ package usecase import ( "context" + "fmt" + "os" + "time" "github.com/mirzahilmi/hackathon/internal/model" "github.com/mirzahilmi/hackathon/internal/repository" "github.com/sirupsen/logrus" + storage_go "github.com/supabase-community/storage-go" ) type CampaignUsecaseItf interface { FetchAll(ctx context.Context) ([]model.Campaign, error) GetWithSubmission(ctx context.Context, id int64) (model.Campaign, error) + Create(ctx context.Context, req model.CampaignRequest) error + Update(ctx context.Context, campaign model.CampaignRequest, id int64) error + Delete(ctx context.Context, id int64) error + GetByID(ctx context.Context, id int64) (model.Campaign, error) + GetAll(ctx context.Context) ([]model.Campaign, error) } type campaignUsecase struct { campaignRepo repository.CampaignRepositoryItf log *logrus.Logger + supabase *storage_go.Client } func NewCampaignUsecase( campaignRepo repository.CampaignRepositoryItf, log *logrus.Logger, + supabase *storage_go.Client, ) CampaignUsecaseItf { - return &campaignUsecase{campaignRepo, log} + return &campaignUsecase{campaignRepo, log, supabase} } func (u *campaignUsecase) FetchAll(ctx context.Context) ([]model.Campaign, error) { @@ -30,7 +41,7 @@ func (u *campaignUsecase) FetchAll(ctx context.Context) ([]model.Campaign, error if err != nil { return nil, err } - campaigns, err := client.GetAll(ctx, ctx.Value(ClientID).(int64)) + campaigns, err := client.GetAllByUserID(ctx, ctx.Value(ClientID).(int64)) if err != nil { return nil, err } @@ -48,3 +59,121 @@ func (u *campaignUsecase) GetWithSubmission(ctx context.Context, id int64) (mode } return campaign, nil } + +func (u *campaignUsecase) Create(ctx context.Context, req model.CampaignRequest) error { + client, err := u.campaignRepo.NewClient(true, nil) + if err != nil { + return err + } + defer client.Rollback() + + req.Picture.Filename = fmt.Sprintf("%d_%s", time.Now().UnixMilli(), req.Picture.Filename) + pict, err := req.Picture.Open() + if err != nil { + return err + } + + _, err = u.supabase.UploadFile(os.Getenv("SUPABASE_BUCKET_ID"), req.Picture.Filename, pict) + if err != nil { + return err + } + + pictUrl := u.supabase.GetPublicUrl(os.Getenv("SUPABASE_BUCKET_ID"), req.Picture.Filename) + + campaign := model.Campaign{ + Picture: pictUrl.SignedURL, + Title: req.Title, + Description: req.Description, + Reward: req.Reward, + } + + err = client.Create(ctx, campaign) + if err != nil { + return err + } + return client.Commit() +} + +func (u *campaignUsecase) Update(ctx context.Context, req model.CampaignRequest, id int64) error { + client, err := u.campaignRepo.NewClient(true, nil) + if err != nil { + return err + } + defer client.Rollback() + + campaign, err := client.GetByID(ctx, id) + if err != nil { + return err + } + + if req.Picture != nil { + req.Picture.Filename = fmt.Sprintf("%d_%s", time.Now().UnixMilli(), req.Picture.Filename) + pict, err := req.Picture.Open() + if err != nil { + return err + } + + _, err = u.supabase.UploadFile(os.Getenv("SUPABASE_BUCKET_ID"), req.Picture.Filename, pict) + if err != nil { + return err + } + + pictUrl := u.supabase.GetPublicUrl(os.Getenv("SUPABASE_BUCKET_ID"), req.Picture.Filename) + campaign.Picture = pictUrl.SignedURL + } + + if req.Title != "" && req.Title != campaign.Title { + campaign.Title = req.Title + } + + if req.Description != "" && req.Description != campaign.Description { + campaign.Description = req.Description + } + + if req.Reward != 0 && req.Reward != campaign.Reward { + campaign.Reward = req.Reward + } + + err = client.Update(ctx, campaign) + if err != nil { + return err + } + return client.Commit() +} + +func (u *campaignUsecase) Delete(ctx context.Context, id int64) error { + client, err := u.campaignRepo.NewClient(true, nil) + if err != nil { + return err + } + defer client.Rollback() + err = client.Delete(ctx, id) + if err != nil { + return err + } + return client.Commit() +} + +func (u *campaignUsecase) GetByID(ctx context.Context, id int64) (model.Campaign, error) { + client, err := u.campaignRepo.NewClient(false, nil) + if err != nil { + return model.Campaign{}, err + } + campaign, err := client.GetByID(ctx, id) + if err != nil { + return model.Campaign{}, ErrCampaignNotExist + } + return campaign, nil +} + +func (u *campaignUsecase) GetAll(ctx context.Context) ([]model.Campaign, error) { + client, err := u.campaignRepo.NewClient(false, nil) + if err != nil { + return nil, err + } + campaigns, err := client.GetAll(ctx) + if err != nil { + return nil, err + } + return campaigns, nil +} diff --git a/internal/usecase/error.go b/internal/usecase/error.go index 57ab7e9..449ec58 100644 --- a/internal/usecase/error.go +++ b/internal/usecase/error.go @@ -12,4 +12,5 @@ var ( ErrIDNotNumeric = errors.New("ID is not numeric") ErrInsufficientBalance = errors.New("Insufficient balance") ErrMerchantNotExist = errors.New("Merchant not exist") + ErrCampaignNotExist = errors.New("Campaign not exist") ) diff --git a/internal/usecase/user.go b/internal/usecase/user.go index 3a07e2a..41ef472 100644 --- a/internal/usecase/user.go +++ b/internal/usecase/user.go @@ -168,16 +168,7 @@ func (h *userUsecase) Exchange(ctx context.Context, exchange model.ExchangeReque return err } - defer func() error { - if err != nil { - if err := userClient.Rollback(); err != nil { - return err - } - return err - } - userClient.Commit() - return nil - }() + defer userClient.Rollback() user, err := userClient.GetByParam(ctx, "ID", ctx.Value(ClientID).(int64)) if err != nil {