Merge pull request 'orders' (#58) from orders into main

Reviewed-on: #58
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
This commit was merged in pull request #58.
This commit is contained in:
2026-04-14 11:20:05 +00:00
24 changed files with 750 additions and 61 deletions

View File

@@ -12,4 +12,6 @@ const (
ProductTranslationSave Permission = "product_translation.save" ProductTranslationSave Permission = "product_translation.save"
ProductTranslationTranslate Permission = "product_translation.translate" ProductTranslationTranslate Permission = "product_translation.translate"
SearchCreateIndex Permission = "search.create_index" SearchCreateIndex Permission = "search.create_index"
OrdersViewAll Permission = "orders.view_all"
OrdersModifyAll Permission = "orders.modify_all"
) )

View File

@@ -124,13 +124,13 @@ func (h *AddressesHandler) RetrieveAddressesInfo(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
addresses_info, err := h.addressesService.RetrieveAddressesInfo(userID) addresses, err := h.addressesService.RetrieveAddresses(userID)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
return c.JSON(response.Make(&addresses_info, 0, i18n.T_(c, response.Message_OK))) return c.JSON(response.Make(addresses, 0, i18n.T_(c, response.Message_OK)))
} }
func (h *AddressesHandler) DeleteAddress(c fiber.Ctx) error { func (h *AddressesHandler) DeleteAddress(c fiber.Ctx) error {

View File

@@ -0,0 +1,171 @@
package restricted
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/orderService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
type OrdersHandler struct {
ordersService *orderService.OrderService
}
func NewOrdersHandler() *OrdersHandler {
ordersService := orderService.New()
return &OrdersHandler{
ordersService: ordersService,
}
}
func OrdersHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewOrdersHandler()
r.Get("/list", handler.ListOrders)
r.Post("/place-new-order", handler.PlaceNewOrder)
r.Post("/change-order-address", handler.ChangeOrderAddress)
r.Get("/change-order-status", handler.ChangeOrderStatus)
return r
}
// when a user (not admin) wants to list orders, we automatically append filter to only view his orders.
// we base permissions and user based on target user only.
func (h *OrdersHandler) ListOrders(c fiber.Ctx) error {
user, ok := localeExtractor.GetCustomer(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
paging, filters, err := query_params.ParseFilters[model.CustomerOrder](c, columnMappingListOrders)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
list, err := h.ordersService.Find(user, paging, filters)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK)))
}
var columnMappingListOrders map[string]string = map[string]string{
"order_id": "b2b_customer_orders.order_id",
"user_id": "b2b_customer_orders.user_id",
"name": "b2b_customer_orders.name",
"country_id": "b2b_customer_orders.country_id",
"status": "b2b_customer_orders.status",
}
func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error {
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
cart_id_attribute := c.Query("cart_id")
cart_id, err := strconv.Atoi(cart_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
country_id_attribute := c.Query("country_id")
country_id, err := strconv.Atoi(country_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
address_info := string(c.Body())
if address_info == "" {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
name := c.Query("name")
err = h.ordersService.PlaceNewOrder(userID, uint(cart_id), name, uint(country_id), address_info)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
// we base permissions and user based on target user only.
func (h *OrdersHandler) ChangeOrderAddress(c fiber.Ctx) error {
user, ok := localeExtractor.GetCustomer(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
order_id_attribute := c.Query("order_id")
order_id, err := strconv.Atoi(order_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
country_id_attribute := c.Query("country_id")
country_id, err := strconv.Atoi(country_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
address_info := string(c.Body())
if address_info == "" {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
err = h.ordersService.ChangeOrderAddress(user, uint(order_id), uint(country_id), address_info)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
// we base permissions and user based on target user only.
// TODO: well, permissions and all that.
func (h *OrdersHandler) ChangeOrderStatus(c fiber.Ctx) error {
user, ok := localeExtractor.GetCustomer(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
order_id_attribute := c.Query("order_id")
order_id, err := strconv.Atoi(order_id_attribute)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
status := c.Query("status")
err = h.ordersService.ChangeOrderStatus(user, uint(order_id), status)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -132,8 +132,13 @@ func (s *Server) Setup() error {
carts := s.restricted.Group("/carts") carts := s.restricted.Group("/carts")
restricted.CartsHandlerRoutes(carts) restricted.CartsHandlerRoutes(carts)
// orders (restricted)
orders := s.restricted.Group("/orders")
restricted.OrdersHandlerRoutes(orders)
specificPrice := s.restricted.Group("/specific-price") specificPrice := s.restricted.Group("/specific-price")
restricted.SpecificPriceHandlerRoutes(specificPrice) restricted.SpecificPriceHandlerRoutes(specificPrice)
// addresses (restricted) // addresses (restricted)
addresses := s.restricted.Group("/addresses") addresses := s.restricted.Group("/addresses")
restricted.AddressesHandlerRoutes(addresses) restricted.AddressesHandlerRoutes(addresses)

View File

@@ -3,7 +3,8 @@ package model
type Address struct { type Address struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"` CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"`
AddressInfo string `gorm:"column:address_info;not null" json:"address_info"` AddressString string `gorm:"column:address_string;not null" json:"address_string"`
AddressUnparsed *AddressUnparsed `gorm:"-" json:"address_unparsed"`
CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"` CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"`
} }
@@ -11,15 +12,7 @@ func (Address) TableName() string {
return "b2b_addresses" return "b2b_addresses"
} }
type AddressUnparsed struct { type AddressUnparsed interface{}
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"`
AddressInfo AddressField `gorm:"column:address_info;not null" json:"address_info"`
CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"`
}
type AddressField interface {
}
// Address template in Poland // Address template in Poland
type AddressPL struct { type AddressPL struct {

27
app/model/order.go Normal file
View File

@@ -0,0 +1,27 @@
package model
type CustomerOrder struct {
OrderID uint `gorm:"column:order_id;primaryKey;autoIncrement" json:"order_id"`
UserID uint `gorm:"column:user_id;not null;index" json:"user_id"`
Name string `gorm:"column:name;not null" json:"name"`
CountryID uint `gorm:"column:country_id;not null" json:"country_id"`
AddressString string `gorm:"column:address_string;not null" json:"address_string"`
AddressUnparsed *AddressUnparsed `gorm:"-" json:"address_unparsed"`
Status string `gorm:"column:status;size:50;not null" json:"status"`
Products []OrderProduct `gorm:"foreignKey:OrderID;references:OrderID" json:"products"`
}
func (CustomerOrder) TableName() string {
return "b2b_customer_orders"
}
type OrderProduct struct {
OrderID uint `gorm:"column:order_id;not null;index" json:"-"`
ProductID uint `gorm:"column:product_id;not null" json:"product_id"`
ProductAttributeID *uint `gorm:"column:product_attribute_id" json:"product_attribute_id,omitempty"`
Amount uint `gorm:"column:amount;not null" json:"amount"`
}
func (OrderProduct) TableName() string {
return "b2b_orders_products"
}

View File

@@ -49,7 +49,7 @@ func (repo *AddressesRepo) UserAddressesAmt(user_id uint) (uint, error) {
func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, country_id uint) error { func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, country_id uint) error {
address := model.Address{ address := model.Address{
CustomerID: user_id, CustomerID: user_id,
AddressInfo: address_info, AddressString: address_info,
CountryID: country_id, CountryID: country_id,
} }
@@ -62,7 +62,7 @@ func (repo *AddressesRepo) UpdateAddress(user_id uint, address_id uint, address_
address := model.Address{ address := model.Address{
ID: address_id, ID: address_id,
CustomerID: user_id, CustomerID: user_id,
AddressInfo: address_info, AddressString: address_info,
CountryID: country_id, CountryID: country_id,
} }

View File

@@ -9,11 +9,12 @@ import (
type UICartsRepo interface { type UICartsRepo interface {
CartsAmount(user_id uint) (uint, error) CartsAmount(user_id uint) (uint, error)
CreateNewCart(user_id uint) (model.CustomerCart, error) CreateNewCart(user_id uint) (model.CustomerCart, error)
UserHasCart(user_id uint, cart_id uint) (uint, error) RemoveCart(user_id uint, cart_id uint) error
UserHasCart(user_id uint, cart_id uint) (bool, error)
UpdateCartName(user_id uint, cart_id uint, new_name string) error UpdateCartName(user_id uint, cart_id uint, new_name string) error
RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error) RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error)
RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error)
CheckProductExists(product_id uint, product_attribute_id *uint) (uint, error) CheckProductExists(product_id uint, product_attribute_id *uint) (bool, error)
AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error
} }
@@ -49,7 +50,15 @@ func (repo *CartsRepo) CreateNewCart(user_id uint) (model.CustomerCart, error) {
return cart, err return cart, err
} }
func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (uint, error) { func (repo *CartsRepo) RemoveCart(user_id uint, cart_id uint) error {
return db.DB.
Table("b2b_customer_carts").
Where("cart_id = ? AND user_id = ?", cart_id, user_id).
Delete(nil).
Error
}
func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (bool, error) {
var amt uint var amt uint
err := db.DB. err := db.DB.
@@ -59,7 +68,7 @@ func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (uint, error) {
Scan(&amt). Scan(&amt).
Error Error
return amt, err return amt >= 1, err
} }
func (repo *CartsRepo) UpdateCartName(user_id uint, cart_id uint, new_name string) error { func (repo *CartsRepo) UpdateCartName(user_id uint, cart_id uint, new_name string) error {
@@ -96,7 +105,7 @@ func (repo *CartsRepo) RetrieveCart(user_id uint, cart_id uint) (*model.Customer
return &cart, err return &cart, err
} }
func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id *uint) (uint, error) { func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id *uint) (bool, error) {
var amt uint var amt uint
if product_attribute_id == nil { if product_attribute_id == nil {
@@ -106,7 +115,7 @@ func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id
Where("id_product = ?", product_id). Where("id_product = ?", product_id).
Scan(&amt). Scan(&amt).
Error Error
return amt, err return amt >= 1, err
} else { } else {
err := db.DB. err := db.DB.
@@ -116,7 +125,7 @@ func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id
Where("ps.id_product = ? AND pas.id_product_attribute = ?", product_id, *product_attribute_id). Where("ps.id_product = ? AND pas.id_product_attribute = ?", product_id, *product_attribute_id).
Scan(&amt). Scan(&amt).
Error Error
return amt, err return amt >= 1, err
} }
} }

View File

@@ -0,0 +1,110 @@
package ordersRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
)
type UIOrdersRepo interface {
UserHasOrder(user_id uint, order_id uint) (bool, error)
Find(user_id uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error)
PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string) error
ChangeOrderAddress(order_id uint, country_id uint, address_info string) error
ChangeOrderStatus(order_id uint, status string) error
}
type OrdersRepo struct{}
func New() UIOrdersRepo {
return &OrdersRepo{}
}
func (repo *OrdersRepo) UserHasOrder(user_id uint, order_id uint) (bool, error) {
var amt uint
err := db.DB.
Table("b2b_customer_orders").
Select("COUNT(*) AS amt").
Where("user_id = ? AND order_id = ?", user_id, order_id).
Scan(&amt).
Error
return amt >= 1, err
}
func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) {
var list []model.CustomerOrder
var total int64
query := db.Get().
Model(&model.CustomerOrder{}).
Preload("Products").
Order("b2b_customer_orders.order_id DESC")
// Apply all filters
if filt != nil {
filt.ApplyAll(query)
}
// run counter first as query is without limit and offset
err := query.Count(&total).Error
if err != nil {
return &find.Found[model.CustomerOrder]{}, err
}
err = query.
Limit(p.Limit()).
Offset(p.Offset()).
Find(&list).Error
if err != nil {
return &find.Found[model.CustomerOrder]{}, err
}
return &find.Found[model.CustomerOrder]{
Items: list,
Count: uint(total),
}, nil
}
func (repo *OrdersRepo) PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string) error {
order := model.CustomerOrder{
UserID: cart.UserID,
Name: name,
CountryID: country_id,
AddressString: address_info,
Status: constdata.NEW_ORDER_STATUS,
Products: make([]model.OrderProduct, 0, len(cart.Products)),
}
for _, product := range cart.Products {
order.Products = append(order.Products, model.OrderProduct{
ProductID: product.ProductID,
ProductAttributeID: product.ProductAttributeID,
Amount: product.Amount,
})
}
return db.DB.Create(&order).Error
}
func (repo *OrdersRepo) ChangeOrderAddress(order_id uint, country_id uint, address_info string) error {
return db.DB.
Table("b2b_customer_orders").
Where("order_id = ?", order_id).
Updates(map[string]interface{}{
"country_id": country_id,
"address_string": address_info,
}).
Error
}
func (repo *OrdersRepo) ChangeOrderStatus(order_id uint, status string) error {
return db.DB.
Table("b2b_customer_orders").
Where("order_id = ?", order_id).
Update("status", status).
Error
}

View File

@@ -21,7 +21,7 @@ func New() *AddressesService {
} }
} }
func (s *AddressesService) GetTemplate(country_id uint) (model.AddressField, error) { func (s *AddressesService) GetTemplate(country_id uint) (model.AddressUnparsed, error) {
switch country_id { switch country_id {
case 1: // Poland case 1: // Poland
@@ -49,7 +49,7 @@ func (s *AddressesService) AddNewAddress(user_id uint, address_info string, coun
return responseErrors.ErrMaxAmtOfAddressesReached return responseErrors.ErrMaxAmtOfAddressesReached
} }
_, err = s.validateAddressJson(address_info, country_id) _, err = s.ValidateAddressJson(address_info, country_id)
if err != nil { if err != nil {
return err return err
} }
@@ -66,7 +66,7 @@ func (s *AddressesService) ModifyAddress(user_id uint, address_id uint, address_
return responseErrors.ErrUserHasNoSuchAddress return responseErrors.ErrUserHasNoSuchAddress
} }
_, err = s.validateAddressJson(address_info, country_id) _, err = s.ValidateAddressJson(address_info, country_id)
if err != nil { if err != nil {
return err return err
} }
@@ -74,30 +74,23 @@ func (s *AddressesService) ModifyAddress(user_id uint, address_id uint, address_
return s.repo.UpdateAddress(user_id, address_id, address_info, country_id) return s.repo.UpdateAddress(user_id, address_id, address_info, country_id)
} }
func (s *AddressesService) RetrieveAddressesInfo(user_id uint) (*[]model.AddressUnparsed, error) { func (s *AddressesService) RetrieveAddresses(user_id uint) (*[]model.Address, error) {
parsed_addresses, err := s.repo.RetrieveAddresses(user_id) addresses, err := s.repo.RetrieveAddresses(user_id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var unparsed_addresses []model.AddressUnparsed for i := 0; i < len(*addresses); i++ {
address_unparsed, err := s.ValidateAddressJson((*addresses)[i].AddressString, (*addresses)[i].CountryID)
for i := 0; i < len(*parsed_addresses); i++ {
var next_address model.AddressUnparsed
next_address.ID = (*parsed_addresses)[i].ID
next_address.CustomerID = (*parsed_addresses)[i].CustomerID
next_address.CountryID = (*parsed_addresses)[i].CountryID
next_address.AddressInfo, err = s.validateAddressJson((*parsed_addresses)[i].AddressInfo, next_address.CountryID)
// log such errors // log such errors
if err != nil { if err != nil {
fmt.Printf("err: %v\n", err) fmt.Printf("err: %v\n", err)
} }
unparsed_addresses = append(unparsed_addresses, next_address) (*addresses)[i].AddressUnparsed = &address_unparsed
} }
return &unparsed_addresses, nil return addresses, nil
} }
func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error { func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error {
@@ -112,7 +105,7 @@ func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error {
} }
// validateAddressJson makes sure that the info string represents a valid json of address in given country // validateAddressJson makes sure that the info string represents a valid json of address in given country
func (s *AddressesService) validateAddressJson(info string, country_id uint) (model.AddressField, error) { func (s *AddressesService) ValidateAddressJson(info string, country_id uint) (model.AddressUnparsed, error) {
dec := json.NewDecoder(strings.NewReader(info)) dec := json.NewDecoder(strings.NewReader(info))
dec.DisallowUnknownFields() dec.DisallowUnknownFields()

View File

@@ -34,12 +34,24 @@ func (s *CartsService) CreateNewCart(user_id uint) (model.CustomerCart, error) {
return cart, nil return cart, nil
} }
func (s *CartsService) UpdateCartName(user_id uint, cart_id uint, new_name string) error { func (s *CartsService) RemoveCart(user_id uint, cart_id uint) error {
amt, err := s.repo.UserHasCart(user_id, cart_id) exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil { if err != nil {
return err return err
} }
if amt != 1 { if !exists {
return responseErrors.ErrUserHasNoSuchCart
}
return s.repo.RemoveCart(user_id, cart_id)
}
func (s *CartsService) UpdateCartName(user_id uint, cart_id uint, new_name string) error {
exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchCart return responseErrors.ErrUserHasNoSuchCart
} }
@@ -51,11 +63,11 @@ func (s *CartsService) RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, er
} }
func (s *CartsService) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) { func (s *CartsService) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) {
amt, err := s.repo.UserHasCart(user_id, cart_id) exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if amt != 1 { if !exists {
return nil, responseErrors.ErrUserHasNoSuchCart return nil, responseErrors.ErrUserHasNoSuchCart
} }
@@ -63,19 +75,19 @@ func (s *CartsService) RetrieveCart(user_id uint, cart_id uint) (*model.Customer
} }
func (s *CartsService) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error { func (s *CartsService) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error {
amt, err := s.repo.UserHasCart(user_id, cart_id) exists, err := s.repo.UserHasCart(user_id, cart_id)
if err != nil { if err != nil {
return err return err
} }
if amt != 1 { if !exists {
return responseErrors.ErrUserHasNoSuchCart return responseErrors.ErrUserHasNoSuchCart
} }
amt, err = s.repo.CheckProductExists(product_id, product_attribute_id) exists, err = s.repo.CheckProductExists(product_id, product_attribute_id)
if err != nil { if err != nil {
return err return err
} }
if amt != 1 { if !exists {
return responseErrors.ErrProductOrItsVariationDoesNotExist return responseErrors.ErrProductOrItsVariationDoesNotExist
} }

View File

@@ -117,6 +117,18 @@ func (s *EmailService) SendNewUserAdminNotification(userEmail, userName, baseURL
return s.SendEmail(s.config.AdminEmail, subject, body) return s.SendEmail(s.config.AdminEmail, subject, body)
} }
// SendNewOrderPlacedNotification sends an email to admin when new order is placed
func (s *EmailService) SendNewOrderPlacedNotification(userID uint) error {
if s.config.AdminEmail == "" {
return nil // No admin email configured
}
subject := "New Order Created"
body := s.newOrderPlacedTemplate(userID)
return s.SendEmail(s.config.AdminEmail, subject, body)
}
// verificationEmailTemplate returns the HTML template for email verification // verificationEmailTemplate returns the HTML template for email verification
func (s *EmailService) verificationEmailTemplate(name, verificationURL string, langID uint) string { func (s *EmailService) verificationEmailTemplate(name, verificationURL string, langID uint) string {
buf := bytes.Buffer{} buf := bytes.Buffer{}
@@ -137,3 +149,10 @@ func (s *EmailService) newUserAdminNotificationTemplate(userEmail, userName, bas
emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf) emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf)
return buf.String() return buf.String()
} }
// newUserAdminNotificationTemplate returns the HTML template for admin notification
func (s *EmailService) newOrderPlacedTemplate(userID uint) string {
buf := bytes.Buffer{}
emails.EmailNewOrderPlacedWrapper(view.EmailLayout[view.EmailNewOrderPlacedData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailNewOrderPlacedData{UserID: userID}}).Render(context.Background(), &buf)
return buf.String()
}

View File

@@ -0,0 +1,145 @@
package orderService
import (
"fmt"
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/cartsRepo"
"git.ma-al.com/goc_daniel/b2b/app/repos/ordersRepo"
"git.ma-al.com/goc_daniel/b2b/app/service/addressesService"
"git.ma-al.com/goc_daniel/b2b/app/service/emailService"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
)
type OrderService struct {
ordersRepo ordersRepo.UIOrdersRepo
cartsRepo cartsRepo.UICartsRepo
addressesService *addressesService.AddressesService
emailService *emailService.EmailService
}
func New() *OrderService {
return &OrderService{
ordersRepo: ordersRepo.New(),
cartsRepo: cartsRepo.New(),
addressesService: addressesService.New(),
emailService: emailService.NewEmailService(),
}
}
func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) {
if !user.HasPermission(perms.OrdersViewAll) {
// append filter to view only this user's orders
idStr := strconv.FormatUint(uint64(user.ID), 10)
filt.Append(filters.Where("b2b_customer_orders.user_id = " + idStr))
}
list, err := s.ordersRepo.Find(user.ID, p, filt)
if err != nil {
return nil, err
}
for i := 0; i < len(list.Items); i++ {
address_unparsed, err := s.addressesService.ValidateAddressJson(list.Items[i].AddressString, list.Items[i].CountryID)
// log such errors
if err != nil {
fmt.Printf("err: %v\n", err)
}
list.Items[i].AddressUnparsed = &address_unparsed
}
return list, nil
}
func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, name string, country_id uint, address_info string) error {
_, err := s.addressesService.ValidateAddressJson(address_info, country_id)
if err != nil {
return err
}
exists, err := s.cartsRepo.UserHasCart(user_id, cart_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchCart
}
cart, err := s.cartsRepo.RetrieveCart(user_id, cart_id)
if err != nil {
return err
}
if len(cart.Products) == 0 {
return responseErrors.ErrEmptyCart
}
if name == "" && cart.Name != nil {
name = *cart.Name
}
// all checks passed
err = s.ordersRepo.PlaceNewOrder(cart, name, country_id, address_info)
if err != nil {
return err
}
// from this point onward we do not cancel this order.
// if no error is returned, remove the cart. This should be smooth
err = s.cartsRepo.RemoveCart(user_id, cart_id)
if err != nil {
// Log error but don't fail placing order
_ = err
}
// send email to admin
go func(user_id uint) {
err := s.emailService.SendNewOrderPlacedNotification(user_id)
if err != nil {
// Log error but don't fail placing order
_ = err
}
}(user_id)
return nil
}
func (s *OrderService) ChangeOrderAddress(user *model.Customer, order_id uint, country_id uint, address_info string) error {
_, err := s.addressesService.ValidateAddressJson(address_info, country_id)
if err != nil {
return err
}
if !user.HasPermission(perms.OrdersModifyAll) {
exists, err := s.ordersRepo.UserHasOrder(user.ID, order_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchOrder
}
}
return s.ordersRepo.ChangeOrderAddress(order_id, country_id, address_info)
}
// This is obiously just an initial version of this function
func (s *OrderService) ChangeOrderStatus(user *model.Customer, order_id uint, status string) error {
if !user.HasPermission(perms.OrdersModifyAll) {
exists, err := s.ordersRepo.UserHasOrder(user.ID, order_id)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrUserHasNoSuchOrder
}
}
return s.ordersRepo.ChangeOrderStatus(order_id, status)
}

View File

@@ -0,0 +1,26 @@
package emails
import (
"git.ma-al.com/goc_daniel/b2b/app/templ/layout"
"git.ma-al.com/goc_daniel/b2b/app/view"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
)
templ EmailNewOrderPlacedWrapper(data view.EmailLayout[view.EmailNewOrderPlacedData]) {
@layout.Base( i18n.T___(data.LangID, "email.email_new_order_placed_notification_title")) {
<div class="container">
<div class="email-wrapper">
<div class="email-header">
<h1>New Order Placed</h1>
</div>
<div class="email-body">
<p>Hello Administrator,</p>
<p>User with id { data.Data.UserID } has placed a new order. </p>
</div>
<div class="email-footer">
<p>&copy; 2024 Gitea Manager. All rights reserved.</p>
</div>
</div>
</div>
}
}

View File

@@ -20,6 +20,9 @@ const MAX_AMOUNT_OF_ADDRESSES_PER_USER = 10
const USER_LOCALE = "user" const USER_LOCALE = "user"
// ORDERS
const NEW_ORDER_STATUS = "PENDING"
// WEBDAV // WEBDAV
const NBYTES_IN_WEBDAV_TOKEN = 32 const NBYTES_IN_WEBDAV_TOKEN = 32
const WEBDAV_HREF_ROOT = "http://localhost:3000/api/v1/webdav/storage" const WEBDAV_HREF_ROOT = "http://localhost:3000/api/v1/webdav/storage"

View File

@@ -66,11 +66,16 @@ var (
ErrUserHasNoSuchCart = errors.New("user does not have cart with given id") ErrUserHasNoSuchCart = errors.New("user does not have cart with given id")
ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist") ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist")
// Typed errors for orders handler
ErrEmptyCart = errors.New("the cart is empty")
ErrUserHasNoSuchOrder = errors.New("user does not have order with given id")
// Typed errors for price reduction handler // Typed errors for price reduction handler
ErrInvalidReductionType = errors.New("invalid reduction type: must be 'amount' or 'percentage'") ErrInvalidReductionType = errors.New("invalid reduction type: must be 'amount' or 'percentage'")
ErrPercentageRequired = errors.New("percentage_reduction required when reduction_type is percentage") ErrPercentageRequired = errors.New("percentage_reduction required when reduction_type is percentage")
ErrPriceRequired = errors.New("price required when reduction_type is amount") ErrPriceRequired = errors.New("price required when reduction_type is amount")
ErrSpecificPriceNotFound = errors.New("price reduction not found") ErrSpecificPriceNotFound = errors.New("price reduction not found")
// Typed errors for storage // Typed errors for storage
ErrAccessDenied = errors.New("access denied!") ErrAccessDenied = errors.New("access denied!")
ErrFolderDoesNotExist = errors.New("folder does not exist") ErrFolderDoesNotExist = errors.New("folder does not exist")
@@ -201,6 +206,11 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrProductOrItsVariationDoesNotExist): case errors.Is(err, ErrProductOrItsVariationDoesNotExist):
return i18n.T_(c, "error.err_product_or_its_variation_does_not_exist") return i18n.T_(c, "error.err_product_or_its_variation_does_not_exist")
case errors.Is(err, ErrEmptyCart):
return i18n.T_(c, "error.err_cart_is_empty")
case errors.Is(err, ErrUserHasNoSuchOrder):
return i18n.T_(c, "error.err_user_has_no_such_order")
case errors.Is(err, ErrAccessDenied): case errors.Is(err, ErrAccessDenied):
return i18n.T_(c, "error.err_access_denied") return i18n.T_(c, "error.err_access_denied")
case errors.Is(err, ErrFolderDoesNotExist): case errors.Is(err, ErrFolderDoesNotExist):
@@ -282,6 +292,8 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrMaxAmtOfCartsReached), errors.Is(err, ErrMaxAmtOfCartsReached),
errors.Is(err, ErrUserHasNoSuchCart), errors.Is(err, ErrUserHasNoSuchCart),
errors.Is(err, ErrProductOrItsVariationDoesNotExist), errors.Is(err, ErrProductOrItsVariationDoesNotExist),
errors.Is(err, ErrEmptyCart),
errors.Is(err, ErrUserHasNoSuchOrder),
errors.Is(err, ErrInvalidReductionType), errors.Is(err, ErrInvalidReductionType),
errors.Is(err, ErrPercentageRequired), errors.Is(err, ErrPercentageRequired),
errors.Is(err, ErrPriceRequired), errors.Is(err, ErrPriceRequired),

View File

@@ -18,3 +18,7 @@ type EmailAdminNotificationData struct {
type EmailPasswordResetData struct { type EmailPasswordResetData struct {
ResetURL string ResetURL string
} }
type EmailNewOrderPlacedData struct {
UserID uint
}

View File

@@ -5,10 +5,10 @@ info:
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/carts/retrieve-cart?cart_id=3 url: http://localhost:3000/api/v1/restricted/carts/retrieve-cart?cart_id=1
params: params:
- name: cart_id - name: cart_id
value: "3" value: "1"
type: query type: query
auth: inherit auth: inherit

View File

@@ -0,0 +1,33 @@
info:
name: change-order-address
type: http
seq: 3
http:
method: GET
url: http://localhost:3000/api/v1/restricted/orders/change-order-address?order_id=1&country_id=1
params:
- name: order_id
value: "1"
type: query
- name: country_id
value: "1"
type: query
body:
type: json
data: |-
{
"postal_code": "31-154",
"city": "Kraków",
"voivodeship": "śląskie",
"street": "Długa",
"building_no": "5",
"recipient": "Adam Adamowicz"
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,22 @@
info:
name: change-order-status
type: http
seq: 4
http:
method: GET
url: http://localhost:3000/api/v1/restricted/orders/change-order-status?order_id=1&status=PAID
params:
- name: order_id
value: "1"
type: query
- name: status
value: PAID
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,7 @@
info:
name: orders
type: folder
seq: 11
request:
auth: inherit

View File

@@ -0,0 +1,31 @@
info:
name: list
type: http
seq: 2
http:
method: GET
url: http://localhost:3000/api/v1/restricted/orders/list?p=1&elems=30&sort=product_id,asc&user_id=2&name=~sdj
params:
- name: p
value: "1"
type: query
- name: elems
value: "30"
type: query
- name: sort
value: product_id,asc
type: query
- name: user_id
value: "2"
type: query
- name: name
value: ~sdj
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,37 @@
info:
name: place-new-order
type: http
seq: 1
http:
method: POST
url: http://localhost:3000/api/v1/restricted/orders/place-new-order?cart_id=1&name=sdjalksd&country_id=1
params:
- name: cart_id
value: "1"
type: query
- name: name
value: sdjalksd
type: query
- name: country_id
value: "1"
type: query
body:
type: json
data: |-
{
"postal_code": "31-154",
"city": "Kraków",
"voivodeship": "małopolskie",
"street": "Długa",
"building_no": "5",
"apartment_no": "7",
"recipient": "Jan Kowalski"
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -130,7 +130,7 @@ FOREIGN KEY (role_id) REFERENCES b2b_roles(id);
-- customer_carts -- customer_carts
CREATE TABLE IF NOT EXISTS b2b_customer_carts ( CREATE TABLE IF NOT EXISTS b2b_customer_carts (
cart_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, cart_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(255) NULL, name VARCHAR(255) NULL,
CONSTRAINT fk_customer_carts_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE CASCADE ON UPDATE CASCADE CONSTRAINT fk_customer_carts_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE CASCADE ON UPDATE CASCADE
@@ -140,8 +140,8 @@ CREATE INDEX IF NOT EXISTS idx_customer_carts_user_id ON b2b_customer_carts (use
-- carts_products -- carts_products
CREATE TABLE IF NOT EXISTS b2b_carts_products ( CREATE TABLE IF NOT EXISTS b2b_carts_products (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
cart_id INT UNSIGNED NOT NULL, cart_id BIGINT UNSIGNED NOT NULL,
product_id INT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL,
product_attribute_id INT NULL, product_attribute_id INT NULL,
amount INT UNSIGNED NOT NULL, amount INT UNSIGNED NOT NULL,
@@ -224,7 +224,7 @@ ON `b2b_countries` (
CREATE TABLE IF NOT EXISTS b2b_addresses ( CREATE TABLE IF NOT EXISTS b2b_addresses (
id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
b2b_customer_id BIGINT UNSIGNED NOT NULL, b2b_customer_id BIGINT UNSIGNED NOT NULL,
address_info TEXT NOT NULL, address_string TEXT NOT NULL,
b2b_country_id BIGINT UNSIGNED NOT NULL, b2b_country_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
CONSTRAINT fk_b2b_addresses_b2b_customers FOREIGN KEY (b2b_customer_id) REFERENCES b2b_customers (id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_b2b_addresses_b2b_customers FOREIGN KEY (b2b_customer_id) REFERENCES b2b_customers (id) ON DELETE CASCADE ON UPDATE CASCADE,
@@ -232,6 +232,34 @@ CREATE TABLE IF NOT EXISTS b2b_addresses (
) ENGINE = InnoDB; ) ENGINE = InnoDB;
-- customer_orders
CREATE TABLE IF NOT EXISTS b2b_customer_orders (
order_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
name TEXT NOT NULL,
country_id BIGINT UNSIGNED NOT NULL,
address_string TEXT NOT NULL,
status VARCHAR(50) NOT NULL,
CONSTRAINT fk_customer_orders_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE NO ACTION ON UPDATE CASCADE,
CONSTRAINT fk_customer_orders_countries FOREIGN KEY (country_id) REFERENCES b2b_countries(id) ON DELETE NO ACTION ON UPDATE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE INDEX idx_customer_orders_user_id ON b2b_customer_orders (user_id);
CREATE INDEX idx_customer_orders_country_id ON b2b_customer_orders (country_id);
-- orders_products
CREATE TABLE IF NOT EXISTS b2b_orders_products (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
product_id INT UNSIGNED NOT NULL,
product_attribute_id INT NULL,
amount INT UNSIGNED NOT NULL,
CONSTRAINT fk_orders_products_customer_orders FOREIGN KEY (order_id) REFERENCES b2b_customer_orders (order_id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_orders_products_product FOREIGN KEY (product_id) REFERENCES ps_product (id_product) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE INDEX IF NOT EXISTS idx_orders_products_order_id ON b2b_orders_products (order_id);
CREATE TABLE b2b_specific_price ( CREATE TABLE b2b_specific_price (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,