Merge pull request #10 from rede5/codex/create-b2b-reputation-system
Add rating and dashboard endpoints
This commit is contained in:
commit
a1a65ab831
5 changed files with 315 additions and 0 deletions
|
|
@ -196,3 +196,45 @@ type CartSummary struct {
|
||||||
TotalCents int64 `json:"total_cents"`
|
TotalCents int64 `json:"total_cents"`
|
||||||
DiscountReason string `json:"discount_reason,omitempty"`
|
DiscountReason string `json:"discount_reason,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Review captures the buyer feedback for a completed order.
|
||||||
|
type Review struct {
|
||||||
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
OrderID uuid.UUID `db:"order_id" json:"order_id"`
|
||||||
|
BuyerID uuid.UUID `db:"buyer_id" json:"buyer_id"`
|
||||||
|
SellerID uuid.UUID `db:"seller_id" json:"seller_id"`
|
||||||
|
Rating int `db:"rating" json:"rating"`
|
||||||
|
Comment string `db:"comment" json:"comment"`
|
||||||
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompanyRating exposes the aggregate score for a seller or pharmacy.
|
||||||
|
type CompanyRating struct {
|
||||||
|
CompanyID uuid.UUID `json:"company_id"`
|
||||||
|
AverageScore float64 `json:"average_score"`
|
||||||
|
TotalReviews int64 `json:"total_reviews"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TopProduct aggregates seller performance per SKU.
|
||||||
|
type TopProduct struct {
|
||||||
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
TotalQuantity int64 `json:"total_quantity"`
|
||||||
|
RevenueCents int64 `json:"revenue_cents"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SellerDashboard summarizes commercial metrics for sellers.
|
||||||
|
type SellerDashboard struct {
|
||||||
|
SellerID uuid.UUID `json:"seller_id"`
|
||||||
|
TotalSalesCents int64 `json:"total_sales_cents"`
|
||||||
|
OrdersCount int64 `json:"orders_count"`
|
||||||
|
TopProducts []TopProduct `json:"top_products"`
|
||||||
|
LowStockAlerts []Product `json:"low_stock_alerts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminDashboard surfaces platform-wide KPIs.
|
||||||
|
type AdminDashboard struct {
|
||||||
|
GMVCents int64 `json:"gmv_cents"`
|
||||||
|
NewCompanies int64 `json:"new_companies"`
|
||||||
|
WindowStartAt time.Time `json:"window_start_at"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,28 @@ func (h *Handler) GetMyCompany(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, http.StatusOK, company)
|
writeJSON(w, http.StatusOK, company)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCompanyRating exposes the average score for a company.
|
||||||
|
func (h *Handler) GetCompanyRating(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.HasSuffix(r.URL.Path, "/rating") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := parseUUIDFromPath(r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rating, err := h.svc.GetCompanyRating(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, rating)
|
||||||
|
}
|
||||||
|
|
||||||
// CreateProduct godoc
|
// CreateProduct godoc
|
||||||
// @Summary Cadastro de produto com rastreabilidade de lote
|
// @Summary Cadastro de produto com rastreabilidade de lote
|
||||||
// @Tags Produtos
|
// @Tags Produtos
|
||||||
|
|
@ -365,6 +387,29 @@ func (h *Handler) UpdateOrderStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateReview allows buyers to rate the seller after delivery.
|
||||||
|
func (h *Handler) CreateReview(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 createReviewRequest
|
||||||
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
review, err := h.svc.CreateReview(r.Context(), *claims.CompanyID, req.OrderID, req.Rating, req.Comment)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusCreated, review)
|
||||||
|
}
|
||||||
|
|
||||||
// AddToCart appends an item to the authenticated buyer cart respecting stock.
|
// AddToCart appends an item to the authenticated buyer cart respecting stock.
|
||||||
func (h *Handler) AddToCart(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) AddToCart(w http.ResponseWriter, r *http.Request) {
|
||||||
claims, ok := middleware.GetClaims(r.Context())
|
claims, ok := middleware.GetClaims(r.Context())
|
||||||
|
|
@ -532,6 +577,65 @@ func (h *Handler) HandlePaymentWebhook(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, http.StatusOK, summary)
|
writeJSON(w, http.StatusOK, summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSellerDashboard aggregates KPIs for the authenticated seller or the requested company.
|
||||||
|
func (h *Handler) GetSellerDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requester, err := getRequester(r)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sellerID := requester.CompanyID
|
||||||
|
if sid := r.URL.Query().Get("seller_id"); sid != "" {
|
||||||
|
id, err := uuid.FromString(sid)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, errors.New("invalid seller_id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sellerID = &id
|
||||||
|
}
|
||||||
|
|
||||||
|
if sellerID == nil {
|
||||||
|
writeError(w, http.StatusBadRequest, errors.New("seller context is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.EqualFold(requester.Role, "Admin") && requester.CompanyID != nil && *sellerID != *requester.CompanyID {
|
||||||
|
writeError(w, http.StatusForbidden, errors.New("not allowed to view other sellers"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard, err := h.svc.GetSellerDashboard(r.Context(), *sellerID)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, dashboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAdminDashboard exposes platform-wide aggregated metrics.
|
||||||
|
func (h *Handler) GetAdminDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requester, err := getRequester(r)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.EqualFold(requester.Role, "Admin") {
|
||||||
|
writeError(w, http.StatusForbidden, errors.New("admin role required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard, err := h.svc.GetAdminDashboard(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, dashboard)
|
||||||
|
}
|
||||||
|
|
||||||
// CreateUser handles the creation of platform users.
|
// CreateUser handles the creation of platform users.
|
||||||
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
requester, err := getRequester(r)
|
requester, err := getRequester(r)
|
||||||
|
|
@ -779,6 +883,12 @@ type addCartItemRequest struct {
|
||||||
Quantity int64 `json:"quantity"`
|
Quantity int64 `json:"quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type createReviewRequest struct {
|
||||||
|
OrderID uuid.UUID `json:"order_id"`
|
||||||
|
Rating int `json:"rating"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}
|
||||||
|
|
||||||
type updateUserRequest struct {
|
type updateUserRequest struct {
|
||||||
CompanyID *uuid.UUID `json:"company_id,omitempty"`
|
CompanyID *uuid.UUID `json:"company_id,omitempty"`
|
||||||
Role *string `json:"role,omitempty"`
|
Role *string `json:"role,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -458,6 +458,97 @@ func (r *Repository) DeleteCartItem(ctx context.Context, id uuid.UUID, buyerID u
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) CreateReview(ctx context.Context, review *domain.Review) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
review.CreatedAt = now
|
||||||
|
|
||||||
|
query := `INSERT INTO reviews (id, order_id, buyer_id, seller_id, rating, comment, created_at)
|
||||||
|
VALUES (:id, :order_id, :buyer_id, :seller_id, :rating, :comment, :created_at)`
|
||||||
|
|
||||||
|
_, err := r.db.NamedExecContext(ctx, query, review)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetCompanyRating(ctx context.Context, companyID uuid.UUID) (*domain.CompanyRating, error) {
|
||||||
|
var row struct {
|
||||||
|
Avg *float64 `db:"avg"`
|
||||||
|
Count int64 `db:"count"`
|
||||||
|
}
|
||||||
|
query := `SELECT AVG(rating)::float AS avg, COUNT(*) AS count FROM reviews WHERE seller_id = $1`
|
||||||
|
if err := r.db.GetContext(ctx, &row, query, companyID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
avg := 0.0
|
||||||
|
if row.Avg != nil {
|
||||||
|
avg = *row.Avg
|
||||||
|
}
|
||||||
|
|
||||||
|
return &domain.CompanyRating{CompanyID: companyID, AverageScore: avg, TotalReviews: row.Count}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) SellerDashboard(ctx context.Context, sellerID uuid.UUID) (*domain.SellerDashboard, error) {
|
||||||
|
var sales struct {
|
||||||
|
Total *int64 `db:"total"`
|
||||||
|
Orders int64 `db:"orders"`
|
||||||
|
}
|
||||||
|
|
||||||
|
baseStatuses := []string{string(domain.OrderStatusPaid), string(domain.OrderStatusInvoiced), string(domain.OrderStatusDelivered)}
|
||||||
|
if err := r.db.GetContext(ctx, &sales, `SELECT COALESCE(SUM(total_cents), 0) AS total, COUNT(*) AS orders FROM orders WHERE seller_id = $1 AND status = ANY($2)`, sellerID, baseStatuses); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var topProducts []domain.TopProduct
|
||||||
|
topQuery := `SELECT oi.product_id, p.name, SUM(oi.quantity) AS total_quantity, SUM(oi.quantity * oi.unit_cents) AS revenue_cents
|
||||||
|
FROM order_items oi
|
||||||
|
JOIN orders o ON o.id = oi.order_id
|
||||||
|
JOIN products p ON p.id = oi.product_id
|
||||||
|
WHERE o.seller_id = $1 AND o.status = ANY($2)
|
||||||
|
GROUP BY oi.product_id, p.name
|
||||||
|
ORDER BY total_quantity DESC
|
||||||
|
LIMIT 5`
|
||||||
|
if err := r.db.SelectContext(ctx, &topProducts, topQuery, sellerID, baseStatuses); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var lowStock []domain.Product
|
||||||
|
if err := r.db.SelectContext(ctx, &lowStock, `SELECT id, seller_id, name, description, batch, expires_at, price_cents, stock, created_at, updated_at FROM products WHERE seller_id = $1 AND stock <= 10 ORDER BY stock ASC, updated_at DESC`, sellerID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
total := int64(0)
|
||||||
|
if sales.Total != nil {
|
||||||
|
total = *sales.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
return &domain.SellerDashboard{
|
||||||
|
SellerID: sellerID,
|
||||||
|
TotalSalesCents: total,
|
||||||
|
OrdersCount: sales.Orders,
|
||||||
|
TopProducts: topProducts,
|
||||||
|
LowStockAlerts: lowStock,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) AdminDashboard(ctx context.Context, since time.Time) (*domain.AdminDashboard, error) {
|
||||||
|
var gmv *int64
|
||||||
|
if err := r.db.GetContext(ctx, &gmv, `SELECT COALESCE(SUM(total_cents), 0) FROM orders WHERE status = ANY($1)`, []string{string(domain.OrderStatusPaid), string(domain.OrderStatusInvoiced), string(domain.OrderStatusDelivered)}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var newCompanies int64
|
||||||
|
if err := r.db.GetContext(ctx, &newCompanies, `SELECT COUNT(*) FROM companies WHERE created_at >= $1`, since); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalGMV := int64(0)
|
||||||
|
if gmv != nil {
|
||||||
|
totalGMV = *gmv
|
||||||
|
}
|
||||||
|
|
||||||
|
return &domain.AdminDashboard{GMVCents: totalGMV, NewCompanies: newCompanies, WindowStartAt: since}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// InitSchema applies a minimal schema for development environments.
|
// InitSchema applies a minimal schema for development environments.
|
||||||
func (r *Repository) InitSchema(ctx context.Context) error {
|
func (r *Repository) InitSchema(ctx context.Context) error {
|
||||||
schema := `
|
schema := `
|
||||||
|
|
@ -546,6 +637,18 @@ CREATE TABLE IF NOT EXISTS cart_items (
|
||||||
UNIQUE (buyer_id, product_id)
|
UNIQUE (buyer_id, product_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS reviews (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
order_id UUID NOT NULL UNIQUE REFERENCES orders(id),
|
||||||
|
buyer_id UUID NOT NULL REFERENCES companies(id),
|
||||||
|
seller_id UUID NOT NULL REFERENCES companies(id),
|
||||||
|
rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
||||||
|
comment TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_reviews_seller_id ON reviews (seller_id);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS shipments (
|
CREATE TABLE IF NOT EXISTS shipments (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
order_id UUID NOT NULL UNIQUE REFERENCES orders(id),
|
order_id UUID NOT NULL UNIQUE REFERENCES orders(id),
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ func New(cfg config.Config) (*Server, error) {
|
||||||
mux.Handle("GET /api/companies", chain(http.HandlerFunc(h.ListCompanies), middleware.Logger, middleware.Gzip))
|
mux.Handle("GET /api/companies", chain(http.HandlerFunc(h.ListCompanies), middleware.Logger, middleware.Gzip))
|
||||||
mux.Handle("PATCH /api/v1/companies/", chain(http.HandlerFunc(h.VerifyCompany), middleware.Logger, middleware.Gzip, adminOnly))
|
mux.Handle("PATCH /api/v1/companies/", chain(http.HandlerFunc(h.VerifyCompany), middleware.Logger, middleware.Gzip, adminOnly))
|
||||||
mux.Handle("GET /api/v1/companies/me", chain(http.HandlerFunc(h.GetMyCompany), middleware.Logger, middleware.Gzip, auth))
|
mux.Handle("GET /api/v1/companies/me", chain(http.HandlerFunc(h.GetMyCompany), middleware.Logger, middleware.Gzip, auth))
|
||||||
|
mux.Handle("GET /api/v1/companies/", chain(http.HandlerFunc(h.GetCompanyRating), middleware.Logger, middleware.Gzip))
|
||||||
|
|
||||||
mux.Handle("POST /api/products", chain(http.HandlerFunc(h.CreateProduct), middleware.Logger, middleware.Gzip))
|
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/products", chain(http.HandlerFunc(h.ListProducts), middleware.Logger, middleware.Gzip))
|
||||||
|
|
@ -83,6 +84,10 @@ func New(cfg config.Config) (*Server, error) {
|
||||||
|
|
||||||
mux.Handle("POST /api/v1/payments/webhook", chain(http.HandlerFunc(h.HandlePaymentWebhook), middleware.Logger, middleware.Gzip))
|
mux.Handle("POST /api/v1/payments/webhook", chain(http.HandlerFunc(h.HandlePaymentWebhook), middleware.Logger, middleware.Gzip))
|
||||||
|
|
||||||
|
mux.Handle("POST /api/v1/reviews", chain(http.HandlerFunc(h.CreateReview), middleware.Logger, middleware.Gzip, auth))
|
||||||
|
mux.Handle("GET /api/v1/dashboard/seller", chain(http.HandlerFunc(h.GetSellerDashboard), middleware.Logger, middleware.Gzip, auth))
|
||||||
|
mux.Handle("GET /api/v1/dashboard/admin", chain(http.HandlerFunc(h.GetAdminDashboard), middleware.Logger, middleware.Gzip, adminOnly))
|
||||||
|
|
||||||
mux.Handle("POST /api/v1/auth/register", chain(http.HandlerFunc(h.Register), middleware.Logger, middleware.Gzip))
|
mux.Handle("POST /api/v1/auth/register", chain(http.HandlerFunc(h.Register), middleware.Logger, middleware.Gzip))
|
||||||
mux.Handle("POST /api/v1/auth/login", chain(http.HandlerFunc(h.Login), middleware.Logger, middleware.Gzip))
|
mux.Handle("POST /api/v1/auth/login", chain(http.HandlerFunc(h.Login), middleware.Logger, middleware.Gzip))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ type Repository interface {
|
||||||
AddCartItem(ctx context.Context, item *domain.CartItem) (*domain.CartItem, error)
|
AddCartItem(ctx context.Context, item *domain.CartItem) (*domain.CartItem, error)
|
||||||
ListCartItems(ctx context.Context, buyerID uuid.UUID) ([]domain.CartItem, error)
|
ListCartItems(ctx context.Context, buyerID uuid.UUID) ([]domain.CartItem, error)
|
||||||
DeleteCartItem(ctx context.Context, id uuid.UUID, buyerID uuid.UUID) error
|
DeleteCartItem(ctx context.Context, id uuid.UUID, buyerID uuid.UUID) error
|
||||||
|
|
||||||
|
CreateReview(ctx context.Context, review *domain.Review) error
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PaymentGateway abstracts Mercado Pago integration.
|
// PaymentGateway abstracts Mercado Pago integration.
|
||||||
|
|
@ -296,6 +301,56 @@ func (s *Service) cartSummary(ctx context.Context, buyerID uuid.UUID) (*domain.C
|
||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateReview stores a buyer rating ensuring the order is delivered and owned by the requester.
|
||||||
|
func (s *Service) CreateReview(ctx context.Context, buyerID, orderID uuid.UUID, rating int, comment string) (*domain.Review, error) {
|
||||||
|
if rating < 1 || rating > 5 {
|
||||||
|
return nil, errors.New("rating must be between 1 and 5")
|
||||||
|
}
|
||||||
|
|
||||||
|
order, err := s.repo.GetOrder(ctx, orderID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.Status != domain.OrderStatusDelivered {
|
||||||
|
return nil, errors.New("only delivered orders can be reviewed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.BuyerID != buyerID {
|
||||||
|
return nil, errors.New("order does not belong to buyer")
|
||||||
|
}
|
||||||
|
|
||||||
|
review := &domain.Review{
|
||||||
|
ID: uuid.Must(uuid.NewV7()),
|
||||||
|
OrderID: orderID,
|
||||||
|
BuyerID: buyerID,
|
||||||
|
SellerID: order.SellerID,
|
||||||
|
Rating: rating,
|
||||||
|
Comment: comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.CreateReview(ctx, review); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return review, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCompanyRating returns the average rating for a seller or pharmacy.
|
||||||
|
func (s *Service) GetCompanyRating(ctx context.Context, companyID uuid.UUID) (*domain.CompanyRating, error) {
|
||||||
|
return s.repo.GetCompanyRating(ctx, companyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSellerDashboard aggregates commercial KPIs for the seller.
|
||||||
|
func (s *Service) GetSellerDashboard(ctx context.Context, sellerID uuid.UUID) (*domain.SellerDashboard, error) {
|
||||||
|
return s.repo.SellerDashboard(ctx, sellerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAdminDashboard exposes marketplace-wide metrics for the last 30 days.
|
||||||
|
func (s *Service) GetAdminDashboard(ctx context.Context) (*domain.AdminDashboard, error) {
|
||||||
|
since := time.Now().AddDate(0, 0, -30)
|
||||||
|
return s.repo.AdminDashboard(ctx, since)
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterAccount creates a company when needed and persists a user bound to it.
|
// 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 {
|
func (s *Service) RegisterAccount(ctx context.Context, company *domain.Company, user *domain.User, password string) error {
|
||||||
if company != nil {
|
if company != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue