From d57adaebecb0216a82f08057fc55e9380f93c37a Mon Sep 17 00:00:00 2001 From: matina Date: Sun, 19 Apr 2026 16:16:36 -0700 Subject: [PATCH 1/2] improve structure of campaign & adding interfaces --- domain/campaign/entity/entitty.go | 4 +- .../1775633533_add_campaign_table.sql | 0 domain/campaign/repository/mysql/db.go | 57 +++++++++++ domain/campaign/repository/repo.go | 29 ------ domain/campaign/service/Interfaces.go | 7 -- domain/campaign/service/createCampaign.go | 98 +++++++++---------- domain/campaign/service/services.go | 35 +++++++ donate_app/repository/mysql/db.go | 70 +++---------- .../service/entity/campaignParticipant.go | 17 ++-- 9 files changed, 161 insertions(+), 156 deletions(-) rename {donate_app => domain/campaign}/repository/migrations/1775633533_add_campaign_table.sql (100%) create mode 100644 domain/campaign/repository/mysql/db.go delete mode 100644 domain/campaign/repository/repo.go delete mode 100644 domain/campaign/service/Interfaces.go create mode 100644 domain/campaign/service/services.go diff --git a/domain/campaign/entity/entitty.go b/domain/campaign/entity/entitty.go index 9df48283..55d7cf9c 100644 --- a/domain/campaign/entity/entitty.go +++ b/domain/campaign/entity/entitty.go @@ -1,4 +1,4 @@ -package domain +package entity import "time" @@ -26,7 +26,7 @@ type Campaign struct { AdminID ID `json:"creator_id"` } -// Behavior +// Behavior func (c *Campaign) Activate() { if c.Status == CampaignDraft { c.Status = CampaignActive diff --git a/donate_app/repository/migrations/1775633533_add_campaign_table.sql b/domain/campaign/repository/migrations/1775633533_add_campaign_table.sql similarity index 100% rename from donate_app/repository/migrations/1775633533_add_campaign_table.sql rename to domain/campaign/repository/migrations/1775633533_add_campaign_table.sql diff --git a/domain/campaign/repository/mysql/db.go b/domain/campaign/repository/mysql/db.go new file mode 100644 index 00000000..eefb0b6f --- /dev/null +++ b/domain/campaign/repository/mysql/db.go @@ -0,0 +1,57 @@ +package mysql + +import ( + "context" + "git.gocasts.ir/ebhomengo/niki/domain/campaign/entity" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" + "git.gocasts.ir/ebhomengo/niki/types" +) + +type DB struct { + conn *mysql.DB +} + +func New(db *mysql.DB) *DB { + return &DB{conn: db} +} + +// CreateCampaign creates a new campaign +func (d *DB) CreateAndSave(ctx context.Context, campaign entity.Campaign) (types.ID, error) { + const Op = "repository.mysql.campaign.create" + + tx, err := d.conn.Conn().BeginTx(ctx, nil) + if err != nil { + return 0, richerror.New(Op).WithErr(err) + } + defer tx.Rollback() + + query := `INSERT INTO campaigns (title, description, goal_amount, raised_amount, + status, deadline_at ,admin_id , created_at ) + VALUES (?, ?, ?, ?, ?, ?, ? , NOW() )` + + result, err := tx.ExecContext(ctx, query, + campaign.Title, + campaign.Description, + campaign.GoalAmount, + campaign.RaisedAmount, + campaign.Status, + campaign.DeadlineAt, + campaign.AdminID, + campaign.CreatedAt, + ) + if err != nil { + return 0, richerror.New(Op).WithErr(err) + } + + campaignID, err := result.LastInsertId() + if err != nil { + return 0, richerror.New(Op).WithErr(err) + } + + if err := tx.Commit(); err != nil { + return 0, richerror.New(Op).WithErr(err) + } + + return types.ID(campaignID), nil +} diff --git a/domain/campaign/repository/repo.go b/domain/campaign/repository/repo.go deleted file mode 100644 index 74605d96..00000000 --- a/domain/campaign/repository/repo.go +++ /dev/null @@ -1,29 +0,0 @@ -package mysql - -import ( - "context" - "git.gocasts.ir/ebhomengo/niki/campaign/entity" - richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" - "git.gocasts.ir/ebhomengo/niki/repository/mysql" - "git.gocasts.ir/ebhomengo/niki/types" -) - - - -type CampaignRepository interface { - CreateAndSave(ctx context.Context, campaign *Campaign) error - FindByID(ctx context.Context, id ID) (*Campaign, error) - List(ctx context.Context, status CampaignStatus, limit, offset int) ([]*Campaign, error) - Delete(ctx context.Context, id ID) error - Update(ctx context.Context, campaign *Campaign) error -} - - - - - - - - - - diff --git a/domain/campaign/service/Interfaces.go b/domain/campaign/service/Interfaces.go deleted file mode 100644 index b82ad074..00000000 --- a/domain/campaign/service/Interfaces.go +++ /dev/null @@ -1,7 +0,0 @@ -package domain - -import ( - "context" - "time" -) - diff --git a/domain/campaign/service/createCampaign.go b/domain/campaign/service/createCampaign.go index 319f5b36..24514606 100644 --- a/domain/campaign/service/createCampaign.go +++ b/domain/campaign/service/createCampaign.go @@ -1,48 +1,50 @@ -package campaign +package service import ( "context" - "errors" // For standard errors + "fmt" + "git.gocasts.ir/ebhomengo/niki/domain/campaign/entity" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "git.gocasts.ir/ebhomengo/niki/types" "time" - - "your_project/domain" - "your_project/pkg/richerror" - "your_project/repository" ) -type ID = unit64 -type EntityCampaign = domain.Campaign +// CreateCampaign handles creation of a new campaign. +func (s *CampaignService) CreateCampaign(ctx context.Context, req entity.Campaign) (types.ID, error) { + const op = "service.campaign.create_campaign" - -type CampaignServiceImp interface { - CreateCampaign(ctx context.Context, req CampaignRepository) (types.ID, error) -} - - -type CampaignService struct { - repo repository.CampaignRepository -} - -func NewCampaignService( - campaignRepo repository.CampaignRepository, -) *CampaignService { - return &CampaignService{ - repo: campaignRepo, + if err := validateCreateCampaignRequest(req); err != nil { + return 0, richerror.New(op).WithErr(err) } + + campaign := entity.Campaign{ + Title: req.Title, + Description: req.Description, + GoalAmount: req.GoalAmount, + RaisedAmount: 0, + Status: req.Status, + DeadlineAt: req.DeadlineAt, + AdminID: req.AdminID, + CreatedAt: time.Now(), + } + + id, err := s.repo.Create(ctx, campaign) + if err != nil { + return 0, richerror.New(op).WithErr(err) + } + + return id, nil } - -func (s *CampaignService) CreateCampaign(ctx context.Context, req CreateCampaignRequest) (ID, error) { - const Op = "service.campaign.create_campaign" - +func validateCreateCampaignRequest(req entity.Campaign) error { if req.Title == "" { - return 0, richerror.New(Op).WithMessage("title is required") + return errRequired("title") } if req.GoalAmount <= 0 { - return 0, richerror.New(Op).WithMessage("goal_amount must be greater than 0") + return errInvalid("goal_amount must be greater than 0") } - if req.AdminID == 0 { - return 0, richerror.New(Op).WithMessage("admin_id is required") + if req.AdminID == 0 { + return errRequired("admin_id") } validStatuses := map[string]bool{ @@ -50,32 +52,20 @@ func (s *CampaignService) CreateCampaign(ctx context.Context, req CreateCampaign "active": true, "completed": true, "cancelled": true, - "paused": true + "paused": true, } - if !validStatuses[req.Status] { - return 0, richerror.New(Op).WithMessage("invalid status provided") + if !validStatuses[string(req.Status)] { + return errInvalid("invalid status provided") } - - newCampaign := &EntityCampaign{ - Title: req.Title, - Description: req.Description, - GoalAmount: req.GoalAmount, - RaisedAmount: 0, // Initially 0 - Status: EntityCampaign.Status(req.Status), - DeadlineAt: req.DeadlineAt, - AdminID: req.AdminID, - CreatedAt: time.Now(), - } - - createdCampaignID, err := s.repo.CreateAndSave(ctx, newCampaign) - if err != nil { - return 0, richerror.New(Op).WithErr(err) - } - - return createdCampaignID, nil + return nil } +// --- Helpers --- +func errRequired(field string) error { + return fmt.Errorf("%s is required", field) +} - - +func errInvalid(msg string) error { + return fmt.Errorf(msg) +} diff --git a/domain/campaign/service/services.go b/domain/campaign/service/services.go new file mode 100644 index 00000000..cc85d8b3 --- /dev/null +++ b/domain/campaign/service/services.go @@ -0,0 +1,35 @@ +package service + +import ( + "context" + _ "fmt" + "git.gocasts.ir/ebhomengo/niki/domain/campaign/entity" + "git.gocasts.ir/ebhomengo/niki/types" +) + +type CampaignFilterParam struct { + AdminID types.ID + Status string + //nil true false + IsArchived *bool +} + +type CampaignStorage interface { + Create(ctx context.Context, c entity.Campaign) (types.ID, error) // باید ID برگرداند + Update(ctx context.Context, c entity.Campaign) error + FindByID(ctx context.Context, id types.ID) (entity.Campaign, error) + FindAll(ctx context.Context, filter CampaignFilterParam) ([]entity.Campaign, error) + Archive(ctx context.Context, id types.ID) error // instead Delete + TotalDonations(ctx context.Context, campaignID types.ID) (int64, error) +} + +type CampaignService struct { + repo CampaignStorage +} + +// NewCampaignService constructs a new CampaignService. +func NewCampaignService(storage CampaignStorage) *CampaignService { + return &CampaignService{ + repo: storage, + } +} diff --git a/donate_app/repository/mysql/db.go b/donate_app/repository/mysql/db.go index 26c0d5ef..358ea1cd 100644 --- a/donate_app/repository/mysql/db.go +++ b/donate_app/repository/mysql/db.go @@ -2,7 +2,7 @@ package mysql import ( "context" - "git.gocasts.ir/ebhomengo/niki/campaign/entity" + "git.gocasts.ir/ebhomengo/niki/donate_app/service/entity" richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" "git.gocasts.ir/ebhomengo/niki/repository/mysql" "git.gocasts.ir/ebhomengo/niki/types" @@ -16,11 +16,9 @@ func New(db *mysql.DB) *DB { return &DB{conn: db} } - - -// CreateCampaign creates a new campaign -func (d *DB) CreateAndSave(ctx context.Context, campaign entity.Campaign) (types.ID, error) { - const Op = "repository.mysql.campaign.create" +// Create adds a new participant to a campaign +func (d *DB) CreateCampaignParticipants(ctx context.Context, participant entity.CampaignParticipant) (types.ID, error) { + const Op = "repository.mysql.campaign_participant.create" tx, err := d.conn.Conn().BeginTx(ctx, nil) if err != nil { @@ -28,67 +26,29 @@ func (d *DB) CreateAndSave(ctx context.Context, campaign entity.Campaign) (types } defer tx.Rollback() - query := `INSERT INTO campaigns (title, description, goal_amount, raised_amount, - status, deadline_at ,admin_id , created_at ) - VALUES (?, ?, ?, ?, ?, ?, ? , NOW() )` + query := `INSERT INTO campaign_participants (id,campaign_id, user_id, amount , created_at) + VALUES (?, ?, ? , ? , NOW())` result, err := tx.ExecContext(ctx, query, - campaign.Title, - campaign.Description, - campaign.GoalAmount, - campaign.RaisedAmount, - campaign.Status, - campaign.DeadlineAt, - campaign.AdminID, - campaign.CreatedAt, - - + participant.ID, + participant.CampaignID, + participant.UserID, + participant.Amount, + participant.CreatedAt, ) + if err != nil { return 0, richerror.New(Op).WithErr(err) } - campaignID, err := result.LastInsertId() + id, err := result.LastInsertId() if err != nil { return 0, richerror.New(Op).WithErr(err) } - if err := tx.Commit(); err != nil { return 0, richerror.New(Op).WithErr(err) } - return types.ID(campaignID), nil + return types.ID(id), nil + } - - - -// Create adds a new participant to a campaign -func (d *DB) CreateCampaignParticipants(ctx context.Context, participant entity.CampaignParticipant) (types.ID, error) { - const Op = "repository.mysql.campaign_participant.create" - - query := `INSERT INTO campaign_participants (id,campaign_id, user_id, amount , created_at) - VALUES (?, ?, ? , ? , NOW())` - - result, err := d.conn.ExecContext(ctx, query, - participant.ID, - participant.CampaignID, - participant.UserID, - participant.Amount, - participant.CreatedAt - ) - if err != nil { - return 0, richerror.New(Op).WithErr(err) - } - - id, err := result.LastInsertId() - if err != nil { - return 0, richerror.New(Op).WithErr(err) - } - - return types.ID(id), nil -} - - - - - diff --git a/donate_app/service/entity/campaignParticipant.go b/donate_app/service/entity/campaignParticipant.go index fb493429..8199852a 100644 --- a/donate_app/service/entity/campaignParticipant.go +++ b/donate_app/service/entity/campaignParticipant.go @@ -1,14 +1,13 @@ -package service +package entity +import "time" type ID uint64 - type CampaignParticipant struct { - ID ID `json:"id"` - CampaignID ID `json:"campaign_id"` - UserID ID `json:"user_id"` - Amount float64 `json:"amount"` - CreatedAt time.Time `json:"created_at"` - -} \ No newline at end of file + ID ID `json:"id"` + CampaignID ID `json:"campaign_id"` + UserID ID `json:"user_id"` + Amount float64 `json:"amount"` + CreatedAt time.Time `json:"created_at"` +} From a986f03e4416261d44ad7a0a4393cd8dd0b11abb Mon Sep 17 00:00:00 2001 From: matina Date: Tue, 21 Apr 2026 00:04:58 -0700 Subject: [PATCH 2/2] delivery & handler --- domain/campaign/entity/entitty.go | 11 ++--- domain/campaign/service/param.go | 27 ++++++++++++ donate_app/delivery/donate_server/handler.go | 5 --- donate_app/delivery/donate_server/routes.go | 7 ---- donate_app/delivery/donate_server/server.go | 16 -------- donate_app/delivery/http/handler.go | 43 ++++++++++++++++++++ donate_app/delivery/http/health_check.go | 12 ++++++ donate_app/delivery/http/server.go | 39 ++++++++++++++++++ donate_app/service/param.go | 25 ++++++++++++ donate_app/service/service.go | 37 ----------------- 10 files changed, 152 insertions(+), 70 deletions(-) create mode 100644 domain/campaign/service/param.go delete mode 100644 donate_app/delivery/donate_server/handler.go delete mode 100644 donate_app/delivery/donate_server/routes.go delete mode 100644 donate_app/delivery/donate_server/server.go create mode 100644 donate_app/delivery/http/handler.go create mode 100644 donate_app/delivery/http/health_check.go create mode 100644 donate_app/delivery/http/server.go diff --git a/domain/campaign/entity/entitty.go b/domain/campaign/entity/entitty.go index 55d7cf9c..f6eca8a0 100644 --- a/domain/campaign/entity/entitty.go +++ b/domain/campaign/entity/entitty.go @@ -1,8 +1,9 @@ package entity -import "time" - -type ID uint64 +import ( + "git.gocasts.ir/ebhomengo/niki/types" + "time" +) type CampaignStatus string @@ -15,7 +16,7 @@ const ( ) type Campaign struct { - ID ID `json:"id"` + ID types.ID `json:"id"` Title string `json:"title"` Description string `json:"description"` GoalAmount float64 `json:"goal_amount"` @@ -23,7 +24,7 @@ type Campaign struct { Status CampaignStatus `json:"status"` CreatedAt time.Time `json:"created_at"` DeadlineAt *time.Time `json:"deadline_at,omitempty"` - AdminID ID `json:"creator_id"` + AdminID types.ID `json:"creator_id"` } // Behavior diff --git a/domain/campaign/service/param.go b/domain/campaign/service/param.go new file mode 100644 index 00000000..f54649ab --- /dev/null +++ b/domain/campaign/service/param.go @@ -0,0 +1,27 @@ +package service + +import ( + "git.gocasts.ir/ebhomengo/niki/types" + "time" +) + +type GetCampaignResponse struct { + ID types.ID `json:"user_id"` +} + +type AddCampaignRequest struct { + ID uint64 `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + GoalAmount float64 `json:"goal_amount"` + DeadlineAt *time.Time `json:"deadline_at,omitempty"` + AdminID types.ID `json:"admin_id"` +} + +type UpdateCampaignRequest struct { + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + GoalAmount *float64 `json:"goal_amount,omitempty"` + DeadlineAt *time.Time `json:"deadline_at,omitempty"` + Status *string `json:"status,omitempty"` // draft/active/completed/paused/cancelled +} diff --git a/donate_app/delivery/donate_server/handler.go b/donate_app/delivery/donate_server/handler.go deleted file mode 100644 index 9f94a9c3..00000000 --- a/donate_app/delivery/donate_server/handler.go +++ /dev/null @@ -1,5 +0,0 @@ -package donate_server - -type Handler struct{} - - diff --git a/donate_app/delivery/donate_server/routes.go b/donate_app/delivery/donate_server/routes.go deleted file mode 100644 index 778a11e5..00000000 --- a/donate_app/delivery/donate_server/routes.go +++ /dev/null @@ -1,7 +0,0 @@ -package donate_server - -import "github.com/labstack/echo/v4" - -func (h Handler) RegisterRoutes(e *echo.Echo) { - -} diff --git a/donate_app/delivery/donate_server/server.go b/donate_app/delivery/donate_server/server.go deleted file mode 100644 index 218c4c71..00000000 --- a/donate_app/delivery/donate_server/server.go +++ /dev/null @@ -1,16 +0,0 @@ -package donate_server - -import ( - httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server" - "github.com/labstack/echo/v4" -) - -type Server struct { - Server httpserver.Server - Handler Handler - Router *echo.Echo -} - -func (s Server) Start() { - s.Handler.RegisterRoutes(s.Router) -} diff --git a/donate_app/delivery/http/handler.go b/donate_app/delivery/http/handler.go new file mode 100644 index 00000000..b20f6dd6 --- /dev/null +++ b/donate_app/delivery/http/handler.go @@ -0,0 +1,43 @@ +package http + +import ( + "git.gocasts.ir/ebhomengo/niki/domain/campaign/entity" + "git.gocasts.ir/ebhomengo/niki/domain/campaign/service" + httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg" + "github.com/labstack/echo/v4" + "net/http" + "time" +) + +type Handler struct { + svc service.CampaignService +} + +func NewHandler(svc service.CampaignService) Handler { + return Handler{svc: svc} +} + +func (h Handler) createCampaign(c echo.Context) error { + + var req entity.Campaign + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{ + "error": "invalid request body", + }) + } + req.CreatedAt = time.Now() + req.RaisedAmount = 0 + + createdID, err := h.svc.CreateCampaign(c.Request().Context(), req) + if err != nil { + msg, code := httpmsg.Error(err) + c.Logger().Errorf("Service error creating campaign: %v (Code: %d)", err, code) + return c.JSON(code, msg) + } + + return c.JSON(http.StatusCreated, map[string]interface{}{ + "message": "campaign created successfully", + "id": createdID, + }) + +} diff --git a/donate_app/delivery/http/health_check.go b/donate_app/delivery/http/health_check.go new file mode 100644 index 00000000..aa0a0254 --- /dev/null +++ b/donate_app/delivery/http/health_check.go @@ -0,0 +1,12 @@ +package http + +import ( + "github.com/labstack/echo/v4" + "net/http" +) + +func (s Server) healthCheck(c echo.Context) error { + return c.JSON(http.StatusOK, echo.Map{ + "message": "everything is good!", + }) +} diff --git a/donate_app/delivery/http/server.go b/donate_app/delivery/http/server.go new file mode 100644 index 00000000..16b4363b --- /dev/null +++ b/donate_app/delivery/http/server.go @@ -0,0 +1,39 @@ +package http + +import ( + "context" + "git.gocasts.ir/ebhomengo/niki/pkg/httpserver" +) + +type Server struct { + handler Handler + HTTPServer *httpserver.Server +} + +func NewServer(handler Handler, hS *httpserver.Server) Server { + return Server{handler: handler, HTTPServer: hS} +} + +func (s Server) Serve() error { + s.registerRoutes() + if err := s.HTTPServer.Start(); err != nil { + return err + } + + return nil +} + +func (s Server) Stop(ctx context.Context) error { + return s.HTTPServer.Stop(ctx) +} + +func (s Server) registerRoutes() { + router := s.HTTPServer.GetRouter() + + router.GET("campaign/health-check", s.healthCheck) + + r := router.Group("campaign") + + r.POST("/", s.handler.createCampaign) + +} diff --git a/donate_app/service/param.go b/donate_app/service/param.go index 6d43c336..f064f16e 100644 --- a/donate_app/service/param.go +++ b/donate_app/service/param.go @@ -1 +1,26 @@ package service + +import ( + "git.gocasts.ir/ebhomengo/niki/types" + "time" +) + +type GetCampaignResponse struct { + ID types.ID `json:"user_id"` +} + +type AddCampaignRequest struct { + Title string `json:"title"` + Description string `json:"description"` + GoalAmount float64 `json:"goal_amount"` + DeadlineAt *time.Time `json:"deadline_at,omitempty"` + AdminID types.ID `json:"admin_id"` +} + +type UpdateCampaignRequest struct { + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + GoalAmount *float64 `json:"goal_amount,omitempty"` + DeadlineAt *time.Time `json:"deadline_at,omitempty"` + Status *string `json:"status,omitempty"` // draft/active/completed/paused/cancelled +} diff --git a/donate_app/service/service.go b/donate_app/service/service.go index e3a7192a..6d43c336 100644 --- a/donate_app/service/service.go +++ b/donate_app/service/service.go @@ -1,38 +1 @@ package service - - - - -import ( - "context" - "errors" - "time" - - "git.gocasts.ir/ebhomengo/niki/campaign/entity" - "git.gocasts.ir/ebhomengo/niki/repository" - richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" - "git.gocasts.ir/ebhomengo/niki/types" -) - - - - -type CampaignService struct { - repo repository.repo -} - - -// type CampaignServiceInterface interface { -// CreateCampaign(ctx context.Context, req CampaignRepository) (types.ID, error) -// } - -func NewCampaignService( - repo repository.CampaignRepository, - participantRepo repository.CampaignParticipantRepository, -) *CampaignService { - return &CampaignService{ - repo: repo, - participantRepo: participantRepo, - } -} -