diff --git a/app/delivery/middleware/perms/permissions.go b/app/delivery/middleware/perms/permissions.go index f30b097..35183f3 100644 --- a/app/delivery/middleware/perms/permissions.go +++ b/app/delivery/middleware/perms/permissions.go @@ -12,4 +12,6 @@ const ( ProductTranslationSave Permission = "product_translation.save" ProductTranslationTranslate Permission = "product_translation.translate" SearchCreateIndex Permission = "search.create_index" + OrdersViewAll Permission = "orders.view_all" + OrdersModifyAll Permission = "orders.modify_all" ) diff --git a/app/delivery/web/api/restricted/addresses.go b/app/delivery/web/api/restricted/addresses.go index 903f011..f33b7cb 100644 --- a/app/delivery/web/api/restricted/addresses.go +++ b/app/delivery/web/api/restricted/addresses.go @@ -124,13 +124,13 @@ func (h *AddressesHandler) RetrieveAddressesInfo(c fiber.Ctx) error { 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 { return c.Status(responseErrors.GetErrorStatus(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 { diff --git a/app/delivery/web/api/restricted/orders.go b/app/delivery/web/api/restricted/orders.go new file mode 100644 index 0000000..186599c --- /dev/null +++ b/app/delivery/web/api/restricted/orders.go @@ -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))) +} diff --git a/app/delivery/web/init.go b/app/delivery/web/init.go index a44cc6b..73559aa 100644 --- a/app/delivery/web/init.go +++ b/app/delivery/web/init.go @@ -133,8 +133,13 @@ func (s *Server) Setup() error { carts := s.restricted.Group("/carts") restricted.CartsHandlerRoutes(carts) + // orders (restricted) + orders := s.restricted.Group("/orders") + restricted.OrdersHandlerRoutes(orders) + specificPrice := s.restricted.Group("/specific-price") restricted.SpecificPriceHandlerRoutes(specificPrice) + // addresses (restricted) addresses := s.restricted.Group("/addresses") restricted.AddressesHandlerRoutes(addresses) diff --git a/app/model/address.go b/app/model/address.go index a84056a..96efaa8 100644 --- a/app/model/address.go +++ b/app/model/address.go @@ -1,25 +1,18 @@ package model type Address struct { - ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"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"` - CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"` + ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"` + 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"` } func (Address) TableName() string { return "b2b_addresses" } -type AddressUnparsed struct { - 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 { -} +type AddressUnparsed interface{} // Address template in Poland type AddressPL struct { diff --git a/app/model/order.go b/app/model/order.go new file mode 100644 index 0000000..a6556b2 --- /dev/null +++ b/app/model/order.go @@ -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" +} diff --git a/app/repos/addressesRepo/addressesRepo.go b/app/repos/addressesRepo/addressesRepo.go index 5f674a0..ce6437d 100644 --- a/app/repos/addressesRepo/addressesRepo.go +++ b/app/repos/addressesRepo/addressesRepo.go @@ -48,9 +48,9 @@ func (repo *AddressesRepo) UserAddressesAmt(user_id uint) (uint, error) { func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, country_id uint) error { address := model.Address{ - CustomerID: user_id, - AddressInfo: address_info, - CountryID: country_id, + CustomerID: user_id, + AddressString: address_info, + CountryID: country_id, } return db.DB. @@ -60,10 +60,10 @@ func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, coun func (repo *AddressesRepo) UpdateAddress(user_id uint, address_id uint, address_info string, country_id uint) error { address := model.Address{ - ID: address_id, - CustomerID: user_id, - AddressInfo: address_info, - CountryID: country_id, + ID: address_id, + CustomerID: user_id, + AddressString: address_info, + CountryID: country_id, } return db.DB. diff --git a/app/repos/cartsRepo/cartsRepo.go b/app/repos/cartsRepo/cartsRepo.go index b15700c..4141dcb 100644 --- a/app/repos/cartsRepo/cartsRepo.go +++ b/app/repos/cartsRepo/cartsRepo.go @@ -9,11 +9,12 @@ import ( type UICartsRepo interface { CartsAmount(user_id uint) (uint, 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 RetrieveCartsInfo(user_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 } @@ -49,7 +50,15 @@ func (repo *CartsRepo) CreateNewCart(user_id uint) (model.CustomerCart, error) { 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 err := db.DB. @@ -59,7 +68,7 @@ func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (uint, error) { Scan(&amt). Error - return amt, err + return amt >= 1, err } 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 } -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 if product_attribute_id == nil { @@ -106,7 +115,7 @@ func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id Where("id_product = ?", product_id). Scan(&amt). Error - return amt, err + return amt >= 1, err } else { 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). Scan(&amt). Error - return amt, err + return amt >= 1, err } } diff --git a/app/repos/ordersRepo/ordersRepo.go b/app/repos/ordersRepo/ordersRepo.go new file mode 100644 index 0000000..c3b8158 --- /dev/null +++ b/app/repos/ordersRepo/ordersRepo.go @@ -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 +} diff --git a/app/service/addressesService/addressesService.go b/app/service/addressesService/addressesService.go index b077486..0b7a820 100644 --- a/app/service/addressesService/addressesService.go +++ b/app/service/addressesService/addressesService.go @@ -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 { case 1: // Poland @@ -49,7 +49,7 @@ func (s *AddressesService) AddNewAddress(user_id uint, address_info string, coun return responseErrors.ErrMaxAmtOfAddressesReached } - _, err = s.validateAddressJson(address_info, country_id) + _, err = s.ValidateAddressJson(address_info, country_id) if err != nil { return err } @@ -66,7 +66,7 @@ func (s *AddressesService) ModifyAddress(user_id uint, address_id uint, address_ return responseErrors.ErrUserHasNoSuchAddress } - _, err = s.validateAddressJson(address_info, country_id) + _, err = s.ValidateAddressJson(address_info, country_id) if err != nil { 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) } -func (s *AddressesService) RetrieveAddressesInfo(user_id uint) (*[]model.AddressUnparsed, error) { - parsed_addresses, err := s.repo.RetrieveAddresses(user_id) +func (s *AddressesService) RetrieveAddresses(user_id uint) (*[]model.Address, error) { + addresses, err := s.repo.RetrieveAddresses(user_id) if err != nil { return nil, err } - var unparsed_addresses []model.AddressUnparsed - - 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) + for i := 0; i < len(*addresses); i++ { + address_unparsed, err := s.ValidateAddressJson((*addresses)[i].AddressString, (*addresses)[i].CountryID) // log such errors if err != nil { 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 { @@ -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 -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.DisallowUnknownFields() diff --git a/app/service/cartsService/cartsService.go b/app/service/cartsService/cartsService.go index c82e7b2..2ed4006 100644 --- a/app/service/cartsService/cartsService.go +++ b/app/service/cartsService/cartsService.go @@ -34,12 +34,24 @@ func (s *CartsService) CreateNewCart(user_id uint) (model.CustomerCart, error) { return cart, nil } -func (s *CartsService) UpdateCartName(user_id uint, cart_id uint, new_name string) error { - amt, err := s.repo.UserHasCart(user_id, cart_id) +func (s *CartsService) RemoveCart(user_id uint, cart_id uint) error { + exists, err := s.repo.UserHasCart(user_id, cart_id) if err != nil { 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 } @@ -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) { - amt, err := s.repo.UserHasCart(user_id, cart_id) + exists, err := s.repo.UserHasCart(user_id, cart_id) if err != nil { return nil, err } - if amt != 1 { + if !exists { 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 { - amt, err := s.repo.UserHasCart(user_id, cart_id) + exists, err := s.repo.UserHasCart(user_id, cart_id) if err != nil { return err } - if amt != 1 { + if !exists { 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 { return err } - if amt != 1 { + if !exists { return responseErrors.ErrProductOrItsVariationDoesNotExist } diff --git a/app/service/emailService/email.go b/app/service/emailService/email.go index 29cc9bb..2c92117 100644 --- a/app/service/emailService/email.go +++ b/app/service/emailService/email.go @@ -117,6 +117,18 @@ func (s *EmailService) SendNewUserAdminNotification(userEmail, userName, baseURL 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 func (s *EmailService) verificationEmailTemplate(name, verificationURL string, langID uint) string { 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) 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() +} diff --git a/app/service/orderService/orderService.go b/app/service/orderService/orderService.go new file mode 100644 index 0000000..5e1a5ca --- /dev/null +++ b/app/service/orderService/orderService.go @@ -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) +} diff --git a/app/templ/emails/emailNewOrderPlacedNotification.templ b/app/templ/emails/emailNewOrderPlacedNotification.templ new file mode 100644 index 0000000..73b9ed4 --- /dev/null +++ b/app/templ/emails/emailNewOrderPlacedNotification.templ @@ -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")) { +
Hello Administrator,
+User with id { data.Data.UserID } has placed a new order.
+