Merge branch 'develop' of https://git.gocasts.ir/ebhomengo/niki into mehdikeshavarz/driver(agent)/loginorregister

This commit is contained in:
fardin 2026-04-29 09:24:30 -04:00
commit 590ed0bb07
316 changed files with 287274 additions and 19849 deletions

34
.air/.air.productapp.toml Normal file
View File

@ -0,0 +1,34 @@
root = "."
tmp_dir = "cmd/productapp/temp"
[build]
bin = "/entrypoint.sh"
args_bin = []
cmd = "go build -mod=mod -buildvcs=false -o ./cmd/productapp/temp/main ./cmd/productapp/"
delay = 1000
exclude_dir = ["vendor", "cmd/productapp/temp"]
exclude_file = []
exclude_regex = []
exclude_unchanged = false
follow_symlink = false
include_dir = []
include_ext = ["go"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true
poll = true
poll_interval = 1000
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false

2
.gitignore vendored
View File

@ -30,3 +30,5 @@ logs/
mise.log
curl
cmd/**/temp/main

124
Makefile
View File

@ -1,54 +1,64 @@
# TODO: add commands for build and run in dev/produciton mode
# --- Variables ---
BINARY_NAME ?= niki
BUILD_DIR ?= bin
ROOT=$(realpath $(dir $(lastword $(MAKEFILE_LIST))))
# ====================================================================================
# General Go Commands
# ====================================================================================
.PHONY: start test build clean mod-tidy lint install-linter help format swagger watch
.PHONY: help confirm lint test format build run docker swagger watch migrate/status migrate/new migrate/up migrate/down
confirm:
@echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ]
lint:
which golangci-lint || (go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.0)
golangci-lint run --config=$(ROOT)/.golangci.yml $(ROOT)/...
start: build
$(BUILD_DIR)/$(BINARY_NAME)
test:
go test -v ./...
build:
@mkdir -p $(BUILD_DIR)
go build -o $(BUILD_DIR)/$(BINARY_NAME) main.go
clean:
rm -rf $(BUILD_DIR)/
mod-tidy:
go mod tidy
lint:
golangci-lint run
install-linter:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
format:
@which gofumpt || (go install mvdan.cc/gofumpt@latest)
@gofumpt -l -w $(ROOT)
@gofumpt -l -w .
@which gci || (go install github.com/daixiang0/gci@latest)
@gci write $(ROOT) --skip-generated --skip-vendor
@which golangci-lint || (go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.0)
@gci write . --skip-generated --skip-vendor
@golangci-lint run --fix
build:
go build -o niki main.go
run:
go run main.go --migrate
docker:
sudo docker compose up -d
swagger:
swag init
# Live Reload
watch:
@if command -v CompileDaemon > /dev/null; then \
CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./niki"; \
CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./$(BUILD_DIR)/$(BINARY_NAME)"; \
else \
read -p "Go's 'CompileDaemon' is not installed on your machine. Do you want to install it? [Y/n] " choice; \
read -p "Go's 'CompileDaemon' is not installed. Do you want to install it? [Y/n] " choice; \
if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \
go install github.com/githubnemo/CompileDaemon@latest; \
CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./niki"; \
CompileDaemon -exclude-dir=.git -exclude=".#*" -command="./$(BUILD_DIR)/$(BINARY_NAME)"; \
else \
echo "You chose not to install CompileDaemon. Exiting..."; \
exit 1; \
fi; \
fi
# ====================================================================================
# Database Migration Commands (legacy niki-core)
# ====================================================================================
.PHONY: migrate/status migrate/new migrate/up migrate/down
migrate/status:
@sql-migrate status -env="production" -config=repository/mysql/dbconfig.yml
@ -64,3 +74,67 @@ migrate/up: confirm
migrate/down: confirm
@echo 'Tearing down last migration...'
@sql-migrate down -env="production" -config=repository/mysql/dbconfig.yml -limit=1
confirm:
@echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ]
# ====================================================================================
# Productapp Service Commands
# ====================================================================================
.PHONY: productapp-up productapp-up-logs productapp-rebuild productapp-watch
PRODUCTAPP_ENV = deploy/productapp/development/.env
PRODUCTAPP_COMPOSE = deploy/productapp/development/docker-compose.yml
productapp-up:
@echo "Building and starting productapp full stack..."
docker compose --env-file $(PRODUCTAPP_ENV) -f $(PRODUCTAPP_COMPOSE) up --build -d
@echo "productapp stack is up."
productapp-down:
@echo "Building and starting productapp full stack..."
docker compose --env-file $(PRODUCTAPP_ENV) -f $(PRODUCTAPP_COMPOSE) down
@echo "productapp stack is up."
productapp-up-logs:
@echo "Building and starting productapp full stack..."
docker compose --env-file $(PRODUCTAPP_ENV) -f $(PRODUCTAPP_COMPOSE) up --build
# ====================================================================================
# Docker Commands (legacy)
# ====================================================================================
.PHONY: docker
docker:
docker compose up -d
# ====================================================================================
# Help Target
# ====================================================================================
help:
@echo "Available targets:"
@echo ""
@echo "General Go Commands:"
@echo " start - Build and run niki-core locally"
@echo " test - Run tests"
@echo " build - Compile binary"
@echo " clean - Remove build artifacts"
@echo " mod-tidy - Clean up dependencies"
@echo " format - Format code (gofumpt + gci + lint fix)"
@echo " lint - Run linters"
@echo " install-linter - Install golangci-lint"
@echo " swagger - Generate swagger docs"
@echo " watch - Live reload with CompileDaemon"
@echo ""
@echo "Database Migration (niki-core):"
@echo " migrate/status - Show migration status"
@echo " migrate/new - Create new migration (name=<name>)"
@echo " migrate/up - Run migrations up"
@echo " migrate/down - Rollback last migration"
@echo ""
@echo "Productapp Service:"
@echo " productapp-up - Build and start full stack in background"
@echo " productapp-up-logs - Build and start full stack with logs"
@echo ""
@echo "Docker (legacy):"
@echo " docker - Start docker-compose services"

View File

@ -0,0 +1,21 @@
package command
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "payment",
Short: "Payment service",
Long: "Payment service CLI",
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View File

@ -0,0 +1,122 @@
package command
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
paymentapp "git.gocasts.ir/ebhomengo/niki/paymentapp"
"git.gocasts.ir/ebhomengo/niki/pkg/database"
)
var configPath string
var serveCmd = &cobra.Command{
Use: "serve",
Short: "start payment service",
RunE: startServer,
}
func init() {
serveCmd.Flags().StringVar(
&configPath,
"config",
"deploy/payment/development/config.yaml",
"config file path",
)
rootCmd.AddCommand(serveCmd)
}
func startServer(cmd *cobra.Command, args []string) error {
// -------------------------
// load config
// -------------------------
//TODO --chage to Loader
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("🚩 read config error: %w", err)
}
var cfg paymentapp.Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return fmt.Errorf("🚩 parse config error: %w", err)
}
// -------------------------
// connect database
// -------------------------
dbConn, err := database.Connect(database.Config{
Port: cfg.Postgres.Port,
Host: cfg.Postgres.Host,
Username: cfg.Postgres.User,
DBName: cfg.Postgres.DbName,
Password: cfg.Postgres.Password,
Driver: cfg.Postgres.Driver,
SSLMode: cfg.Postgres.SSLMode,
MaxIdleConns: cfg.Postgres.MaxIdleConns,
MaxOpenConns: cfg.Postgres.MaxOpenConns,
ConnMaxLifetime: cfg.Postgres.ConnMaxLifetime,
})
if err != nil {
return err
}
defer database.Close(dbConn.DB)
// -------------------------
// context
// -------------------------
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// -------------------------
// setup app
// -------------------------
app, err := paymentapp.Setup(ctx, cfg, dbConn)
if err != nil {
return err
}
// -------------------------
// start server
// -------------------------
go func() {
if err := app.Start(); err != nil {
fmt.Println("🚩 server error:", err)
cancel()
}
}()
fmt.Println("payment service started 🏃‍➡️")
// -------------------------
// shutdown
// -------------------------
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
fmt.Println("shutting down 🥱...")
shutdownCtx, shutdownCancel := context.WithTimeout(
context.Background(),
10*time.Second,
)
defer shutdownCancel()
return app.Stop(shutdownCtx)
}

7
cmd/payment/main.go Normal file
View File

@ -0,0 +1,7 @@
package main
import "git.gocasts.ir/ebhomengo/niki/cmd/payment/command"
func main() {
command.Execute()
}

View File

@ -1,13 +1,16 @@
package command
import (
"fmt"
"context"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"git.gocasts.ir/ebhomengo/niki/productapp"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
"github.com/spf13/cobra"
)
@ -25,27 +28,53 @@ var serveCmd = &cobra.Command{
func serve() {
log.Println("Product Service Starting...")
// TODO: Initialize database connection
// TODO: Initialize service dependencies
// TODO: Setup HTTP server with routes
cfg := productapp.Config{
HTTPServer: productapp.HTTPServerConfig{
Port: getEnvInt("HTTP_PORT", 8080),
},
Database: mysql.Config{
Username: getEnv("DB_USERNAME", "root"),
Password: getEnv("DB_PASSWORD", ""),
Port: getEnvInt("DB_PORT", 3306),
Host: getEnv("DB_HOST", "localhost"),
DBName: getEnv("DB_NAME", "niki_db"),
},
}
if p, err := strconv.Atoi(port); err == nil && p > 0 {
cfg.HTTPServer.Port = p
}
app := productapp.Setup(cfg)
// Setup graceful shutdown
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("Shutting down Product Service gracefully...")
os.Exit(0)
app.Start()
}()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Product Service OK!")
})
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Printf("Product Service listening on port %s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatalf("Failed to start server: %v", err)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.Stop(ctx); err != nil {
log.Fatalf("Server shutdown error: %v", err)
}
log.Println("Product Service stopped.")
}
func getEnvInt(key string, defaultValue int) int {
val := os.Getenv(key)
if val == "" {
return defaultValue
}
n, err := strconv.Atoi(val)
if err != nil {
return defaultValue
}
return n
}
func init() {

View File

@ -0,0 +1,19 @@
http:
addr: ":9090" #--
host: "localhost"
port: 9090
read_timeout: 5
write_timeout: 10
idle_timeout: 60
postgres:
host: "localhost"
port: 5432
driver: "postgres"
user: h1user
password: h1pass
dbName: h1db
sslMode: disable
maxIdleConns: 15
maxOpenConns: 100
connMaxLifetime: 5

View File

@ -0,0 +1,27 @@
services:
app:
container_name: h1-app
build: .
ports:
- "9090:9090"
depends_on:
- postgres
environment:
CONFIG_PATH: "/app/config.yaml"
volumes:
- ./config.yaml:/app/config.yaml
postgres:
image: postgres:15
container_name: h1-postgres
environment:
POSTGRES_USER: h1user
POSTGRES_PASSWORD: h1pass
POSTGRES_DB: h1db
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:

View File

@ -0,0 +1,19 @@
ARG GO_IMAGE_NAME
ARG GO_IMAGE_VERSION
FROM ${GO_IMAGE_NAME}:${GO_IMAGE_VERSION}
ENV GOPROXY=https://package-mirror.liara.ir/repository/go/
ENV GOSUMDB=off
WORKDIR /home/app
COPY go.mod go.sum ./
RUN go mod download
RUN go install github.com/air-verse/air@latest
RUN printf '#!/bin/sh\n./cmd/productapp/temp/main migrate --up\nexec ./cmd/productapp/temp/main serve\n' > /entrypoint.sh && chmod +x /entrypoint.sh
COPY . .
CMD ["air", "-c", "/home/app/.air/.air.productapp.toml"]

View File

@ -0,0 +1,20 @@
services:
productapp-mysql:
image: mirror2.chabokan.net/mysql:8.0
container_name: productapp-mysql
restart: always
ports:
- "3307:3306"
volumes:
- productapp-mysql-data:/var/lib/mysql
environment:
MYSQL_DATABASE: niki_db
MYSQL_ROOT_PASSWORD: secret
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
volumes:
productapp-mysql-data:

View File

@ -0,0 +1,43 @@
services:
productapp-app:
build:
context: ../../..
dockerfile: deploy/productapp/development/Dockerfile
args:
GO_IMAGE_NAME: ${GO_IMAGE_NAME}
GO_IMAGE_VERSION: ${GO_IMAGE_VERSION}
container_name: productapp-app
ports:
- "8080:8080"
volumes:
- ../../..:/home/app
environment:
DB_HOST: productapp-mysql
DB_USERNAME: root
DB_PASSWORD: secret
DB_NAME: niki_db
MIGRATION_PATH: /home/app/productapp/repository/migrations
depends_on:
productapp-mysql:
condition: service_healthy
restart: unless-stopped
productapp-mysql:
image: mirror2.chabokan.net/mysql:8.0
container_name: productapp-mysql
restart: always
ports:
- "3307:3306"
volumes:
- productapp-mysql-data:/var/lib/mysql
environment:
MYSQL_DATABASE: niki_db
MYSQL_ROOT_PASSWORD: secret
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
volumes:
productapp-mysql-data:

View File

View File

View File

@ -1,8 +1,9 @@
package domain
package entity
import "time"
type ID uint64
import (
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type CampaignStatus string
@ -15,7 +16,7 @@ const (
)
type Campaign struct {
ID ID `json:"id"`
ID types.ID `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
GoalAmount float64 `json:"goal_amount"`
@ -23,7 +24,7 @@ type Campaign struct {
Status CampaignStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
DeadlineAt *time.Time `json:"deadline_at,omitempty"`
AdminID ID `json:"creator_id"`
AdminID types.ID `json:"creator_id"`
}
// Behavior

View File

@ -0,0 +1,57 @@
package mysql
import (
"context"
"git.gocasts.ir/ebhomengo/niki/domain/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) CreateAndSave(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
}

View File

@ -1,29 +0,0 @@
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

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

View File

@ -1,48 +1,50 @@
package campaign
package service
import (
"context"
"errors" // For standard errors
"fmt"
"git.gocasts.ir/ebhomengo/niki/domain/campaign/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/types"
"time"
"your_project/domain"
"your_project/pkg/richerror"
"your_project/repository"
)
type ID = unit64
type EntityCampaign = domain.Campaign
// CreateCampaign handles creation of a new campaign.
func (s *CampaignService) CreateCampaign(ctx context.Context, req entity.Campaign) (types.ID, error) {
const op = "service.campaign.create_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,
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(),
}
id, err := s.repo.Create(ctx, campaign)
if err != nil {
return 0, richerror.New(op).WithErr(err)
}
return id, nil
}
func (s *CampaignService) CreateCampaign(ctx context.Context, req CreateCampaignRequest) (ID, error) {
const Op = "service.campaign.create_campaign"
func validateCreateCampaignRequest(req entity.Campaign) error {
if req.Title == "" {
return 0, richerror.New(Op).WithMessage("title is required")
return errRequired("title")
}
if req.GoalAmount <= 0 {
return 0, richerror.New(Op).WithMessage("goal_amount must be greater than 0")
return errInvalid("goal_amount must be greater than 0")
}
if req.AdminID == 0 {
return 0, richerror.New(Op).WithMessage("admin_id is required")
return errRequired("admin_id")
}
validStatuses := map[string]bool{
@ -50,32 +52,20 @@ func (s *CampaignService) CreateCampaign(ctx context.Context, req CreateCampaign
"active": true,
"completed": true,
"cancelled": true,
"paused": true
"paused": true,
}
if !validStatuses[req.Status] {
return 0, richerror.New(Op).WithMessage("invalid status provided")
if !validStatuses[string(req.Status)] {
return errInvalid("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
return nil
}
// --- Helpers ---
func errRequired(field string) error {
return fmt.Errorf("%s is required", field)
}
func errInvalid(msg string) error {
return fmt.Errorf(msg)
}

View File

@ -0,0 +1,27 @@
package service
import (
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type GetCampaignResponse struct {
ID types.ID `json:"user_id"`
}
type AddCampaignRequest struct {
ID uint64 `json:"id"`
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
}

View File

@ -0,0 +1,35 @@
package service
import (
"context"
_ "fmt"
"git.gocasts.ir/ebhomengo/niki/domain/campaign/entity"
"git.gocasts.ir/ebhomengo/niki/types"
)
type CampaignFilterParam struct {
AdminID types.ID
Status string
//nil true false
IsArchived *bool
}
type CampaignStorage interface {
Create(ctx context.Context, c entity.Campaign) (types.ID, error) // باید ID برگرداند
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)
Archive(ctx context.Context, id types.ID) error // instead Delete
TotalDonations(ctx context.Context, campaignID types.ID) (int64, error)
}
type CampaignService struct {
repo CampaignStorage
}
// NewCampaignService constructs a new CampaignService.
func NewCampaignService(storage CampaignStorage) *CampaignService {
return &CampaignService{
repo: storage,
}
}

View File

@ -0,0 +1,11 @@
package mysql
import "git.gocasts.ir/ebhomengo/niki/repository/mysql"
type DB struct {
conn *mysql.DB
}
func New(db *mysql.DB) *DB {
return &DB{conn: db}
}

View File

@ -0,0 +1 @@
package mysql

View File

@ -0,0 +1,8 @@
package service
type Service struct {
repo Repo
}
type Repo interface {
}

View File

@ -0,0 +1,16 @@
package entity
import (
"encoding/json"
"time"
)
type Gateway struct {
ID uint
Name string
Code string
IsActive bool
Config json.RawMessage
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -0,0 +1,45 @@
package entity
import "time"
type Currency string
const (
CurrencyIRR Currency = "IRR"
CurrencyUSD Currency = "USD"
)
type PaymentStatus string
const (
PaymentStatusPending PaymentStatus = "Pending"
PaymentStatusSuccess PaymentStatus = "Success"
PaymentStatusFailed PaymentStatus = "Failed"
PaymentStatusCancelled PaymentStatus = "Cancelled"
//...
)
type PayableType string
const (
PayableTypeDonate PayableType = "Donate"
PayableTypeOrder PayableType = "Order"
PayableTypeWalet PayableType = "WaletCharge"
)
type Payment struct {
ID uint
UserID uint
MethodID uint
GatewayID uint
PayableType PayableType
PayableID uint
TotalAmount int64
PaidAmount int64
Currency Currency
Status PaymentStatus
Description string
CreatedAt time.Time
UpdatedAt time.Time
PaidAt *time.Time
}

View File

@ -0,0 +1,11 @@
package entity
import "time"
type PaymentMethod struct {
ID uint
Name string
IsActive bool
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -0,0 +1,35 @@
package entity
import (
"encoding/json"
"time"
)
type TransactionType string
const (
TransactionTypeRequest TransactionType = "request"
TransactionTypeVerify TransactionType = "verify"
)
type TransactionStatus string
const (
TransactionStatusPending = "Pending"
TransactionStatusSuccess = "Success"
TransactionStatusFailed = "Failed"
)
type PaymentTransaction struct {
ID uint
PaymentID uint
Type TransactionType
RequestData json.RawMessage
ResponseData json.RawMessage
RefID string
Status TransactionStatus
GatewayToken string
ErrorMessage string
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -0,0 +1,24 @@
package gateway
import (
"fmt"
"git.gocasts.ir/ebhomengo/niki/domain/payment/service"
)
type gatewayFactoryImpl struct{}
func NewGatewayFactory() service.GatewayFactory {
return &gatewayFactoryImpl{}
}
func (f gatewayFactoryImpl) GetGatewayAdapter(code string) (service.GatewayPort, error) {
switch code {
case "melat":
return nil, nil
case "zarinpal":
return nil, nil
default:
return nil, fmt.Errorf("Gateway Code(%s) is wrong!", code)
}
}

View File

@ -0,0 +1 @@
package gateway

View File

@ -0,0 +1,33 @@
package repository
import (
"context"
"database/sql"
"git.gocasts.ir/ebhomengo/niki/domain/payment/entity"
)
type PaymentRepository struct {
DB *sql.DB
}
// CreatePayment implements [service.PaymentRepo].
func (*PaymentRepository) CreatePayment(ctx context.Context, p *entity.Payment) error {
panic("unimplemented")
}
// CreateTransaction implements [service.PaymentRepo].
func (p *PaymentRepository) CreateTransaction(ctx context.Context, t *entity.PaymentTransaction) error {
panic("unimplemented")
}
// UpdateTransaction implements [service.PaymentRepo].
func (p *PaymentRepository) UpdateTransaction(ctx context.Context, t *entity.PaymentTransaction) error {
panic("unimplemented")
}
func NewPaymentRepository(db *sql.DB) *PaymentRepository {
return &PaymentRepository{
DB: db,
}
}

View File

@ -0,0 +1,23 @@
package repository
import (
"context"
"database/sql"
"git.gocasts.ir/ebhomengo/niki/domain/payment/entity"
)
type PaymentMethodRepository struct {
DB *sql.DB
}
// GetGatewayByCode implements [service.PaymentMethodRepo].
func (p *PaymentMethodRepository) GetGatewayByCode(ctx context.Context, code string) (entity.Gateway, error) {
panic("unimplemented")
}
func NewPaymentMethodRepository(db *sql.DB) *PaymentMethodRepository {
return &PaymentMethodRepository{
DB: db,
}
}

View File

@ -0,0 +1 @@
package database

View File

@ -0,0 +1,17 @@
package service
import "git.gocasts.ir/ebhomengo/niki/domain/payment/entity"
type InitiatePaymentRequest struct {
UserID uint
PayableType entity.PayableType
PayableID uint
GatewayCode string
CallbackURL string
Amount int64
}
type InitiatePaymentResponse struct {
PaymentID uint
RedirectURL string
}

View File

@ -0,0 +1,105 @@
package service
import (
"context"
"errors"
"fmt"
"time"
"git.gocasts.ir/ebhomengo/niki/domain/payment/entity"
)
type PaymentService struct {
paymentRepo PaymentRepo
paymentMethodRepo PaymentMethodRepo
gwFactory GatewayFactory
}
type PaymentRepo interface {
CreatePayment(ctx context.Context, p *entity.Payment) error
CreateTransaction(ctx context.Context, t *entity.PaymentTransaction) error
UpdateTransaction(ctx context.Context, t *entity.PaymentTransaction) error
}
type PaymentMethodRepo interface {
GetGatewayByCode(ctx context.Context, code string) (entity.Gateway, error)
}
type GatewayFactory interface {
GetGatewayAdapter(code string) (GatewayPort, error)
}
type GatewayPort interface {
Request(amount int64, callbackURL string, description string) (token string, redirectURL string, rawReq []byte, rawRes []byte, err error)
}
func NewPaymentService(pr PaymentRepo, pmr PaymentMethodRepo, gwf GatewayFactory) *PaymentService {
return &PaymentService{
paymentRepo: pr,
paymentMethodRepo: pmr,
gwFactory: gwf,
}
}
func (s *PaymentService) InitiatePayment(ctx context.Context, req InitiatePaymentRequest) (*InitiatePaymentResponse, error) {
gateway, err := s.paymentMethodRepo.GetGatewayByCode(ctx, req.GatewayCode)
if err != nil || !gateway.IsActive {
return nil, errors.New("gateway is not available")
}
payment := &entity.Payment{
UserID: req.UserID,
PayableType: req.PayableType,
PayableID: req.PayableID,
GatewayID: gateway.ID,
TotalAmount: req.Amount,
Currency: entity.CurrencyIRR,
Status: entity.PaymentStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.paymentRepo.CreatePayment(ctx, payment); err != nil {
return nil, fmt.Errorf("failed to create payment: %w", err)
}
transaction := &entity.PaymentTransaction{
PaymentID: payment.ID,
Type: entity.TransactionTypeRequest,
Status: entity.TransactionStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.paymentRepo.CreateTransaction(ctx, transaction); err != nil {
return nil, fmt.Errorf("failed to create initial transaction: %w", err)
}
gwAdapter, err := s.gwFactory.GetGatewayAdapter(gateway.Code)
if err != nil {
return nil, fmt.Errorf("failed to load gateway adapter: %w", err)
}
desc := fmt.Sprintf(" پرداخت برای %s شماره %d", req.PayableType, req.PayableID)
token, redirectURL, rawReq, rawRes, gwErr := gwAdapter.Request(req.Amount, req.CallbackURL, desc)
transaction.RequestData = rawReq
transaction.ResponseData = rawRes
transaction.UpdatedAt = time.Now()
if gwErr != nil {
transaction.Status = entity.TransactionStatusFailed
transaction.ErrorMessage = gwErr.Error()
_ = s.paymentRepo.UpdateTransaction(ctx, transaction)
return nil, fmt.Errorf("gateway request failed: %w", gwErr)
}
transaction.Status = entity.TransactionStatusSuccess
transaction.GatewayToken = token
if err := s.paymentRepo.UpdateTransaction(ctx, transaction); err != nil {
return nil, fmt.Errorf("failed to update transaction with token: %w", err)
}
return &InitiatePaymentResponse{
PaymentID: payment.ID,
RedirectURL: redirectURL,
}, nil
}

View File

@ -0,0 +1,4 @@
package service
type PaymentMethodService struct {
}

View File

@ -1,4 +1,4 @@
package cart
package entity
import (
"git.gocasts.ir/ebhomengo/niki/types"

View File

@ -4,8 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/service/cart"
"git.gocasts.ir/ebhomengo/niki/types"
"github.com/redis/go-redis/v9"
"strconv"
@ -43,7 +43,7 @@ func (r Repo) itemKey(productID types.ID) string {
return fmt.Sprintf("item:%d", productID)
}
func (r Repo) AddItem(ctx context.Context, userID types.ID, item cart.Item) error {
func (r Repo) AddItem(ctx context.Context, userID types.ID, item entity.Item) error {
const op = "shoppingbasketapp.repository.AddItem"
cartKey := r.cartKey(userID)
@ -65,7 +65,7 @@ func (r Repo) AddItem(ctx context.Context, userID types.ID, item cart.Item) erro
} else {
existsItem, _ := r.client.HGet(ctx, cartKey, itemKey).Result()
if existsItem != "" {
var i cart.Item
var i entity.Item
if err := json.Unmarshal([]byte(existsItem), &i); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
@ -91,31 +91,31 @@ func parsInt(s string) int64 {
return i
}
func (r Repo) GetCart(ctx context.Context, userID types.ID) (cart.Cart, error) {
func (r Repo) GetCart(ctx context.Context, userID types.ID) (entity.Cart, error) {
const op = "shoppingbasketapp.repository.GetCart"
cartKey := r.cartKey(userID)
exists, err := r.client.Exists(ctx, cartKey).Result()
if err != nil {
return cart.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if exists == 0 {
return cart.Cart{}, richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found shopping basket")
return entity.Cart{}, richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found shopping basket")
}
allCart, err := r.client.HGetAll(ctx, cartKey).Result()
if err != nil {
return cart.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
c := cart.Cart{Items: []cart.Item{}}
c := entity.Cart{Items: []entity.Item{}}
for field, value := range allCart {
if strings.HasPrefix(field, "item:") {
var i cart.Item
var i entity.Item
if err := json.Unmarshal([]byte(value), &i); err != nil {
return cart.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
c.Items = append(c.Items, i)
@ -185,7 +185,7 @@ func (r Repo) UpdateQuantity(ctx context.Context, userID, productID types.ID, qu
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
var item cart.Item
var item entity.Item
if err := json.Unmarshal([]byte(data), &item); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
@ -228,7 +228,7 @@ func (r Repo) updateTotalPrice(ctx context.Context, cartKey string) error {
for field, value := range allFields {
if strings.HasPrefix(field, "item:") {
var item cart.Item
var item entity.Item
if err := json.Unmarshal([]byte(value), &item); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)

View File

@ -1,6 +1,9 @@
package cart
package service
import "git.gocasts.ir/ebhomengo/niki/types"
import (
"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
"git.gocasts.ir/ebhomengo/niki/types"
)
type AddToCartRequest struct {
UserID types.ID `json:"user_id"`
@ -12,7 +15,7 @@ type AddToCartRequest struct {
type GetCartResponse struct {
UserID types.ID `json:"user_id"`
Items []Item `json:"items"`
Items []entity.Item `json:"items"`
TotalPrice types.Price `json:"total_price"`
CreatedAt int64 `json:"created_at"`
ExpireAt int64 `json:"expire_at"`

View File

@ -1,7 +1,8 @@
package cart
package service
import (
"context"
"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
"git.gocasts.ir/ebhomengo/niki/pkg/logger"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/types"
@ -9,8 +10,8 @@ import (
)
type Repository interface {
AddItem(ctx context.Context, userID types.ID, item Item) error
GetCart(ctx context.Context, userID types.ID) (Cart, error)
AddItem(ctx context.Context, userID types.ID, item entity.Item) error
GetCart(ctx context.Context, userID types.ID) (entity.Cart, error)
DeleteItem(ctx context.Context, userID, productID types.ID) error
UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error
DeleteCart(ctx context.Context, userID types.ID) error
@ -33,7 +34,7 @@ func (s Service) AddToBasket(ctx context.Context, req AddToCartRequest) error {
return err
}
return s.repo.AddItem(ctx, req.UserID, Item{
return s.repo.AddItem(ctx, req.UserID, entity.Item{
ProductID: req.ProductID,
Quantity: req.Quantity,
Price: req.Price,

View File

@ -1,4 +1,4 @@
package cart
package service
import (
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"

View File

@ -0,0 +1,28 @@
package entity
import "time"
type Transaction struct {
ID uint64
UserID uint64
Amount float64
Currency Currency
ActionType TransactionType
Timestamp time.Time
}
type TransactionType string
const (
TransactionTypeDeposit TransactionType = "deposit"
TransactionTypeWithdraw TransactionType = "withdraw"
TransactionTypeRefund TransactionType = "refund"
TransactionTypeDonate TransactionType = "donate"
)
type Currency string
const (
IRR Currency = "IRR"
USD Currency = "USD"
)

View File

@ -0,0 +1,23 @@
package entity
import "time"
type Wallet struct {
ID uint64
UserID uint64 // user unique ID
Balance float64
Currency Currency
UpdatedAt time.Time
Status WalletStatus // "active", "frozen", "closed"
}
type WalletStatus string
const (
Frozen WalletStatus = "frozen" // when need to check , approve ,validate , solve sth (but deposit is possible)
Active WalletStatus = "active" // when everything is ok
// ??
// Closed WalletStatus = "closed" // when need to check , approve ,validate , solve sth (exp : security problem)
)

View File

@ -0,0 +1,15 @@
package param
import (
"git.gocasts.ir/ebhomengo/niki/domain/wallet/entity"
)
type CreateTransactionRequest struct {
UserID uint64 `json:"user_id"`
Amount float64 `json:"amount"`
Currency entity.Currency `json:"currency"`
ActionType entity.TransactionType `json:"action_type"`
}
type InsertTransactionResponse struct {
}

View File

@ -0,0 +1,24 @@
package param
import (
"time"
"git.gocasts.ir/ebhomengo/niki/domain/wallet/entity"
)
type TransactionRequest struct {
UserID uint64 `json:"user_id"`
}
type TransactionResponse struct {
Transaction []TransactionInfo `json:"transaction"`
}
type TransactionInfo struct {
ID uint64 `json:"id"`
UserID uint64 `json:"user_id"`
Amount float64 `json:"amount"`
Currency entity.Currency `json:"currency"`
ActionType entity.TransactionType `json:"action_type"`
Timestamp time.Time `json:"timestamp"`
}

View File

@ -0,0 +1,21 @@
package param
import (
"time"
"git.gocasts.ir/ebhomengo/niki/domain/wallet/entity"
)
type WalletRequest struct {
UserID uint64 `json:"user_id"`
}
type WalletResponse struct {
Wallet WalletInfo `json:"wallet"`
}
type WalletInfo struct {
Balance float64 `json:"balance"`
UpdatedAt time.Time `json:"updated_at"`
Status entity.WalletStatus `json:"status"`
}

View File

@ -0,0 +1,11 @@
package postgres
import "git.gocasts.ir/ebhomengo/niki/pkg/database/postgres"
type DB struct {
conn *postgres.DB
}
func New(conn *postgres.DB) *DB {
return &DB{conn: conn}
}

View File

@ -0,0 +1,5 @@
production:
dialect: postgres
datasource: "host=127.0.0.1 port=5432 user=wallet password=wallet2123 dbname=wallet_db sslmode=disable"
dir: domain/wallet/repository/postgres/migrations
table: wallet_migrationsns

View File

@ -0,0 +1,31 @@
package service
import (
"context"
"time"
"git.gocasts.ir/ebhomengo/niki/domain/wallet/entity"
"git.gocasts.ir/ebhomengo/niki/domain/wallet/param"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
)
func (s Service) CreateTransaction(ctx context.Context, request param.CreateTransactionRequest) (param.InsertTransactionResponse, error) {
const op = richerror.Op("wallet.service.CreateTransaction")
transaction := entity.Transaction{
ID: 0,
UserID: request.UserID,
Amount: request.Amount,
Currency: request.Currency,
ActionType: request.ActionType,
Timestamp: time.Now(),
}
err := s.repo.InsertTransaction(ctx, transaction)
if err != nil {
return param.InsertTransactionResponse{}, err
}
return param.InsertTransactionResponse{}, nil
}

View File

@ -0,0 +1,27 @@
package service
import (
"context"
"git.gocasts.ir/ebhomengo/niki/domain/wallet/entity"
)
type Repository interface {
GetTransactionListByUserID(ctx context.Context, UserID uint64) ([]entity.Transaction, error)
GetWalletByUserID(ctx context.Context, UserID uint64) (entity.Wallet, error)
InsertTransaction(ctx context.Context, transaction entity.Transaction) error
}
type Config struct {
}
type Service struct {
repo Repository
cfg Config
}
func New(repo Repository, cfg Config) Service {
return Service{repo: repo, cfg: cfg}
}

View File

@ -0,0 +1,39 @@
package service
import (
"context"
"git.gocasts.ir/ebhomengo/niki/domain/wallet/entity"
"git.gocasts.ir/ebhomengo/niki/domain/wallet/param"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
)
func (s Service) GetUserTransactionHistory(ctx context.Context, request param.TransactionRequest) (param.TransactionResponse, error) {
const op = richerror.Op("wallet.service.GetUserTransactionHistory")
transactionList, err := s.repo.GetTransactionListByUserID(ctx, request.UserID)
if err != nil {
return param.TransactionResponse{}, err
}
return param.TransactionResponse{Transaction: transactionEntityToTransactionInfo(transactionList)}, nil
}
func transactionEntityToTransactionInfo(TransactionList []entity.Transaction) []param.TransactionInfo {
transactionInfoList := make([]param.TransactionInfo, len(TransactionList))
for i, transaction := range TransactionList {
transactionInfoList[i] = param.TransactionInfo{
ID: transaction.ID,
UserID: transaction.UserID,
Amount: transaction.Amount,
Currency: transaction.Currency,
ActionType: transaction.ActionType,
Timestamp: transaction.Timestamp,
}
}
return transactionInfoList
}

View File

@ -0,0 +1,27 @@
package service
import (
"context"
"git.gocasts.ir/ebhomengo/niki/domain/wallet/param"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
)
func (s Service) GetUserWallet(ctx context.Context, request param.WalletRequest) (param.WalletResponse, error) {
const op = richerror.Op("wallet.service.GetUserWallet")
wallet, err := s.repo.GetWalletByUserID(ctx, request.UserID)
if err != nil {
return param.WalletResponse{}, err
}
return param.WalletResponse{
Wallet: param.WalletInfo{
Balance: wallet.Balance,
UpdatedAt: wallet.UpdatedAt,
Status: wallet.Status,
},
}, nil
}

View File

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

View File

@ -1,7 +0,0 @@
package donate_server
import "github.com/labstack/echo/v4"
func (h Handler) RegisterRoutes(e *echo.Echo) {
}

View File

@ -1,16 +0,0 @@
package donate_server
import (
httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server"
"github.com/labstack/echo/v4"
)
type Server struct {
Server httpserver.Server
Handler Handler
Router *echo.Echo
}
func (s Server) Start() {
s.Handler.RegisterRoutes(s.Router)
}

View File

@ -0,0 +1,43 @@
package http
import (
"git.gocasts.ir/ebhomengo/niki/domain/campaign/entity"
"git.gocasts.ir/ebhomengo/niki/domain/campaign/service"
httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg"
"github.com/labstack/echo/v4"
"net/http"
"time"
)
type Handler struct {
svc service.CampaignService
}
func NewHandler(svc service.CampaignService) Handler {
return Handler{svc: svc}
}
func (h Handler) createCampaign(c echo.Context) error {
var req entity.Campaign
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 {
msg, code := httpmsg.Error(err)
c.Logger().Errorf("Service error creating campaign: %v (Code: %d)", err, code)
return c.JSON(code, msg)
}
return c.JSON(http.StatusCreated, map[string]interface{}{
"message": "campaign created successfully",
"id": createdID,
})
}

View File

@ -0,0 +1,12 @@
package http
import (
"github.com/labstack/echo/v4"
"net/http"
)
func (s Server) healthCheck(c echo.Context) error {
return c.JSON(http.StatusOK, echo.Map{
"message": "everything is good!",
})
}

View File

@ -0,0 +1,39 @@
package http
import (
"context"
"git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
)
type Server struct {
handler Handler
HTTPServer *httpserver.Server
}
func NewServer(handler Handler, hS *httpserver.Server) Server {
return Server{handler: handler, HTTPServer: hS}
}
func (s Server) Serve() error {
s.registerRoutes()
if err := s.HTTPServer.Start(); err != nil {
return err
}
return nil
}
func (s Server) Stop(ctx context.Context) error {
return s.HTTPServer.Stop(ctx)
}
func (s Server) registerRoutes() {
router := s.HTTPServer.GetRouter()
router.GET("campaign/health-check", s.healthCheck)
r := router.Group("campaign")
r.POST("/", s.handler.createCampaign)
}

View File

@ -2,7 +2,7 @@ package mysql
import (
"context"
"git.gocasts.ir/ebhomengo/niki/campaign/entity"
"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"
@ -16,11 +16,9 @@ func New(db *mysql.DB) *DB {
return &DB{conn: db}
}
// CreateCampaign creates a new campaign
func (d *DB) CreateAndSave(ctx context.Context, campaign entity.Campaign) (types.ID, error) {
const Op = "repository.mysql.campaign.create"
// 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 {
@ -28,54 +26,17 @@ func (d *DB) CreateAndSave(ctx context.Context, campaign entity.Campaign) (types
}
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,
result, err := tx.ExecContext(ctx, query,
participant.ID,
participant.CampaignID,
participant.UserID,
participant.Amount,
participant.CreatedAt
participant.CreatedAt,
)
if err != nil {
return 0, richerror.New(Op).WithErr(err)
}
@ -84,11 +45,10 @@ func (d *DB) CreateCampaignParticipants(ctx context.Context, participant entity.
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
}

View File

@ -1,14 +1,13 @@
package service
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"`
}

View File

@ -1 +1,26 @@
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
}

View File

@ -1,38 +1 @@
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.repo
}
// type CampaignServiceInterface interface {
// CreateCampaign(ctx context.Context, req CampaignRepository) (types.ID, error)
// }
func NewCampaignService(
repo repository.CampaignRepository,
participantRepo repository.CampaignParticipantRepository,
) *CampaignService {
return &CampaignService{
repo: repo,
participantRepo: participantRepo,
}
}

11
gamificationapp/app.go Normal file
View File

@ -0,0 +1,11 @@
package gamificationapp
type Application struct {
Config Config
}
func setUp(cnf Config) *Application {
return &Application{
cnf,
}
}

View File

@ -0,0 +1,4 @@
package gamificationapp
type Config struct {
}

View File

@ -0,0 +1,15 @@
package gamification
import (
gamification "git.gocasts.ir/ebhomengo/niki/domain/gamification/service"
)
type Handler struct {
GamificationSvc gamification.Service
}
func New(gamificationSvc gamification.Service) *Handler {
return &Handler{
GamificationSvc: gamificationSvc,
}
}

View File

@ -0,0 +1,7 @@
package gamification
import "github.com/labstack/echo/v4"
func (h Handler) SetRoutes(e *echo.Echo) {
}

View File

@ -0,0 +1,13 @@
package http
import "git.gocasts.ir/ebhomengo/niki/gamificationapp/delivery/http/gamification"
type Server struct {
gamificationHandler *gamification.Handler
}
func New(handler gamification.Handler) *Server {
return &Server{
gamificationHandler: &handler,
}
}

12
go.mod
View File

@ -9,10 +9,14 @@ require (
github.com/go-sql-driver/mysql v1.9.3
github.com/gocasters/rankr v0.0.0-20260222055437-aadc1fdc6a1d
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/jalaali/go-jalaali v0.0.0-20250521085720-bf793ab67800
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d
github.com/knadh/koanf v1.5.0
github.com/labstack/echo-jwt/v4 v4.4.0
github.com/labstack/echo/v4 v4.15.1
github.com/labstack/gommon v0.4.2
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.19
github.com/ory/dockertest/v3 v3.12.0
github.com/redis/go-redis/v9 v9.18.0
github.com/rubenv/sql-migrate v1.8.1
@ -21,9 +25,10 @@ require (
github.com/swaggo/echo-swagger v1.5.2
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.48.0
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gopkg.in/natefinch/lumberjack.v2 v2.2.1
google.golang.org/grpc v1.79.2
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
)
require (
@ -90,9 +95,6 @@ require (
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect

30
go.sum
View File

@ -98,6 +98,10 @@ github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
@ -147,6 +151,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -158,13 +164,14 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
@ -205,6 +212,8 @@ github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2
github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jalaali/go-jalaali v0.0.0-20250521085720-bf793ab67800 h1:lvIuaX7hO0eO3Rlev+cVnlsoExR3i/JXxu88zt4JHPg=
github.com/jalaali/go-jalaali v0.0.0-20250521085720-bf793ab67800/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
@ -225,8 +234,6 @@ github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBF
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@ -408,6 +415,18 @@ github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaD
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
@ -529,6 +548,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -536,7 +557,6 @@ google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRn
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=

View File

@ -1 +1,55 @@
package paymentapp
import (
"context"
"git.gocasts.ir/ebhomengo/niki/domain/payment/gateway"
"git.gocasts.ir/ebhomengo/niki/domain/payment/repository"
"git.gocasts.ir/ebhomengo/niki/domain/payment/service"
"git.gocasts.ir/ebhomengo/niki/paymentapp/delivery/grpc"
"git.gocasts.ir/ebhomengo/niki/pkg/database"
sgrpc "git.gocasts.ir/ebhomengo/niki/pkg/grpc"
)
type App struct {
paymentrepo service.PaymentRepo
paymentMethodRepo service.PaymentMethodRepo
gwFactory service.GatewayFactory
paymentService *service.PaymentService
paymenthandler *grpc.Handler
rpcServer *sgrpc.RPCServer
server grpc.PaymentGrpcServer
}
func Setup(ctx context.Context, cfg Config, conn *database.Database) (*App, error) {
paymentrepo := repository.NewPaymentRepository(conn.DB)
paymentMethodRepo := repository.NewPaymentMethodRepository(conn.DB)
gwFactory := gateway.NewGatewayFactory()
paymentService := service.NewPaymentService(paymentrepo, paymentMethodRepo, gwFactory)
paymenthandler := grpc.NewHandler(paymentService)
rpcServer := sgrpc.New(sgrpc.Config(cfg.Grpc))
server := grpc.NewPaymentGrpcServer(rpcServer, paymenthandler)
return &App{
paymentrepo: paymentrepo,
paymentMethodRepo: paymentMethodRepo,
gwFactory: gwFactory,
paymentService: paymentService,
paymenthandler: paymenthandler,
rpcServer: rpcServer,
server: server,
}, nil
}
func (a *App) Start() error {
a.server.Serve()
return nil
}
func (a *App) Stop(ctx context.Context) error {
return nil
}

View File

@ -1 +1,32 @@
package paymentapp
import "time"
type GrpcConfig struct {
Port int `yaml:"port"`
NetworkType string `yaml:"type"`
ShutDownCtxTimeout time.Duration `yaml:"shutdown_context_timeout"`
}
type PostgresConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Driver string `yaml:"driver"`
User string `yaml:"user"`
Password string `yaml:"password"`
DbName string `yaml:"dbName"`
SSLMode string `yaml:"sslMode"`
MaxIdleConns int `yaml:"maxIdleConns"`
MaxOpenConns int `yaml:"maxOpenConns"`
ConnMaxLifetime int `yaml:"connMaxLifetime"`
PathOfMigrations string `yaml:"pathOfMigrations"`
}
type LoggerConfig struct {
Level string `yaml:"level" json:"level"`
}
type Config struct {
Grpc GrpcConfig `yaml:"grpc" json:"grpc"`
Postgres PostgresConfig `yaml:"postgres" json:"postgres"`
Logger LoggerConfig `yaml:"logger" json:"logger"`
}

View File

@ -0,0 +1,42 @@
package grpc
import (
"context"
"git.gocasts.ir/ebhomengo/niki/domain/payment/entity"
"git.gocasts.ir/ebhomengo/niki/domain/payment/service"
paymentpb "git.gocasts.ir/ebhomengo/niki/paymentapp/protobuf"
)
type Handler struct {
paymentpb.UnimplementedPaymentServiceServer
scvPayment *service.PaymentService
}
func NewHandler(s *service.PaymentService) *Handler {
return &Handler{
scvPayment: s,
}
}
func (h *Handler) InitiatePayment(ctx context.Context, req *paymentpb.InitiatePaymentRequest) (*paymentpb.InitiatePaymentResponse, error) {
paymentResponse, err := h.scvPayment.InitiatePayment(ctx, service.InitiatePaymentRequest{
UserID: uint(req.GetUserId()),
PayableType: entity.PayableType(req.GetPayableType()),
PayableID: uint(req.GetPayableId()),
GatewayCode: req.GetGatewayCode(),
CallbackURL: req.GetCallbackUrl(),
Amount: req.GetAmount(),
})
if err != nil {
return nil, err
}
response := &paymentpb.InitiatePaymentResponse{
PaymentId: uint64(paymentResponse.PaymentID),
RedirectUrl: paymentResponse.RedirectURL,
}
return response, nil
}

View File

@ -0,0 +1,37 @@
package grpc
import (
"fmt"
"net"
paymentpb "git.gocasts.ir/ebhomengo/niki/paymentapp/protobuf"
"git.gocasts.ir/ebhomengo/niki/pkg/grpc"
)
type PaymentGrpcServer struct {
server *grpc.RPCServer
handler *Handler
}
func NewPaymentGrpcServer(server *grpc.RPCServer, handler *Handler) PaymentGrpcServer {
return PaymentGrpcServer{
server: server,
handler: handler,
}
}
func (s PaymentGrpcServer) Serve() error {
listener, err := net.Listen(s.server.Config.NetworkType, fmt.Sprintf(":%d", s.server.Config.Port))
if err != nil {
return err
}
paymentpb.RegisterPaymentServiceServer(s.server.Server, s.handler)
if err := s.server.Server.Serve(listener); err != nil {
return err
}
return nil
}
func (s PaymentGrpcServer) Stop() {
s.server.Stop()
}

View File

@ -1,15 +0,0 @@
package http
type Handler struct {
}
func NewHandler(userService UserService) *Handler {
return &Handler{
}
}
func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}

View File

@ -1,5 +0,0 @@
package http
func (s *Server) registerRoutes() {
s.httpServer.GET("/health", s.handler.HealthCheck)
}

View File

@ -1,22 +0,0 @@
package http
type Server struct {
httpServer *httpserver.Server
handler *Handler
}
func NewServer(httpServer *httpserver.Server, handler *Handler) *Server {
return &Server{
httpServer: httpServer,
handler: handler,
}
}
func (s *Server) Serve() error {
s.registerRoutes()
return s.httpServer.Start()
}
func (s *Server) Stop() error {
return s.httpServer.Stop()
}

View File

@ -0,0 +1,230 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v4.25.1
// source: paymentapp/protobuf/payment.proto
package paymentpb
import (
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type InitiatePaymentRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserId uint64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
PayableType string `protobuf:"bytes,2,opt,name=payable_type,json=payableType,proto3" json:"payable_type,omitempty"`
PayableId uint64 `protobuf:"varint,3,opt,name=payable_id,json=payableId,proto3" json:"payable_id,omitempty"`
GatewayCode string `protobuf:"bytes,4,opt,name=gateway_code,json=gatewayCode,proto3" json:"gateway_code,omitempty"`
CallbackUrl string `protobuf:"bytes,5,opt,name=callback_url,json=callbackUrl,proto3" json:"callback_url,omitempty"`
Amount int64 `protobuf:"varint,6,opt,name=amount,proto3" json:"amount,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *InitiatePaymentRequest) Reset() {
*x = InitiatePaymentRequest{}
mi := &file_paymentapp_protobuf_payment_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *InitiatePaymentRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InitiatePaymentRequest) ProtoMessage() {}
func (x *InitiatePaymentRequest) ProtoReflect() protoreflect.Message {
mi := &file_paymentapp_protobuf_payment_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InitiatePaymentRequest.ProtoReflect.Descriptor instead.
func (*InitiatePaymentRequest) Descriptor() ([]byte, []int) {
return file_paymentapp_protobuf_payment_proto_rawDescGZIP(), []int{0}
}
func (x *InitiatePaymentRequest) GetUserId() uint64 {
if x != nil {
return x.UserId
}
return 0
}
func (x *InitiatePaymentRequest) GetPayableType() string {
if x != nil {
return x.PayableType
}
return ""
}
func (x *InitiatePaymentRequest) GetPayableId() uint64 {
if x != nil {
return x.PayableId
}
return 0
}
func (x *InitiatePaymentRequest) GetGatewayCode() string {
if x != nil {
return x.GatewayCode
}
return ""
}
func (x *InitiatePaymentRequest) GetCallbackUrl() string {
if x != nil {
return x.CallbackUrl
}
return ""
}
func (x *InitiatePaymentRequest) GetAmount() int64 {
if x != nil {
return x.Amount
}
return 0
}
type InitiatePaymentResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
PaymentId uint64 `protobuf:"varint,1,opt,name=payment_id,json=paymentId,proto3" json:"payment_id,omitempty"`
RedirectUrl string `protobuf:"bytes,2,opt,name=redirect_url,json=redirectUrl,proto3" json:"redirect_url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *InitiatePaymentResponse) Reset() {
*x = InitiatePaymentResponse{}
mi := &file_paymentapp_protobuf_payment_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *InitiatePaymentResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InitiatePaymentResponse) ProtoMessage() {}
func (x *InitiatePaymentResponse) ProtoReflect() protoreflect.Message {
mi := &file_paymentapp_protobuf_payment_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InitiatePaymentResponse.ProtoReflect.Descriptor instead.
func (*InitiatePaymentResponse) Descriptor() ([]byte, []int) {
return file_paymentapp_protobuf_payment_proto_rawDescGZIP(), []int{1}
}
func (x *InitiatePaymentResponse) GetPaymentId() uint64 {
if x != nil {
return x.PaymentId
}
return 0
}
func (x *InitiatePaymentResponse) GetRedirectUrl() string {
if x != nil {
return x.RedirectUrl
}
return ""
}
var File_paymentapp_protobuf_payment_proto protoreflect.FileDescriptor
const file_paymentapp_protobuf_payment_proto_rawDesc = "" +
"\n" +
"!paymentapp/protobuf/payment.proto\x12\apayment\"\xd1\x01\n" +
"\x16InitiatePaymentRequest\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x04R\x06userId\x12!\n" +
"\fpayable_type\x18\x02 \x01(\tR\vpayableType\x12\x1d\n" +
"\n" +
"payable_id\x18\x03 \x01(\x04R\tpayableId\x12!\n" +
"\fgateway_code\x18\x04 \x01(\tR\vgatewayCode\x12!\n" +
"\fcallback_url\x18\x05 \x01(\tR\vcallbackUrl\x12\x16\n" +
"\x06amount\x18\x06 \x01(\x03R\x06amount\"[\n" +
"\x17InitiatePaymentResponse\x12\x1d\n" +
"\n" +
"payment_id\x18\x01 \x01(\x04R\tpaymentId\x12!\n" +
"\fredirect_url\x18\x02 \x01(\tR\vredirectUrl2f\n" +
"\x0ePaymentService\x12T\n" +
"\x0fInitiatePayment\x12\x1f.payment.InitiatePaymentRequest\x1a .payment.InitiatePaymentResponseB=Z;git.gocasts.ir/ebhomengo/niki/paymentapp/protobuf;paymentpbb\x06proto3"
var (
file_paymentapp_protobuf_payment_proto_rawDescOnce sync.Once
file_paymentapp_protobuf_payment_proto_rawDescData []byte
)
func file_paymentapp_protobuf_payment_proto_rawDescGZIP() []byte {
file_paymentapp_protobuf_payment_proto_rawDescOnce.Do(func() {
file_paymentapp_protobuf_payment_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_paymentapp_protobuf_payment_proto_rawDesc), len(file_paymentapp_protobuf_payment_proto_rawDesc)))
})
return file_paymentapp_protobuf_payment_proto_rawDescData
}
var file_paymentapp_protobuf_payment_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_paymentapp_protobuf_payment_proto_goTypes = []any{
(*InitiatePaymentRequest)(nil), // 0: payment.InitiatePaymentRequest
(*InitiatePaymentResponse)(nil), // 1: payment.InitiatePaymentResponse
}
var file_paymentapp_protobuf_payment_proto_depIdxs = []int32{
0, // 0: payment.PaymentService.InitiatePayment:input_type -> payment.InitiatePaymentRequest
1, // 1: payment.PaymentService.InitiatePayment:output_type -> payment.InitiatePaymentResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_paymentapp_protobuf_payment_proto_init() }
func file_paymentapp_protobuf_payment_proto_init() {
if File_paymentapp_protobuf_payment_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_paymentapp_protobuf_payment_proto_rawDesc), len(file_paymentapp_protobuf_payment_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_paymentapp_protobuf_payment_proto_goTypes,
DependencyIndexes: file_paymentapp_protobuf_payment_proto_depIdxs,
MessageInfos: file_paymentapp_protobuf_payment_proto_msgTypes,
}.Build()
File_paymentapp_protobuf_payment_proto = out.File
file_paymentapp_protobuf_payment_proto_goTypes = nil
file_paymentapp_protobuf_payment_proto_depIdxs = nil
}

View File

@ -0,0 +1,25 @@
syntax = "proto3";
package payment;
option go_package = "git.gocasts.ir/ebhomengo/niki/paymentapp/protobuf;paymentpb";
service PaymentService {
rpc InitiatePayment (InitiatePaymentRequest) returns (InitiatePaymentResponse);
}
message InitiatePaymentRequest {
uint64 user_id = 1;
string payable_type = 2;
uint64 payable_id = 3;
string gateway_code = 4;
string callback_url = 5;
int64 amount = 6;
}
message InitiatePaymentResponse {
uint64 payment_id = 1;
string redirect_url = 2;
}

View File

@ -0,0 +1,121 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v4.25.1
// source: paymentapp/protobuf/payment.proto
package paymentpb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
PaymentService_InitiatePayment_FullMethodName = "/payment.PaymentService/InitiatePayment"
)
// PaymentServiceClient is the client API for PaymentService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type PaymentServiceClient interface {
InitiatePayment(ctx context.Context, in *InitiatePaymentRequest, opts ...grpc.CallOption) (*InitiatePaymentResponse, error)
}
type paymentServiceClient struct {
cc grpc.ClientConnInterface
}
func NewPaymentServiceClient(cc grpc.ClientConnInterface) PaymentServiceClient {
return &paymentServiceClient{cc}
}
func (c *paymentServiceClient) InitiatePayment(ctx context.Context, in *InitiatePaymentRequest, opts ...grpc.CallOption) (*InitiatePaymentResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InitiatePaymentResponse)
err := c.cc.Invoke(ctx, PaymentService_InitiatePayment_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// PaymentServiceServer is the server API for PaymentService service.
// All implementations must embed UnimplementedPaymentServiceServer
// for forward compatibility.
type PaymentServiceServer interface {
InitiatePayment(context.Context, *InitiatePaymentRequest) (*InitiatePaymentResponse, error)
mustEmbedUnimplementedPaymentServiceServer()
}
// UnimplementedPaymentServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedPaymentServiceServer struct{}
func (UnimplementedPaymentServiceServer) InitiatePayment(context.Context, *InitiatePaymentRequest) (*InitiatePaymentResponse, error) {
return nil, status.Error(codes.Unimplemented, "method InitiatePayment not implemented")
}
func (UnimplementedPaymentServiceServer) mustEmbedUnimplementedPaymentServiceServer() {}
func (UnimplementedPaymentServiceServer) testEmbeddedByValue() {}
// UnsafePaymentServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to PaymentServiceServer will
// result in compilation errors.
type UnsafePaymentServiceServer interface {
mustEmbedUnimplementedPaymentServiceServer()
}
func RegisterPaymentServiceServer(s grpc.ServiceRegistrar, srv PaymentServiceServer) {
// If the following call panics, it indicates UnimplementedPaymentServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&PaymentService_ServiceDesc, srv)
}
func _PaymentService_InitiatePayment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(InitiatePaymentRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(PaymentServiceServer).InitiatePayment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: PaymentService_InitiatePayment_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PaymentServiceServer).InitiatePayment(ctx, req.(*InitiatePaymentRequest))
}
return interceptor(ctx, in, info, handler)
}
// PaymentService_ServiceDesc is the grpc.ServiceDesc for PaymentService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var PaymentService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "payment.PaymentService",
HandlerType: (*PaymentServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "InitiatePayment",
Handler: _PaymentService_InitiatePayment_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "paymentapp/protobuf/payment.proto",
}

View File

@ -1 +0,0 @@
package service

View File

@ -1 +0,0 @@
package service

View File

@ -1 +0,0 @@
package service

15
pkg/database/config.go Normal file
View File

@ -0,0 +1,15 @@
package database
type Config struct {
Driver string `koanf:"driver"`
Host string `koanf:"host"`
Port int `koanf:"port"`
Username string `koanf:"user"`
Password string `koanf:"password"`
DBName string `koanf:"db_name"`
SSLMode string `koanf:"ssl_mode"`
MaxIdleConns int `koanf:"max_idle_conns"`
MaxOpenConns int `koanf:"max_open_conns"`
ConnMaxLifetime int `koanf:"conn_max_lifetime"`
PathOfMigrations string `koanf:"path_of_migrations"`
}

45
pkg/database/db.go Normal file
View File

@ -0,0 +1,45 @@
package database
import (
"database/sql"
"fmt"
"time"
)
type DSNBuilder interface {
BuildDSN(config Config) string
}
type Database struct {
DB *sql.DB
Dialect string
}
func Connect(config Config) (*Database, error) {
dsnB, DSNErr := GetDSNBuilder(config.Driver)
if DSNErr != nil {
return nil, DSNErr
}
dsn := dsnB.BuildDSN(config)
conn, err := sql.Open(config.Driver, dsn)
if err != nil {
return nil, fmt.Errorf("error opening database: %w", err)
}
if err := conn.Ping(); err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
conn.SetMaxIdleConns(config.MaxIdleConns)
conn.SetMaxOpenConns(config.MaxOpenConns)
conn.SetConnMaxLifetime(time.Duration(config.ConnMaxLifetime) * time.Second)
fmt.Println("Database connection established successfully")
return &Database{DB: conn, Dialect: config.Driver}, err
}
func Close(conn *sql.DB) error {
return conn.Close()
}

49
pkg/database/driver.go Normal file
View File

@ -0,0 +1,49 @@
package database
import (
"fmt"
_ "github.com/go-sql-driver/mysql" // MySQL
_ "github.com/lib/pq" // PostgreSQL
_ "github.com/mattn/go-sqlite3" // SQLite
)
var DSNRegistry = make(map[string]DSNBuilder)
func RegisterDSNBuilder(dsn string, builder DSNBuilder) {
DSNRegistry[dsn] = builder
}
func GetDSNBuilder(dsn string) (DSNBuilder, error) {
builder, exists := DSNRegistry[dsn]
if !exists {
return nil, fmt.Errorf("database driver %s is not registered", dsn)
}
return builder, nil
}
func init() {
RegisterDSNBuilder("postgres", &PostgresDSNBuilder{})
RegisterDSNBuilder("mysql", &MySQLDSNBuilder{})
RegisterDSNBuilder("sqlite", &SQLiteDSNBuilder{})
}
type PostgresDSNBuilder struct{}
func (p *PostgresDSNBuilder) BuildDSN(config Config) string {
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
config.Host, config.Port, config.Username, config.Password, config.DBName, config.SSLMode)
}
type MySQLDSNBuilder struct{}
func (m *MySQLDSNBuilder) BuildDSN(config Config) string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?tls=%s",
config.Username, config.Password, config.Host, config.Port, config.DBName, config.SSLMode)
}
type SQLiteDSNBuilder struct{}
func (s *SQLiteDSNBuilder) BuildDSN(config Config) string {
return config.DBName
}

View File

@ -0,0 +1,97 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"sync"
"time"
querier "git.gocasts.ir/ebhomengo/niki/pkg/query_transaction/sql"
_ "github.com/jackc/pgx/v5/stdlib"
)
type Config struct {
Host string `koanf:"host"`
Port int `koanf:"port"`
User string `koanf:"user"`
Password string `koanf:"password"`
DbName string `koanf:"dbName"`
SSLMode string `koanf:"sslMode"`
MaxIdleConn int `koanf:"maxIdleConns"`
MaxOpenConn int `koanf:"maxOpenConns"`
ConnMaxLifetime int `koanf:"connMaxLifetime"`
PathOfMigrations string `koanf:"pathOfMigrations"`
}
type DB struct {
config Config
db *querier.SQLDB
mu sync.Mutex
statements map[statementKey]*sql.Stmt
}
func (db *DB) Conn() *querier.SQLDB {
return db.db
}
func New(config Config) *DB {
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
config.Host, config.Port, config.User, config.Password, config.DbName, config.SSLMode)
db, err := sql.Open("pgx", dsn)
if err != nil {
panic(fmt.Errorf("can't open postgres db: %w", err))
}
maxIdle := config.MaxIdleConn
maxOpen := config.MaxOpenConn
lifetime := time.Duration(config.ConnMaxLifetime) * time.Second
db.SetMaxIdleConns(maxIdle)
db.SetMaxOpenConns(maxOpen)
db.SetConnMaxLifetime(lifetime)
return &DB{
config: config,
db: &querier.SQLDB{DB: db},
statements: make(map[statementKey]*sql.Stmt),
}
}
func (db *DB) PrepareStatement(ctx context.Context, key statementKey, query string) (*sql.Stmt, error) {
db.mu.Lock()
defer db.mu.Unlock()
if stmt, ok := db.statements[key]; ok {
return stmt, nil
}
stmt, err := db.db.PrepareContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("prepare statement %q: %w", key, err)
}
db.statements[key] = stmt
return stmt, nil
}
func (db *DB) CloseStatements() error {
db.mu.Lock()
defer db.mu.Unlock()
var lastErr error
for key, stmt := range db.statements {
if err := stmt.Close(); err != nil {
lastErr = err
}
delete(db.statements, key)
}
return lastErr
}
func (db *DB) Close() error {
return db.db.DB.Close()
}

View File

@ -0,0 +1 @@
package migrator

View File

@ -0,0 +1,9 @@
package postgres
type statementKey uint
const (
StatementKeyAWalletGetTransactionHistory statementKey = iota + 1
StatementKeyWalletInsertTransaction
StatementKeyWalletGetUserWallet
)

View File

@ -1,6 +1,23 @@
package errmsg
type ErrorResponse struct {
Message string `json:"message"` // General error message
Errors map[string]interface{} `json:"errors,omitempty"` // Additional detail of error
InternalErrCode string `json:"internal_error_code,omitempty"` // Custom error code (optional)
}
func (e ErrorResponse) Error() string {
return e.Message
}
const (
ErrValidationFailed = "input validation failed"
ErrUnexpectedError = "unexpected error occurred"
ErrInvalidRequestFormat = "invalid request format"
ErrGetUserInfo = "get user info failed"
ErrFailedDecodeBase64 = "decode data to base 64 failed"
ErrFailedUnmarshalJson = "unmarshal data to JSON failed"
ErrUnauthorized = "unauthorized"
ErrorMsgAdminNotAllowed = "admin is not allowed"
ErrorMsgNotFound = "record not found"
ErrorMsgSomethingWentWrong = "something went wrong"

View File

@ -1,24 +1,25 @@
package grpc
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type Config struct {
type Client struct {
Host string `koanf:"host"`
Port int `koanf:"port"`
}
func NewClient(cfg Config) (*grpc.ClientConn, error) {
func NewClient(cfg Client) (*grpc.ClientConn, error) {
address := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
grpcConn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
conn, err := grpc.DialContext(context.Background(), address, grpc.WithInsecure())
fmt.Println(err)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to connect to gRPC server: %w", err)
}
return grpcConn, nil
return conn, nil
}

View File

@ -1,14 +1,28 @@
package grpc
import "google.golang.org/grpc"
import (
"time"
"google.golang.org/grpc"
)
type Config struct {
Port int `koanf:"port"`
NetworkType string `koanf:"type"`
ShutDownCtxTimeout time.Duration `koanf:"shutdown_context_timeout"`
}
type RPCServer struct {
Config Config
Server *grpc.Server
}
func New() RPCServer {
return RPCServer{
Server: grpc.NewServer(),
func New(cfg Config) *RPCServer {
grpcServer := grpc.NewServer()
return &RPCServer{
Server: grpcServer,
Config: cfg,
}
}

View File

@ -1,7 +1,9 @@
package http_server
package httpserver
import (
"context"
"fmt"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
@ -9,6 +11,12 @@ import (
type Config struct {
Port int `koanf:"port"`
Cors Cors `koanf:"cors"`
ShutDownCtxTimeout time.Duration `koanf:"shutdown_context_timeout"`
}
type Cors struct {
AllowOrigins []string `koanf:"allow_origins"`
}
type Server struct {
@ -16,10 +24,15 @@ type Server struct {
Config Config
}
func NewServer(cfg Config) Server {
func New(cfg Config) Server {
e := echo.New()
e.Use(middleware.RequestLogger())
e.Use(middleware.RequestID())
e.Use(middleware.Logger())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: cfg.Cors.AllowOrigins,
}))
e.Use(middleware.Recover())
return Server{
@ -28,6 +41,16 @@ func NewServer(cfg Config) Server {
}
}
// register custom handler
func (s Server) RegisterHandler(route string, handler echo.HandlerFunc) {
s.Router.GET(route, handler)
}
// start server
func (s Server) Start() error {
return s.Router.Start(fmt.Sprintf(":%d", s.Config.Port))
}
func (s Server) Stop(ctx context.Context) error {
return s.Router.Shutdown(ctx)
}

View File

@ -1,8 +1,69 @@
package productapp
import "net/http"
import (
"context"
"fmt"
"git.gocasts.ir/ebhomengo/niki/productapp/service/product"
"log"
httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server"
producthttp "git.gocasts.ir/ebhomengo/niki/productapp/delivery/http"
productdb "git.gocasts.ir/ebhomengo/niki/productapp/repository/database"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type HTTPServerConfig struct {
Port int
}
type Config struct {
HTTPServer HTTPServerConfig
Database mysql.Config
}
type Application struct {
Config Config
HTTPServer *http.Server
HTTPServer *producthttp.Server
productSvc product.Service
DB *productdb.DB
Echo *echo.Echo
}
func Setup(cfg Config) *Application {
db := mysql.New(cfg.Database)
productDB := productdb.New(db)
e := echo.New()
e.Use(middleware.Recover())
hsrv := &httpserver.Server{
Router: e,
}
productSvc := product.New(productDB)
srv := producthttp.NewServer(hsrv, productSvc)
srv.RegisterRoutes()
return &Application{
Config: cfg,
productSvc: productSvc,
HTTPServer: srv,
DB: productDB,
Echo: e,
}
}
func (app *Application) Start() {
address := fmt.Sprintf(":%d", app.Config.HTTPServer.Port)
log.Printf("Product Service listening on %s", address)
if err := app.Echo.Start(address); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
func (app *Application) Stop(ctx context.Context) error {
return app.Echo.Shutdown(ctx)
}

View File

@ -1,4 +1 @@
package productapp
type Config struct {
}

View File

@ -1,17 +1,38 @@
package http
import (
"net/http"
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
"git.gocasts.ir/ebhomengo/niki/productapp/service/product"
"github.com/labstack/echo/v4"
"log/slog"
"net/http"
)
type Handler struct{}
func NewHandler() *Handler {
return &Handler{}
type Handler struct {
productService product.Service
Logger *slog.Logger
}
func (h Handler) HealthCheck(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
func NewHandler(productService product.Service) *Handler {
return &Handler{
productService: productService,
}
}
func (h Handler) getProductList(c echo.Context) error {
var req product.GetProductListRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, errmsg.ErrorResponse{
Message: errmsg.ErrInvalidRequestFormat,
})
}
response, err := h.productService.GetProducts(c.Request().Context(), req)
if err != nil {
// todo handle validation error
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, response)
}

View File

@ -0,0 +1,11 @@
package http
import (
"net/http"
"github.com/labstack/echo/v4"
)
func (h Handler) HealthCheck(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"status": "healthy"})
}

Some files were not shown because too many files have changed in this diff Show More