Add shipping settings and calculation
This commit is contained in:
parent
d9886fae62
commit
fd60888706
9 changed files with 486 additions and 0 deletions
44
backend/internal/domain/shipping.go
Normal file
44
backend/internal/domain/shipping.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
// ShippingMethodType defines supported fulfillment modes.
|
||||
type ShippingMethodType string
|
||||
|
||||
const (
|
||||
ShippingMethodPickup ShippingMethodType = "pickup"
|
||||
ShippingMethodOwnDelivery ShippingMethodType = "own_delivery"
|
||||
ShippingMethodThirdParty ShippingMethodType = "third_party_delivery"
|
||||
ShippingOptionTypePickup = "pickup"
|
||||
ShippingOptionTypeDelivery = "delivery"
|
||||
)
|
||||
|
||||
// ShippingMethod stores vendor configuration for pickup or delivery.
|
||||
type ShippingMethod struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
VendorID uuid.UUID `db:"vendor_id" json:"vendor_id"`
|
||||
Type ShippingMethodType `db:"type" json:"type"`
|
||||
Active bool `db:"active" json:"active"`
|
||||
PreparationMinutes int `db:"preparation_minutes" json:"preparation_minutes"`
|
||||
MaxRadiusKm float64 `db:"max_radius_km" json:"max_radius_km"`
|
||||
MinFeeCents int64 `db:"min_fee_cents" json:"min_fee_cents"`
|
||||
PricePerKmCents int64 `db:"price_per_km_cents" json:"price_per_km_cents"`
|
||||
FreeShippingThresholdCents *int64 `db:"free_shipping_threshold_cents" json:"free_shipping_threshold_cents,omitempty"`
|
||||
PickupAddress string `db:"pickup_address" json:"pickup_address"`
|
||||
PickupHours string `db:"pickup_hours" json:"pickup_hours"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// ShippingOption presents calculated options to the buyer.
|
||||
type ShippingOption struct {
|
||||
Type string `json:"type"`
|
||||
ValueCents int64 `json:"value_cents"`
|
||||
EstimatedMinutes int `json:"estimated_minutes"`
|
||||
Description string `json:"description"`
|
||||
DistanceKm float64 `json:"distance_km,omitempty"`
|
||||
}
|
||||
|
|
@ -144,6 +144,31 @@ type updateStatusRequest struct {
|
|||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type shippingMethodRequest struct {
|
||||
Type string `json:"type"`
|
||||
Active bool `json:"active"`
|
||||
PreparationMinutes int `json:"preparation_minutes"`
|
||||
MaxRadiusKm float64 `json:"max_radius_km"`
|
||||
MinFeeCents int64 `json:"min_fee_cents"`
|
||||
PricePerKmCents int64 `json:"price_per_km_cents"`
|
||||
FreeShippingThresholdCents *int64 `json:"free_shipping_threshold_cents,omitempty"`
|
||||
PickupAddress string `json:"pickup_address,omitempty"`
|
||||
PickupHours string `json:"pickup_hours,omitempty"`
|
||||
}
|
||||
|
||||
type shippingSettingsRequest struct {
|
||||
Methods []shippingMethodRequest `json:"methods"`
|
||||
}
|
||||
|
||||
type shippingCalculateRequest struct {
|
||||
VendorID uuid.UUID `json:"vendor_id"`
|
||||
CartTotalCents int64 `json:"cart_total_cents"`
|
||||
BuyerLatitude *float64 `json:"buyer_latitude,omitempty"`
|
||||
BuyerLongitude *float64 `json:"buyer_longitude,omitempty"`
|
||||
AddressID *uuid.UUID `json:"address_id,omitempty"`
|
||||
PostalCode string `json:"postal_code,omitempty"`
|
||||
}
|
||||
|
||||
// --- Utility Functions ---
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type MockRepository struct {
|
|||
products []domain.Product
|
||||
users []domain.User
|
||||
orders []domain.Order
|
||||
shipping []domain.ShippingMethod
|
||||
}
|
||||
|
||||
func NewMockRepository() *MockRepository {
|
||||
|
|
@ -29,6 +30,7 @@ func NewMockRepository() *MockRepository {
|
|||
products: make([]domain.Product, 0),
|
||||
users: make([]domain.User, 0),
|
||||
orders: make([]domain.Order, 0),
|
||||
shipping: make([]domain.ShippingMethod, 0),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -232,6 +234,33 @@ func (m *MockRepository) AdminDashboard(ctx context.Context, since time.Time) (*
|
|||
return &domain.AdminDashboard{}, nil
|
||||
}
|
||||
|
||||
func (m *MockRepository) GetShippingMethodsByVendor(ctx context.Context, vendorID uuid.UUID) ([]domain.ShippingMethod, error) {
|
||||
var methods []domain.ShippingMethod
|
||||
for _, method := range m.shipping {
|
||||
if method.VendorID == vendorID {
|
||||
methods = append(methods, method)
|
||||
}
|
||||
}
|
||||
return methods, nil
|
||||
}
|
||||
|
||||
func (m *MockRepository) UpsertShippingMethods(ctx context.Context, methods []domain.ShippingMethod) error {
|
||||
for _, method := range methods {
|
||||
updated := false
|
||||
for i, existing := range m.shipping {
|
||||
if existing.VendorID == method.VendorID && existing.Type == method.Type {
|
||||
m.shipping[i] = method
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
m.shipping = append(m.shipping, method)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MockPaymentGateway implements the PaymentGateway interface for testing
|
||||
type MockPaymentGateway struct{}
|
||||
|
||||
|
|
|
|||
195
backend/internal/http/handler/shipping_handler.go
Normal file
195
backend/internal/http/handler/shipping_handler.go
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
|
||||
"github.com/saveinmed/backend-go/internal/domain"
|
||||
)
|
||||
|
||||
// GetShippingSettings godoc
|
||||
// @Summary Get vendor shipping settings
|
||||
// @Description Returns pickup and delivery settings for a vendor.
|
||||
// @Tags Shipping
|
||||
// @Produce json
|
||||
// @Param vendor_id path string true "Vendor ID"
|
||||
// @Success 200 {array} domain.ShippingMethod
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 403 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/shipping/settings/{vendor_id} [get]
|
||||
func (h *Handler) GetShippingSettings(w http.ResponseWriter, r *http.Request) {
|
||||
vendorID, err := parseUUIDFromPath(r.URL.Path)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
requester, err := getRequester(r)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if !strings.EqualFold(requester.Role, "Admin") {
|
||||
if requester.CompanyID == nil || *requester.CompanyID != vendorID {
|
||||
writeError(w, http.StatusForbidden, errors.New("not allowed to view shipping settings"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
methods, err := h.svc.GetShippingMethods(r.Context(), vendorID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, methods)
|
||||
}
|
||||
|
||||
// UpsertShippingSettings godoc
|
||||
// @Summary Update vendor shipping settings
|
||||
// @Description Stores pickup and delivery settings for a vendor.
|
||||
// @Tags Shipping
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param vendor_id path string true "Vendor ID"
|
||||
// @Param payload body shippingSettingsRequest true "Shipping settings"
|
||||
// @Success 200 {array} domain.ShippingMethod
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 403 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/shipping/settings/{vendor_id} [put]
|
||||
func (h *Handler) UpsertShippingSettings(w http.ResponseWriter, r *http.Request) {
|
||||
vendorID, err := parseUUIDFromPath(r.URL.Path)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
requester, err := getRequester(r)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if !strings.EqualFold(requester.Role, "Admin") {
|
||||
if requester.CompanyID == nil || *requester.CompanyID != vendorID {
|
||||
writeError(w, http.StatusForbidden, errors.New("not allowed to update shipping settings"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var req shippingSettingsRequest
|
||||
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if len(req.Methods) == 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("methods are required"))
|
||||
return
|
||||
}
|
||||
|
||||
methods := make([]domain.ShippingMethod, 0, len(req.Methods))
|
||||
for _, method := range req.Methods {
|
||||
methodType, err := parseShippingMethodType(method.Type)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if method.PreparationMinutes < 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("preparation_minutes must be >= 0"))
|
||||
return
|
||||
}
|
||||
if methodType != domain.ShippingMethodPickup {
|
||||
if method.MaxRadiusKm <= 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("max_radius_km must be > 0 for delivery methods"))
|
||||
return
|
||||
}
|
||||
if method.MinFeeCents < 0 || method.PricePerKmCents < 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("delivery pricing must be >= 0"))
|
||||
return
|
||||
}
|
||||
if method.FreeShippingThresholdCents != nil && *method.FreeShippingThresholdCents <= 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("free_shipping_threshold_cents must be > 0"))
|
||||
return
|
||||
}
|
||||
}
|
||||
if methodType == domain.ShippingMethodPickup && method.Active {
|
||||
if strings.TrimSpace(method.PickupAddress) == "" || strings.TrimSpace(method.PickupHours) == "" {
|
||||
writeError(w, http.StatusBadRequest, errors.New("pickup_address and pickup_hours are required for active pickup"))
|
||||
return
|
||||
}
|
||||
}
|
||||
methods = append(methods, domain.ShippingMethod{
|
||||
Type: methodType,
|
||||
Active: method.Active,
|
||||
PreparationMinutes: method.PreparationMinutes,
|
||||
MaxRadiusKm: method.MaxRadiusKm,
|
||||
MinFeeCents: method.MinFeeCents,
|
||||
PricePerKmCents: method.PricePerKmCents,
|
||||
FreeShippingThresholdCents: method.FreeShippingThresholdCents,
|
||||
PickupAddress: strings.TrimSpace(method.PickupAddress),
|
||||
PickupHours: strings.TrimSpace(method.PickupHours),
|
||||
})
|
||||
}
|
||||
|
||||
updated, err := h.svc.UpsertShippingMethods(r.Context(), vendorID, methods)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, updated)
|
||||
}
|
||||
|
||||
// CalculateShipping godoc
|
||||
// @Summary Calculate shipping options
|
||||
// @Description Calculates shipping or pickup options based on vendor config and buyer location.
|
||||
// @Tags Shipping
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body shippingCalculateRequest true "Calculation inputs"
|
||||
// @Success 200 {array} domain.ShippingOption
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/shipping/calculate [post]
|
||||
func (h *Handler) CalculateShipping(w http.ResponseWriter, r *http.Request) {
|
||||
var req shippingCalculateRequest
|
||||
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.VendorID == uuid.Nil {
|
||||
writeError(w, http.StatusBadRequest, errors.New("vendor_id is required"))
|
||||
return
|
||||
}
|
||||
if req.BuyerLatitude == nil || req.BuyerLongitude == nil {
|
||||
if req.AddressID != nil || req.PostalCode != "" {
|
||||
writeError(w, http.StatusBadRequest, errors.New("address_id or postal_code geocoding is not supported; provide buyer_latitude and buyer_longitude"))
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusBadRequest, errors.New("buyer_latitude and buyer_longitude are required"))
|
||||
return
|
||||
}
|
||||
|
||||
options, err := h.svc.CalculateShippingOptions(r.Context(), req.VendorID, *req.BuyerLatitude, *req.BuyerLongitude, req.CartTotalCents)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, options)
|
||||
}
|
||||
|
||||
func parseShippingMethodType(value string) (domain.ShippingMethodType, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case string(domain.ShippingMethodPickup):
|
||||
return domain.ShippingMethodPickup, nil
|
||||
case string(domain.ShippingMethodOwnDelivery):
|
||||
return domain.ShippingMethodOwnDelivery, nil
|
||||
case string(domain.ShippingMethodThirdParty):
|
||||
return domain.ShippingMethodThirdParty, nil
|
||||
default:
|
||||
return "", errors.New("invalid shipping method type")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
CREATE TABLE IF NOT EXISTS shipping_methods (
|
||||
id UUID PRIMARY KEY,
|
||||
vendor_id UUID NOT NULL REFERENCES companies(id),
|
||||
type TEXT NOT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
preparation_minutes INT NOT NULL DEFAULT 0,
|
||||
max_radius_km DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
min_fee_cents BIGINT NOT NULL DEFAULT 0,
|
||||
price_per_km_cents BIGINT NOT NULL DEFAULT 0,
|
||||
free_shipping_threshold_cents BIGINT,
|
||||
pickup_address TEXT,
|
||||
pickup_hours TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
UNIQUE (vendor_id, type)
|
||||
);
|
||||
|
|
@ -952,3 +952,55 @@ func (r *Repository) AdminDashboard(ctx context.Context, since time.Time) (*doma
|
|||
|
||||
return &domain.AdminDashboard{GMVCents: totalGMV, NewCompanies: newCompanies, WindowStartAt: since}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetShippingMethodsByVendor(ctx context.Context, vendorID uuid.UUID) ([]domain.ShippingMethod, error) {
|
||||
query := `SELECT id, vendor_id, type, active, preparation_minutes, max_radius_km, min_fee_cents, price_per_km_cents, free_shipping_threshold_cents, pickup_address, pickup_hours, created_at, updated_at
|
||||
FROM shipping_methods WHERE vendor_id = $1 ORDER BY type`
|
||||
var methods []domain.ShippingMethod
|
||||
if err := r.db.SelectContext(ctx, &methods, query, vendorID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return methods, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpsertShippingMethods(ctx context.Context, methods []domain.ShippingMethod) error {
|
||||
if len(methods) == 0 {
|
||||
return nil
|
||||
}
|
||||
query := `INSERT INTO shipping_methods (id, vendor_id, type, active, preparation_minutes, max_radius_km, min_fee_cents, price_per_km_cents, free_shipping_threshold_cents, pickup_address, pickup_hours, created_at, updated_at)
|
||||
VALUES (:id, :vendor_id, :type, :active, :preparation_minutes, :max_radius_km, :min_fee_cents, :price_per_km_cents, :free_shipping_threshold_cents, :pickup_address, :pickup_hours, :created_at, :updated_at)
|
||||
ON CONFLICT (vendor_id, type) DO UPDATE
|
||||
SET active = EXCLUDED.active,
|
||||
preparation_minutes = EXCLUDED.preparation_minutes,
|
||||
max_radius_km = EXCLUDED.max_radius_km,
|
||||
min_fee_cents = EXCLUDED.min_fee_cents,
|
||||
price_per_km_cents = EXCLUDED.price_per_km_cents,
|
||||
free_shipping_threshold_cents = EXCLUDED.free_shipping_threshold_cents,
|
||||
pickup_address = EXCLUDED.pickup_address,
|
||||
pickup_hours = EXCLUDED.pickup_hours,
|
||||
updated_at = EXCLUDED.updated_at`
|
||||
|
||||
tx, err := r.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
now := time.Now().UTC()
|
||||
for i := range methods {
|
||||
methods[i].CreatedAt = now
|
||||
methods[i].UpdatedAt = now
|
||||
if _, err = tx.NamedExecContext(ctx, query, methods[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,9 @@ func New(cfg config.Config) (*Server, error) {
|
|||
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 /api/v1/shipping/settings/{vendor_id}", chain(http.HandlerFunc(h.GetShippingSettings), middleware.Logger, middleware.Gzip, auth))
|
||||
mux.Handle("PUT /api/v1/shipping/settings/{vendor_id}", chain(http.HandlerFunc(h.UpsertShippingSettings), middleware.Logger, middleware.Gzip, auth))
|
||||
mux.Handle("POST /api/v1/shipping/calculate", chain(http.HandlerFunc(h.CalculateShipping), middleware.Logger, middleware.Gzip))
|
||||
|
||||
mux.Handle("GET /docs/", httpSwagger.Handler(httpSwagger.URL("/docs/doc.json")))
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package usecase
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -55,6 +56,8 @@ type Repository interface {
|
|||
GetCompanyRating(ctx context.Context, companyID uuid.UUID) (*domain.CompanyRating, error)
|
||||
SellerDashboard(ctx context.Context, sellerID uuid.UUID) (*domain.SellerDashboard, error)
|
||||
AdminDashboard(ctx context.Context, since time.Time) (*domain.AdminDashboard, error)
|
||||
GetShippingMethodsByVendor(ctx context.Context, vendorID uuid.UUID) ([]domain.ShippingMethod, error)
|
||||
UpsertShippingMethods(ctx context.Context, methods []domain.ShippingMethod) error
|
||||
}
|
||||
|
||||
// PaymentGateway abstracts Mercado Pago integration.
|
||||
|
|
@ -108,6 +111,96 @@ func (s *Service) GetCompany(ctx context.Context, id uuid.UUID) (*domain.Company
|
|||
return s.repo.GetCompany(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetShippingMethods(ctx context.Context, vendorID uuid.UUID) ([]domain.ShippingMethod, error) {
|
||||
return s.repo.GetShippingMethodsByVendor(ctx, vendorID)
|
||||
}
|
||||
|
||||
func (s *Service) UpsertShippingMethods(ctx context.Context, vendorID uuid.UUID, methods []domain.ShippingMethod) ([]domain.ShippingMethod, error) {
|
||||
if len(methods) == 0 {
|
||||
return nil, errors.New("shipping methods are required")
|
||||
}
|
||||
for i := range methods {
|
||||
if methods[i].Type == "" {
|
||||
return nil, errors.New("shipping method type is required")
|
||||
}
|
||||
if methods[i].ID == uuid.Nil {
|
||||
methods[i].ID = uuid.Must(uuid.NewV7())
|
||||
}
|
||||
methods[i].VendorID = vendorID
|
||||
}
|
||||
if err := s.repo.UpsertShippingMethods(ctx, methods); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.GetShippingMethodsByVendor(ctx, vendorID)
|
||||
}
|
||||
|
||||
func (s *Service) CalculateShippingOptions(ctx context.Context, vendorID uuid.UUID, buyerLat, buyerLng float64, cartTotalCents int64) ([]domain.ShippingOption, error) {
|
||||
company, err := s.repo.GetCompany(ctx, vendorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if company == nil {
|
||||
return nil, errors.New("vendor not found")
|
||||
}
|
||||
|
||||
methods, err := s.repo.GetShippingMethodsByVendor(ctx, vendorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
distance := domain.HaversineDistance(company.Latitude, company.Longitude, buyerLat, buyerLng)
|
||||
|
||||
var options []domain.ShippingOption
|
||||
for _, method := range methods {
|
||||
if !method.Active {
|
||||
continue
|
||||
}
|
||||
switch method.Type {
|
||||
case domain.ShippingMethodPickup:
|
||||
description := "Pickup at seller location"
|
||||
if method.PickupAddress != "" {
|
||||
description = "Pickup at " + method.PickupAddress
|
||||
}
|
||||
if method.PickupHours != "" {
|
||||
description += " (" + method.PickupHours + ")"
|
||||
}
|
||||
options = append(options, domain.ShippingOption{
|
||||
Type: domain.ShippingOptionTypePickup,
|
||||
ValueCents: 0,
|
||||
EstimatedMinutes: method.PreparationMinutes,
|
||||
Description: description,
|
||||
DistanceKm: distance,
|
||||
})
|
||||
case domain.ShippingMethodOwnDelivery, domain.ShippingMethodThirdParty:
|
||||
if method.MaxRadiusKm > 0 && distance > method.MaxRadiusKm {
|
||||
continue
|
||||
}
|
||||
variableCost := int64(math.Round(distance * float64(method.PricePerKmCents)))
|
||||
price := method.MinFeeCents
|
||||
if variableCost > price {
|
||||
price = variableCost
|
||||
}
|
||||
if method.FreeShippingThresholdCents != nil && cartTotalCents >= *method.FreeShippingThresholdCents {
|
||||
price = 0
|
||||
}
|
||||
estimatedMinutes := method.PreparationMinutes + int(math.Round(distance*5))
|
||||
description := "Delivery via own fleet"
|
||||
if method.Type == domain.ShippingMethodThirdParty {
|
||||
description = "Delivery via third-party courier"
|
||||
}
|
||||
options = append(options, domain.ShippingOption{
|
||||
Type: domain.ShippingOptionTypeDelivery,
|
||||
ValueCents: price,
|
||||
EstimatedMinutes: estimatedMinutes,
|
||||
Description: description,
|
||||
DistanceKm: distance,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateCompany(ctx context.Context, company *domain.Company) error {
|
||||
return s.repo.UpdateCompany(ctx, company)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ type MockRepository struct {
|
|||
orders []domain.Order
|
||||
cartItems []domain.CartItem
|
||||
reviews []domain.Review
|
||||
shipping []domain.ShippingMethod
|
||||
}
|
||||
|
||||
func NewMockRepository() *MockRepository {
|
||||
|
|
@ -27,6 +28,7 @@ func NewMockRepository() *MockRepository {
|
|||
orders: make([]domain.Order, 0),
|
||||
cartItems: make([]domain.CartItem, 0),
|
||||
reviews: make([]domain.Review, 0),
|
||||
shipping: make([]domain.ShippingMethod, 0),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -251,6 +253,33 @@ func (m *MockRepository) AdminDashboard(ctx context.Context, since time.Time) (*
|
|||
return &domain.AdminDashboard{GMVCents: 1000000}, nil
|
||||
}
|
||||
|
||||
func (m *MockRepository) GetShippingMethodsByVendor(ctx context.Context, vendorID uuid.UUID) ([]domain.ShippingMethod, error) {
|
||||
var methods []domain.ShippingMethod
|
||||
for _, method := range m.shipping {
|
||||
if method.VendorID == vendorID {
|
||||
methods = append(methods, method)
|
||||
}
|
||||
}
|
||||
return methods, nil
|
||||
}
|
||||
|
||||
func (m *MockRepository) UpsertShippingMethods(ctx context.Context, methods []domain.ShippingMethod) error {
|
||||
for _, method := range methods {
|
||||
updated := false
|
||||
for i, existing := range m.shipping {
|
||||
if existing.VendorID == method.VendorID && existing.Type == method.Type {
|
||||
m.shipping[i] = method
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
m.shipping = append(m.shipping, method)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MockPaymentGateway for testing
|
||||
type MockPaymentGateway struct{}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue