forked from ebhomengo/niki
Merge branch 'develop' into feature/product-entities-ISSUE-267
This commit is contained in:
commit
0469d34280
|
|
@ -0,0 +1,8 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
ID int8
|
||||||
|
Type NotificationType
|
||||||
|
Provider string
|
||||||
|
Config string
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import "git.gocasts.ir/ebhomengo/niki/domain/notification/entity"
|
||||||
|
|
||||||
|
type sender interface {
|
||||||
|
Send(notification entity.Notification) error
|
||||||
|
}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
package donate
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package donateapp
|
|
||||||
|
|
||||||
type Config struct{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package donate_server
|
|
||||||
|
|
||||||
type Handler struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHandler() Handler {
|
|
||||||
return Handler{}
|
|
||||||
}
|
|
||||||
|
|
@ -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`;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
package mysql
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
package service
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
package service
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package doanteApp
|
package donate_app
|
||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
|
|
@ -6,3 +6,5 @@ type Application struct {
|
||||||
Config Config
|
Config Config
|
||||||
HTTPServer *http.Server
|
HTTPServer *http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package donate_server
|
||||||
|
|
||||||
|
type Handler struct{}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
// --- Type Aliases ---
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
type ID uint64
|
||||||
|
|
@ -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`;
|
||||||
|
|
@ -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`;
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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"`
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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"`
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue