first commit
This commit is contained in:
77
services/cart/routes.go
Normal file
77
services/cart/routes.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gorilla/mux"
|
||||
"sismedika.com/sas/westone/services/auth"
|
||||
"sismedika.com/sas/westone/types"
|
||||
"sismedika.com/sas/westone/utils"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
store types.ProductStore
|
||||
orderStore types.OrderStore
|
||||
userStore types.UserStore
|
||||
}
|
||||
|
||||
func NewHandler(
|
||||
store types.ProductStore,
|
||||
orderStore types.OrderStore,
|
||||
userStore types.UserStore,
|
||||
) *Handler {
|
||||
return &Handler{
|
||||
store: store,
|
||||
orderStore: orderStore,
|
||||
userStore: userStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterRoutes(router *mux.Router) {
|
||||
cart := router.PathPrefix("/cart").Subrouter()
|
||||
cart.Use(auth.AuthMiddleware)
|
||||
cart.HandleFunc("/checkout", h.handleCheckout).Methods(http.MethodPost)
|
||||
// router.HandleFunc("/cart/checkout", auth.WithJWTAuth(h.handleCheckout)).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
func (h *Handler) handleCheckout(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserIDFromContext(r.Context())
|
||||
|
||||
var cart types.CartCheckoutPayload
|
||||
if err := utils.ParseJSON(r, &cart); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := utils.Validate.Struct(cart); err != nil {
|
||||
errors := err.(validator.ValidationErrors)
|
||||
utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", errors))
|
||||
return
|
||||
}
|
||||
|
||||
productIds, err := getCartItemsIDs(cart.Items)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
// get products
|
||||
products, err := h.store.GetProductsByID(productIds)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
orderID, totalPrice, err := h.createOrder(products, cart.Items, userID)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"total_price": totalPrice,
|
||||
"order_id": orderID,
|
||||
})
|
||||
}
|
||||
214
services/cart/routes_test.go
Normal file
214
services/cart/routes_test.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"sismedika.com/sas/westone/types"
|
||||
)
|
||||
|
||||
var mockProducts = []types.Product{
|
||||
{ID: 1, Name: "product 1", Price: 10, Quantity: 100},
|
||||
{ID: 2, Name: "product 2", Price: 20, Quantity: 200},
|
||||
{ID: 3, Name: "product 3", Price: 30, Quantity: 300},
|
||||
{ID: 4, Name: "empty stock", Price: 30, Quantity: 0},
|
||||
{ID: 5, Name: "almost stock", Price: 30, Quantity: 1},
|
||||
}
|
||||
|
||||
func TestCartServiceHandler(t *testing.T) {
|
||||
productStore := &mockProductStore{}
|
||||
orderStore := &mockOrderStore{}
|
||||
handler := NewHandler(productStore, orderStore, nil)
|
||||
|
||||
t.Run("should fail to checkout if the cart items do not exist", func(t *testing.T) {
|
||||
payload := types.CartCheckoutPayload{
|
||||
Items: []types.CartCheckoutItem{
|
||||
{ProductID: 99, Quantity: 100},
|
||||
},
|
||||
}
|
||||
|
||||
marshalled, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost)
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail to checkout if the cart has negative quantities", func(t *testing.T) {
|
||||
payload := types.CartCheckoutPayload{
|
||||
Items: []types.CartCheckoutItem{
|
||||
{ProductID: 1, Quantity: 0}, // invalid quantity
|
||||
},
|
||||
}
|
||||
|
||||
marshalled, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost)
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail to checkout if there is no stock for an item", func(t *testing.T) {
|
||||
payload := types.CartCheckoutPayload{
|
||||
Items: []types.CartCheckoutItem{
|
||||
{ProductID: 4, Quantity: 2},
|
||||
},
|
||||
}
|
||||
|
||||
marshalled, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost)
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail to checkout if there is not enough stock", func(t *testing.T) {
|
||||
payload := types.CartCheckoutPayload{
|
||||
Items: []types.CartCheckoutItem{
|
||||
{ProductID: 5, Quantity: 2},
|
||||
},
|
||||
}
|
||||
|
||||
marshalled, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost)
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should checkout and calculate the price correctly", func(t *testing.T) {
|
||||
payload := types.CartCheckoutPayload{
|
||||
Items: []types.CartCheckoutItem{
|
||||
{ProductID: 1, Quantity: 10},
|
||||
{ProductID: 2, Quantity: 20},
|
||||
{ProductID: 5, Quantity: 1},
|
||||
},
|
||||
}
|
||||
|
||||
marshalled, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost)
|
||||
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("expected status code %d, got %d", http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
var response map[string]interface{}
|
||||
if err := json.NewDecoder(rr.Body).Decode(&response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if response["total_price"] != 530.0 {
|
||||
t.Errorf("expected total price to be 530, got %f", response["total_price"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type mockProductStore struct{}
|
||||
|
||||
func (m *mockProductStore) GetProductByID(productID int) (*types.Product, error) {
|
||||
return &types.Product{}, nil
|
||||
}
|
||||
|
||||
func (m *mockProductStore) GetProducts() ([]*types.Product, error) {
|
||||
return []*types.Product{}, nil
|
||||
}
|
||||
|
||||
func (m *mockProductStore) CreateProduct(product types.CreateProductPayload) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockProductStore) GetProductsByID(ids []int) ([]types.Product, error) {
|
||||
return mockProducts, nil
|
||||
}
|
||||
|
||||
func (m *mockProductStore) UpdateProduct(product types.Product) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockOrderStore struct{}
|
||||
|
||||
func (m *mockOrderStore) CreateOrder(order types.Order) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *mockOrderStore) CreateOrderItem(orderItem types.OrderItem) error {
|
||||
return nil
|
||||
}
|
||||
96
services/cart/service.go
Normal file
96
services/cart/service.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sismedika.com/sas/westone/types"
|
||||
)
|
||||
|
||||
func getCartItemsIDs(items []types.CartCheckoutItem) ([]int, error) {
|
||||
productIds := make([]int, len(items))
|
||||
for i, item := range items {
|
||||
if item.Quantity <= 0 {
|
||||
return nil, fmt.Errorf("invalid quantity for product %d", item.ProductID)
|
||||
}
|
||||
|
||||
productIds[i] = item.ProductID
|
||||
}
|
||||
|
||||
return productIds, nil
|
||||
}
|
||||
|
||||
func checkIfCartIsInStock(cartItems []types.CartCheckoutItem, products map[int]types.Product) error {
|
||||
if len(cartItems) == 0 {
|
||||
return fmt.Errorf("cart is empty")
|
||||
}
|
||||
|
||||
for _, item := range cartItems {
|
||||
product, ok := products[item.ProductID]
|
||||
if !ok {
|
||||
return fmt.Errorf("product %d is not available in the store, please refresh your cart", item.ProductID)
|
||||
}
|
||||
|
||||
if product.Quantity < item.Quantity {
|
||||
return fmt.Errorf("product %s is not available in the quantity requested", product.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateTotalPrice(cartItems []types.CartCheckoutItem, products map[int]types.Product) float64 {
|
||||
var total float64
|
||||
|
||||
for _, item := range cartItems {
|
||||
product := products[item.ProductID]
|
||||
total += product.Price * float64(item.Quantity)
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
func (h *Handler) createOrder(products []types.Product, cartItems []types.CartCheckoutItem, userID int) (int, float64, error) {
|
||||
// create a map of products for easier access
|
||||
productsMap := make(map[int]types.Product)
|
||||
for _, product := range products {
|
||||
productsMap[product.ID] = product
|
||||
}
|
||||
|
||||
// check if all products are available
|
||||
if err := checkIfCartIsInStock(cartItems, productsMap); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// calculate total price
|
||||
totalPrice := calculateTotalPrice(cartItems, productsMap)
|
||||
|
||||
// reduce the quantity of products in the store
|
||||
for _, item := range cartItems {
|
||||
product := productsMap[item.ProductID]
|
||||
product.Quantity -= item.Quantity
|
||||
h.store.UpdateProduct(product)
|
||||
}
|
||||
|
||||
// create order record
|
||||
orderID, err := h.orderStore.CreateOrder(types.Order{
|
||||
UserID: userID,
|
||||
Total: totalPrice,
|
||||
Status: "pending",
|
||||
Address: "some address", // could fetch address from a user addresses table
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// create order the items records
|
||||
for _, item := range cartItems {
|
||||
h.orderStore.CreateOrderItem(types.OrderItem{
|
||||
OrderID: orderID,
|
||||
ProductID: item.ProductID,
|
||||
Quantity: item.Quantity,
|
||||
Price: productsMap[item.ProductID].Price,
|
||||
})
|
||||
}
|
||||
|
||||
return orderID, totalPrice, nil
|
||||
}
|
||||
Reference in New Issue
Block a user