From df292897ededf57777cb1a589a894fdbfd10d7f0 Mon Sep 17 00:00:00 2001 From: matina Date: Wed, 15 Apr 2026 01:08:42 -0700 Subject: [PATCH 1/2] refactor --- donate_app/repository/mysql/db.go | 6 +++++- donate_app/service/service.go | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/donate_app/repository/mysql/db.go b/donate_app/repository/mysql/db.go index 6a89ad81..26c0d5ef 100644 --- a/donate_app/repository/mysql/db.go +++ b/donate_app/repository/mysql/db.go @@ -16,8 +16,10 @@ func New(db *mysql.DB) *DB { return &DB{conn: db} } + + // CreateCampaign creates a new campaign -func (d *DB) CreateCampaign(ctx context.Context, campaign entity.Campaign) (types.ID, error) { +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) @@ -58,6 +60,8 @@ func (d *DB) CreateCampaign(ctx context.Context, campaign entity.Campaign) (type return types.ID(campaignID), 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" diff --git a/donate_app/service/service.go b/donate_app/service/service.go index b5141dd2..2462cdfc 100644 --- a/donate_app/service/service.go +++ b/donate_app/service/service.go @@ -19,7 +19,6 @@ import ( type CampaignService struct { repo repository.CampaignRepository - participantRepo repository.CampaignParticipantRepository } From 588cf26ef4d13f97677ffb196d101e2d8821caab Mon Sep 17 00:00:00 2001 From: matina Date: Wed, 15 Apr 2026 01:28:40 -0700 Subject: [PATCH 2/2] feat & improve :(campaign domain) --- domain/campaign/entity/entitty.go | 48 ++++++++++++++ domain/campaign/repository/repo.go | 29 ++++++++ domain/campaign/service/Interfaces.go | 7 ++ domain/campaign/service/createCampaign.go | 81 +++++++++++++++++++++++ donate_app/service/entity/campiagn.go | 30 --------- donate_app/service/service.go | 60 ++--------------- 6 files changed, 169 insertions(+), 86 deletions(-) create mode 100644 domain/campaign/entity/entitty.go create mode 100644 domain/campaign/repository/repo.go create mode 100644 domain/campaign/service/Interfaces.go create mode 100644 domain/campaign/service/createCampaign.go delete mode 100644 donate_app/service/entity/campiagn.go diff --git a/domain/campaign/entity/entitty.go b/domain/campaign/entity/entitty.go new file mode 100644 index 00000000..9df48283 --- /dev/null +++ b/domain/campaign/entity/entitty.go @@ -0,0 +1,48 @@ +package domain + +import "time" + +type ID uint64 + +type CampaignStatus string + +const ( + CampaignDraft CampaignStatus = "draft" + CampaignActive CampaignStatus = "active" + CampaignFinished CampaignStatus = "completed" + CampaignPaused CampaignStatus = "paused" + CampaignCanceled CampaignStatus = "cancelled" +) + +type Campaign struct { + ID ID `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + GoalAmount float64 `json:"goal_amount"` + RaisedAmount float64 `json:"raised_amount"` + Status CampaignStatus `json:"status"` + CreatedAt time.Time `json:"created_at"` + DeadlineAt *time.Time `json:"deadline_at,omitempty"` + AdminID ID `json:"creator_id"` +} + +// Behavior +func (c *Campaign) Activate() { + if c.Status == CampaignDraft { + c.Status = CampaignActive + } +} + +func (c *Campaign) AddFunds(amount float64) { + c.RaisedAmount += amount + if c.RaisedAmount >= c.GoalAmount { + c.Status = CampaignFinished + } +} + +func (c *Campaign) IsExpired(now time.Time) bool { + if c.DeadlineAt == nil { + return false + } + return now.After(*c.DeadlineAt) +} diff --git a/domain/campaign/repository/repo.go b/domain/campaign/repository/repo.go new file mode 100644 index 00000000..74605d96 --- /dev/null +++ b/domain/campaign/repository/repo.go @@ -0,0 +1,29 @@ +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 new file mode 100644 index 00000000..b82ad074 --- /dev/null +++ b/domain/campaign/service/Interfaces.go @@ -0,0 +1,7 @@ +package domain + +import ( + "context" + "time" +) + diff --git a/domain/campaign/service/createCampaign.go b/domain/campaign/service/createCampaign.go new file mode 100644 index 00000000..319f5b36 --- /dev/null +++ b/domain/campaign/service/createCampaign.go @@ -0,0 +1,81 @@ +package campaign + +import ( + "context" + "errors" // For standard errors + "time" + + "your_project/domain" + "your_project/pkg/richerror" + "your_project/repository" +) + +type ID = unit64 +type EntityCampaign = domain.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, + } +} + + +func (s *CampaignService) CreateCampaign(ctx context.Context, req CreateCampaignRequest) (ID, error) { + const Op = "service.campaign.create_campaign" + + if req.Title == "" { + return 0, richerror.New(Op).WithMessage("title is required") + } + if req.GoalAmount <= 0 { + return 0, richerror.New(Op).WithMessage("goal_amount must be greater than 0") + } + if req.AdminID == 0 { + return 0, richerror.New(Op).WithMessage("admin_id is required") + } + + validStatuses := map[string]bool{ + "draft": true, + "active": true, + "completed": true, + "cancelled": true, + "paused": true + } + if !validStatuses[req.Status] { + return 0, richerror.New(Op).WithMessage("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 +} + + + + diff --git a/donate_app/service/entity/campiagn.go b/donate_app/service/entity/campiagn.go deleted file mode 100644 index 157ef030..00000000 --- a/donate_app/service/entity/campiagn.go +++ /dev/null @@ -1,30 +0,0 @@ -package service - -import ( - "time" -) - -// CampaignStatus represents the possible states of a campaign -type CampaignStatus string - -const ( - StatusActive CampaignStatus = "active" - StatusCompleted CampaignStatus = "completed" - StatusCancelled CampaignStatus = "cancelled" - StatusExpired CampaignStatus = "expired" // New status for deadlines -) - -type ID uint64 - -type Campaign struct { - ID ID `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - GoalAmount float64 `json:"goal_amount"` - RaisedAmount float64 `json:"raised_amount"` - Status CampaignStatus `json:"status"` - CreatedAt time.Time `json:"created_at"` - DeadlineAt *time.Time `json:"deadline_at,omitempty"` - - AdminID ID `json:"creator_id"` -} diff --git a/donate_app/service/service.go b/donate_app/service/service.go index 2462cdfc..e3a7192a 100644 --- a/donate_app/service/service.go +++ b/donate_app/service/service.go @@ -18,15 +18,14 @@ import ( type CampaignService struct { - repo repository.CampaignRepository + repo repository.repo } -type CampaignServiceInterface interface { - CreateCampaign(ctx context.Context, req CampaignRepository) (types.ID, error) -} +// type CampaignServiceInterface interface { +// CreateCampaign(ctx context.Context, req CampaignRepository) (types.ID, error) +// } -// NewCampaignService creates a new campaign service func NewCampaignService( repo repository.CampaignRepository, participantRepo repository.CampaignParticipantRepository, @@ -37,54 +36,3 @@ func NewCampaignService( } } - -// CreateCampaign creates a new campaign -func (s *CampaignService) CreateCampaign(ctx context.Context, req CreateCampaignRequest) (types.ID, error) { - const Op = "service.campaign.create_campaign" - - // Business Logic: Validation - if req.Title == "" { - return 0, richerror.New(Op).WithErr(errors.New("title is required")) - } - - if req.GoalAmount <= 0 { - return 0, richerror.New(Op).WithErr(errors.New("goal_amount must be greater than 0")) - } - - if req.AdminID == 0 { - return 0, richerror.New(Op).WithErr(errors.New("admin_id is required")) - } - - // Business Logic: Validate status - validStatuses := map[string]bool{ - "draft": true, - "active": true, - "completed": true, - "cancelled": true, - } - - if !validStatuses[req.Status] { - return 0, richerror.New(Op).WithErr(errors.New("invalid status")) - } - - - // Create campaign entity - campaign := entity.Campaign{ - Title: req.Title, - Description: req.Description, - GoalAmount: req.GoalAmount, - RaisedAmount: 0, // Initially 0 - Status: entity.CampaignStatus(req.Status), - DeadlineAt: req.DeadlineAt, - AdminID: req.AdminID, - CreatedAt: time.Now(), - } - - // Call repository - campaignID, err := s.repo.CreateCampaign(ctx, campaign) - if err != nil { - return 0, richerror.New(Op).WithErr(err) - } - - return campaignID, nil -}