Skip to content

Commit 0e1572e

Browse files
committed
update endpoints
1 parent 2a34e2f commit 0e1572e

File tree

14 files changed

+1216
-3
lines changed

14 files changed

+1216
-3
lines changed

cmd/main.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package main
33
import (
44
"bullet-cloud-api/internal/addresses"
55
"bullet-cloud-api/internal/auth"
6+
"bullet-cloud-api/internal/cart"
67
"bullet-cloud-api/internal/categories"
78
"bullet-cloud-api/internal/config"
89
"bullet-cloud-api/internal/database"
910
"bullet-cloud-api/internal/handlers"
11+
"bullet-cloud-api/internal/orders"
1012
"bullet-cloud-api/internal/products"
1113
"bullet-cloud-api/internal/users"
1214
"context"
@@ -36,13 +38,17 @@ func main() {
3638
productRepo := products.NewPostgresProductRepository(dbPool)
3739
categoryRepo := categories.NewPostgresCategoryRepository(dbPool)
3840
addressRepo := addresses.NewPostgresAddressRepository(dbPool)
41+
cartRepo := cart.NewPostgresCartRepository(dbPool)
42+
orderRepo := orders.NewPostgresOrderRepository(dbPool)
3943
authHandler := handlers.NewAuthHandler(userRepo, cfg.JWTSecret, defaultJWTExpiry)
4044
userHandler := handlers.NewUserHandler(userRepo, addressRepo)
4145
productHandler := handlers.NewProductHandler(productRepo)
4246
categoryHandler := handlers.NewCategoryHandler(categoryRepo)
47+
cartHandler := handlers.NewCartHandler(cartRepo, productRepo)
48+
orderHandler := handlers.NewOrderHandler(orderRepo, cartRepo, addressRepo)
4349
authMiddleware := auth.NewMiddleware(cfg.JWTSecret, userRepo)
4450

45-
r := setupRoutes(authHandler, userHandler, productHandler, categoryHandler, authMiddleware)
51+
r := setupRoutes(authHandler, userHandler, productHandler, categoryHandler, cartHandler, orderHandler, authMiddleware)
4652

4753
port := os.Getenv("API_PORT")
4854
if port == "" {
@@ -80,7 +86,7 @@ func main() {
8086
log.Println("Server exited properly")
8187
}
8288

83-
func setupRoutes(ah *handlers.AuthHandler, uh *handlers.UserHandler, ph *handlers.ProductHandler, ch *handlers.CategoryHandler, mw *auth.Middleware) *mux.Router {
89+
func setupRoutes(ah *handlers.AuthHandler, uh *handlers.UserHandler, ph *handlers.ProductHandler, ch *handlers.CategoryHandler, cartH *handlers.CartHandler, oh *handlers.OrderHandler, mw *auth.Middleware) *mux.Router {
8490
r := mux.NewRouter()
8591

8692
apiV1 := r.PathPrefix("/api").Subrouter()
@@ -114,5 +120,20 @@ func setupRoutes(ah *handlers.AuthHandler, uh *handlers.UserHandler, ph *handler
114120
protectedCategoryRoutes.HandleFunc("/{id:[0-9a-fA-F-]+}", ch.UpdateCategory).Methods("PUT")
115121
protectedCategoryRoutes.HandleFunc("/{id:[0-9a-fA-F-]+}", ch.DeleteCategory).Methods("DELETE")
116122

123+
protectedCartRoutes := apiV1.PathPrefix("/cart").Subrouter()
124+
protectedCartRoutes.Use(mw.Authenticate)
125+
protectedCartRoutes.HandleFunc("", cartH.GetCart).Methods("GET")
126+
protectedCartRoutes.HandleFunc("/items", cartH.AddItem).Methods("POST")
127+
protectedCartRoutes.HandleFunc("/items/{productId:[0-9a-fA-F-]+}", cartH.UpdateItem).Methods("PUT")
128+
protectedCartRoutes.HandleFunc("/items/{productId:[0-9a-fA-F-]+}", cartH.DeleteItem).Methods("DELETE")
129+
protectedCartRoutes.HandleFunc("", cartH.ClearCart).Methods("DELETE")
130+
131+
protectedOrderRoutes := apiV1.PathPrefix("/orders").Subrouter()
132+
protectedOrderRoutes.Use(mw.Authenticate)
133+
protectedOrderRoutes.HandleFunc("", oh.CreateOrder).Methods("POST")
134+
protectedOrderRoutes.HandleFunc("", oh.ListOrders).Methods("GET")
135+
protectedOrderRoutes.HandleFunc("/{id:[0-9a-fA-F-]+}", oh.GetOrder).Methods("GET")
136+
protectedOrderRoutes.HandleFunc("/{id:[0-9a-fA-F-]+}/cancel", oh.CancelOrder).Methods("PATCH")
137+
117138
return r
118139
}

internal/cart/repository.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package cart
2+
3+
import (
4+
"bullet-cloud-api/internal/models"
5+
"context"
6+
"errors"
7+
8+
"github.com/google/uuid"
9+
"github.com/jackc/pgx/v5"
10+
"github.com/jackc/pgx/v5/pgxpool"
11+
)
12+
13+
var (
14+
ErrCartNotFound = errors.New("cart not found")
15+
ErrCartItemNotFound = errors.New("cart item not found")
16+
ErrProductNotInCart = errors.New("product not found in cart")
17+
)
18+
19+
// CartRepository defines the interface for cart data operations.
20+
type CartRepository interface {
21+
// GetOrCreateCartByUserID finds the cart for a user or creates one if it doesn't exist.
22+
GetOrCreateCartByUserID(ctx context.Context, userID uuid.UUID) (*models.Cart, error)
23+
// GetCartItems retrieves all items currently in the specified cart.
24+
GetCartItems(ctx context.Context, cartID uuid.UUID) ([]models.CartItem, error)
25+
// AddItem adds a product to the cart or updates its quantity if it already exists.
26+
AddItem(ctx context.Context, cartID, productID uuid.UUID, quantity int, price float64) (*models.CartItem, error)
27+
// UpdateItemQuantity changes the quantity of an existing item in the cart.
28+
UpdateItemQuantity(ctx context.Context, cartID, productID uuid.UUID, quantity int) (*models.CartItem, error)
29+
// RemoveItem removes a specific product from the cart.
30+
RemoveItem(ctx context.Context, cartID, productID uuid.UUID) error
31+
// ClearCart removes all items from a specific cart.
32+
ClearCart(ctx context.Context, cartID uuid.UUID) error
33+
// FindCartItem retrieves a specific item from a cart.
34+
FindCartItem(ctx context.Context, cartID, productID uuid.UUID) (*models.CartItem, error)
35+
}
36+
37+
// postgresCartRepository implements CartRepository using PostgreSQL.
38+
type postgresCartRepository struct {
39+
db *pgxpool.Pool
40+
}
41+
42+
// NewPostgresCartRepository creates a new instance of postgresCartRepository.
43+
func NewPostgresCartRepository(db *pgxpool.Pool) CartRepository {
44+
return &postgresCartRepository{db: db}
45+
}
46+
47+
// GetOrCreateCartByUserID finds or creates a cart for the user.
48+
func (r *postgresCartRepository) GetOrCreateCartByUserID(ctx context.Context, userID uuid.UUID) (*models.Cart, error) {
49+
// Try to find existing cart
50+
queryFind := `SELECT id, user_id, created_at, updated_at FROM carts WHERE user_id = $1`
51+
cart := &models.Cart{}
52+
err := r.db.QueryRow(ctx, queryFind, userID).Scan(
53+
&cart.ID, &cart.UserID, &cart.CreatedAt, &cart.UpdatedAt,
54+
)
55+
56+
if err == nil {
57+
return cart, nil // Cart found
58+
}
59+
60+
// If no cart found, create one
61+
if errors.Is(err, pgx.ErrNoRows) {
62+
queryCreate := `
63+
INSERT INTO carts (user_id)
64+
VALUES ($1)
65+
RETURNING id, user_id, created_at, updated_at
66+
`
67+
errCreate := r.db.QueryRow(ctx, queryCreate, userID).Scan(
68+
&cart.ID, &cart.UserID, &cart.CreatedAt, &cart.UpdatedAt,
69+
)
70+
if errCreate != nil {
71+
// Handle potential unique constraint violation if called concurrently (unlikely with user_id unique)
72+
return nil, errCreate
73+
}
74+
return cart, nil // Cart created
75+
}
76+
77+
// Other unexpected error during find
78+
return nil, err
79+
}
80+
81+
// GetCartItems retrieves all items for a given cart ID.
82+
func (r *postgresCartRepository) GetCartItems(ctx context.Context, cartID uuid.UUID) ([]models.CartItem, error) {
83+
// Query includes JOIN to get product details (optional, adjust fields as needed)
84+
// query := `
85+
// SELECT ci.id, ci.cart_id, ci.product_id, ci.quantity, ci.price, ci.created_at, ci.updated_at,
86+
// p.name as product_name, p.description as product_description -- Example JOIN
87+
// FROM cart_items ci
88+
// JOIN products p ON ci.product_id = p.id
89+
// WHERE ci.cart_id = $1
90+
// ORDER BY ci.created_at ASC
91+
// `
92+
query := `
93+
SELECT id, cart_id, product_id, quantity, price, created_at, updated_at
94+
FROM cart_items
95+
WHERE cart_id = $1
96+
ORDER BY created_at ASC
97+
`
98+
rows, err := r.db.Query(ctx, query, cartID)
99+
if err != nil {
100+
return nil, err
101+
}
102+
defer rows.Close()
103+
104+
items, err := pgx.CollectRows(rows, pgx.RowToStructByName[models.CartItem])
105+
if err != nil {
106+
return nil, err
107+
}
108+
return items, nil
109+
}
110+
111+
// AddItem adds or updates a product in the cart.
112+
func (r *postgresCartRepository) AddItem(ctx context.Context, cartID, productID uuid.UUID, quantity int, price float64) (*models.CartItem, error) {
113+
query := `
114+
INSERT INTO cart_items (cart_id, product_id, quantity, price)
115+
VALUES ($1, $2, $3, $4)
116+
ON CONFLICT (cart_id, product_id) DO UPDATE SET
117+
quantity = cart_items.quantity + EXCLUDED.quantity,
118+
price = EXCLUDED.price, -- Update price in case it changed
119+
updated_at = NOW()
120+
RETURNING id, cart_id, product_id, quantity, price, created_at, updated_at
121+
`
122+
item := &models.CartItem{}
123+
err := r.db.QueryRow(ctx, query, cartID, productID, quantity, price).Scan(
124+
&item.ID,
125+
&item.CartID,
126+
&item.ProductID,
127+
&item.Quantity,
128+
&item.Price,
129+
&item.CreatedAt,
130+
&item.UpdatedAt,
131+
)
132+
133+
if err != nil {
134+
// Handle potential FK violations (cart_id or product_id invalid)
135+
return nil, err
136+
}
137+
return item, nil
138+
}
139+
140+
// UpdateItemQuantity updates the quantity of a specific item.
141+
func (r *postgresCartRepository) UpdateItemQuantity(ctx context.Context, cartID, productID uuid.UUID, quantity int) (*models.CartItem, error) {
142+
if quantity <= 0 {
143+
// If quantity is zero or less, remove the item instead
144+
return nil, r.RemoveItem(ctx, cartID, productID)
145+
}
146+
147+
query := `
148+
UPDATE cart_items
149+
SET quantity = $1, updated_at = NOW()
150+
WHERE cart_id = $2 AND product_id = $3
151+
RETURNING id, cart_id, product_id, quantity, price, created_at, updated_at
152+
`
153+
item := &models.CartItem{}
154+
err := r.db.QueryRow(ctx, query, quantity, cartID, productID).Scan(
155+
&item.ID,
156+
&item.CartID,
157+
&item.ProductID,
158+
&item.Quantity,
159+
&item.Price,
160+
&item.CreatedAt,
161+
&item.UpdatedAt,
162+
)
163+
164+
if err != nil {
165+
if errors.Is(err, pgx.ErrNoRows) {
166+
return nil, ErrProductNotInCart
167+
}
168+
return nil, err
169+
}
170+
return item, nil
171+
}
172+
173+
// RemoveItem deletes an item from the cart.
174+
func (r *postgresCartRepository) RemoveItem(ctx context.Context, cartID, productID uuid.UUID) error {
175+
query := `DELETE FROM cart_items WHERE cart_id = $1 AND product_id = $2`
176+
result, err := r.db.Exec(ctx, query, cartID, productID)
177+
if err != nil {
178+
return err
179+
}
180+
if result.RowsAffected() == 0 {
181+
return ErrProductNotInCart // Item wasn't in the cart
182+
}
183+
return nil
184+
}
185+
186+
// ClearCart removes all items from a given cart.
187+
func (r *postgresCartRepository) ClearCart(ctx context.Context, cartID uuid.UUID) error {
188+
query := `DELETE FROM cart_items WHERE cart_id = $1`
189+
_, err := r.db.Exec(ctx, query, cartID)
190+
// We don't necessarily return an error if cart was already empty
191+
return err
192+
}
193+
194+
// FindCartItem retrieves a specific item from a cart.
195+
func (r *postgresCartRepository) FindCartItem(ctx context.Context, cartID, productID uuid.UUID) (*models.CartItem, error) {
196+
query := `
197+
SELECT id, cart_id, product_id, quantity, price, created_at, updated_at
198+
FROM cart_items
199+
WHERE cart_id = $1 AND product_id = $2
200+
`
201+
item := &models.CartItem{}
202+
err := r.db.QueryRow(ctx, query, cartID, productID).Scan(
203+
&item.ID,
204+
&item.CartID,
205+
&item.ProductID,
206+
&item.Quantity,
207+
&item.Price,
208+
&item.CreatedAt,
209+
&item.UpdatedAt,
210+
)
211+
212+
if err != nil {
213+
if errors.Is(err, pgx.ErrNoRows) {
214+
return nil, ErrProductNotInCart
215+
}
216+
return nil, err
217+
}
218+
return item, nil
219+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-- +migrate Down
2+
-- SQL in this section is executed when the migration is rolled back.
3+
4+
-- Drop trigger on cart_items
5+
DROP TRIGGER IF EXISTS update_cart_items_updated_at ON cart_items;
6+
7+
-- Drop indices on cart_items
8+
DROP INDEX IF EXISTS idx_cart_items_product_id;
9+
DROP INDEX IF EXISTS idx_cart_items_cart_id;
10+
11+
-- Drop the cart_items table (constraints/FKs are dropped with the table)
12+
DROP TABLE IF EXISTS cart_items;
13+
14+
-- Drop trigger on carts
15+
DROP TRIGGER IF EXISTS update_carts_updated_at ON carts;
16+
17+
-- Drop index on carts
18+
DROP INDEX IF EXISTS idx_carts_user_id;
19+
20+
-- Drop the carts table
21+
DROP TABLE IF EXISTS carts;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
-- +migrate Up
2+
-- SQL in this section is executed when the migration is applied.
3+
4+
-- Create the carts table (one cart per user)
5+
CREATE TABLE IF NOT EXISTS carts (
6+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
7+
user_id UUID UNIQUE NOT NULL,
8+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
9+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10+
11+
CONSTRAINT fk_carts_user
12+
FOREIGN KEY(user_id) REFERENCES users(id)
13+
ON DELETE CASCADE -- If user is deleted, their cart is deleted
14+
);
15+
16+
-- Index on user_id for faster lookup
17+
CREATE INDEX IF NOT EXISTS idx_carts_user_id ON carts(user_id);
18+
19+
-- Trigger for updated_at on carts
20+
CREATE TRIGGER update_carts_updated_at
21+
BEFORE UPDATE ON carts
22+
FOR EACH ROW
23+
EXECUTE FUNCTION update_updated_at_column();
24+
25+
-- Create the cart_items table
26+
CREATE TABLE IF NOT EXISTS cart_items (
27+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
28+
cart_id UUID NOT NULL,
29+
product_id UUID NOT NULL,
30+
quantity INT NOT NULL CHECK (quantity > 0),
31+
price NUMERIC(10, 2) NOT NULL CHECK (price >= 0), -- Price at the time of adding
32+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
33+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
34+
35+
CONSTRAINT fk_cart_items_cart
36+
FOREIGN KEY(cart_id) REFERENCES carts(id)
37+
ON DELETE CASCADE, -- If cart is deleted, items are deleted
38+
39+
CONSTRAINT fk_cart_items_product
40+
FOREIGN KEY(product_id) REFERENCES products(id)
41+
ON DELETE CASCADE, -- If product is deleted, remove item from cart
42+
43+
-- Ensure a product appears only once per cart (update quantity instead)
44+
CONSTRAINT unique_cart_product UNIQUE (cart_id, product_id)
45+
);
46+
47+
-- Indices for performance
48+
CREATE INDEX IF NOT EXISTS idx_cart_items_cart_id ON cart_items(cart_id);
49+
CREATE INDEX IF NOT EXISTS idx_cart_items_product_id ON cart_items(product_id);
50+
51+
-- Trigger for updated_at on cart_items
52+
CREATE TRIGGER update_cart_items_updated_at
53+
BEFORE UPDATE ON cart_items
54+
FOR EACH ROW
55+
EXECUTE FUNCTION update_updated_at_column();
56+
57+
58+
-- +migrate Down
59+
-- SQL section moved to the .down.sql file
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
-- +migrate Down
2+
-- SQL in this section is executed when the migration is rolled back.
3+
4+
-- Drop trigger on order_items
5+
DROP TRIGGER IF EXISTS update_order_items_updated_at ON order_items;
6+
7+
-- Drop indices on order_items
8+
DROP INDEX IF EXISTS idx_order_items_product_id;
9+
DROP INDEX IF EXISTS idx_order_items_order_id;
10+
11+
-- Drop the order_items table
12+
DROP TABLE IF EXISTS order_items;
13+
14+
-- Drop trigger on orders
15+
DROP TRIGGER IF EXISTS update_orders_updated_at ON orders;
16+
17+
-- Drop indices on orders
18+
DROP INDEX IF EXISTS idx_orders_status;
19+
DROP INDEX IF EXISTS idx_orders_user_id;
20+
21+
-- Drop the orders table
22+
DROP TABLE IF EXISTS orders;
23+
24+
-- Optional: Drop the ENUM type if it was created
25+
-- DROP TYPE IF EXISTS order_status;

0 commit comments

Comments
 (0)