feat(order): add create order

This commit is contained in:
Sahar Mokarrami 2026-04-04 11:34:30 +03:30
parent a193abeb56
commit c841ffb21d
26 changed files with 448 additions and 48 deletions

View File

@ -0,0 +1 @@
package command

View File

@ -0,0 +1 @@
package command

View File

@ -0,0 +1 @@
package command

63
cmd/purchaseapp/main.go Normal file
View File

@ -0,0 +1,63 @@
package main
import (
"flag"
"fmt"
"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"
)
func MariaDB() *mysql.DB {
cfg := mysql.Config{
Username: "niki",
Password: "nikiappt0lk2o20",
Port: 3306,
Host: "localhost",
DBName: "niki_db",
}
migrate := flag.Bool("migrate", false, "perform database migration")
flag.Parse()
if *migrate {
migrator.New(migrator.Config{
MysqlConfig: cfg,
MigrationPath: "./purchaseapp/repository/mysql/migration",
MigrationDBName: "gorp_migrations",
}).Up()
}
return mysql.New(cfg)
}
func main() {
cfg := mysql.Config{
Username: "niki",
Password: "nikiappt0lk2o20",
Port: 3306,
Host: "localhost",
DBName: "niki_db",
}
db := mysql.New(cfg)
defer func() {
if err := db.CloseStatements(); err != nil {
fmt.Printf("Error closing statements: %v\n", err)
}
}()
orderRepo := purchaseMysql.New(db)
orderSvc := Service(orderRepo)
server := HTTPServer(orderSvc)
server.Serve()
}
func HTTPServer(orderSvc order.Service) *http.Server {
return http.New(orderSvc)
}
func Service(orderRepo *purchaseMysql.DB) order.Service {
return order.New(orderRepo)
}

View File

@ -12,12 +12,13 @@ import (
)
const (
defaultPrefix = "EB_"
defaultDelimiter = "."
defaultSeparator = "__"
defaultYamlFilePath = "config.yml"
defaultPrefix = "EB_"
defaultDelimiter = "."
defaultSeparator = "__"
)
var defaultYamlFilePath = "config.yml"
var c Config
type Option struct {

View File

@ -1 +1,59 @@
package purchaseapp
import (
"fmt"
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"
)
type Application struct {
Config Config
HTTPServer *purchaseHTTP.Server
purchaseService purchaseService.Service
PurchaseHandler *purchaseHandler.Handler
PurchaseRepo purchaseService.Repo
DB *mysql.DB
}
func SetUp() *Application {
cfg := mysql.Config{
Username: "niki",
Password: "nikiappt0lk2o20",
Port: 3306,
Host: "localhost",
DBName: "niki_db",
}
db := mysql.New(cfg)
defer func() {
if err := db.CloseStatements(); err != nil {
fmt.Printf("Error closing statements: %v\n", err)
}
}()
orderRepo := purchaseMysql.New(db)
orderSvc := Service(orderRepo)
server := HTTPServer(orderSvc)
return &Application{
Config: Config{},
HTTPServer: server,
purchaseService: orderSvc,
PurchaseHandler: purchaseHandler.New(orderSvc),
PurchaseRepo: orderRepo,
DB: db,
}
}
func HTTPServer(orderSvc purchaseService.Service) *purchaseHTTP.Server {
return purchaseHTTP.New(orderSvc)
}
func Service(orderRepo *purchaseMysql.DB) purchaseService.Service {
return purchaseService.New(orderRepo)
}

View File

@ -1,4 +1,9 @@
package purchaseapp
import (
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
)
type Config struct {
Mysql mysql.Config `koanf:"mariadb"`
}

View File

@ -1,8 +0,0 @@
package invoice
type Handler struct {
}
func New() *Handler {
return &Handler{}
}

View File

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

View File

@ -1,7 +1,69 @@
package order
type Handler struct{}
import (
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
"git.gocasts.ir/ebhomengo/niki/types"
"github.com/labstack/echo/v4"
"net/http"
"time"
)
func New() *Handler {
return &Handler{}
type Handler struct {
orderSvc *order.Service
}
func New(orderSvc order.Service) *Handler {
return &Handler{orderSvc: &orderSvc}
}
func (h *Handler) CreateOrderHandler(c echo.Context) error {
var req order.CreateOrderRequest
if err := c.Bind(&req); err != nil {
msg, code := getErrorDataFromRichError(err)
return echo.NewHTTPError(code, msg)
}
orderItems := req.OrderItems
order := entity.Order{
ID: 0,
UserID: types.ID(req.UserID),
TotalAmount: types.Price(req.TotalAmount),
TotalDiscount: types.Price(req.TotalDiscount),
ShippingID: types.ID(req.ShippingID),
PaymentMethod: req.PaymentMethod,
ProcessStatus: entity.WaitingToPay,
PaymentStatus: entity.UnPaid,
Address: req.Address,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
resp, lErr := h.orderSvc.CreateOrder(order, orderItems)
if lErr != nil {
msg, code := getErrorDataFromRichError(lErr)
return echo.NewHTTPError(code, msg)
}
return c.JSON(http.StatusOK, resp)
}
func getErrorDataFromRichError(err error) (message string, code int) {
switch err.(type) {
case richerror.RichError:
re := err.(richerror.RichError)
return re.Message(), mapKindToCode(re.Kind())
default:
return err.Error(), http.StatusBadRequest
}
}
func mapKindToCode(kind richerror.Kind) int {
switch kind {
case richerror.KindInvalid:
return http.StatusUnprocessableEntity
default:
return http.StatusBadRequest
}
}

View File

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

View File

@ -1,33 +1,32 @@
package http
import (
httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/invoice"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/order"
orderService "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Server struct {
HTTPServer *httpserver.Server
OrderHandler *order.Handler
InvoiceHandler *invoice.Handler
OrderHandler *order.Handler
}
func New(httpserver *httpserver.Server) *Server {
func New(orderSvc orderService.Service) *Server {
return &Server{
HTTPServer: httpserver,
OrderHandler: order.New(),
InvoiceHandler: invoice.New(),
OrderHandler: order.New(orderSvc),
}
}
func (s *Server) Serve() {
s.RegisterRoutes()
e := echo.New()
e.Use(middleware.RequestLogger())
e.GET("/purchase/health-check", s.healthCheck)
s.OrderHandler.SetRoutes(e)
if err := e.Start(":8088"); err != nil {
e.Logger.Error("failed to start server", "error", err)
}
}
func (s *Server) Stop() {}
func (s *Server) RegisterRoutes() {
s.HTTPServer.Router.GET("/purchase/health-check", s.healthCheck)
s.OrderHandler.SetRoutes(s.HTTPServer.Router)
s.InvoiceHandler.SetRoutes(s.HTTPServer.Router)
}

View File

@ -1,4 +0,0 @@
package entity
type Invoice struct {
}

View File

@ -1,4 +1,59 @@
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

@ -0,0 +1,23 @@
-- +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,
`user_id` INT NOT NULL,
`address` TEXT,
`shipping_id` INT NOT NULL,
`payment_method` ENUM('online', 'wallet', 'cart') DEFAULT 'online',
`payment_status` ENUM('unpaid', 'paid', 'cancelled') 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,
`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`)
);
-- +migrate Down
DROP TABLE `orders`;

View File

@ -0,0 +1,19 @@
-- +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 `order_items` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`order_id` INT NOT NULL,
`product_id` INT NOT NULL,
`quantity` INT DEFAULT 1,
`price` INT NOT NULL,
`price_with_discount` INT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`)
);
-- +migrate Down
DROP TABLE `order_items`;

View File

@ -0,0 +1,69 @@
package mysql
import (
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"
tx, err := d.conn.Conn().Begin()
if err != nil {
return 0, richerror.New(Op).WithErr(err)
}
defer tx.Rollback()
query := "insert into orders(user_id, address, shipping_id," +
" payment_method, payment_status, process_status," +
" total_amount, total_discount) values (?, ?, ?, ?, ?, ?, ?, ?);"
res, oErr := tx.Exec(query, order.UserID, order.Address, order.ShippingID,
order.PaymentMethod, order.PaymentStatus, order.ProcessStatus,
order.TotalAmount, order.TotalDiscount)
if oErr != nil {
return 0, richerror.New(Op).WithErr(oErr)
}
orderID, insertIDErr := res.LastInsertId()
if insertIDErr != nil {
return 0, richerror.New(Op).WithErr(insertIDErr)
}
orderItemQuery := "insert into order_items(order_id, product_id, quantity, price, price_with_discount) values(?, ?, ?, ?, ?);"
for _, item := range orderItems {
_, iErr := tx.Exec(orderItemQuery, orderID, item.ProductID, item.Quantity, item.Price, item.PriceWithDiscount)
if iErr != nil {
return 0, richerror.New(Op).WithErr(iErr)
}
}
if err := tx.Commit(); err != nil {
return 0, richerror.New(Op).WithErr(err)
}
return types.ID(orderID), nil
}
func (d *DB) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
const Op = "repository.mysql.order.update-order-process-status"
_, err := d.conn.Conn().Exec("update orders set process_status=? where id=?;", status, orderID)
if err != nil {
return false, richerror.New(Op).WithErr(err)
}
return true, nil
}

View File

@ -1 +0,0 @@
package invoice

View File

@ -1,4 +0,0 @@
package invoice
type Service struct {
}

View File

@ -1 +0,0 @@
package invoice

View File

@ -1 +1,20 @@
package order
import (
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
"git.gocasts.ir/ebhomengo/niki/types"
)
type CreateOrderRequest struct {
UserID types.ID `json:"user_id"`
Address string `json:"address"`
ShippingID types.ID `json:"shipping_id"`
PaymentMethod entity.PaymentMethod `json:"payment_method"`
TotalAmount types.Price `json:"total_amount"`
TotalDiscount types.Price `json:"total_discount"`
OrderItems []entity.OrderItem `json:"order_items"`
}
type CreateOrderResponse struct {
OrderID types.ID
}

View File

@ -1,4 +1,42 @@
package order
import (
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/purchaseapp/entity"
"git.gocasts.ir/ebhomengo/niki/types"
)
type Service struct {
repo Repo
}
type Repo interface {
CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error)
UpdateOrderProcessStatus(orderID types.ID, status string) (bool, 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"
orderID, err := s.repo.CreateOrder(order, orderItems)
if err != nil {
return CreateOrderResponse{}, richerror.New(Op).WithErr(err)
}
return CreateOrderResponse{OrderID: orderID}, nil
}
func (s Service) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) {
const Op = "purchaseapp.service.UpdateOrderProcessStatus"
_, err := s.repo.UpdateOrderProcessStatus(orderID, status)
if err != nil {
return false, richerror.New(Op).WithErr(err)
}
return true, nil
}

View File

@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"sync"
"time"

3
types/count.go Normal file
View File

@ -0,0 +1,3 @@
package types
type Count uint32

3
types/id.go Normal file
View File

@ -0,0 +1,3 @@
package types
type ID uint64

3
types/price.go Normal file
View File

@ -0,0 +1,3 @@
package types
type Price uint64