Add shipment endpoints and payment webhook split handling
This commit is contained in:
parent
94c27ec7dc
commit
ce825fd1d5
5 changed files with 295 additions and 26 deletions
|
|
@ -87,14 +87,15 @@ type InventoryAdjustment struct {
|
||||||
|
|
||||||
// Order captures the status lifecycle and payment intent.
|
// Order captures the status lifecycle and payment intent.
|
||||||
type Order struct {
|
type Order struct {
|
||||||
ID uuid.UUID `db:"id" json:"id"`
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
BuyerID uuid.UUID `db:"buyer_id" json:"buyer_id"`
|
BuyerID uuid.UUID `db:"buyer_id" json:"buyer_id"`
|
||||||
SellerID uuid.UUID `db:"seller_id" json:"seller_id"`
|
SellerID uuid.UUID `db:"seller_id" json:"seller_id"`
|
||||||
Status OrderStatus `db:"status" json:"status"`
|
Status OrderStatus `db:"status" json:"status"`
|
||||||
TotalCents int64 `db:"total_cents" json:"total_cents"`
|
TotalCents int64 `db:"total_cents" json:"total_cents"`
|
||||||
Items []OrderItem `json:"items"`
|
Items []OrderItem `json:"items"`
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
Shipping ShippingAddress `json:"shipping"`
|
||||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrderItem stores SKU-level batch tracking.
|
// OrderItem stores SKU-level batch tracking.
|
||||||
|
|
@ -118,6 +119,51 @@ type PaymentPreference struct {
|
||||||
PaymentURL string `json:"payment_url"`
|
PaymentURL string `json:"payment_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaymentWebhookEvent represents Mercado Pago notifications with split amounts.
|
||||||
|
type PaymentWebhookEvent struct {
|
||||||
|
PaymentID string `json:"payment_id"`
|
||||||
|
OrderID uuid.UUID `json:"order_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
MarketplaceFee int64 `json:"marketplace_fee"`
|
||||||
|
SellerAmount int64 `json:"seller_amount"`
|
||||||
|
TotalPaidAmount int64 `json:"total_paid_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentSplitResult echoes the amounts distributed between actors.
|
||||||
|
type PaymentSplitResult struct {
|
||||||
|
OrderID uuid.UUID `json:"order_id"`
|
||||||
|
PaymentID string `json:"payment_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
MarketplaceFee int64 `json:"marketplace_fee"`
|
||||||
|
SellerReceivable int64 `json:"seller_receivable"`
|
||||||
|
TotalPaidAmount int64 `json:"total_paid_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShippingAddress captures delivery details at order time.
|
||||||
|
type ShippingAddress struct {
|
||||||
|
RecipientName string `json:"recipient_name" db:"shipping_recipient_name"`
|
||||||
|
Street string `json:"street" db:"shipping_street"`
|
||||||
|
Number string `json:"number" db:"shipping_number"`
|
||||||
|
Complement string `json:"complement,omitempty" db:"shipping_complement"`
|
||||||
|
District string `json:"district" db:"shipping_district"`
|
||||||
|
City string `json:"city" db:"shipping_city"`
|
||||||
|
State string `json:"state" db:"shipping_state"`
|
||||||
|
ZipCode string `json:"zip_code" db:"shipping_zip_code"`
|
||||||
|
Country string `json:"country" db:"shipping_country"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shipment stores freight label data and tracking linkage.
|
||||||
|
type Shipment struct {
|
||||||
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
OrderID uuid.UUID `db:"order_id" json:"order_id"`
|
||||||
|
Carrier string `db:"carrier" json:"carrier"`
|
||||||
|
TrackingCode string `db:"tracking_code" json:"tracking_code"`
|
||||||
|
ExternalTracking string `db:"external_tracking" json:"external_tracking"`
|
||||||
|
Status string `db:"status" json:"status"`
|
||||||
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
// OrderStatus enumerates supported transitions.
|
// OrderStatus enumerates supported transitions.
|
||||||
type OrderStatus string
|
type OrderStatus string
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,7 @@ func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
BuyerID: req.BuyerID,
|
BuyerID: req.BuyerID,
|
||||||
SellerID: req.SellerID,
|
SellerID: req.SellerID,
|
||||||
Items: req.Items,
|
Items: req.Items,
|
||||||
|
Shipping: req.Shipping,
|
||||||
}
|
}
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
|
|
@ -454,6 +455,83 @@ func (h *Handler) CreatePaymentPreference(w http.ResponseWriter, r *http.Request
|
||||||
writeJSON(w, http.StatusCreated, pref)
|
writeJSON(w, http.StatusCreated, pref)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateShipment godoc
|
||||||
|
// @Summary Gera guia de postagem/transporte
|
||||||
|
// @Tags Logistica
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param shipment body createShipmentRequest true "Dados de envio"
|
||||||
|
// @Success 201 {object} domain.Shipment
|
||||||
|
// @Router /api/v1/shipments [post]
|
||||||
|
func (h *Handler) CreateShipment(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req createShipmentRequest
|
||||||
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shipment := &domain.Shipment{
|
||||||
|
OrderID: req.OrderID,
|
||||||
|
Carrier: req.Carrier,
|
||||||
|
TrackingCode: req.TrackingCode,
|
||||||
|
ExternalTracking: req.ExternalTracking,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.CreateShipment(r.Context(), shipment); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusCreated, shipment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShipmentByOrderID godoc
|
||||||
|
// @Summary Rastreia entrega
|
||||||
|
// @Tags Logistica
|
||||||
|
// @Produce json
|
||||||
|
// @Param order_id path string true "Order ID"
|
||||||
|
// @Success 200 {object} domain.Shipment
|
||||||
|
// @Router /api/v1/shipments/{order_id} [get]
|
||||||
|
func (h *Handler) GetShipmentByOrderID(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orderID, err := parseUUIDFromPath(r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shipment, err := h.svc.GetShipmentByOrderID(r.Context(), orderID)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, shipment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePaymentWebhook godoc
|
||||||
|
// @Summary Recebe notificações do Mercado Pago
|
||||||
|
// @Tags Pagamentos
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param notification body domain.PaymentWebhookEvent true "Evento do gateway"
|
||||||
|
// @Success 200 {object} domain.PaymentSplitResult
|
||||||
|
// @Router /api/v1/payments/webhook [post]
|
||||||
|
func (h *Handler) HandlePaymentWebhook(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var event domain.PaymentWebhookEvent
|
||||||
|
if err := decodeJSON(r.Context(), r, &event); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, err := h.svc.HandlePaymentWebhook(r.Context(), event)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, summary)
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
@ -772,9 +850,17 @@ type registerProductRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type createOrderRequest struct {
|
type createOrderRequest struct {
|
||||||
BuyerID uuid.UUID `json:"buyer_id"`
|
BuyerID uuid.UUID `json:"buyer_id"`
|
||||||
SellerID uuid.UUID `json:"seller_id"`
|
SellerID uuid.UUID `json:"seller_id"`
|
||||||
Items []domain.OrderItem `json:"items"`
|
Items []domain.OrderItem `json:"items"`
|
||||||
|
Shipping domain.ShippingAddress `json:"shipping"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type createShipmentRequest struct {
|
||||||
|
OrderID uuid.UUID `json:"order_id"`
|
||||||
|
Carrier string `json:"carrier"`
|
||||||
|
TrackingCode string `json:"tracking_code"`
|
||||||
|
ExternalTracking string `json:"external_tracking"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type updateStatusRequest struct {
|
type updateStatusRequest struct {
|
||||||
|
|
|
||||||
|
|
@ -114,9 +114,9 @@ func (r *Repository) CreateOrder(ctx context.Context, order *domain.Order) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
orderQuery := `INSERT INTO orders (id, buyer_id, seller_id, status, total_cents, created_at, updated_at)
|
orderQuery := `INSERT INTO orders (id, buyer_id, seller_id, status, total_cents, shipping_recipient_name, shipping_street, shipping_number, shipping_complement, shipping_district, shipping_city, shipping_state, shipping_zip_code, shipping_country, created_at, updated_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`
|
||||||
if _, err := tx.ExecContext(ctx, orderQuery, order.ID, order.BuyerID, order.SellerID, order.Status, order.TotalCents, order.CreatedAt, order.UpdatedAt); err != nil {
|
if _, err := tx.ExecContext(ctx, orderQuery, order.ID, order.BuyerID, order.SellerID, order.Status, order.TotalCents, order.Shipping.RecipientName, order.Shipping.Street, order.Shipping.Number, order.Shipping.Complement, order.Shipping.District, order.Shipping.City, order.Shipping.State, order.Shipping.ZipCode, order.Shipping.Country, order.CreatedAt, order.UpdatedAt); err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -136,9 +136,26 @@ VALUES ($1, $2, $3, $4, $5, $6, $7)`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) {
|
func (r *Repository) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) {
|
||||||
var order domain.Order
|
var row struct {
|
||||||
orderQuery := `SELECT id, buyer_id, seller_id, status, total_cents, created_at, updated_at FROM orders WHERE id = $1`
|
ID uuid.UUID `db:"id"`
|
||||||
if err := r.db.GetContext(ctx, &order, orderQuery, id); err != nil {
|
BuyerID uuid.UUID `db:"buyer_id"`
|
||||||
|
SellerID uuid.UUID `db:"seller_id"`
|
||||||
|
Status domain.OrderStatus `db:"status"`
|
||||||
|
TotalCents int64 `db:"total_cents"`
|
||||||
|
ShippingRecipientName string `db:"shipping_recipient_name"`
|
||||||
|
ShippingStreet string `db:"shipping_street"`
|
||||||
|
ShippingNumber string `db:"shipping_number"`
|
||||||
|
ShippingComplement string `db:"shipping_complement"`
|
||||||
|
ShippingDistrict string `db:"shipping_district"`
|
||||||
|
ShippingCity string `db:"shipping_city"`
|
||||||
|
ShippingState string `db:"shipping_state"`
|
||||||
|
ShippingZipCode string `db:"shipping_zip_code"`
|
||||||
|
ShippingCountry string `db:"shipping_country"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
}
|
||||||
|
orderQuery := `SELECT id, buyer_id, seller_id, status, total_cents, shipping_recipient_name, shipping_street, shipping_number, shipping_complement, shipping_district, shipping_city, shipping_state, shipping_zip_code, shipping_country, created_at, updated_at FROM orders WHERE id = $1`
|
||||||
|
if err := r.db.GetContext(ctx, &row, orderQuery, id); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,8 +164,28 @@ func (r *Repository) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order,
|
||||||
if err := r.db.SelectContext(ctx, &items, itemQuery, id); err != nil {
|
if err := r.db.SelectContext(ctx, &items, itemQuery, id); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
order.Items = items
|
order := &domain.Order{
|
||||||
return &order, nil
|
ID: row.ID,
|
||||||
|
BuyerID: row.BuyerID,
|
||||||
|
SellerID: row.SellerID,
|
||||||
|
Status: row.Status,
|
||||||
|
TotalCents: row.TotalCents,
|
||||||
|
Items: items,
|
||||||
|
Shipping: domain.ShippingAddress{
|
||||||
|
RecipientName: row.ShippingRecipientName,
|
||||||
|
Street: row.ShippingStreet,
|
||||||
|
Number: row.ShippingNumber,
|
||||||
|
Complement: row.ShippingComplement,
|
||||||
|
District: row.ShippingDistrict,
|
||||||
|
City: row.ShippingCity,
|
||||||
|
State: row.ShippingState,
|
||||||
|
ZipCode: row.ShippingZipCode,
|
||||||
|
Country: row.ShippingCountry,
|
||||||
|
},
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
UpdatedAt: row.UpdatedAt,
|
||||||
|
}
|
||||||
|
return order, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error {
|
func (r *Repository) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error {
|
||||||
|
|
@ -167,6 +204,27 @@ func (r *Repository) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) CreateShipment(ctx context.Context, shipment *domain.Shipment) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
shipment.CreatedAt = now
|
||||||
|
shipment.UpdatedAt = now
|
||||||
|
|
||||||
|
query := `INSERT INTO shipments (id, order_id, carrier, tracking_code, external_tracking, status, created_at, updated_at)
|
||||||
|
VALUES (:id, :order_id, :carrier, :tracking_code, :external_tracking, :status, :created_at, :updated_at)`
|
||||||
|
|
||||||
|
_, err := r.db.NamedExecContext(ctx, query, shipment)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetShipmentByOrderID(ctx context.Context, orderID uuid.UUID) (*domain.Shipment, error) {
|
||||||
|
var shipment domain.Shipment
|
||||||
|
query := `SELECT id, order_id, carrier, tracking_code, external_tracking, status, created_at, updated_at FROM shipments WHERE order_id = $1`
|
||||||
|
if err := r.db.GetContext(ctx, &shipment, query, orderID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &shipment, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) AdjustInventory(ctx context.Context, productID uuid.UUID, delta int64, reason string) (*domain.InventoryItem, error) {
|
func (r *Repository) AdjustInventory(ctx context.Context, productID uuid.UUID, delta int64, reason string) (*domain.InventoryItem, error) {
|
||||||
tx, err := r.db.BeginTxx(ctx, nil)
|
tx, err := r.db.BeginTxx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -452,6 +510,15 @@ CREATE TABLE IF NOT EXISTS orders (
|
||||||
seller_id UUID NOT NULL REFERENCES companies(id),
|
seller_id UUID NOT NULL REFERENCES companies(id),
|
||||||
status TEXT NOT NULL,
|
status TEXT NOT NULL,
|
||||||
total_cents BIGINT NOT NULL,
|
total_cents BIGINT NOT NULL,
|
||||||
|
shipping_recipient_name TEXT,
|
||||||
|
shipping_street TEXT,
|
||||||
|
shipping_number TEXT,
|
||||||
|
shipping_complement TEXT,
|
||||||
|
shipping_district TEXT,
|
||||||
|
shipping_city TEXT,
|
||||||
|
shipping_state TEXT,
|
||||||
|
shipping_zip_code TEXT,
|
||||||
|
shipping_country TEXT,
|
||||||
created_at TIMESTAMPTZ NOT NULL,
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
updated_at TIMESTAMPTZ NOT NULL
|
updated_at TIMESTAMPTZ NOT NULL
|
||||||
);
|
);
|
||||||
|
|
@ -478,6 +545,17 @@ CREATE TABLE IF NOT EXISTS cart_items (
|
||||||
updated_at TIMESTAMPTZ NOT NULL,
|
updated_at TIMESTAMPTZ NOT NULL,
|
||||||
UNIQUE (buyer_id, product_id)
|
UNIQUE (buyer_id, product_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS shipments (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
order_id UUID NOT NULL UNIQUE REFERENCES orders(id),
|
||||||
|
carrier TEXT NOT NULL,
|
||||||
|
tracking_code TEXT,
|
||||||
|
external_tracking TEXT,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
`
|
`
|
||||||
|
|
||||||
if _, err := r.db.ExecContext(ctx, schema); err != nil {
|
if _, err := r.db.ExecContext(ctx, schema); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ func New(cfg config.Config) (*Server, error) {
|
||||||
|
|
||||||
repo := postgres.New(db)
|
repo := postgres.New(db)
|
||||||
gateway := payments.NewMercadoPagoGateway(cfg.MercadoPagoBaseURL, cfg.MarketplaceCommission)
|
gateway := payments.NewMercadoPagoGateway(cfg.MercadoPagoBaseURL, cfg.MarketplaceCommission)
|
||||||
svc := usecase.NewService(repo, gateway, cfg.JWTSecret, cfg.JWTExpiresIn)
|
svc := usecase.NewService(repo, gateway, cfg.MarketplaceCommission, cfg.JWTSecret, cfg.JWTExpiresIn)
|
||||||
h := handler.New(svc)
|
h := handler.New(svc)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
@ -78,6 +78,11 @@ func New(cfg config.Config) (*Server, error) {
|
||||||
mux.Handle("PATCH /api/orders/", chain(http.HandlerFunc(h.UpdateOrderStatus), middleware.Logger, middleware.Gzip, auth))
|
mux.Handle("PATCH /api/orders/", chain(http.HandlerFunc(h.UpdateOrderStatus), middleware.Logger, middleware.Gzip, auth))
|
||||||
mux.Handle("POST /api/orders/", chain(http.HandlerFunc(h.CreatePaymentPreference), middleware.Logger, middleware.Gzip, auth))
|
mux.Handle("POST /api/orders/", chain(http.HandlerFunc(h.CreatePaymentPreference), middleware.Logger, middleware.Gzip, auth))
|
||||||
|
|
||||||
|
mux.Handle("POST /api/v1/shipments", chain(http.HandlerFunc(h.CreateShipment), middleware.Logger, middleware.Gzip, auth))
|
||||||
|
mux.Handle("GET /api/v1/shipments/", chain(http.HandlerFunc(h.GetShipmentByOrderID), middleware.Logger, middleware.Gzip, auth))
|
||||||
|
|
||||||
|
mux.Handle("POST /api/v1/payments/webhook", chain(http.HandlerFunc(h.HandlePaymentWebhook), 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/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))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package usecase
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
|
@ -29,6 +30,8 @@ type Repository interface {
|
||||||
CreateOrder(ctx context.Context, order *domain.Order) error
|
CreateOrder(ctx context.Context, order *domain.Order) error
|
||||||
GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error)
|
GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error)
|
||||||
UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error
|
UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error
|
||||||
|
CreateShipment(ctx context.Context, shipment *domain.Shipment) error
|
||||||
|
GetShipmentByOrderID(ctx context.Context, orderID uuid.UUID) (*domain.Shipment, error)
|
||||||
|
|
||||||
CreateUser(ctx context.Context, user *domain.User) error
|
CreateUser(ctx context.Context, user *domain.User) error
|
||||||
ListUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error)
|
ListUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error)
|
||||||
|
|
@ -48,15 +51,16 @@ type PaymentGateway interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
repo Repository
|
repo Repository
|
||||||
pay PaymentGateway
|
pay PaymentGateway
|
||||||
jwtSecret []byte
|
jwtSecret []byte
|
||||||
tokenTTL time.Duration
|
tokenTTL time.Duration
|
||||||
|
marketplaceCommission float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService wires use cases together.
|
// NewService wires use cases together.
|
||||||
func NewService(repo Repository, pay PaymentGateway, jwtSecret string, tokenTTL time.Duration) *Service {
|
func NewService(repo Repository, pay PaymentGateway, commissionPct float64, jwtSecret string, tokenTTL time.Duration) *Service {
|
||||||
return &Service{repo: repo, pay: pay, jwtSecret: []byte(jwtSecret), tokenTTL: tokenTTL}
|
return &Service{repo: repo, pay: pay, jwtSecret: []byte(jwtSecret), tokenTTL: tokenTTL, marketplaceCommission: commissionPct}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) RegisterCompany(ctx context.Context, company *domain.Company) error {
|
func (s *Service) RegisterCompany(ctx context.Context, company *domain.Company) error {
|
||||||
|
|
@ -103,6 +107,22 @@ func (s *Service) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status do
|
||||||
return s.repo.UpdateOrderStatus(ctx, id, status)
|
return s.repo.UpdateOrderStatus(ctx, id, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateShipment persists a freight label for an order if not already present.
|
||||||
|
func (s *Service) CreateShipment(ctx context.Context, shipment *domain.Shipment) error {
|
||||||
|
if _, err := s.repo.GetOrder(ctx, shipment.OrderID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
shipment.ID = uuid.Must(uuid.NewV7())
|
||||||
|
shipment.Status = "Label gerada"
|
||||||
|
return s.repo.CreateShipment(ctx, shipment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShipmentByOrderID returns freight details for an order.
|
||||||
|
func (s *Service) GetShipmentByOrderID(ctx context.Context, orderID uuid.UUID) (*domain.Shipment, error) {
|
||||||
|
return s.repo.GetShipmentByOrderID(ctx, orderID)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) CreatePaymentPreference(ctx context.Context, id uuid.UUID) (*domain.PaymentPreference, error) {
|
func (s *Service) CreatePaymentPreference(ctx context.Context, id uuid.UUID) (*domain.PaymentPreference, error) {
|
||||||
order, err := s.repo.GetOrder(ctx, id)
|
order, err := s.repo.GetOrder(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -111,6 +131,40 @@ func (s *Service) CreatePaymentPreference(ctx context.Context, id uuid.UUID) (*d
|
||||||
return s.pay.CreatePreference(ctx, order)
|
return s.pay.CreatePreference(ctx, order)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandlePaymentWebhook processes Mercado Pago notifications ensuring split consistency.
|
||||||
|
func (s *Service) HandlePaymentWebhook(ctx context.Context, event domain.PaymentWebhookEvent) (*domain.PaymentSplitResult, error) {
|
||||||
|
order, err := s.repo.GetOrder(ctx, event.OrderID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedMarketplaceFee := int64(float64(order.TotalCents) * (s.marketplaceCommission / 100))
|
||||||
|
marketplaceFee := event.MarketplaceFee
|
||||||
|
if marketplaceFee == 0 {
|
||||||
|
marketplaceFee = expectedMarketplaceFee
|
||||||
|
}
|
||||||
|
|
||||||
|
sellerReceivable := order.TotalCents - marketplaceFee
|
||||||
|
if event.SellerAmount > 0 {
|
||||||
|
sellerReceivable = event.SellerAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(event.Status, "approved") || strings.EqualFold(event.Status, "paid") {
|
||||||
|
if err := s.repo.UpdateOrderStatus(ctx, order.ID, domain.OrderStatusPaid); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &domain.PaymentSplitResult{
|
||||||
|
OrderID: order.ID,
|
||||||
|
PaymentID: event.PaymentID,
|
||||||
|
Status: event.Status,
|
||||||
|
MarketplaceFee: marketplaceFee,
|
||||||
|
SellerReceivable: sellerReceivable,
|
||||||
|
TotalPaidAmount: event.TotalPaidAmount,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) CreateUser(ctx context.Context, user *domain.User, password string) error {
|
func (s *Service) CreateUser(ctx context.Context, user *domain.User, password string) error {
|
||||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue