diff --git a/domain/notification/entity/channel.go b/domain/notification/entity/channel.go new file mode 100644 index 00000000..4dfcc563 --- /dev/null +++ b/domain/notification/entity/channel.go @@ -0,0 +1,8 @@ +package entity + +type Channel struct { + ID int8 + Type NotificationType + Provider string + Config string +} diff --git a/domain/notification/entity/notification.go b/domain/notification/entity/notification.go new file mode 100644 index 00000000..96887c62 --- /dev/null +++ b/domain/notification/entity/notification.go @@ -0,0 +1,51 @@ +package entity + +type Notification struct { + ID int8 + Type NotificationType + Recipinet string + Body string + Status NotificationStatus +} + +type NotificationType uint8 + +const ( + Email NotificationType = iota + 1 + SMS + Push +) + +func (t NotificationType) String() string { + switch t { + case Email: + return "Email" + case SMS: + return "SMS" + case Push: + return "Push" + default: + return "Unknown" + } +} + +type NotificationStatus uint8 + +const ( + Pending NotificationStatus = iota + 1 + Success + Failed +) + +func (t NotificationStatus) String() string { + switch t { + case Pending: + return "Pending" + case Success: + return "Success" + case Failed: + return "Failed" + default: + return "Unknown" + } +} diff --git a/domain/notification/messagebroker/redis.go b/domain/notification/messagebroker/redis.go new file mode 100644 index 00000000..c6974a9e --- /dev/null +++ b/domain/notification/messagebroker/redis.go @@ -0,0 +1,17 @@ +package messagebroker + +import "git.gocasts.ir/ebhomengo/niki/domain/notification/entity" + +type redis struct { +} + +func (r *redis) AddItem(notification entity.Notification) error { + return nil +} + +func (r *redis) RemoveItem(notification entity.Notification) error { + return nil +} +func (r *redis) HealthCheck() error { + return nil +} diff --git a/domain/notification/service/additem.go b/domain/notification/service/additem.go new file mode 100644 index 00000000..d79a9677 --- /dev/null +++ b/domain/notification/service/additem.go @@ -0,0 +1,67 @@ +package service + +import ( + _ "time" + + "git.gocasts.ir/ebhomengo/niki/domain/notification/entity" +) + +type Notifservice struct { + MessageBroker MessageBroker + Repository Repository + Channel Channel +} + +type MessageBroker interface { + AddItem(notification entity.Notification) error + RemoveItem(notification entity.Notification) error + HealthCheck() error +} + +type Repository interface { + AddItem(notification entity.Notification) error +} + +type Channel interface { + SendMessage(notification entity.Notification) error +} +type NotificationServiceRequest struct { + Body string + Type entity.NotificationType + Recipinet string +} + +func (n *Notifservice) NewNotification(r NotificationServiceRequest) *entity.Notification { + // TODO add validation of notification properties + return &entity.Notification{ + Type: r.Type, + Recipinet: r.Recipinet, + Body: r.Body, + Status: entity.Pending, + } +} + +func (n *Notifservice) Send(notification entity.Notification) error { + if err := n.Channel.SendMessage(notification); err != nil { + return err + } + return nil +} + +func (n *Notifservice) Add(notification *entity.Notification) error { + err := n.MessageBroker.AddItem(*notification) + if err != nil { + return err + } + return nil +} + +func (n *Notifservice) Archive(notification *entity.Notification) error { + if err := n.MessageBroker.RemoveItem(*notification); err != nil { + return err + } + if err := n.Repository.AddItem(*notification); err != nil { + return err + } + return nil +} diff --git a/domain/notification/service/send.go b/domain/notification/service/send.go new file mode 100644 index 00000000..1d2c9211 --- /dev/null +++ b/domain/notification/service/send.go @@ -0,0 +1,7 @@ +package service + +import "git.gocasts.ir/ebhomengo/niki/domain/notification/entity" + +type sender interface { + Send(notification entity.Notification) error +} diff --git a/donateApp/cmd/donate/main.go b/donateApp/cmd/donate/main.go deleted file mode 100644 index 30b9cd45..00000000 --- a/donateApp/cmd/donate/main.go +++ /dev/null @@ -1 +0,0 @@ -package donate diff --git a/donateApp/config.go b/donateApp/config.go deleted file mode 100644 index c55690d5..00000000 --- a/donateApp/config.go +++ /dev/null @@ -1,5 +0,0 @@ -package donateapp - -type Config struct{ - -} diff --git a/donateApp/delivery/donate_server/handler.go b/donateApp/delivery/donate_server/handler.go deleted file mode 100644 index 7cc44499..00000000 --- a/donateApp/delivery/donate_server/handler.go +++ /dev/null @@ -1,8 +0,0 @@ -package donate_server - -type Handler struct { -} - -func NewHandler() Handler { - return Handler{} -} diff --git a/donateApp/repository/migrations/1774070672_add_donate_table.sql b/donateApp/repository/migrations/1774070672_add_donate_table.sql deleted file mode 100644 index 7950546f..00000000 --- a/donateApp/repository/migrations/1774070672_add_donate_table.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +migrate Up -CREATE TABLE `donates` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `id`(`id` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 84 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_persian_ci ROW_FORMAT = Dynamic; - --- +migrate Down -DROP TABLE IF EXISTS `donates`; \ No newline at end of file diff --git a/donateApp/repository/mysql/db.go b/donateApp/repository/mysql/db.go deleted file mode 100644 index b0843023..00000000 --- a/donateApp/repository/mysql/db.go +++ /dev/null @@ -1 +0,0 @@ -package mysql diff --git a/donateApp/service/entity.go b/donateApp/service/entity.go deleted file mode 100644 index 6d43c336..00000000 --- a/donateApp/service/entity.go +++ /dev/null @@ -1 +0,0 @@ -package service diff --git a/donateApp/service/service.go b/donateApp/service/service.go deleted file mode 100644 index 6d43c336..00000000 --- a/donateApp/service/service.go +++ /dev/null @@ -1 +0,0 @@ -package service diff --git a/donateApp/app.go b/donate_app/app.go similarity index 79% rename from donateApp/app.go rename to donate_app/app.go index d762c024..d280a647 100644 --- a/donateApp/app.go +++ b/donate_app/app.go @@ -1,8 +1,10 @@ -package doanteApp +package donate_app import "net/http" type Application struct { Config Config HTTPServer *http.Server -} \ No newline at end of file +} + + diff --git a/donate_app/config.go b/donate_app/config.go new file mode 100644 index 00000000..b670aced --- /dev/null +++ b/donate_app/config.go @@ -0,0 +1,17 @@ +package donate_app + + + + +import ( + "git.gocasts.ir/ebhomengo/niki/repository/mysql" +) + +type Config struct { + Mysql mysql.Config `koanf:"mariadb"` +} + + +type Config struct{ + +} diff --git a/donate_app/delivery/donate_server/handler.go b/donate_app/delivery/donate_server/handler.go new file mode 100644 index 00000000..9f94a9c3 --- /dev/null +++ b/donate_app/delivery/donate_server/handler.go @@ -0,0 +1,5 @@ +package donate_server + +type Handler struct{} + + diff --git a/donateApp/delivery/donate_server/routes.go b/donate_app/delivery/donate_server/routes.go similarity index 100% rename from donateApp/delivery/donate_server/routes.go rename to donate_app/delivery/donate_server/routes.go diff --git a/donateApp/delivery/donate_server/server.go b/donate_app/delivery/donate_server/server.go similarity index 100% rename from donateApp/delivery/donate_server/server.go rename to donate_app/delivery/donate_server/server.go diff --git a/donate_app/pkg/types.go b/donate_app/pkg/types.go new file mode 100644 index 00000000..ad2823b5 --- /dev/null +++ b/donate_app/pkg/types.go @@ -0,0 +1,4 @@ +// --- Type Aliases --- +package pkg + +type ID uint64 diff --git a/donate_app/repository/migrations/1775633533_add_campaign_table.sql b/donate_app/repository/migrations/1775633533_add_campaign_table.sql new file mode 100644 index 00000000..65aead6e --- /dev/null +++ b/donate_app/repository/migrations/1775633533_add_campaign_table.sql @@ -0,0 +1,18 @@ +-- +migrate Up + +CREATE TABLE `campaigns` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `title` VARCHAR(255) NOT NULL, + `description` TEXT, + `goal_amount` DECIMAL(15,2) NOT NULL, + `raised_amount` DECIMAL(15,2) DEFAULT 0, + `status` VARCHAR(50) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `deadline_at` TIMESTAMP, + `admin_id` BIGINT NOT NULL, + FOREIGN KEY (`admin_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT +); + + +-- +migrate Down +DROP TABLE `campaigns`; diff --git a/donate_app/repository/migrations/1775633805_add_campaign_participants_table.sql b/donate_app/repository/migrations/1775633805_add_campaign_participants_table.sql new file mode 100644 index 00000000..779a9bd2 --- /dev/null +++ b/donate_app/repository/migrations/1775633805_add_campaign_participants_table.sql @@ -0,0 +1,19 @@ +-- +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/mysql/db.go b/donate_app/repository/mysql/db.go new file mode 100644 index 00000000..6a89ad81 --- /dev/null +++ b/donate_app/repository/mysql/db.go @@ -0,0 +1,90 @@ +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 DB struct { + conn *mysql.DB +} + +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) { + 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 +} + +// 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 new file mode 100644 index 00000000..fb493429 --- /dev/null +++ b/donate_app/service/entity/campaignParticipant.go @@ -0,0 +1,14 @@ +package service + + +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 diff --git a/donate_app/service/entity/campiagn.go b/donate_app/service/entity/campiagn.go new file mode 100644 index 00000000..157ef030 --- /dev/null +++ b/donate_app/service/entity/campiagn.go @@ -0,0 +1,30 @@ +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/donateApp/service/param.go b/donate_app/service/param.go similarity index 100% rename from donateApp/service/param.go rename to donate_app/service/param.go diff --git a/donate_app/service/service.go b/donate_app/service/service.go new file mode 100644 index 00000000..b5141dd2 --- /dev/null +++ b/donate_app/service/service.go @@ -0,0 +1,91 @@ +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.CampaignRepository + participantRepo repository.CampaignParticipantRepository +} + + +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, +) *CampaignService { + return &CampaignService{ + repo: repo, + participantRepo: participantRepo, + } +} + + +// 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 +} diff --git a/donateApp/service/validator.go b/donate_app/service/validator.go similarity index 100% rename from donateApp/service/validator.go rename to donate_app/service/validator.go