Merge pull request 'donate' (#279) from donate into develop

Reviewed-on: ebhomengo/niki#279
This commit is contained in:
hossein 2026-04-15 05:52:08 +00:00
commit 03cba27f4a
7 changed files with 174 additions and 88 deletions

View File

@ -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)
}

View File

@ -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
}

View File

@ -0,0 +1,7 @@
package domain
import (
"context"
"time"
)

View File

@ -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
}

View File

@ -16,8 +16,10 @@ func New(db *mysql.DB) *DB {
return &DB{conn: db} return &DB{conn: db}
} }
// CreateCampaign creates a new campaign // 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" const Op = "repository.mysql.campaign.create"
tx, err := d.conn.Conn().BeginTx(ctx, nil) 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 return types.ID(campaignID), nil
} }
// Create adds a new participant to a campaign // Create adds a new participant to a campaign
func (d *DB) CreateCampaignParticipants(ctx context.Context, participant entity.CampaignParticipant) (types.ID, error) { func (d *DB) CreateCampaignParticipants(ctx context.Context, participant entity.CampaignParticipant) (types.ID, error) {
const Op = "repository.mysql.campaign_participant.create" const Op = "repository.mysql.campaign_participant.create"

View File

@ -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"`
}

View File

@ -18,16 +18,14 @@ import (
type CampaignService struct { type CampaignService struct {
repo repository.CampaignRepository repo repository.repo
participantRepo repository.CampaignParticipantRepository
} }
type CampaignServiceInterface interface { // type CampaignServiceInterface interface {
CreateCampaign(ctx context.Context, req CampaignRepository) (types.ID, error) // CreateCampaign(ctx context.Context, req CampaignRepository) (types.ID, error)
} // }
// NewCampaignService creates a new campaign service
func NewCampaignService( func NewCampaignService(
repo repository.CampaignRepository, repo repository.CampaignRepository,
participantRepo repository.CampaignParticipantRepository, participantRepo repository.CampaignParticipantRepository,
@ -38,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
}