Add inventory and cart endpoints
This commit is contained in:
parent
a674eb8169
commit
e96ba8a49b
5 changed files with 439 additions and 1 deletions
|
|
@ -59,6 +59,32 @@ type Product struct {
|
|||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// InventoryItem exposes stock tracking tied to product batches.
|
||||
type InventoryItem struct {
|
||||
ProductID uuid.UUID `db:"product_id" json:"product_id"`
|
||||
SellerID uuid.UUID `db:"seller_id" json:"seller_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Batch string `db:"batch" json:"batch"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
Quantity int64 `db:"quantity" json:"quantity"`
|
||||
PriceCents int64 `db:"price_cents" json:"price_cents"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// InventoryFilter allows filtering by expiration window.
|
||||
type InventoryFilter struct {
|
||||
ExpiringBefore *time.Time
|
||||
}
|
||||
|
||||
// InventoryAdjustment records manual stock corrections.
|
||||
type InventoryAdjustment struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
ProductID uuid.UUID `db:"product_id" json:"product_id"`
|
||||
Delta int64 `db:"delta" json:"delta"`
|
||||
Reason string `db:"reason" json:"reason"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
}
|
||||
|
||||
// Order captures the status lifecycle and payment intent.
|
||||
type Order struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
|
|
@ -101,3 +127,26 @@ const (
|
|||
OrderStatusInvoiced OrderStatus = "Faturado"
|
||||
OrderStatusDelivered OrderStatus = "Entregue"
|
||||
)
|
||||
|
||||
// CartItem stores buyer selections with unit pricing.
|
||||
type CartItem struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
BuyerID uuid.UUID `db:"buyer_id" json:"buyer_id"`
|
||||
ProductID uuid.UUID `db:"product_id" json:"product_id"`
|
||||
Quantity int64 `db:"quantity" json:"quantity"`
|
||||
UnitCents int64 `db:"unit_cents" json:"unit_cents"`
|
||||
ProductName string `db:"product_name" json:"product_name"`
|
||||
Batch string `db:"batch" json:"batch"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// CartSummary aggregates cart totals and discounts.
|
||||
type CartSummary struct {
|
||||
Items []CartItem `json:"items"`
|
||||
SubtotalCents int64 `json:"subtotal_cents"`
|
||||
DiscountCents int64 `json:"discount_cents"`
|
||||
TotalCents int64 `json:"total_cents"`
|
||||
DiscountReason string `json:"discount_reason,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,8 +46,13 @@ func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
var companyID uuid.UUID
|
||||
if req.CompanyID != nil {
|
||||
companyID = *req.CompanyID
|
||||
}
|
||||
|
||||
user := &domain.User{
|
||||
CompanyID: req.CompanyID,
|
||||
CompanyID: companyID,
|
||||
Role: req.Role,
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
|
|
@ -222,6 +227,50 @@ func (h *Handler) ListProducts(w http.ResponseWriter, r *http.Request) {
|
|||
writeJSON(w, http.StatusOK, products)
|
||||
}
|
||||
|
||||
// ListInventory exposes stock with expiring batch filters.
|
||||
func (h *Handler) ListInventory(w http.ResponseWriter, r *http.Request) {
|
||||
var filter domain.InventoryFilter
|
||||
if days := r.URL.Query().Get("expires_in_days"); days != "" {
|
||||
n, err := strconv.Atoi(days)
|
||||
if err != nil || n < 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("invalid expires_in_days"))
|
||||
return
|
||||
}
|
||||
expires := time.Now().Add(time.Duration(n) * 24 * time.Hour)
|
||||
filter.ExpiringBefore = &expires
|
||||
}
|
||||
|
||||
inventory, err := h.svc.ListInventory(r.Context(), filter)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, inventory)
|
||||
}
|
||||
|
||||
// AdjustInventory handles manual stock corrections.
|
||||
func (h *Handler) AdjustInventory(w http.ResponseWriter, r *http.Request) {
|
||||
var req inventoryAdjustRequest
|
||||
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Delta == 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("delta must be non-zero"))
|
||||
return
|
||||
}
|
||||
|
||||
item, err := h.svc.AdjustInventory(r.Context(), req.ProductID, req.Delta, req.Reason)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreateOrder godoc
|
||||
// @Summary Criação de pedido com split
|
||||
// @Tags Pedidos
|
||||
|
|
@ -315,6 +364,68 @@ func (h *Handler) UpdateOrderStatus(w http.ResponseWriter, r *http.Request) {
|
|||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// AddToCart appends an item to the authenticated buyer cart respecting stock.
|
||||
func (h *Handler) AddToCart(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := middleware.GetClaims(r.Context())
|
||||
if !ok || claims.CompanyID == nil {
|
||||
writeError(w, http.StatusBadRequest, errors.New("missing buyer context"))
|
||||
return
|
||||
}
|
||||
|
||||
var req addCartItemRequest
|
||||
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
summary, err := h.svc.AddItemToCart(r.Context(), *claims.CompanyID, req.ProductID, req.Quantity)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, summary)
|
||||
}
|
||||
|
||||
// GetCart returns cart contents and totals for the authenticated buyer.
|
||||
func (h *Handler) GetCart(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := middleware.GetClaims(r.Context())
|
||||
if !ok || claims.CompanyID == nil {
|
||||
writeError(w, http.StatusBadRequest, errors.New("missing buyer context"))
|
||||
return
|
||||
}
|
||||
|
||||
summary, err := h.svc.ListCart(r.Context(), *claims.CompanyID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, summary)
|
||||
}
|
||||
|
||||
// DeleteCartItem removes a product from the cart and returns the updated totals.
|
||||
func (h *Handler) DeleteCartItem(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := middleware.GetClaims(r.Context())
|
||||
if !ok || claims.CompanyID == nil {
|
||||
writeError(w, http.StatusBadRequest, errors.New("missing buyer context"))
|
||||
return
|
||||
}
|
||||
|
||||
id, err := parseUUIDFromPath(r.URL.Path)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
summary, err := h.svc.RemoveCartItem(r.Context(), *claims.CompanyID, id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, summary)
|
||||
}
|
||||
|
||||
// CreatePaymentPreference godoc
|
||||
// @Summary Cria preferência de pagamento Mercado Pago com split nativo
|
||||
// @Tags Pagamentos
|
||||
|
|
@ -579,6 +690,17 @@ type authResponse struct {
|
|||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
type inventoryAdjustRequest struct {
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
Delta int64 `json:"delta"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type addCartItemRequest struct {
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
}
|
||||
|
||||
type updateUserRequest struct {
|
||||
CompanyID *uuid.UUID `json:"company_id,omitempty"`
|
||||
Role *string `json:"role,omitempty"`
|
||||
|
|
|
|||
|
|
@ -95,6 +95,15 @@ func (r *Repository) ListProducts(ctx context.Context) ([]domain.Product, error)
|
|||
return products, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetProduct(ctx context.Context, id uuid.UUID) (*domain.Product, error) {
|
||||
var product domain.Product
|
||||
query := `SELECT id, seller_id, name, description, batch, expires_at, price_cents, stock, created_at, updated_at FROM products WHERE id = $1`
|
||||
if err := r.db.GetContext(ctx, &product, query, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &product, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateOrder(ctx context.Context, order *domain.Order) error {
|
||||
now := time.Now().UTC()
|
||||
order.CreatedAt = now
|
||||
|
|
@ -158,6 +167,80 @@ func (r *Repository) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) AdjustInventory(ctx context.Context, productID uuid.UUID, delta int64, reason string) (*domain.InventoryItem, error) {
|
||||
tx, err := r.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var product domain.Product
|
||||
if err := tx.GetContext(ctx, &product, `SELECT id, seller_id, name, batch, expires_at, price_cents, stock, updated_at FROM products WHERE id = $1 FOR UPDATE`, productID); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newStock := product.Stock + delta
|
||||
if newStock < 0 {
|
||||
_ = tx.Rollback()
|
||||
return nil, errors.New("inventory cannot be negative")
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE products SET stock = $1, updated_at = $2 WHERE id = $3`, newStock, now, productID); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
adj := domain.InventoryAdjustment{
|
||||
ID: uuid.Must(uuid.NewV7()),
|
||||
ProductID: productID,
|
||||
Delta: delta,
|
||||
Reason: reason,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
if _, err := tx.NamedExecContext(ctx, `INSERT INTO inventory_adjustments (id, product_id, delta, reason, created_at) VALUES (:id, :product_id, :delta, :reason, :created_at)`, &adj); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &domain.InventoryItem{
|
||||
ProductID: productID,
|
||||
SellerID: product.SellerID,
|
||||
Name: product.Name,
|
||||
Batch: product.Batch,
|
||||
ExpiresAt: product.ExpiresAt,
|
||||
Quantity: newStock,
|
||||
PriceCents: product.PriceCents,
|
||||
UpdatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListInventory(ctx context.Context, filter domain.InventoryFilter) ([]domain.InventoryItem, error) {
|
||||
args := []any{}
|
||||
clauses := []string{}
|
||||
if filter.ExpiringBefore != nil {
|
||||
clauses = append(clauses, fmt.Sprintf("expires_at <= $%d", len(args)+1))
|
||||
args = append(args, *filter.ExpiringBefore)
|
||||
}
|
||||
|
||||
where := ""
|
||||
if len(clauses) > 0 {
|
||||
where = " WHERE " + strings.Join(clauses, " AND ")
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`SELECT id AS product_id, seller_id, name, batch, expires_at, stock AS quantity, price_cents, updated_at FROM products%s ORDER BY expires_at ASC`, where)
|
||||
var items []domain.InventoryItem
|
||||
if err := r.db.SelectContext(ctx, &items, query, args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateUser(ctx context.Context, user *domain.User) error {
|
||||
now := time.Now().UTC()
|
||||
user.CreatedAt = now
|
||||
|
|
@ -256,6 +339,67 @@ func (r *Repository) DeleteUser(ctx context.Context, id uuid.UUID) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) AddCartItem(ctx context.Context, item *domain.CartItem) (*domain.CartItem, error) {
|
||||
now := time.Now().UTC()
|
||||
item.CreatedAt = now
|
||||
item.UpdatedAt = now
|
||||
|
||||
query := `INSERT INTO cart_items (id, buyer_id, product_id, quantity, unit_cents, batch, expires_at, created_at, updated_at)
|
||||
VALUES (:id, :buyer_id, :product_id, :quantity, :unit_cents, :batch, :expires_at, :created_at, :updated_at)
|
||||
ON CONFLICT (buyer_id, product_id) DO UPDATE
|
||||
SET quantity = cart_items.quantity + EXCLUDED.quantity,
|
||||
unit_cents = EXCLUDED.unit_cents,
|
||||
batch = EXCLUDED.batch,
|
||||
expires_at = EXCLUDED.expires_at,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING id, buyer_id, product_id, quantity, unit_cents, batch, expires_at, created_at, updated_at`
|
||||
|
||||
var saved domain.CartItem
|
||||
rows, err := r.db.NamedQueryContext(ctx, query, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if rows.Next() {
|
||||
if err := rows.StructScan(&saved); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &saved, nil
|
||||
}
|
||||
return nil, errors.New("failed to persist cart item")
|
||||
}
|
||||
|
||||
func (r *Repository) ListCartItems(ctx context.Context, buyerID uuid.UUID) ([]domain.CartItem, error) {
|
||||
query := `SELECT ci.id, ci.buyer_id, ci.product_id, ci.quantity, ci.unit_cents, ci.batch, ci.expires_at, ci.created_at, ci.updated_at,
|
||||
p.name AS product_name
|
||||
FROM cart_items ci
|
||||
JOIN products p ON p.id = ci.product_id
|
||||
WHERE ci.buyer_id = $1
|
||||
ORDER BY ci.created_at DESC`
|
||||
|
||||
var items []domain.CartItem
|
||||
if err := r.db.SelectContext(ctx, &items, query, buyerID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteCartItem(ctx context.Context, id uuid.UUID, buyerID uuid.UUID) error {
|
||||
res, err := r.db.ExecContext(ctx, "DELETE FROM cart_items WHERE id = $1 AND buyer_id = $2", id, buyerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows == 0 {
|
||||
return errors.New("cart item not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitSchema applies a minimal schema for development environments.
|
||||
func (r *Repository) InitSchema(ctx context.Context) error {
|
||||
schema := `
|
||||
|
|
@ -294,6 +438,14 @@ CREATE TABLE IF NOT EXISTS products (
|
|||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inventory_adjustments (
|
||||
id UUID PRIMARY KEY,
|
||||
product_id UUID NOT NULL REFERENCES products(id),
|
||||
delta BIGINT NOT NULL,
|
||||
reason TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id UUID PRIMARY KEY,
|
||||
buyer_id UUID NOT NULL REFERENCES companies(id),
|
||||
|
|
@ -313,6 +465,19 @@ CREATE TABLE IF NOT EXISTS order_items (
|
|||
batch TEXT NOT NULL,
|
||||
expires_at DATE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cart_items (
|
||||
id UUID PRIMARY KEY,
|
||||
buyer_id UUID NOT NULL REFERENCES companies(id),
|
||||
product_id UUID NOT NULL REFERENCES products(id),
|
||||
quantity BIGINT NOT NULL,
|
||||
unit_cents BIGINT NOT NULL,
|
||||
batch TEXT,
|
||||
expires_at DATE,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
UNIQUE (buyer_id, product_id)
|
||||
);
|
||||
`
|
||||
|
||||
if _, err := r.db.ExecContext(ctx, schema); err != nil {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ func New(cfg config.Config) (*Server, error) {
|
|||
mux.Handle("POST /api/products", chain(http.HandlerFunc(h.CreateProduct), middleware.Logger, middleware.Gzip))
|
||||
mux.Handle("GET /api/products", chain(http.HandlerFunc(h.ListProducts), middleware.Logger, middleware.Gzip))
|
||||
|
||||
mux.Handle("GET /api/v1/inventory", chain(http.HandlerFunc(h.ListInventory), middleware.Logger, middleware.Gzip, auth))
|
||||
mux.Handle("POST /api/v1/inventory/adjust", chain(http.HandlerFunc(h.AdjustInventory), middleware.Logger, middleware.Gzip, auth))
|
||||
|
||||
mux.Handle("POST /api/orders", chain(http.HandlerFunc(h.CreateOrder), middleware.Logger, middleware.Gzip, auth))
|
||||
mux.Handle("GET /api/orders/", chain(http.HandlerFunc(h.GetOrder), middleware.Logger, middleware.Gzip, auth))
|
||||
mux.Handle("PATCH /api/orders/", chain(http.HandlerFunc(h.UpdateOrderStatus), middleware.Logger, middleware.Gzip, auth))
|
||||
|
|
@ -84,6 +87,10 @@ func New(cfg config.Config) (*Server, error) {
|
|||
mux.Handle("PUT /api/v1/users/", chain(http.HandlerFunc(h.UpdateUser), middleware.Logger, middleware.Gzip, auth))
|
||||
mux.Handle("DELETE /api/v1/users/", chain(http.HandlerFunc(h.DeleteUser), middleware.Logger, middleware.Gzip, auth))
|
||||
|
||||
mux.Handle("POST /api/v1/cart", chain(http.HandlerFunc(h.AddToCart), middleware.Logger, middleware.Gzip, auth))
|
||||
mux.Handle("GET /api/v1/cart", chain(http.HandlerFunc(h.GetCart), middleware.Logger, middleware.Gzip, auth))
|
||||
mux.Handle("DELETE /api/v1/cart/", chain(http.HandlerFunc(h.DeleteCartItem), middleware.Logger, middleware.Gzip, auth))
|
||||
|
||||
mux.Handle("GET /swagger/", httpSwagger.Handler(httpSwagger.URL("/swagger/doc.json")))
|
||||
|
||||
return &Server{cfg: cfg, db: db, mux: mux}, nil
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ type Repository interface {
|
|||
|
||||
CreateProduct(ctx context.Context, product *domain.Product) error
|
||||
ListProducts(ctx context.Context) ([]domain.Product, error)
|
||||
GetProduct(ctx context.Context, id uuid.UUID) (*domain.Product, error)
|
||||
AdjustInventory(ctx context.Context, productID uuid.UUID, delta int64, reason string) (*domain.InventoryItem, error)
|
||||
ListInventory(ctx context.Context, filter domain.InventoryFilter) ([]domain.InventoryItem, error)
|
||||
|
||||
CreateOrder(ctx context.Context, order *domain.Order) error
|
||||
GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error)
|
||||
|
|
@ -33,6 +36,10 @@ type Repository interface {
|
|||
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||
UpdateUser(ctx context.Context, user *domain.User) error
|
||||
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||
|
||||
AddCartItem(ctx context.Context, item *domain.CartItem) (*domain.CartItem, error)
|
||||
ListCartItems(ctx context.Context, buyerID uuid.UUID) ([]domain.CartItem, error)
|
||||
DeleteCartItem(ctx context.Context, id uuid.UUID, buyerID uuid.UUID) error
|
||||
}
|
||||
|
||||
// PaymentGateway abstracts Mercado Pago integration.
|
||||
|
|
@ -74,6 +81,14 @@ func (s *Service) ListProducts(ctx context.Context) ([]domain.Product, error) {
|
|||
return s.repo.ListProducts(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) ListInventory(ctx context.Context, filter domain.InventoryFilter) ([]domain.InventoryItem, error) {
|
||||
return s.repo.ListInventory(ctx, filter)
|
||||
}
|
||||
|
||||
func (s *Service) AdjustInventory(ctx context.Context, productID uuid.UUID, delta int64, reason string) (*domain.InventoryItem, error) {
|
||||
return s.repo.AdjustInventory(ctx, productID, delta, reason)
|
||||
}
|
||||
|
||||
func (s *Service) CreateOrder(ctx context.Context, order *domain.Order) error {
|
||||
order.ID = uuid.Must(uuid.NewV7())
|
||||
order.Status = domain.OrderStatusPending
|
||||
|
|
@ -147,6 +162,86 @@ func (s *Service) DeleteUser(ctx context.Context, id uuid.UUID) error {
|
|||
return s.repo.DeleteUser(ctx, id)
|
||||
}
|
||||
|
||||
// AddItemToCart validates stock, persists the item and returns the refreshed summary.
|
||||
func (s *Service) AddItemToCart(ctx context.Context, buyerID, productID uuid.UUID, quantity int64) (*domain.CartSummary, error) {
|
||||
if quantity <= 0 {
|
||||
return nil, errors.New("quantity must be greater than zero")
|
||||
}
|
||||
|
||||
product, err := s.repo.GetProduct(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cartItems, err := s.repo.ListCartItems(ctx, buyerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var currentQty int64
|
||||
for _, it := range cartItems {
|
||||
if it.ProductID == productID {
|
||||
currentQty += it.Quantity
|
||||
}
|
||||
}
|
||||
|
||||
if product.Stock < currentQty+quantity {
|
||||
return nil, errors.New("insufficient stock for requested quantity")
|
||||
}
|
||||
|
||||
_, err = s.repo.AddCartItem(ctx, &domain.CartItem{
|
||||
ID: uuid.Must(uuid.NewV7()),
|
||||
BuyerID: buyerID,
|
||||
ProductID: productID,
|
||||
Quantity: quantity,
|
||||
UnitCents: product.PriceCents,
|
||||
Batch: product.Batch,
|
||||
ExpiresAt: product.ExpiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.cartSummary(ctx, buyerID)
|
||||
}
|
||||
|
||||
// ListCart returns the current cart with B2B discounts applied.
|
||||
func (s *Service) ListCart(ctx context.Context, buyerID uuid.UUID) (*domain.CartSummary, error) {
|
||||
return s.cartSummary(ctx, buyerID)
|
||||
}
|
||||
|
||||
// RemoveCartItem deletes a cart row and returns the refreshed cart summary.
|
||||
func (s *Service) RemoveCartItem(ctx context.Context, buyerID, cartItemID uuid.UUID) (*domain.CartSummary, error) {
|
||||
if err := s.repo.DeleteCartItem(ctx, cartItemID, buyerID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.cartSummary(ctx, buyerID)
|
||||
}
|
||||
|
||||
func (s *Service) cartSummary(ctx context.Context, buyerID uuid.UUID) (*domain.CartSummary, error) {
|
||||
items, err := s.repo.ListCartItems(ctx, buyerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var subtotal int64
|
||||
for i := range items {
|
||||
subtotal += items[i].UnitCents * items[i].Quantity
|
||||
}
|
||||
|
||||
summary := &domain.CartSummary{
|
||||
Items: items,
|
||||
SubtotalCents: subtotal,
|
||||
}
|
||||
|
||||
if subtotal >= 100000 { // apply 5% B2B discount for large baskets
|
||||
summary.DiscountCents = int64(float64(subtotal) * 0.05)
|
||||
summary.DiscountReason = "5% B2B volume discount"
|
||||
}
|
||||
summary.TotalCents = subtotal - summary.DiscountCents
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// RegisterAccount creates a company when needed and persists a user bound to it.
|
||||
func (s *Service) RegisterAccount(ctx context.Context, company *domain.Company, user *domain.User, password string) error {
|
||||
if company != nil {
|
||||
|
|
|
|||
Loading…
Reference in a new issue