From d4f65ba68a8b3f6eceeb24cb479cb779080ed3ee Mon Sep 17 00:00:00 2001 From: matina Date: Tue, 28 Apr 2026 01:46:18 -0700 Subject: [PATCH 1/4] mapper for service & params & improve donation flow --- domain/campaign/entity/entitty.go | 2 + .../1775633533_add_campaign_table.sql | 2 + domain/campaign/repository/mysql/db.go | 7 +++- .../{createCampaign.go => create-campaign.go} | 18 ++------ domain/campaign/service/mapper/main.go | 22 ++++++++++ domain/campaign/service/param.go | 18 +++----- domain/campaign/service/services.go | 2 +- donate_app/config.go | 8 ---- donate_app/delivery/http/handler.go | 13 +++--- donate_app/pkg/types.go | 4 -- ...633805_add_campaign_participants_table.sql | 19 --------- .../1775633805_donation_campaign_table.sql | 28 +++++++++++++ donate_app/repository/mysql/db.go | 41 ------------------- .../service/entity/campaignParticipant.go | 13 ------ donate_app/service/entity/donation.go | 20 +++++++++ donate_app/service/param.go | 26 ------------ donate_app/service/validator.go | 1 - 17 files changed, 98 insertions(+), 146 deletions(-) rename domain/campaign/service/{createCampaign.go => create-campaign.go} (73%) create mode 100644 domain/campaign/service/mapper/main.go delete mode 100644 donate_app/pkg/types.go delete mode 100644 donate_app/repository/migrations/1775633805_add_campaign_participants_table.sql create mode 100644 donate_app/repository/migrations/1775633805_donation_campaign_table.sql delete mode 100644 donate_app/service/entity/campaignParticipant.go create mode 100644 donate_app/service/entity/donation.go delete mode 100644 donate_app/service/param.go delete mode 100644 donate_app/service/validator.go diff --git a/domain/campaign/entity/entitty.go b/domain/campaign/entity/entitty.go index f6eca8a0..5e79f0b6 100644 --- a/domain/campaign/entity/entitty.go +++ b/domain/campaign/entity/entitty.go @@ -19,6 +19,8 @@ type Campaign struct { ID types.ID `json:"id"` Title string `json:"title"` Description string `json:"description"` + Link string `json:"link"` + Slogan string `json:"slogan"` // GoalAmount float64 `json:"goal_amount"` RaisedAmount float64 `json:"raised_amount"` Status CampaignStatus `json:"status"` diff --git a/domain/campaign/repository/migrations/1775633533_add_campaign_table.sql b/domain/campaign/repository/migrations/1775633533_add_campaign_table.sql index 65aead6e..d3e973ee 100644 --- a/domain/campaign/repository/migrations/1775633533_add_campaign_table.sql +++ b/domain/campaign/repository/migrations/1775633533_add_campaign_table.sql @@ -4,6 +4,8 @@ CREATE TABLE `campaigns` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `title` VARCHAR(255) NOT NULL, `description` TEXT, + `link` VARCHAR(255) NULL, + `slogan` VARCHAR(255) NULL, `goal_amount` DECIMAL(15,2) NOT NULL, `raised_amount` DECIMAL(15,2) DEFAULT 0, `status` VARCHAR(50) NOT NULL, diff --git a/domain/campaign/repository/mysql/db.go b/domain/campaign/repository/mysql/db.go index eefb0b6f..e9cddcc1 100644 --- a/domain/campaign/repository/mysql/db.go +++ b/domain/campaign/repository/mysql/db.go @@ -17,7 +17,7 @@ func New(db *mysql.DB) *DB { } // CreateCampaign creates a new campaign -func (d *DB) CreateAndSave(ctx context.Context, campaign entity.Campaign) (types.ID, error) { +func (d *DB) Create(ctx context.Context, campaign entity.Campaign) (types.ID, error) { const Op = "repository.mysql.campaign.create" tx, err := d.conn.Conn().BeginTx(ctx, nil) @@ -26,13 +26,16 @@ func (d *DB) CreateAndSave(ctx context.Context, campaign entity.Campaign) (types } defer tx.Rollback() - query := `INSERT INTO campaigns (title, description, goal_amount, raised_amount, + query := `INSERT INTO campaigns (title, description,link, slogan , + goal_amount, raised_amount, status, deadline_at ,admin_id , created_at ) VALUES (?, ?, ?, ?, ?, ?, ? , NOW() )` result, err := tx.ExecContext(ctx, query, campaign.Title, campaign.Description, + campaign.Link, + campaign.Slogan, campaign.GoalAmount, campaign.RaisedAmount, campaign.Status, diff --git a/domain/campaign/service/createCampaign.go b/domain/campaign/service/create-campaign.go similarity index 73% rename from domain/campaign/service/createCampaign.go rename to domain/campaign/service/create-campaign.go index 24514606..372ee293 100644 --- a/domain/campaign/service/createCampaign.go +++ b/domain/campaign/service/create-campaign.go @@ -3,30 +3,20 @@ package service import ( "context" "fmt" - "git.gocasts.ir/ebhomengo/niki/domain/campaign/entity" + "git.gocasts.ir/ebhomengo/niki/domain/campaign/service/mapper" richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" "git.gocasts.ir/ebhomengo/niki/types" - "time" ) // CreateCampaign handles creation of a new campaign. -func (s *CampaignService) CreateCampaign(ctx context.Context, req entity.Campaign) (types.ID, error) { +func (s *CampaignService) CreateCampaign(ctx context.Context, req CreateCampaignRequest) (types.ID, error) { const op = "service.campaign.create_campaign" 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(), - } + campaign := mapper.ToCampaignEntity(req) id, err := s.repo.Create(ctx, campaign) if err != nil { @@ -36,7 +26,7 @@ func (s *CampaignService) CreateCampaign(ctx context.Context, req entity.Campaig return id, nil } -func validateCreateCampaignRequest(req entity.Campaign) error { +func validateCreateCampaignRequest(req CreateCampaignRequest) error { if req.Title == "" { return errRequired("title") } diff --git a/domain/campaign/service/mapper/main.go b/domain/campaign/service/mapper/main.go new file mode 100644 index 00000000..a03c256f --- /dev/null +++ b/domain/campaign/service/mapper/main.go @@ -0,0 +1,22 @@ +package mapper + +import ( + "git.gocasts.ir/ebhomengo/niki/domain/campaign/entity" + param "git.gocasts.ir/ebhomengo/niki/domain/campaign/service" + "time" +) + +func ToCampaignEntity(req param.CreateCampaignRequest) entity.Campaign { + return entity.Campaign{ + Title: req.Title, + Description: req.Description, + Link: req.Link, + Slogan: req.Slogan, + GoalAmount: req.GoalAmount, + RaisedAmount: 0, + Status: entity.CampaignStatus(req.Status), + DeadlineAt: req.DeadlineAt, + AdminID: req.AdminID, + CreatedAt: time.Now(), + } +} diff --git a/domain/campaign/service/param.go b/domain/campaign/service/param.go index f54649ab..ecb9a430 100644 --- a/domain/campaign/service/param.go +++ b/domain/campaign/service/param.go @@ -6,22 +6,16 @@ import ( ) type GetCampaignResponse struct { - ID types.ID `json:"user_id"` + ID types.ID `json:"campaign_id"` } -type AddCampaignRequest struct { - ID uint64 `json:"id"` +type CreateCampaignRequest struct { Title string `json:"title"` Description string `json:"description"` + Link string `json:"link"` + Slogan string `json:"slogan" validate:"max=255"` GoalAmount float64 `json:"goal_amount"` + Status string `json:"status,omitempty"` 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 + AdminID types.ID `json:"admin_id" validate:"required"` } diff --git a/domain/campaign/service/services.go b/domain/campaign/service/services.go index cc85d8b3..9e027fb9 100644 --- a/domain/campaign/service/services.go +++ b/domain/campaign/service/services.go @@ -15,7 +15,7 @@ type CampaignFilterParam struct { } type CampaignStorage interface { - Create(ctx context.Context, c entity.Campaign) (types.ID, error) // باید ID برگرداند + Create(ctx context.Context, c entity.Campaign) (types.ID, error) 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) diff --git a/donate_app/config.go b/donate_app/config.go index b670aced..575e20db 100644 --- a/donate_app/config.go +++ b/donate_app/config.go @@ -1,8 +1,5 @@ package donate_app - - - import ( "git.gocasts.ir/ebhomengo/niki/repository/mysql" ) @@ -10,8 +7,3 @@ import ( type Config struct { Mysql mysql.Config `koanf:"mariadb"` } - - -type Config struct{ - -} diff --git a/donate_app/delivery/http/handler.go b/donate_app/delivery/http/handler.go index b20f6dd6..357b0ddf 100644 --- a/donate_app/delivery/http/handler.go +++ b/donate_app/delivery/http/handler.go @@ -1,12 +1,13 @@ package http import ( - "git.gocasts.ir/ebhomengo/niki/domain/campaign/entity" "git.gocasts.ir/ebhomengo/niki/domain/campaign/service" + param "git.gocasts.ir/ebhomengo/niki/domain/campaign/service" + "git.gocasts.ir/ebhomengo/niki/pkg/claim" httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg" + "git.gocasts.ir/ebhomengo/niki/types" "github.com/labstack/echo/v4" "net/http" - "time" ) type Handler struct { @@ -19,14 +20,16 @@ func NewHandler(svc service.CampaignService) Handler { func (h Handler) createCampaign(c echo.Context) error { - var req entity.Campaign + claims := claim.GetClaimsFromEchoContext(c) + + var req param.CreateCampaignRequest + + req.AdminID = types.ID(claims.UserID) 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 { diff --git a/donate_app/pkg/types.go b/donate_app/pkg/types.go deleted file mode 100644 index ad2823b5..00000000 --- a/donate_app/pkg/types.go +++ /dev/null @@ -1,4 +0,0 @@ -// --- Type Aliases --- -package pkg - -type ID uint64 diff --git a/donate_app/repository/migrations/1775633805_add_campaign_participants_table.sql b/donate_app/repository/migrations/1775633805_add_campaign_participants_table.sql deleted file mode 100644 index 779a9bd2..00000000 --- a/donate_app/repository/migrations/1775633805_add_campaign_participants_table.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +migrate Up - -CREATE TABLE `campaign_participants` ( - `id` BIGINT PRIMARY KEY AUTO_INCREMENT, - `campaign_id` BIGINT NOT NULL, - `user_id` BIGINT NOT NULL, - `amount` DECIMAL(15,2) NOT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (`campaign_id`) - REFERENCES `campaigns`(`id`) - ON DELETE CASCADE, - FOREIGN KEY (`user_id`) - REFERENCES `users`(`id`) - ON DELETE CASCADE, - UNIQUE KEY `unique_campaign_user` (`campaign_id`, `user_id`) -); - --- +migrate Down -DROP TABLE `campaign_participants`; diff --git a/donate_app/repository/migrations/1775633805_donation_campaign_table.sql b/donate_app/repository/migrations/1775633805_donation_campaign_table.sql new file mode 100644 index 00000000..8898d22a --- /dev/null +++ b/donate_app/repository/migrations/1775633805_donation_campaign_table.sql @@ -0,0 +1,28 @@ +-- +migrate Up +CREATE TABLE `donation_flows` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `campaign_id` BIGINT NOT NULL, + `user_id` BIGINT NULL, + `source_type` VARCHAR(50) NOT NULL, -- e.g., "affiliate", "app", "qr", "sms" + `source_name` VARCHAR(100) NOT NULL, -- e.g., "instagram", "donate_app" + `referral_code` VARCHAR(100), + `link` VARCHAR(255) NOT NULL, + `clicks` BIGINT DEFAULT 0, + `conversions` BIGINT DEFAULT 0, + `donations_total` DECIMAL(15,2) DEFAULT 0.00, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (`campaign_id`) + REFERENCES `campaigns`(`id`) + ON DELETE CASCADE, + + FOREIGN KEY (`user_id`) + REFERENCES `users`(`id`) + ON DELETE SET NULL +); + +CREATE INDEX `idx_flow_campaign_id` ON `donation_flows`(`campaign_id`); +CREATE INDEX `idx_flow_source` ON `donation_flows`(`source_type`, `source_name`); + +-- +migrate Down +DROP TABLE IF EXISTS `donation_flows`; diff --git a/donate_app/repository/mysql/db.go b/donate_app/repository/mysql/db.go index 358ea1cd..6e11f9aa 100644 --- a/donate_app/repository/mysql/db.go +++ b/donate_app/repository/mysql/db.go @@ -1,11 +1,7 @@ package mysql import ( - "context" - "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" ) type DB struct { @@ -15,40 +11,3 @@ type DB struct { func New(db *mysql.DB) *DB { return &DB{conn: db} } - -// 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 { - return 0, richerror.New(Op).WithErr(err) - } - defer tx.Rollback() - - query := `INSERT INTO campaign_participants (id,campaign_id, user_id, amount , created_at) - VALUES (?, ?, ? , ? , NOW())` - - result, err := tx.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) - } - if err := tx.Commit(); 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 deleted file mode 100644 index 8199852a..00000000 --- a/donate_app/service/entity/campaignParticipant.go +++ /dev/null @@ -1,13 +0,0 @@ -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"` -} diff --git a/donate_app/service/entity/donation.go b/donate_app/service/entity/donation.go new file mode 100644 index 00000000..740a360a --- /dev/null +++ b/donate_app/service/entity/donation.go @@ -0,0 +1,20 @@ +package entity + +import ( + "git.gocasts.ir/ebhomengo/niki/types" + "time" +) + +type DonationApp struct { + ID types.ID `json:"id"` + CampaignID types.ID `json:"campaign_id"` + UserID *types.ID `json:"user_id,omitempty"` + SourceType string `json:"source_type"` // : "affiliate", "app", "qr", "sms", "social" + SourceName string `json:"source_name"` // "user_name", "donate_app", "instagram" + ReferralCode string `json:"referral_code"` + Link string `json:"link"` + Clicks int64 `json:"clicks"` + Conversions int64 `json:"conversions"` + DonationsTotal float64 `json:"donations_total"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/donate_app/service/param.go b/donate_app/service/param.go deleted file mode 100644 index f064f16e..00000000 --- a/donate_app/service/param.go +++ /dev/null @@ -1,26 +0,0 @@ -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/validator.go b/donate_app/service/validator.go deleted file mode 100644 index 6d43c336..00000000 --- a/donate_app/service/validator.go +++ /dev/null @@ -1 +0,0 @@ -package service From 6807b1c962e966f2a11ac15ae96b438bac4f3d61 Mon Sep 17 00:00:00 2001 From: mokarramis Date: Sun, 3 May 2026 14:42:50 +0330 Subject: [PATCH 2/4] refactor: remove data from cmd/order package to manage them later --- cmd/purchaseapp/main.go | 56 ----------------------------------------- 1 file changed, 56 deletions(-) diff --git a/cmd/purchaseapp/main.go b/cmd/purchaseapp/main.go index ad0283df..9d7dcf23 100644 --- a/cmd/purchaseapp/main.go +++ b/cmd/purchaseapp/main.go @@ -1,63 +1,7 @@ package main -import ( - "flag" - "fmt" - purchaseMysql "git.gocasts.ir/ebhomengo/niki/domain/purchase/repository/mysql" - "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http" - "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order" - "git.gocasts.ir/ebhomengo/niki/repository/migrator" - "git.gocasts.ir/ebhomengo/niki/repository/mysql" -) - -func MariaDB() *mysql.DB { - cfg := mysql.Config{ - Username: "niki", - Password: "nikiappt0lk2o20", - Port: 3306, - Host: "localhost", - DBName: "niki_db", - } - migrate := flag.Bool("migrate", false, "perform database migration") - flag.Parse() - if *migrate { - migrator.New(migrator.Config{ - MysqlConfig: cfg, - MigrationPath: "./purchaseapp/repository/mysql/migration", - MigrationDBName: "gorp_migrations", - }).Up() - } - - return mysql.New(cfg) -} - func main() { - cfg := mysql.Config{ - Username: "niki", - Password: "nikiappt0lk2o20", - Port: 3306, - Host: "localhost", - DBName: "niki_db", - } - db := mysql.New(cfg) - defer func() { - if err := db.CloseStatements(); err != nil { - fmt.Printf("Error closing statements: %v\n", err) - } - }() - - orderRepo := purchaseMysql.New(db) - - orderSvc := Service(orderRepo) - server := HTTPServer(orderSvc) - server.Serve() } -func HTTPServer(orderSvc order.Service) *http.Server { - return http.New(orderSvc) -} -func Service(orderRepo *purchaseMysql.DB) order.Service { - return order.New(orderRepo) -} From 11c73efd66ebff9b0f3ea30b27923cfb1b6f85ed Mon Sep 17 00:00:00 2001 From: matina Date: Tue, 5 May 2026 11:12:03 -0700 Subject: [PATCH 3/4] adding status interface --- domain/campaign/entity/entitty.go | 38 +++++------ domain/campaign/service/create-campaign.go | 20 +++++- domain/campaign/service/mapper/main.go | 22 ------- domain/campaign/service/services.go | 9 ++- .../service/update-campaign-status.go | 63 +++++++++++++++++++ types/status.go | 11 ++++ 6 files changed, 113 insertions(+), 50 deletions(-) delete mode 100644 domain/campaign/service/mapper/main.go create mode 100644 domain/campaign/service/update-campaign-status.go create mode 100644 types/status.go diff --git a/domain/campaign/entity/entitty.go b/domain/campaign/entity/entitty.go index 5e79f0b6..463a3af8 100644 --- a/domain/campaign/entity/entitty.go +++ b/domain/campaign/entity/entitty.go @@ -5,41 +5,31 @@ import ( "time" ) -type CampaignStatus string - -const ( - CampaignDraft CampaignStatus = "draft" - CampaignActive CampaignStatus = "active" - CampaignFinished CampaignStatus = "completed" - CampaignPaused CampaignStatus = "paused" - CampaignCanceled CampaignStatus = "cancelled" -) - type Campaign struct { - ID types.ID `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Link string `json:"link"` - Slogan string `json:"slogan"` // - 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 types.ID `json:"creator_id"` + ID types.ID `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Link string `json:"link"` + Slogan string `json:"slogan"` // + GoalAmount float64 `json:"goal_amount"` + RaisedAmount float64 `json:"raised_amount"` + Status types.CampaignStatus `json:"status"` + CreatedAt time.Time `json:"created_at"` + DeadlineAt *time.Time `json:"deadline_at,omitempty"` + AdminID types.ID `json:"creator_id"` } // Behavior func (c *Campaign) Activate() { - if c.Status == CampaignDraft { - c.Status = CampaignActive + if c.Status == types.CampaignDraft { + c.Status = types.CampaignActive } } func (c *Campaign) AddFunds(amount float64) { c.RaisedAmount += amount if c.RaisedAmount >= c.GoalAmount { - c.Status = CampaignFinished + c.Status = types.CampaignFinished } } diff --git a/domain/campaign/service/create-campaign.go b/domain/campaign/service/create-campaign.go index 372ee293..3d61b93c 100644 --- a/domain/campaign/service/create-campaign.go +++ b/domain/campaign/service/create-campaign.go @@ -3,11 +3,27 @@ package service import ( "context" "fmt" - "git.gocasts.ir/ebhomengo/niki/domain/campaign/service/mapper" + "git.gocasts.ir/ebhomengo/niki/domain/campaign/entity" richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" "git.gocasts.ir/ebhomengo/niki/types" + "time" ) +func ToCampaignEntity(req CreateCampaignRequest) entity.Campaign { + return entity.Campaign{ + Title: req.Title, + Description: req.Description, + Link: req.Link, + Slogan: req.Slogan, + GoalAmount: req.GoalAmount, + RaisedAmount: 0, + Status: types.CampaignStatus(req.Status), + DeadlineAt: req.DeadlineAt, + AdminID: req.AdminID, + CreatedAt: time.Now(), + } +} + // CreateCampaign handles creation of a new campaign. func (s *CampaignService) CreateCampaign(ctx context.Context, req CreateCampaignRequest) (types.ID, error) { const op = "service.campaign.create_campaign" @@ -16,7 +32,7 @@ func (s *CampaignService) CreateCampaign(ctx context.Context, req CreateCampaign return 0, richerror.New(op).WithErr(err) } - campaign := mapper.ToCampaignEntity(req) + campaign := ToCampaignEntity(req) id, err := s.repo.Create(ctx, campaign) if err != nil { diff --git a/domain/campaign/service/mapper/main.go b/domain/campaign/service/mapper/main.go deleted file mode 100644 index a03c256f..00000000 --- a/domain/campaign/service/mapper/main.go +++ /dev/null @@ -1,22 +0,0 @@ -package mapper - -import ( - "git.gocasts.ir/ebhomengo/niki/domain/campaign/entity" - param "git.gocasts.ir/ebhomengo/niki/domain/campaign/service" - "time" -) - -func ToCampaignEntity(req param.CreateCampaignRequest) entity.Campaign { - return entity.Campaign{ - Title: req.Title, - Description: req.Description, - Link: req.Link, - Slogan: req.Slogan, - GoalAmount: req.GoalAmount, - RaisedAmount: 0, - Status: entity.CampaignStatus(req.Status), - DeadlineAt: req.DeadlineAt, - AdminID: req.AdminID, - CreatedAt: time.Now(), - } -} diff --git a/domain/campaign/service/services.go b/domain/campaign/service/services.go index 9e027fb9..a34e6256 100644 --- a/domain/campaign/service/services.go +++ b/domain/campaign/service/services.go @@ -14,6 +14,11 @@ type CampaignFilterParam struct { IsArchived *bool } +type CampaignStatus interface { + FindActiveCampaigns(ctx context.Context) ([]entity.Campaign, error) + UpdateStatus(ctx context.Context, id types.ID, status types.CampaignStatus) error +} + type CampaignStorage interface { Create(ctx context.Context, c entity.Campaign) (types.ID, error) Update(ctx context.Context, c entity.Campaign) error @@ -24,10 +29,10 @@ type CampaignStorage interface { } type CampaignService struct { - repo CampaignStorage + repo CampaignStorage + repoStatus CampaignStatus } -// NewCampaignService constructs a new CampaignService. func NewCampaignService(storage CampaignStorage) *CampaignService { return &CampaignService{ repo: storage, diff --git a/domain/campaign/service/update-campaign-status.go b/domain/campaign/service/update-campaign-status.go new file mode 100644 index 00000000..90f54a13 --- /dev/null +++ b/domain/campaign/service/update-campaign-status.go @@ -0,0 +1,63 @@ +package service + +import ( + "context" + "git.gocasts.ir/ebhomengo/niki/types" + "time" +) + +func (s *CampaignService) MonitorCampaignProgress(ctx context.Context) { + + //c := cron.New() + // + //c.AddFunc("@hourly", func() { + // s.checkAndCompleteCampaigns(context.Background()) + //}) + // + //c.Start() + + //ticker := time.NewTicker(1 * time.Hour) // Check every hour + //defer ticker.Stop() + // + //for { + // select { + // case <-ticker.C: + // s.checkAndCompleteCampaigns(ctx) + // case <-ctx.Done(): + // return + // } + //} +} + +func (s *CampaignService) checkAndCompleteCampaigns(ctx context.Context) { + activeCampaigns, err := s.repoStatus.FindActiveCampaigns(ctx) // Method to fetch active campaigns + if err != nil { + // Log the error, but don't stop the monitor + return + } + + for _, campaign := range activeCampaigns { + // Check if deadline has passed + if campaign.DeadlineAt != nil && campaign.DeadlineAt.Before(time.Now()) { + if campaign.Status != types.CampaignFinished { + campaign.Status = types.CampaignFinished + err := s.repoStatus.UpdateStatus(ctx, campaign.ID, types.CampaignFinished) // Method to update status + if err != nil { + // Log error for this specific campaign + } + } + continue // Move to next campaign + } + + // Check if goal is met + if campaign.RaisedAmount >= campaign.GoalAmount { + if campaign.Status != types.CampaignFinished { + campaign.Status = types.CampaignFinished + err := s.repoStatus.UpdateStatus(ctx, campaign.ID, types.CampaignFinished) // Method to update status + if err != nil { + // Log error for this specific campaign + } + } + } + } +} diff --git a/types/status.go b/types/status.go new file mode 100644 index 00000000..6f65d0fb --- /dev/null +++ b/types/status.go @@ -0,0 +1,11 @@ +package types + +type CampaignStatus string + +const ( + CampaignDraft CampaignStatus = "draft" + CampaignActive CampaignStatus = "active" + CampaignFinished CampaignStatus = "completed" + CampaignPaused CampaignStatus = "paused" + CampaignCanceled CampaignStatus = "cancelled" +) From d564883081151991793add9152cfbc55044ea4fe Mon Sep 17 00:00:00 2001 From: matina Date: Wed, 6 May 2026 02:38:22 -0700 Subject: [PATCH 4/4] contract protobuf for campaign servic --- domain/campaign/service/param.go | 9 +++ .../service/update-campaign-status.go | 81 ++++++++++--------- donate_app/protobuf/campaign.proto | 22 +++++ donate_app/service/service.go | 1 - 4 files changed, 72 insertions(+), 41 deletions(-) create mode 100644 donate_app/protobuf/campaign.proto delete mode 100644 donate_app/service/service.go diff --git a/domain/campaign/service/param.go b/domain/campaign/service/param.go index ecb9a430..10a95c73 100644 --- a/domain/campaign/service/param.go +++ b/domain/campaign/service/param.go @@ -19,3 +19,12 @@ type CreateCampaignRequest struct { DeadlineAt *time.Time `json:"deadline_at,omitempty"` AdminID types.ID `json:"admin_id" validate:"required"` } + +type CompletedCampaignResponse struct { + TotalChecked uint64 `json:"total_checked"` + TotalFinished uint64 `json:"total_finished"` +} + +type FilterRequest struct { + Limit uint32 `json:"total_checked"` +} diff --git a/domain/campaign/service/update-campaign-status.go b/domain/campaign/service/update-campaign-status.go index 90f54a13..15ccb54f 100644 --- a/domain/campaign/service/update-campaign-status.go +++ b/domain/campaign/service/update-campaign-status.go @@ -6,58 +6,59 @@ import ( "time" ) -func (s *CampaignService) MonitorCampaignProgress(ctx context.Context) { +func (s *CampaignService) MonitorCampaignProgress(ctx context.Context, req FilterRequest) { - //c := cron.New() - // - //c.AddFunc("@hourly", func() { - // s.checkAndCompleteCampaigns(context.Background()) - //}) - // - //c.Start() + ticker := time.NewTicker(1 * time.Hour) + defer ticker.Stop() - //ticker := time.NewTicker(1 * time.Hour) // Check every hour - //defer ticker.Stop() - // - //for { - // select { - // case <-ticker.C: - // s.checkAndCompleteCampaigns(ctx) - // case <-ctx.Done(): - // return - // } - //} + for { + select { + case <-ticker.C: + s.CheckAndCompleteCampaigns(ctx, req) + case <-ctx.Done(): + return + } + } } -func (s *CampaignService) checkAndCompleteCampaigns(ctx context.Context) { - activeCampaigns, err := s.repoStatus.FindActiveCampaigns(ctx) // Method to fetch active campaigns +func (s *CampaignService) CheckAndCompleteCampaigns(ctx context.Context, req FilterRequest) (CompletedCampaignResponse, error) { + + now := time.Now() + + //TODO:with filter request later complete + activeCampaigns, err := s.repoStatus.FindActiveCampaigns(ctx) if err != nil { - // Log the error, but don't stop the monitor - return + return CompletedCampaignResponse{}, err } + var totalChecked uint64 + var totalFinished uint64 + for _, campaign := range activeCampaigns { - // Check if deadline has passed - if campaign.DeadlineAt != nil && campaign.DeadlineAt.Before(time.Now()) { - if campaign.Status != types.CampaignFinished { - campaign.Status = types.CampaignFinished - err := s.repoStatus.UpdateStatus(ctx, campaign.ID, types.CampaignFinished) // Method to update status - if err != nil { - // Log error for this specific campaign - } - } - continue // Move to next campaign + + totalChecked++ + + shouldFinish := false + + if campaign.DeadlineAt != nil && campaign.DeadlineAt.Before(now) { + shouldFinish = true } - // Check if goal is met if campaign.RaisedAmount >= campaign.GoalAmount { - if campaign.Status != types.CampaignFinished { - campaign.Status = types.CampaignFinished - err := s.repoStatus.UpdateStatus(ctx, campaign.ID, types.CampaignFinished) // Method to update status - if err != nil { - // Log error for this specific campaign - } + shouldFinish = true + } + + if shouldFinish && campaign.Status != types.CampaignFinished { + if err := s.repoStatus.UpdateStatus(ctx, campaign.ID, types.CampaignFinished); err != nil { + continue } + + totalFinished++ } } + + return CompletedCampaignResponse{ + TotalChecked: totalChecked, + TotalFinished: totalFinished, + }, nil } diff --git a/donate_app/protobuf/campaign.proto b/donate_app/protobuf/campaign.proto new file mode 100644 index 00000000..0c14172d --- /dev/null +++ b/donate_app/protobuf/campaign.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package campaign; + +option go_package = "git.gocasts.ir/ebhomengo/niki/donate_app/protobuf;campaignpb"; + +service CampaignService { + rpc CheckAndCompleteCampaigns (CheckAndCompleteCampaignsRequest) returns (CheckAndCompleteCampaignsResponse); +} + +message CheckAndCompleteCampaignsRequest { + FilterRequest filter = 1; +} + +message CheckAndCompleteCampaignsResponse { + uint64 total_checked = 1; + uint64 total_finished = 2; +} + +message FilterRequest { + uint32 limit = 1; +} \ No newline at end of file diff --git a/donate_app/service/service.go b/donate_app/service/service.go deleted file mode 100644 index 6d43c336..00000000 --- a/donate_app/service/service.go +++ /dev/null @@ -1 +0,0 @@ -package service