merge with develop

This commit is contained in:
matina 2026-04-19 16:30:45 -07:00
commit 58c1f57de8
53 changed files with 2108 additions and 200 deletions

View File

@ -3,8 +3,8 @@ package main
import (
"flag"
"fmt"
purchaseMysql "git.gocasts.ir/ebhomengo/niki/domain/purchase/repository/mysql"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http"
purchaseMysql "git.gocasts.ir/ebhomengo/niki/purchaseapp/repository/mysql"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
"git.gocasts.ir/ebhomengo/niki/repository/migrator"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"

View File

@ -0,0 +1 @@
package command

View File

@ -0,0 +1,52 @@
package command
import (
cfgloader "git.gocasts.ir/ebhomengo/niki/pkg/cfg_loader"
"git.gocasts.ir/ebhomengo/niki/pkg/path"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp"
"github.com/spf13/cobra"
"log"
"os"
"path/filepath"
)
var RootCmd = &cobra.Command{
Use: "shoppingbasket_service",
Short: "A CLI for shoppingbasket service",
Long: `shoppingbasket Service CLI is a tool to manage and run
the shoppingbasket service, including migrations and server startup.`,
}
func loadAppConfig() shoppingbasketapp.Config {
var cfg shoppingbasketapp.Config
projectRoot, err := path.PathProjectRoot()
if err != nil {
log.Fatalf("error finding project root: %v", err)
}
yamlPath := os.Getenv("CONFIG_PATH")
if yamlPath == "" {
defaultConfig := filepath.Join(projectRoot, "deploy", "shoppingbasket", "development", "config.yml")
if _, err := os.Stat(defaultConfig); err == nil {
yamlPath = defaultConfig
} else {
yamlPath = filepath.Join(projectRoot, "deploy", "shoppingbasket", "development", "config.local.yml")
}
}
options := cfgloader.Option{
Prefix: "SHOPPINGBASKET_",
Delimiter: ".",
Separator: "__",
YamlFilePath: yamlPath,
CallbackEnv: nil,
}
if err := cfgloader.Load(options, &cfg); err != nil {
log.Fatalf("Failed to load benefactor config: %v", err)
}
return cfg
}

View File

@ -0,0 +1,43 @@
package command
import (
"context"
"fmt"
"git.gocasts.ir/ebhomengo/niki/pkg/logger"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp"
"github.com/labstack/gommon/log"
"github.com/spf13/cobra"
)
var ServeCmd = &cobra.Command{
Use: "serve",
Short: "Start shoppingbasket service",
Long: `This command starts the main shoppingbasket service.`,
Run: func(cmd *cobra.Command, args []string) {
},
}
func serve() {
var cfg = loadAppConfig()
logger.Init(cfg.Logger)
l := logger.L()
l.Info("Starting shoppingbasket service...")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
app, err := shoppingbasketapp.Setup(ctx, cfg)
if err != nil {
l.Error("failed initialize shopping basket app", "error", err)
log.Fatalf(fmt.Sprintf("error starting shopping basket app: %v", err))
}
app.Start()
}
func init() {
RootCmd.AddCommand(ServeCmd)
}

View File

@ -0,0 +1,12 @@
package main
import (
"git.gocasts.ir/ebhomengo/niki/cmd/shoppingbasketapp/command"
"os"
)
func main() {
if err := command.RootCmd.Execute(); err != nil {
os.Exit(1)
}
}

View File

@ -0,0 +1,25 @@
redis:
host: "localhost"
port: 6379
password: ""
db: 0
repo:
kart_key_prefix: "shopping-basket-cart:"
ttl: 3600s
http_server:
host: "localhost"
port: 8080
shutdown_context_timeout: 10s
cors:
allow_origins:
- "*"
logger:
level: "debug" # Can be `debug`, `info`, `warn`, `error`
file_path: "logs/shoppingbasketapp/service.log"
use_local_time: true
file_max_size_in_mb: 10
file_max_age_in_days: 7

View File

@ -0,0 +1,49 @@
package entity
import (
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type Order struct {
ID types.ID
UserID types.ID
TotalAmount types.Price
TotalDiscount types.Price
ShippingID types.ID
PaymentMethod PaymentMethod
ProcessStatus ProcessStatus
PaymentStatus PaymentStatus
AddressID types.ID
CreatedAt time.Time
UpdatedAt time.Time
}
type PaymentMethod string
const (
Online PaymentMethod = "online"
Wallet = "wallet"
Cart = "cart"
)
type ProcessStatus string
const (
WaitingToPay ProcessStatus = "waiting-to-pay"
Processing = "processing"
Accepted = "accepted"
Preparing = "preparing"
Prepared = "prepared"
GivenToPost = "given-to-post"
Delivered = "delivered"
Cancelled = "cancelled"
SystemCancellation = "system-cancellation"
)
type PaymentStatus string
const (
Paid PaymentStatus = "paid"
UnPaid = "unpaid"
)

View File

@ -0,0 +1,16 @@
package entity
import (
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type OrderItem struct {
ID types.ID
ProductID types.ID
Price types.Price
Quantity types.Count
PriceWithDiscount types.Price
OrderID types.ID
CreatedAt time.Time
}

View File

@ -0,0 +1,10 @@
package entity
import "git.gocasts.ir/ebhomengo/niki/types"
type Shipping struct {
ID types.ID
Name string
Price types.Price
IsActive bool
}

View File

@ -3,11 +3,11 @@
-- https://www.grouparoo.com/blog/varchar-191#why-varchar-and-not-text
CREATE TABLE `orders` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`user_id` INT NOT NULL,
`address` TEXT,
`user_id` INT,
`address_id` INT,
`shipping_id` INT NOT NULL,
`payment_method` ENUM('online', 'wallet', 'cart') DEFAULT 'online',
`payment_status` ENUM('unpaid', 'paid', 'cancelled') DEFAULT 'unpaid',
`payment_status` ENUM('unpaid', 'paid') DEFAULT 'unpaid',
`process_status` ENUM('waiting-to-pay', 'processing', 'accepted', 'preparing', 'prepared', 'given-to-post', 'delivered', 'cancelled') DEFAULT 'waiting-to-pay',
`total_amount` INT NOT NULL,
`total_discount` INT NULL,
@ -15,7 +15,7 @@ CREATE TABLE `orders` (
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
-- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
-- FOREIGN KEY (`shipping_id`) REFERENCES `shippings`(`id`)
FOREIGN KEY (`shipping_id`) REFERENCES `shippings`(`id`)
);

View File

@ -0,0 +1,16 @@
-- +migrate Up
-- please read this article to understand why we use VARCHAR(191)
-- https://www.grouparoo.com/blog/varchar-191#why-varchar-and-not-text
CREATE TABLE `orders` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR (191),
`price` INT NOT NULL ,
`is_active` INT NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- +migrate Down
DROP TABLE `orders`;

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

@ -1,23 +1,14 @@
package mysql
import (
entity "git.gocasts.ir/ebhomengo/niki/domain/order/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
"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}
}
func (d *DB) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error) {
const Op = "repository.mysql.order.createorder"
const Op = "domain.repository.mysql.order.create-order"
tx, err := d.conn.Conn().Begin()
if err != nil {
@ -26,10 +17,10 @@ func (d *DB) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (typ
defer tx.Rollback()
query := "insert into orders(user_id, address, shipping_id," +
query := "insert into orders(user_id, address_id, shipping_id," +
" payment_method, payment_status, process_status," +
" total_amount, total_discount) values (?, ?, ?, ?, ?, ?, ?, ?);"
res, oErr := tx.Exec(query, order.UserID, order.Address, order.ShippingID,
res, oErr := tx.Exec(query, order.UserID, order.AddressID, order.ShippingID,
order.PaymentMethod, order.PaymentStatus, order.ProcessStatus,
order.TotalAmount, order.TotalDiscount)
@ -57,7 +48,7 @@ func (d *DB) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (typ
}
func (d *DB) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
const Op = "repository.mysql.order.update-order-process-status"
const Op = "domain.repository.mysql.order.update-order-process-status"
_, err := d.conn.Conn().Exec("update orders set process_status=? where id=?;", status, orderID)
if err != nil {
@ -67,3 +58,35 @@ func (d *DB) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, er
return true, nil
}
func (d *DB) GetShipping() ([]entity.Shipping, error) {
const Op = "domain.repository.mysql.order.get-shipping"
rows, err := d.conn.Conn().Query("select * from shippings where is_active=1")
if err != nil {
return []entity.Shipping{}, richerror.New(Op).WithErr(err)
}
defer rows.Close()
var shippings []entity.Shipping
for rows.Next() {
var s entity.Shipping
err := rows.Scan(
&s.ID,
&s.Name,
&s.Price,
&s.IsActive,
)
if err != nil {
return nil, richerror.New(Op).WithErr(err)
}
shippings = append(shippings, s)
}
return shippings, nil
}

View File

@ -1,9 +1,9 @@
package order
package service
import (
entity "git.gocasts.ir/ebhomengo/niki/domain/order/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
"git.gocasts.ir/ebhomengo/niki/types"
types "git.gocasts.ir/ebhomengo/niki/types"
)
type Service struct {
@ -13,26 +13,27 @@ type Service struct {
type Repo interface {
CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error)
UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error)
GetShipping() ([]entity.Shipping, error)
}
func New(orderRepo Repo) Service {
return Service{repo: orderRepo}
}
func (s Service) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (CreateOrderResponse, error) {
const Op = "purchaseapp.service.CreateOrder"
func (s *Service) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error) {
const Op = "domain.order.service.order.CreateOrder"
orderID, err := s.repo.CreateOrder(order, orderItems)
if err != nil {
return CreateOrderResponse{}, richerror.New(Op).WithErr(err)
return 0, richerror.New(Op).WithErr(err)
}
return CreateOrderResponse{OrderID: orderID}, nil
return orderID, nil
}
func (s Service) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
func (s *Service) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
const Op = "purchaseapp.service.UpdateOrderProcessStatus"
const Op = "domain.order.service.order.UpdateOrderProcessStatus"
_, err := s.repo.UpdateOrderProcessStatus(orderID, status)
if err != nil {
return false, richerror.New(Op).WithErr(err)

View File

@ -0,0 +1,16 @@
package service
import (
"git.gocasts.ir/ebhomengo/niki/domain/order/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
)
func (s *Service) GetShipping() ([]entity.Shipping, error) {
const Op = "domain.order.service.shipping.get-shipping"
shippings, err := s.repo.GetShipping()
if err != nil {
return []entity.Shipping{}, richerror.New(Op)
}
return shippings, nil
}

View File

@ -16,10 +16,10 @@ const (
)
type Config struct {
FilePath string
UseLocalTime bool
FileMaxSizeInMB int
FileMaxAgeInDays int
FilePath string `koanf:"file_path"`
UseLocalTime bool `koanf:"use_local_time"`
FileMaxSizeInMB int `koanf:"file_max_size_in_mb"`
FileMaxAgeInDays int `koanf:"file_max_age_in_days"`
}
var l *slog.Logger

View File

@ -3,17 +3,17 @@ package patientapp
import (
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
"git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic"
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
"github.com/labstack/echo/v4"
)
type Application struct {
//Config Config
HTTPServer *config.EchoServer
DB *mysql.DataBase
DB *mysql.DB
}
func Setup(cfg config.Config, conn *mysql.DataBase) Application {
func Setup(cfg config.Config, db *mysql.DB) Application {
e := echo.New()
@ -25,13 +25,13 @@ func Setup(cfg config.Config, conn *mysql.DataBase) Application {
return Application{
//Config: config,
HTTPServer: &server,
DB: conn,
DB: db,
}
}
func (a Application) Start() {
server := analytic.NewServer(a.HTTPServer)
server := analytic.NewServer(a.HTTPServer, a.DB)
_ = server.Serve()
}

View File

@ -1,15 +1,26 @@
package main
import (
"os"
"strconv"
"time"
"git.gocasts.ir/ebhomengo/niki/patientapp"
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
)
func main() {
db := mysql.DataBase{}
dbConf := mysql.Config{
Username: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PASS"),
Host: os.Getenv("DB_HOST"),
DBName: os.Getenv("DB_NAME"),
}
port, _ := strconv.Atoi(os.Getenv("DB_PORT"))
dbConf.Port = port
db := mysql.New(dbConf)
cfg := config.Config{
Port: 8080,
@ -19,7 +30,7 @@ func main() {
ShutDownCtxTimeout: 5 * time.Second,
}
app := patientapp.Setup(cfg, &db)
app := patientapp.Setup(cfg, db)
app.Start()

View File

@ -1,15 +1,15 @@
package analytic
import (
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
repo "git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
analytic2 "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
"github.com/labstack/echo/v4"
)
func NewPatientAnalyticRouter(s *echo.Group) {
func NewPatientAnalyticRouter(s *echo.Group, db *mysql.DB) {
mysqlRepo := mysql.NewPatientRepo()
//rpcRepo := grpc.NewPatientRepo()
mysqlRepo := repo.NewPatientRepo(db)
analyticService := analytic2.NewPatientAnalyticService(mysqlRepo)

View File

@ -5,16 +5,19 @@ import (
"fmt"
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
)
type Server struct {
HTTPServer *config.EchoServer
Db *mysql.DB
}
func NewServer(server *config.EchoServer) *Server {
func NewServer(server *config.EchoServer, db *mysql.DB) *Server {
return &Server{
HTTPServer: server,
Db: db,
}
}
@ -35,7 +38,7 @@ func (s Server) RegisterRoutes() {
{
// Analytic Group
analyticGroup := v1.Group("/analytic")
NewPatientAnalyticRouter(analyticGroup)
NewPatientAnalyticRouter(analyticGroup, s.Db)
}
}

View File

@ -0,0 +1,32 @@
version: "3.9"
services:
mysql:
image: mysql:8.0
container_name: patient
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: patient
MYSQL_USER: appuser
MYSQL_PASSWORD: apppass
TZ: Asia/Tehran
command: [
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci",
"--default-authentication-plugin=mysql_native_password"
]
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./repository/migration:/docker-entrypoint-initdb.d
networks:
- backend
volumes:
mysql_data:
networks:
backend:
driver: bridge

View File

@ -0,0 +1,481 @@
-- MySQL dump 10.13 Distrib 8.0.29, for Linux (x86_64)
--
-- Host: localhost Database: patient
-- ------------------------------------------------------
-- Server version 8.0.29
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `audits`
--
DROP TABLE IF EXISTS `audits`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `audits` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_type` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`event` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`auditable_type` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`auditable_id` bigint unsigned NOT NULL,
`old_values` text COLLATE utf8mb4_unicode_ci,
`new_values` text COLLATE utf8mb4_unicode_ci,
`url` text COLLATE utf8mb4_unicode_ci,
`ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`user_agent` varchar(1023) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`tags` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `audits_auditable_type_auditable_id_index` (`auditable_type`,`auditable_id`),
KEY `audits_user_id_user_type_index` (`user_id`,`user_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `bandage_user`
--
DROP TABLE IF EXISTS `bandage_user`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `bandage_user` (
`user_id` bigint unsigned NOT NULL,
`bandage_id` bigint unsigned NOT NULL,
`qty` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `bandages`
--
DROP TABLE IF EXISTS `bandages`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `bandages` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `cities`
--
DROP TABLE IF EXISTS `cities`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cities` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`state_id` int unsigned NOT NULL,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=445 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `drug_user`
--
DROP TABLE IF EXISTS `drug_user`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `drug_user` (
`user_id` bigint unsigned NOT NULL,
`drug_id` bigint unsigned NOT NULL,
`qty` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `drugs`
--
DROP TABLE IF EXISTS `drugs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `drugs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `failed_jobs`
--
DROP TABLE IF EXISTS `failed_jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `failed_jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
`queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `logs`
--
DROP TABLE IF EXISTS `logs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `logs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` int unsigned NOT NULL,
`type` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`log` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20739 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `migrations`
--
DROP TABLE IF EXISTS `migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `migrations` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`batch` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `model_has_permissions`
--
DROP TABLE IF EXISTS `model_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`model_type` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`model_id`,`model_type`),
KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `model_has_roles`
--
DROP TABLE IF EXISTS `model_has_roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_roles` (
`role_id` bigint unsigned NOT NULL,
`model_type` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`role_id`,`model_id`,`model_type`),
KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `password_resets`
--
DROP TABLE IF EXISTS `password_resets`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `password_resets` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint DEFAULT NULL,
`time` int DEFAULT NULL,
`token` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=227 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `permissions`
--
DROP TABLE IF EXISTS `permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `permissions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `reports`
--
DROP TABLE IF EXISTS `reports`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `reports` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL,
`agent_id` bigint unsigned NOT NULL,
`type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`interviewee` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`interviewee_name` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`interviewee_ratio` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`content` text COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `reports_user_id_foreign` (`user_id`),
CONSTRAINT `reports_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=19846 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `role_has_permissions`
--
DROP TABLE IF EXISTS `role_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `role_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`role_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`role_id`),
KEY `role_has_permissions_role_id_foreign` (`role_id`),
CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `roles`
--
DROP TABLE IF EXISTS `roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `roles` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `states`
--
DROP TABLE IF EXISTS `states`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `states` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `user_files`
--
DROP TABLE IF EXISTS `user_files`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user_files` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL,
`name` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`label` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL,
`status` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`message` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=40633 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `user_metas`
--
DROP TABLE IF EXISTS `user_metas`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user_metas` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL,
`birthPlaceState` smallint unsigned NOT NULL DEFAULT '0',
`birthPlaceCity` smallint unsigned NOT NULL DEFAULT '0',
`religion` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`atba` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`nationality` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`atba_birthPlaceState` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`atba_birthPlaceCity` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`height` smallint unsigned DEFAULT NULL,
`weight` smallint unsigned DEFAULT NULL,
`eyeColor` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bloodType` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`addressState` smallint unsigned NOT NULL,
`addressCity` smallint unsigned NOT NULL,
`address` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`postalCode` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
`phone` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
`mobile` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`fMobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mMobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`ePhoneName` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`ePhoneNumber` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`houseType` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
`housePrice` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`houseMortgage` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`houseRent` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`fAlive` tinyint(1) DEFAULT NULL,
`divorced` tinyint(1) NOT NULL DEFAULT '0',
`devorced` tinyint NOT NULL DEFAULT '0',
`fName` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`fBirthDate` date DEFAULT NULL,
`fNationalCode` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`fAtba` tinyint(1) NOT NULL DEFAULT '0',
`fNationality` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`fJob` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`fEdu` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mAlive` tinyint(1) DEFAULT NULL,
`mName` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mBirthDate` date DEFAULT NULL,
`mNationalCode` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mAtba` tinyint(1) NOT NULL DEFAULT '0',
`mNationality` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mJob` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mEdu` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`ratio` tinyint(1) DEFAULT NULL,
`ratioType` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`liveChild` tinyint unsigned NOT NULL,
`ebChild` tinyint unsigned NOT NULL,
`Edu` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`skills` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`insurance` tinyint(1) NOT NULL,
`insuranceYear` tinyint unsigned DEFAULT NULL,
`insuranceMonth` tinyint unsigned DEFAULT NULL,
`insuranceCoverage` tinyint(1) NOT NULL,
`insuranceCoverageNumber` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`insuranceCoveragBranch` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`insuranceCoverageType` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`supplementaryInsurance` tinyint(1) NOT NULL,
`supplementaryInsuranceName` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`supportOrganization` tinyint(1) NOT NULL,
`supportOrganizationName` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`dailyProblemType` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`spouseName` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`spouseBirthDate` date DEFAULT NULL,
`spouseNationalCode` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`spouseAtba` tinyint(1) NOT NULL DEFAULT '0',
`spouseNationality` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`spouseJob` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`spouseEdu` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`spouseMobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`spouseAlive` tinyint(1) DEFAULT NULL,
`shenasname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`shenasnamePlace` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_metas_user_id_foreign` (`user_id`),
CONSTRAINT `user_metas_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1461 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`last_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`username` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`pic` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`birth_date` date DEFAULT NULL,
`gender` tinyint(1) NOT NULL,
`marital` smallint unsigned DEFAULT NULL,
`deceased` date DEFAULT NULL,
`seyyed` tinyint(1) NOT NULL DEFAULT '0',
`profile_number` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`sheba` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bank_account` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bank_account_name` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`profile_status` tinyint unsigned DEFAULT NULL,
`medical_information_status` tinyint unsigned DEFAULT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`drug_description` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bandage_description` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bandage_period` tinyint unsigned DEFAULT NULL,
`bandage_months` text COLLATE utf8mb4_unicode_ci,
`twoFA_enabled` tinyint(1) NOT NULL DEFAULT '0',
`twoFA_secret` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`is_active` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `users_username_unique` (`username`),
KEY `users_twofa_enabled_index` (`twoFA_enabled`),
KEY `users_is_active_index` (`is_active`)
) ENGINE=InnoDB AUTO_INCREMENT=1918 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2026-04-14 7:28:21

View File

@ -1,35 +0,0 @@
package grpc
import (
"context"
"git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
)
type AnalyticRepository struct{}
func NewPatientRepo() *AnalyticRepository {
return &AnalyticRepository{}
}
func (db *AnalyticRepository) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
return nil, nil
}
func (db *AnalyticRepository) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
return 0, nil
}
func (db *AnalyticRepository) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
return nil, nil
}
func (db *AnalyticRepository) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
return nil, nil
}

View File

@ -2,35 +2,385 @@ package mysql
import (
"context"
"fmt"
"git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
)
type DataBase struct{}
func NewPatientRepo() *DataBase {
return &DataBase{}
type DataBase struct {
conn *mysql.DB
}
func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
func NewPatientRepo(db *mysql.DB) *DataBase {
return nil, nil
return &DataBase{
conn: db,
}
}
func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.UserMeta, error) {
const Op = "repository.mysql.patient.get"
tx, err := db.conn.Conn().BeginTx(ctx, nil)
if err != nil {
return []entity.UserMeta{}, richerror.New(Op).WithErr(err)
}
defer tx.Rollback()
query := `
SELECT
um.id,
um.user_id,
u.birthDate,
u.sex,
um.birthPlaceState,
um.birthPlaceCity,
um.nationality,
um.addressState,
um.addressCity,
um.address,
um.phone,
um.mobile,
um.spouseName,
um.created_at,
um.updated_at
FROM user_metas um
JOIN users u ON u.id = um.user_id
WHERE 1=1
`
args := []any{}
// Birthdate filters (FROM = born after)
if f.DOBFrom != nil && *f.DOBFrom != "" {
query += " AND u.birth_date >= ?"
args = append(args, *f.DOBFrom)
}
if f.DOBTo != nil && *f.DOBTo != "" {
query += " AND u.birth_date <= ?"
args = append(args, *f.DOBTo)
}
// Sex
if f.Sex != nil && *f.Sex != "" {
query += " AND u.sex = ?"
args = append(args, *f.Sex)
}
// Nationality
if f.Nationality != "" {
query += " AND um.nationality = ?"
args = append(args, f.Nationality)
}
// Address
if f.AddressState != 0 {
query += " AND um.addressState = ?"
args = append(args, f.AddressState)
}
if f.AddressCity != 0 {
query += " AND um.addressCity = ?"
args = append(args, f.AddressCity)
}
// Search on fields from user_metas and users
if f.Search != nil && *f.Search != "" {
like := "%" + *f.Search + "%"
query += `
AND (
um.fName LIKE ? OR
um.mName LIKE ? OR
um.spouseName LIKE ? OR
um.phone LIKE ? OR
um.mobile LIKE ?
)
`
args = append(args, like, like, like, like, like)
}
query += " ORDER BY id DESC"
if f.Limit > 0 {
query += " LIMIT ?"
args = append(args, f.Limit)
}
if f.Offset > 0 {
query += " OFFSET ?"
args = append(args, f.Offset)
}
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, richerror.New(Op).WithErr(err)
}
defer rows.Close()
var result []entity.UserMeta
for rows.Next() {
var u entity.UserMeta
if err := rows.Scan(
&u.ID, &u.UserID, &u.BirthDate, &u.Gender, &u.BirthPlaceState, &u.BirthPlaceCity,
&u.Religion, &u.Nationality, &u.AddressState, &u.AddressCity,
&u.Address, &u.Mobile, &u.SpouseName, &u.SpouseAlive,
&u.CreatedAt, &u.UpdatedAt,
); err != nil {
return nil, richerror.New(Op).WithErr(err)
}
result = append(result, u)
}
if err := rows.Err(); err != nil {
return nil, richerror.New(Op).WithErr(err)
}
return result, nil
}
func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
const Op = "repository.mysql.patient.count"
return 0, nil
tx, err := db.conn.Conn().BeginTx(ctx, nil)
if err != nil {
return 0, richerror.New(Op).WithErr(err)
}
defer tx.Rollback()
query := `
SELECT COUNT(*)
FROM user_metas um
JOIN users u ON u.id = um.user_id
WHERE 1=1
`
args := []any{}
// Birthdate range
if f.DOBFrom != nil && *f.DOBFrom != "" {
query += " AND u.birth_date >= ?"
args = append(args, *f.DOBFrom)
}
if f.DOBTo != nil && *f.DOBTo != "" {
query += " AND u.birth_date <= ?"
args = append(args, *f.DOBTo)
}
// Sex
if f.Sex != nil && *f.Sex != "" {
query += " AND u.sex = ?"
args = append(args, *f.Sex)
}
// Nationality
if f.Nationality != "" {
query += " AND um.nationality = ?"
args = append(args, f.Nationality)
}
// Address
if f.AddressState != 0 {
query += " AND um.addressState = ?"
args = append(args, f.AddressState)
}
if f.AddressCity != 0 {
query += " AND um.addressCity = ?"
args = append(args, f.AddressCity)
}
// Search
if f.Search != nil && *f.Search != "" {
like := "%" + *f.Search + "%"
query += `
AND (
um.fName LIKE ? OR
um.mName LIKE ? OR
um.spouseName LIKE ? OR
um.phone LIKE ? OR
um.mobile LIKE ?
)
`
args = append(args, like, like, like, like, like)
}
var count int
err = tx.QueryRowContext(ctx, query, args...).Scan(&count)
if err != nil {
return 0, fmt.Errorf("%s: query error: %w", Op, err)
}
return count, nil
}
func (db *DataBase) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
const Op = "repository.mysql.patient.map_summary"
return nil, nil
tx, err := db.conn.Conn().BeginTx(ctx, nil)
if err != nil {
return nil, richerror.New(Op).WithErr(err)
}
defer tx.Rollback()
query := `
SELECT
um.addressCity AS city_id,
ANY_VALUE(um.lat) AS lat,
ANY_VALUE(um.lng) AS lng,
COUNT(*) AS user_count
FROM user_metas um
JOIN users u ON u.id = um.user_id
JOIN cities c ON c.id = um.addressCity
WHERE c.state_id = ?
`
args := []any{provinceID}
// Birthdate filters
if f.MinDOB != nil && *f.MinDOB != "" {
query += " AND u.birth_date >= ?"
args = append(args, *f.MinDOB)
}
if f.MaxDOB != nil && *f.MaxDOB != "" {
query += " AND u.birth_date <= ?"
args = append(args, *f.MaxDOB)
}
// Sex filter
if f.Sex != nil && *f.Sex != "" {
query += " AND u.sex = ?"
args = append(args, *f.Sex)
}
// Search filter
if f.Search != nil && *f.Search != "" {
like := "%" + *f.Search + "%"
query += `
AND (
um.fName LIKE ? OR
um.mName LIKE ? OR
um.spouseName LIKE ? OR
um.phone LIKE ? OR
um.mobile LIKE ?
)
`
args = append(args, like, like, like, like, like)
}
// Group by city
query += " GROUP BY um.addressCity"
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("%s: query error: %w", Op, err)
}
defer rows.Close()
result := make(map[uint][]entity.MapSummaryItem)
for rows.Next() {
var item entity.MapSummaryItem
err := rows.Scan(
&item.ID,
&item.Latitude,
&item.Longitude,
&item.Count,
)
if err != nil {
return nil, fmt.Errorf("%s: scan error: %w", Op, err)
}
result[item.ID] = append(result[item.ID], item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("%s: rows error: %w", Op, err)
}
return result, nil
}
func (db *DataBase) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
return nil, nil
const Op = "repository.mysql.patient.summary_by_province"
tx, err := db.conn.Conn().BeginTx(ctx, nil)
if err != nil {
return nil, richerror.New(Op).WithErr(err)
}
defer tx.Rollback()
query := `
SELECT
c.state_id,
ANY_VALUE(c.lat) AS lat,
ANY_VALUE(c.lng) AS lng,
COUNT(*) AS total
FROM user_metas um
JOIN users u ON u.id = um.user_id
JOIN cities c ON c.id = um.addressCity
WHERE 1 = 1
`
args := []any{}
// Birthdate filters
if f.MinDOB != nil && *f.MinDOB != "" {
query += " AND u.birth_date >= ?"
args = append(args, *f.MinDOB)
}
if f.MaxDOB != nil && *f.MaxDOB != "" {
query += " AND u.birth_date <= ?"
args = append(args, *f.MaxDOB)
}
// Sex filter
if f.Sex != nil && *f.Sex != "" {
query += " AND u.sex = ?"
args = append(args, *f.Sex)
}
// Search filter
if f.Search != nil && *f.Search != "" {
like := "%" + *f.Search + "%"
query += `
AND (
um.fName LIKE ? OR
um.mName LIKE ? OR
um.spouseName LIKE ? OR
um.phone LIKE ? OR
um.mobile LIKE ?
)
`
args = append(args, like, like, like, like, like)
}
query += " GROUP BY c.state_id"
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("%s: query error: %w", Op, err)
}
defer rows.Close()
result := make(map[uint][]entity.MapSummaryItem)
for rows.Next() {
var item entity.MapSummaryItem
if err := rows.Scan(&item.ID, &item.Latitude, &item.Longitude, &item.Count); err != nil {
return nil, fmt.Errorf("%s: scan error: %w", Op, err)
}
result[item.ID] = []entity.MapSummaryItem{item}
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("%s: rows error: %w", Op, err)
}
return result, nil
}

View File

@ -6,12 +6,12 @@ import (
type ListPatientAnalyticRequest struct {
// All fields are optional
MinAge *int `query:"minAge,omitempty"`
MaxAge *int `query:"maxAge,omitempty"`
Sex *entity.Sex `query:"sex,omitempty"`
MinAge *int `query:"minAge,omitempty"`
MaxAge *int `query:"maxAge,omitempty"`
Sex *string `query:"sex,omitempty"`
City *int64 `query:"city,omitempty"`
Province *int64 `query:"province,omitempty"`
City *uint16 `query:"city,omitempty"`
Province *uint16 `query:"province,omitempty"`
Search *string `query:"search,omitempty"`
@ -19,11 +19,11 @@ type ListPatientAnalyticRequest struct {
}
type PatientAnalyticItem struct {
ID int64 `json:"id"`
ID uint64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"Last_name"`
DateOfBirth string `json:"dob,omitempty"`
Sex entity.Sex `json:"sex"`
Sex string `json:"sex"`
Phone string `json:"phone"`
Address entity.Address `json:"address"`
}
@ -33,17 +33,17 @@ type PatientAnalyticResponse struct {
Total int `json:"total"`
}
func ToPatientResponse(patient entity.Patient) PatientAnalyticItem {
func ToPatientResponse(patient entity.UserMeta) PatientAnalyticItem {
return PatientAnalyticItem{
ID: patient.ID,
FirstName: patient.FirstName,
FirstName: patient.Name,
LastName: patient.LastName,
DateOfBirth: patient.DateOfBirth,
Sex: patient.Sex,
Phone: patient.Phone,
DateOfBirth: patient.BirthDate,
Sex: patient.Gender,
Phone: *patient.Mobile,
Address: entity.Address{
ProvinceID: patient.Address.ProvinceID,
CityID: patient.Address.CityID,
ProvinceID: patient.AddressState,
CityID: patient.AddressCity,
},
}
}

View File

@ -7,11 +7,11 @@ import (
type PatientFilter struct {
DOBFrom *string // born after
DOBTo *string // born before
Sex *entity.Sex
Sex *string
City *int64
Province *int64
Country *int64
Nationality string
AddressState uint16
AddressCity uint16
Search *string

View File

@ -16,7 +16,7 @@ var (
)
type Repository interface {
GetPatients(ctx context.Context, f PatientFilter) ([]entity.Patient, error)
GetPatients(ctx context.Context, f PatientFilter) ([]entity.UserMeta, error)
CountPatients(ctx context.Context, f PatientFilter) (int, error)
SummaryByCity(ctx context.Context, provinceID uint, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error)
@ -41,14 +41,14 @@ func (s Service) List(ctx context.Context, req ListPatientAnalyticRequest) (Pati
dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now())
filter := PatientFilter{
DOBFrom: dobFrom,
DOBTo: dobTo,
Sex: req.Sex,
City: req.City,
Province: req.Province,
Search: req.Search,
Limit: limit,
Offset: offset,
DOBFrom: dobFrom,
DOBTo: dobTo,
Sex: req.Sex,
AddressCity: *req.City,
AddressState: *req.Province,
Search: req.Search,
Limit: limit,
Offset: offset,
}
items, err := s.repository.GetPatients(ctx, filter)

View File

@ -7,8 +7,8 @@ type Address struct {
Name string
Lat float64
Lon float64
CityID uint
ProvinceID uint
CityID uint16
ProvinceID uint16
}
type AddressAggregated struct {

View File

@ -9,9 +9,8 @@ const (
)
type MapSummaryItem struct {
LocationID int64
Name string
Count int
CentroidLat float64
CentroidLng float64
ID uint `json:"id"` // city_id OR state_id
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Count int `json:"count"`
}

View File

@ -1,5 +1,7 @@
package entity
import "time"
type Patient struct {
ID int64
FirstName string
@ -15,6 +17,27 @@ type Patient struct {
EndDate string
}
type UserMeta struct {
ID uint64 `db:"id"`
UserID uint64 `db:"user_id"`
Name string `db:"name"`
LastName string `db:"last_name"`
Gender string `db:"gender"`
BirthDate string `db:"birth_date"`
BirthPlaceState uint16 `db:"birthPlaceState"`
BirthPlaceCity uint16 `db:"birthPlaceCity"`
Religion *string `db:"religion"`
Nationality *string `db:"nationality"`
AddressState uint16 `db:"addressState"`
AddressCity uint16 `db:"addressCity"`
Address string `db:"address"`
Mobile *string `db:"mobile"`
SpouseName *string `db:"spouseName"`
SpouseAlive *bool `db:"spouseAlive"`
CreatedAt *time.Time `db:"created_at"`
UpdatedAt *time.Time `db:"updated_at"`
}
// Sex ================================== Sex type ==========================================
type Sex string

View File

@ -3,11 +3,13 @@ package httpserver
import (
"context"
"fmt"
echomiddleware "github.com/gocasters/rankr/pkg/echo_middleware"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"strings"
"sync"
"time"
)
@ -19,7 +21,6 @@ type Config struct {
HideBanner bool `koanf:"hide_banner"`
HidePort bool `koanf:"hide_port"`
PublicPaths []string `koanf:"public_paths"`
// Optional Otel middleware can be injected from outside.
OtelMiddleware echo.MiddlewareFunc
}

View File

@ -3,9 +3,9 @@ package purchaseapp
import (
"context"
"fmt"
purchaseMysql "git.gocasts.ir/ebhomengo/niki/domain/purchase/repository/mysql"
purchaseHTTP "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http"
purchaseHandler "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/order"
purchaseMysql "git.gocasts.ir/ebhomengo/niki/purchaseapp/repository/mysql"
purchaseService "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
)

View File

@ -1,9 +1,9 @@
package order
import (
entity "git.gocasts.ir/ebhomengo/niki/domain/order/entity"
order "git.gocasts.ir/ebhomengo/niki/domain/order/service"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
"github.com/labstack/echo/v4"
"net/http"
"time"
@ -18,7 +18,7 @@ func New(orderSvc order.Service) *Handler {
}
func (h *Handler) CreateOrderHandler(c echo.Context) error {
var req order.CreateOrderRequest
var req CreateOrderRequest
if err := c.Bind(&req); err != nil {
msg, code := getErrorDataFromRichError(err)
return echo.NewHTTPError(code, msg)
@ -34,7 +34,7 @@ func (h *Handler) CreateOrderHandler(c echo.Context) error {
PaymentMethod: req.PaymentMethod,
ProcessStatus: entity.WaitingToPay,
PaymentStatus: entity.UnPaid,
Address: req.Address,
AddressID: req.AddressID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}

View File

@ -1,13 +1,13 @@
package order
import (
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
"git.gocasts.ir/ebhomengo/niki/domain/order/entity"
"git.gocasts.ir/ebhomengo/niki/types"
)
type CreateOrderRequest struct {
UserID types.ID `json:"user_id"`
Address string `json:"address"`
AddressID types.ID `json:"address_id"`
ShippingID types.ID `json:"shipping_id"`
PaymentMethod entity.PaymentMethod `json:"payment_method"`
TotalAmount types.Price `json:"total_amount"`

View File

@ -4,5 +4,4 @@ import "github.com/labstack/echo/v4"
func (h Handler) SetRoutes(e *echo.Echo) {
e.POST("/order/create", h.CreateOrderHandler)
}

View File

@ -1,19 +1,19 @@
package http
import (
"git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/order"
orderService "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
order "git.gocasts.ir/ebhomengo/niki/domain/order/service"
orderHandler "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/order"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Server struct {
OrderHandler *order.Handler
OrderHandler *orderHandler.Handler
}
func New(orderSvc orderService.Service) *Server {
func New(orderSvc order.Service) *Server {
return &Server{
OrderHandler: order.New(orderSvc),
OrderHandler: orderHandler.New(orderSvc),
}
}

View File

@ -1,59 +0,0 @@
package entity
import (
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type Order struct {
ID types.ID
UserID types.ID
TotalAmount types.Price
TotalDiscount types.Price
ShippingID types.ID
PaymentMethod PaymentMethod
ProcessStatus ProcessStatus
PaymentStatus PaymentStatus
Address string
CreatedAt time.Time
UpdatedAt time.Time
}
type OrderItem struct {
ID types.ID
ProductID types.ID
Price types.Price
Quantity types.Count
PriceWithDiscount types.Price
OrderID types.ID
CreatedAt time.Time
}
type PaymentMethod string
const (
Online PaymentMethod = "online"
Wallet = "wallet"
Cart = "cart"
)
type ProcessStatus string
const (
WaitingToPay ProcessStatus = "waiting-to-pay"
processing = "processing"
accepted = "accepted"
preparing = "preparing"
prepared = "prepared"
givenToPost = "given-to-post"
delivered = "delivered"
cancelled = "cancelled"
)
type PaymentStatus string
const (
Paid PaymentStatus = "paid"
UnPaid = "unpaid"
Cancelled = "cancelled"
)

View File

@ -1 +0,0 @@
package mysql

View File

@ -1 +0,0 @@
package order

126
shoppingbasketapp/app.go Normal file
View File

@ -0,0 +1,126 @@
package shoppingbasketapp
import (
"context"
"fmt"
"git.gocasts.ir/ebhomengo/niki/adapter/redis"
"git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
"git.gocasts.ir/ebhomengo/niki/pkg/logger"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/delivery/http"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/service/cart"
"os"
"os/signal"
"sync"
"syscall"
)
type Application struct {
Repo repository.Repo
Service cart.Service
Handler http.Handler
Server http.Server
Config Config
}
func Setup(ctx context.Context, cfg Config) (Application, error) {
adapter := redis.New(cfg.Redis)
repo := repository.New(adapter.Client(), cfg.Repo)
validator := cart.NewValidate()
svc := cart.New(validator, repo)
handler := http.NewHandler(svc)
httpServer, err := httpserver.New(cfg.HTTPServer)
if err != nil {
logger.L().Error("failed to initialize http server", "error", err)
return Application{}, err
}
server := http.NewServer(handler, httpServer)
return Application{
Repo: repo,
Service: svc,
Handler: handler,
Server: server,
Config: cfg,
}, nil
}
func (app Application) Start() {
var wg sync.WaitGroup
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
startServers(app, &wg)
<-ctx.Done()
logger.L().Info("Shutdown signal received...")
shutdownTimeoutCtx, cancel := context.WithTimeout(context.Background(), app.Config.HTTPServer.ShutdownTimeout)
defer cancel()
if app.shutdownServers(shutdownTimeoutCtx) {
logger.L().Info("Servers shutdown gracefully")
} else {
logger.L().Warn("Shutdown timed out, exiting application")
return
}
wg.Wait()
logger.L().Info("shopping-basket-app stopped")
}
func startServers(app Application, wg *sync.WaitGroup) {
wg.Add(1)
go func() {
defer wg.Wait()
logger.L().Info(fmt.Sprintf("HTTP server starting on port: %d", app.Config.HTTPServer.Port))
if err := app.Server.Serve(); err != nil {
logger.L().Error(fmt.Sprintf("error listen and serve HTTP server on port %d", app.Config.HTTPServer.Port))
}
logger.L().Info(fmt.Sprintf("HTTP server stopped on port %d", app.Config.HTTPServer.Port))
}()
}
func (app Application) shutdownServers(ctx context.Context) bool {
logger.L().Info("Starting server shutdown process...")
shutdownDone := make(chan struct{})
go func() {
var shutdownWg sync.WaitGroup
shutdownWg.Add(1)
go app.shutdownHTTPServe(ctx, &shutdownWg)
shutdownWg.Wait()
close(shutdownDone)
logger.L().Info("All servers have been shut down successfully.")
}()
select {
case <-shutdownDone:
return true
case <-ctx.Done():
return false
}
}
func (app Application) shutdownHTTPServe(parentCtx context.Context, wg *sync.WaitGroup) {
logger.L().Info(fmt.Sprintf("Starting gracefully shutdown for http server on port %d", app.Config.HTTPServer.Port))
defer wg.Done()
httpShutdownCtx, httpCancel := context.WithTimeout(parentCtx, app.Config.HTTPServer.ShutdownTimeout)
defer httpCancel()
if err := app.Server.Stop(httpShutdownCtx); err != nil {
logger.L().Error(fmt.Sprintf("failed http server gracefully shutdown: %v", err))
}
logger.L().Info("Successfully http server gracefully shutdown")
}

View File

@ -0,0 +1,15 @@
package shoppingbasketapp
import (
"git.gocasts.ir/ebhomengo/niki/adapter/redis"
"git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
logger "git.gocasts.ir/ebhomengo/niki/pkg/logger"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository"
)
type Config struct {
Redis redis.Config `koanf:"redis" json:"redis"`
Repo repository.Config `koanf:"repo" json:"repo"`
HTTPServer httpserver.Config `koanf:"http_server" json:"http_server"`
Logger logger.Config `koanf:"logger" json:"logger"`
}

View File

@ -0,0 +1,117 @@
package http
import (
"git.gocasts.ir/ebhomengo/niki/pkg/claim"
httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/service/cart"
"git.gocasts.ir/ebhomengo/niki/types"
"github.com/labstack/echo/v4"
"net/http"
"strconv"
)
type Handler struct {
svc cart.Service
}
func NewHandler(svc cart.Service) Handler {
return Handler{svc: svc}
}
func (h Handler) addToBasket(c echo.Context) error {
claims := claim.GetClaimsFromEchoContext(c)
var req cart.AddToCartRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid request body",
})
}
req.UserID = types.ID(claims.UserID)
if err := h.svc.AddToBasket(c.Request().Context(), req); err != nil {
msg, code := httpmsg.Error(err)
return c.JSON(code, msg)
}
return c.NoContent(http.StatusNoContent)
}
func (h Handler) getCart(c echo.Context) error {
claims := claim.GetClaimsFromEchoContext(c)
res, err := h.svc.GetCart(c.Request().Context(), types.ID(claims.UserID))
if err != nil {
msg, code := httpmsg.Error(err)
return c.JSON(code, msg)
}
return c.JSON(http.StatusOK, res)
}
func (h Handler) removeCart(c echo.Context) error {
claims := claim.GetClaimsFromEchoContext(c)
if err := h.svc.ClearCart(c.Request().Context(), types.ID(claims.UserID)); err != nil {
msg, code := httpmsg.Error(err)
return c.JSON(code, msg)
}
return c.NoContent(http.StatusNoContent)
}
func (h Handler) removeItem(c echo.Context) error {
claims := claim.GetClaimsFromEchoContext(c)
p := c.Param("productID")
pID, err := strconv.Atoi(p)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid product id",
})
}
var req cart.RemoveFromCartRequest
req.UserID = types.ID(claims.UserID)
req.ProductID = types.ID(pID)
if err := h.svc.RemoveFromCart(c.Request().Context(), req); err != nil {
msg, code := httpmsg.Error(err)
return c.JSON(code, msg)
}
return c.NoContent(http.StatusNoContent)
}
func (h Handler) updateQuantity(c echo.Context) error {
claims := claim.GetClaimsFromEchoContext(c)
p := c.Param("productID")
pID, err := strconv.Atoi(p)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid product id",
})
}
qStr := c.Param("quantity")
q, err := strconv.Atoi(qStr)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid quantity",
})
}
var req cart.UpdateQuantityRequest
req.UserID = types.ID(claims.UserID)
req.ProductID = types.ID(pID)
req.Quantity = q
if err := h.svc.UpdateQuantity(c.Request().Context(), req); err != nil {
msg, code := httpmsg.Error(err)
return c.JSON(code, msg)
}
return c.NoContent(http.StatusNoContent)
}

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,43 @@
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("shoppingbasket/health-check", s.healthCheck)
r := router.Group("shoppingbasket/cart") // Authentication is required
r.GET("/", s.handler.getCart)
r.DELETE("/", s.handler.removeCart)
r.POST("/items", s.handler.addToBasket)
r.DELETE("/items/:productID", s.handler.removeItem)
r.PUT("/items/:productID/:quantity", s.handler.updateQuantity)
}

View File

@ -0,0 +1,272 @@
package repository
import (
"context"
"encoding/json"
"fmt"
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"
"strings"
"time"
)
const (
FieldNumber = 5
UserIDField = "user_id"
CreatedAtField = "created_at"
ExpireAtField = "expire_at"
TotalPriceField = "total_price"
)
type Config struct {
KartKeyPrefix string `koanf:"kart_key_prefix"`
TTL time.Duration `koanf:"ttl"`
}
type Repo struct {
client *redis.Client
config Config
}
func New(client *redis.Client, cfg Config) Repo {
return Repo{client: client, config: cfg}
}
func (r Repo) cartKey(userID types.ID) string {
return r.config.KartKeyPrefix + fmt.Sprintf("%d", userID)
}
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 {
const op = "shoppingbasketapp.repository.AddItem"
cartKey := r.cartKey(userID)
itemKey := r.itemKey(item.ProductID)
now := time.Now().UnixNano()
itemJson, _ := json.Marshal(item)
exists, _ := r.client.Exists(ctx, cartKey).Result()
if exists == 0 {
r.client.HSet(ctx, cartKey, map[string]interface{}{
UserIDField: userID,
itemKey: string(itemJson),
TotalPriceField: item.Price * types.Price(item.Quantity),
CreatedAtField: now,
ExpireAtField: now + r.config.TTL.Nanoseconds(),
})
} else {
existsItem, _ := r.client.HGet(ctx, cartKey, itemKey).Result()
if existsItem != "" {
var i cart.Item
if err := json.Unmarshal([]byte(existsItem), &i); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
item.Quantity += i.Quantity
itemJson, _ = json.Marshal(item)
}
r.client.HSet(ctx, cartKey, itemKey, string(itemJson))
r.client.HSet(ctx, cartKey, ExpireAtField, now+r.config.TTL.Nanoseconds())
if err := r.updateTotalPrice(ctx, cartKey); err != nil {
return err
}
}
r.client.Expire(ctx, cartKey, r.config.TTL)
return nil
}
func parsInt(s string) int64 {
i, _ := strconv.ParseInt(s, 10, 64)
return i
}
func (r Repo) GetCart(ctx context.Context, userID types.ID) (cart.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)
}
if exists == 0 {
return cart.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)
}
c := cart.Cart{Items: []cart.Item{}}
for field, value := range allCart {
if strings.HasPrefix(field, "item:") {
var i cart.Item
if err := json.Unmarshal([]byte(value), &i); err != nil {
return cart.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
c.Items = append(c.Items, i)
continue
}
switch field {
case UserIDField:
c.UserID = types.ID(parsInt(value))
case TotalPriceField:
c.TotalPrice = types.Price(parsInt(value))
case CreatedAtField:
c.CreatedAt = parsInt(value)
case ExpireAtField:
c.ExpireAt = parsInt(value)
}
}
return c, nil
}
func (r Repo) DeleteItem(ctx context.Context, userID, productID types.ID) error {
const op = "shoppingbasketapp.repository.DeleteItem"
cartKey := r.cartKey(userID)
itemKey := r.itemKey(productID)
if err := r.existsCart(ctx, cartKey); err != nil {
return err
}
if err := r.existsItem(ctx, cartKey, itemKey); err != nil {
return err
}
if err := r.client.HDel(ctx, cartKey, itemKey).Err(); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
num, err := r.client.HLen(ctx, cartKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if num < FieldNumber {
return r.DeleteCart(ctx, userID)
}
return r.updateTotalPrice(ctx, cartKey)
}
func (r Repo) UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error {
const op = "shoppingbasketapp.repository.UpdateQuantity"
cartKey := r.cartKey(userID)
itemKey := r.itemKey(productID)
if err := r.existsCart(ctx, cartKey); err != nil {
return err
}
if err := r.existsItem(ctx, cartKey, itemKey); err != nil {
return err
}
data, err := r.client.HGet(ctx, cartKey, itemKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
var item cart.Item
if err := json.Unmarshal([]byte(data), &item); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
item.Quantity = quantity
j, _ := json.Marshal(item)
if err := r.client.HSet(ctx, cartKey, itemKey, string(j)).Err(); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
return r.updateTotalPrice(ctx, cartKey)
}
func (r Repo) DeleteCart(ctx context.Context, userID types.ID) error {
const op = "shoppingbasketapp.repository.DeleteCart"
cartKey := r.cartKey(userID)
if err := r.existsCart(ctx, cartKey); err != nil {
return err
}
if err := r.client.Del(ctx, cartKey).Err(); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
return nil
}
func (r Repo) updateTotalPrice(ctx context.Context, cartKey string) error {
const op = "shoppingbasketapp.repository.updateTotalPrice"
allFields, err := r.client.HGetAll(ctx, cartKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
var total types.Price
for field, value := range allFields {
if strings.HasPrefix(field, "item:") {
var item cart.Item
if err := json.Unmarshal([]byte(value), &item); err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
total += item.Price * types.Price(item.Quantity)
}
}
return r.client.HSet(ctx, cartKey, TotalPriceField, int64(total)).Err()
}
func (r Repo) existsCart(ctx context.Context, cartKey string) error {
const op = "shoppingbasketapp.repository.existsCart"
exists, err := r.client.Exists(ctx, cartKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if exists == 0 {
return richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found shopping basket")
}
return nil
}
func (r Repo) existsItem(ctx context.Context, cartKey, itemKey string) error {
const op = "shoppingbasketapp.repository.existsCart"
exists, err := r.client.HExists(ctx, cartKey, itemKey).Result()
if err != nil {
return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err)
}
if !exists {
return richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found product form shopping basket")
}
return nil
}

View File

@ -0,0 +1,21 @@
package cart
import (
"git.gocasts.ir/ebhomengo/niki/types"
)
type Item struct {
ProductID types.ID
Quantity int
Price types.Price
Name string
AddedAt int64
}
type Cart struct {
UserID types.ID
Items []Item
TotalPrice types.Price
ExpireAt int64
CreatedAt int64
}

View File

@ -0,0 +1,30 @@
package cart
import "git.gocasts.ir/ebhomengo/niki/types"
type AddToCartRequest struct {
UserID types.ID `json:"user_id"`
ProductID types.ID `json:"product_id"`
Quantity int `json:"quantity"`
Price types.Price `json:"price"`
Name string `json:"name"`
}
type GetCartResponse struct {
UserID types.ID `json:"user_id"`
Items []Item `json:"items"`
TotalPrice types.Price `json:"total_price"`
CreatedAt int64 `json:"created_at"`
ExpireAt int64 `json:"expire_at"`
}
type RemoveFromCartRequest struct {
UserID types.ID `json:"user_id"`
ProductID types.ID `json:"product_id"`
}
type UpdateQuantityRequest struct {
UserID types.ID `json:"user_id"`
ProductID types.ID `json:"product_id"`
Quantity int `json:"quantity"`
}

View File

@ -0,0 +1,103 @@
package cart
import (
"context"
"git.gocasts.ir/ebhomengo/niki/pkg/logger"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type Repository interface {
AddItem(ctx context.Context, userID types.ID, item Item) error
GetCart(ctx context.Context, userID types.ID) (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
}
type Service struct {
validate Validate
repo Repository
}
func New(val Validate, repo Repository) Service {
return Service{validate: val, repo: repo}
}
func (s Service) AddToBasket(ctx context.Context, req AddToCartRequest) error {
const op = "shoppingbasketapp.service.AddToBasket"
if err := s.validate.ValidateAddToCart(req); err != nil {
logger.L().Error("shoppingbasket-service-AddToBasket", "error", err)
return err
}
return s.repo.AddItem(ctx, req.UserID, Item{
ProductID: req.ProductID,
Quantity: req.Quantity,
Price: req.Price,
Name: req.Name,
AddedAt: time.Now().UnixNano(),
})
}
func (s Service) GetCart(ctx context.Context, userID types.ID) (GetCartResponse, error) {
const op = "shoppingbasketapp.service.GetCart"
if userID < 1 {
logger.L().Error("shoppingbasket-service-GetCart", "error", "user id must be greater than 1")
return GetCartResponse{}, richerror.New(op).WithKind(richerror.KindInvalid).WithMessage("invalid user id")
}
res, err := s.repo.GetCart(ctx, userID)
if err != nil {
logger.L().Error("shoppingbasket-service-GetCart", "error", err)
return GetCartResponse{}, richerror.New(op).WithErr(err)
}
return GetCartResponse{
UserID: res.UserID,
Items: res.Items,
TotalPrice: res.TotalPrice,
CreatedAt: res.CreatedAt,
ExpireAt: res.ExpireAt,
}, nil
}
func (s Service) RemoveFromCart(ctx context.Context, req RemoveFromCartRequest) error {
const op = "shoppingbaskerapp.service.RemoveFromCart"
if err := s.validate.ValidateRemoveFromCart(req); err != nil {
logger.L().Error("shoppingbasket-service-RemoveFromCart", "error", err)
return err
}
return s.repo.DeleteItem(ctx, req.UserID, req.ProductID)
}
func (s Service) UpdateQuantity(ctx context.Context, req UpdateQuantityRequest) error {
const op = "shoppingbaskerapp.service.UpdateQuantity"
if err := s.validate.ValidateUpdateQuantity(req); err != nil {
logger.L().Error("shoppingbasket-service-UpdateQuantity", "error", err)
return err
}
if req.Quantity == 0 {
return s.repo.DeleteItem(ctx, req.UserID, req.ProductID)
}
return s.repo.UpdateQuantity(ctx, req.UserID, req.ProductID, req.Quantity)
}
func (s Service) ClearCart(ctx context.Context, userID types.ID) error {
const op = "shoppingbaskerapp.service.ClearCart"
if userID < 1 {
logger.L().Error("shoppingbasket-service-ClearCart", "error", "user id must be greater than 1")
return richerror.New(op).WithKind(richerror.KindInvalid).
WithMessage("invalid user id")
}
return s.repo.DeleteCart(ctx, userID)
}

View File

@ -0,0 +1,91 @@
package cart
import (
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
validation "github.com/go-ozzo/ozzo-validation/v4"
)
const (
ErrValidationPositive = "must be positive"
ErrValidationInvalidInput = "invalid input"
)
type Validate struct{}
func NewValidate() Validate {
return Validate{}
}
func (v Validate) ValidateAddToCart(req AddToCartRequest) error {
const op = "shoppingbasketapp.service.AddToCart"
if err := validation.ValidateStruct(&req,
validation.Field(&req.UserID, validation.Required),
validation.Field(&req.ProductID, validation.Required),
validation.Field(&req.Price, validation.Required, validation.Min(int64(1)).Error(ErrValidationPositive)),
validation.Field(&req.Quantity, validation.Min(int(1)).Error(ErrValidationPositive)),
validation.Field(&req.Name, validation.Required)); err != nil {
fieldErr := make(map[string]interface{})
vErr, ok := err.(validation.Errors)
if ok {
for key, value := range vErr {
if value != nil {
fieldErr[key] = value.Error()
}
}
}
return richerror.New(op).WithMessage(ErrValidationInvalidInput).
WithMeta(fieldErr).WithErr(err).WithKind(richerror.KindInvalid)
}
return nil
}
func (v Validate) ValidateRemoveFromCart(req RemoveFromCartRequest) error {
const op = "shoppingbasketapp.service.ValidateRemoveFromCart"
if err := validation.ValidateStruct(&req,
validation.Field(&req.UserID, validation.Required),
validation.Field(&req.ProductID, validation.Required)); err != nil {
fieldErrs := make(map[string]interface{})
vErr, ok := err.(validation.Errors)
if ok {
for key, value := range vErr {
if value != nil {
fieldErrs[key] = value.Error()
}
}
}
return richerror.New(op).WithMessage(ErrValidationInvalidInput).
WithKind(richerror.KindInvalid).WithMeta(fieldErrs).WithErr(err)
}
return nil
}
func (v Validate) ValidateUpdateQuantity(req UpdateQuantityRequest) error {
const op = "shoppingbasketapp.service.ValidateUpdateQuantity"
if err := validation.ValidateStruct(&req,
validation.Field(&req.UserID, validation.Required),
validation.Field(&req.ProductID, validation.Required),
validation.Field(&req.Quantity, validation.Required, validation.Min(int(1)))); err != nil {
fieldErrs := make(map[string]interface{})
vErr, ok := err.(validation.Errors)
if ok {
for key, value := range vErr {
if value != nil {
fieldErrs[key] = value.Error()
}
}
}
return richerror.New(op).WithMessage(ErrValidationInvalidInput).
WithKind(richerror.KindInvalid).WithMeta(fieldErrs).WithErr(err)
}
return nil
}