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

Reviewed-on: ebhomengo/niki#269
This commit is contained in:
hossein 2026-04-08 06:15:44 +00:00
commit 1a78be4596
21 changed files with 292 additions and 30 deletions

View File

@ -1 +0,0 @@
package donate

View File

@ -1,5 +0,0 @@
package donateapp
type Config struct{
}

View File

@ -1,8 +0,0 @@
package donate_server
type Handler struct {
}
func NewHandler() Handler {
return Handler{}
}

View File

@ -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`;

View File

@ -1 +0,0 @@
package mysql

View File

@ -1 +0,0 @@
package service

View File

@ -1 +0,0 @@
package service

View File

@ -1,4 +1,4 @@
package doanteApp
package donate_app
import "net/http"
@ -6,3 +6,5 @@ type Application struct {
Config Config
HTTPServer *http.Server
}

17
donate_app/config.go Normal file
View File

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

View File

@ -0,0 +1,5 @@
package donate_server
type Handler struct{}

4
donate_app/pkg/types.go Normal file
View File

@ -0,0 +1,4 @@
// --- Type Aliases ---
package pkg
type ID uint64

View File

@ -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`;

View File

@ -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`;

View File

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

View File

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

View File

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

View File

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