1209 lines
40 KiB
Go
1209 lines
40 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid/v5"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/lib/pq"
|
|
|
|
"github.com/saveinmed/backend-go/internal/domain"
|
|
)
|
|
|
|
// Repository implements the data access layer using sqlx + pgx.
|
|
type Repository struct {
|
|
db *sqlx.DB
|
|
}
|
|
|
|
// New creates a Postgres-backed repository and configures pooling.
|
|
func New(db *sqlx.DB) *Repository {
|
|
return &Repository{db: db}
|
|
}
|
|
|
|
func (r *Repository) CreateCompany(ctx context.Context, company *domain.Company) error {
|
|
now := time.Now().UTC()
|
|
company.CreatedAt = now
|
|
company.UpdatedAt = now
|
|
|
|
query := `INSERT INTO companies (id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, phone, operating_hours, is_24_hours, created_at, updated_at)
|
|
VALUES (:id, :cnpj, :corporate_name, :category, :license_number, :is_verified, :latitude, :longitude, :city, :state, :phone, :operating_hours, :is_24_hours, :created_at, :updated_at)`
|
|
|
|
_, err := r.db.NamedExecContext(ctx, query, company)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) ListCompanies(ctx context.Context, filter domain.CompanyFilter) ([]domain.Company, int64, error) {
|
|
baseQuery := `FROM companies`
|
|
var args []any
|
|
var clauses []string
|
|
|
|
if filter.Category != "" {
|
|
clauses = append(clauses, fmt.Sprintf("category = $%d", len(args)+1))
|
|
args = append(args, filter.Category)
|
|
}
|
|
if filter.Search != "" {
|
|
clauses = append(clauses, fmt.Sprintf("(corporate_name ILIKE $%d OR cnpj ILIKE $%d)", len(args)+1, len(args)+1))
|
|
args = append(args, "%"+filter.Search+"%")
|
|
}
|
|
if filter.City != "" {
|
|
clauses = append(clauses, fmt.Sprintf("city = $%d", len(args)+1))
|
|
args = append(args, filter.City)
|
|
}
|
|
if filter.State != "" {
|
|
clauses = append(clauses, fmt.Sprintf("state = $%d", len(args)+1))
|
|
args = append(args, filter.State)
|
|
}
|
|
|
|
where := ""
|
|
if len(clauses) > 0 {
|
|
where = " WHERE " + strings.Join(clauses, " AND ")
|
|
}
|
|
|
|
var total int64
|
|
if err := r.db.GetContext(ctx, &total, "SELECT count(*) "+baseQuery+where, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if filter.Limit <= 0 {
|
|
filter.Limit = 20
|
|
}
|
|
args = append(args, filter.Limit, filter.Offset)
|
|
listQuery := fmt.Sprintf("SELECT id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, phone, operating_hours, is_24_hours, created_at, updated_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, where, len(args)-1, len(args))
|
|
|
|
var companies []domain.Company
|
|
if err := r.db.SelectContext(ctx, &companies, listQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return companies, total, nil
|
|
}
|
|
|
|
func (r *Repository) GetCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) {
|
|
var company domain.Company
|
|
query := `SELECT id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, phone, operating_hours, is_24_hours, created_at, updated_at FROM companies WHERE id = $1`
|
|
if err := r.db.GetContext(ctx, &company, query, id); err != nil {
|
|
return nil, err
|
|
}
|
|
return &company, nil
|
|
}
|
|
|
|
func (r *Repository) UpdateCompany(ctx context.Context, company *domain.Company) error {
|
|
company.UpdatedAt = time.Now().UTC()
|
|
|
|
query := `UPDATE companies
|
|
SET cnpj = :cnpj, corporate_name = :corporate_name, category = :category, license_number = :license_number, is_verified = :is_verified, latitude = :latitude, longitude = :longitude, city = :city, state = :state, phone = :phone, operating_hours = :operating_hours, is_24_hours = :is_24_hours, updated_at = :updated_at
|
|
WHERE id = :id`
|
|
|
|
res, err := r.db.NamedExecContext(ctx, query, company)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rows == 0 {
|
|
return errors.New("company not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) DeleteCompany(ctx context.Context, id uuid.UUID) error {
|
|
var count int
|
|
if err := r.db.GetContext(ctx, &count, `SELECT COUNT(*) FROM users WHERE company_id = $1`, id); err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return errors.New("company has related users")
|
|
}
|
|
if err := r.db.GetContext(ctx, &count, `SELECT COUNT(*) FROM products WHERE seller_id = $1`, id); err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return errors.New("company has related products")
|
|
}
|
|
if err := r.db.GetContext(ctx, &count, `SELECT COUNT(*) FROM orders WHERE buyer_id = $1 OR seller_id = $1`, id); err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return errors.New("company has related orders")
|
|
}
|
|
if err := r.db.GetContext(ctx, &count, `SELECT COUNT(*) FROM reviews WHERE buyer_id = $1 OR seller_id = $1`, id); err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return errors.New("company has related reviews")
|
|
}
|
|
if err := r.db.GetContext(ctx, &count, `SELECT COUNT(*) FROM cart_items WHERE buyer_id = $1`, id); err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return errors.New("company has related cart items")
|
|
}
|
|
|
|
res, err := r.db.ExecContext(ctx, `DELETE FROM companies WHERE id = $1`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rows == 0 {
|
|
return errors.New("company not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) CreateProduct(ctx context.Context, product *domain.Product) error {
|
|
query := `INSERT INTO products (id, seller_id, ean_code, name, description, manufacturer, category, subcategory, batch, expires_at, price_cents, stock, observations)
|
|
VALUES (:id, :seller_id, :ean_code, :name, :description, :manufacturer, :category, :subcategory, :batch, :expires_at, :price_cents, :stock, :observations)
|
|
RETURNING created_at, updated_at`
|
|
|
|
rows, err := r.db.NamedQueryContext(ctx, query, product)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
if rows.Next() {
|
|
if err := rows.Scan(&product.CreatedAt, &product.UpdatedAt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return rows.Err()
|
|
}
|
|
|
|
func (r *Repository) ListProducts(ctx context.Context, filter domain.ProductFilter) ([]domain.Product, int64, error) {
|
|
baseQuery := `FROM products`
|
|
var args []any
|
|
var clauses []string
|
|
|
|
if filter.SellerID != nil {
|
|
clauses = append(clauses, fmt.Sprintf("seller_id = $%d", len(args)+1))
|
|
args = append(args, *filter.SellerID)
|
|
}
|
|
if filter.Search != "" {
|
|
clauses = append(clauses, fmt.Sprintf("name ILIKE $%d", len(args)+1))
|
|
args = append(args, "%"+filter.Search+"%")
|
|
}
|
|
|
|
where := ""
|
|
if len(clauses) > 0 {
|
|
where = " WHERE " + strings.Join(clauses, " AND ")
|
|
}
|
|
|
|
var total int64
|
|
if err := r.db.GetContext(ctx, &total, "SELECT count(*) "+baseQuery+where, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if filter.Limit <= 0 {
|
|
filter.Limit = 20
|
|
}
|
|
args = append(args, filter.Limit, filter.Offset)
|
|
listQuery := fmt.Sprintf("SELECT id, seller_id, ean_code, name, description, manufacturer, category, subcategory, batch, expires_at, price_cents, stock, observations, created_at, updated_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, where, len(args)-1, len(args))
|
|
|
|
var products []domain.Product
|
|
if err := r.db.SelectContext(ctx, &products, listQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return products, total, nil
|
|
}
|
|
|
|
// ListRecords returns marketplace listings using a window function for total count.
|
|
func (r *Repository) ListRecords(ctx context.Context, filter domain.RecordSearchFilter) ([]domain.Product, int64, error) {
|
|
baseQuery := `FROM products`
|
|
var args []any
|
|
var clauses []string
|
|
|
|
if filter.Query != "" {
|
|
clauses = append(clauses, fmt.Sprintf("(name ILIKE $%d OR description ILIKE $%d)", len(args)+1, len(args)+1))
|
|
args = append(args, "%"+filter.Query+"%")
|
|
}
|
|
if filter.CreatedAfter != nil {
|
|
clauses = append(clauses, fmt.Sprintf("created_at >= $%d", len(args)+1))
|
|
args = append(args, *filter.CreatedAfter)
|
|
}
|
|
if filter.CreatedBefore != nil {
|
|
clauses = append(clauses, fmt.Sprintf("created_at <= $%d", len(args)+1))
|
|
args = append(args, *filter.CreatedBefore)
|
|
}
|
|
|
|
where := ""
|
|
if len(clauses) > 0 {
|
|
where = " WHERE " + strings.Join(clauses, " AND ")
|
|
}
|
|
|
|
sortColumns := map[string]string{
|
|
"created_at": "created_at",
|
|
"updated_at": "updated_at",
|
|
}
|
|
sortBy := sortColumns[strings.ToLower(filter.SortBy)]
|
|
if sortBy == "" {
|
|
sortBy = "updated_at"
|
|
}
|
|
sortOrder := strings.ToUpper(filter.SortOrder)
|
|
if sortOrder != "ASC" {
|
|
sortOrder = "DESC"
|
|
}
|
|
|
|
if filter.Limit <= 0 {
|
|
filter.Limit = 20
|
|
}
|
|
args = append(args, filter.Limit, filter.Offset)
|
|
|
|
listQuery := fmt.Sprintf(`SELECT id, seller_id, ean_code, name, description, manufacturer, category, subcategory, batch, expires_at, price_cents, stock, observations, created_at, updated_at,
|
|
COUNT(*) OVER() AS total_count
|
|
%s%s ORDER BY %s %s LIMIT $%d OFFSET $%d`, baseQuery, where, sortBy, sortOrder, len(args)-1, len(args))
|
|
|
|
type recordRow struct {
|
|
domain.Product
|
|
TotalCount int64 `db:"total_count"`
|
|
}
|
|
var rows []recordRow
|
|
if err := r.db.SelectContext(ctx, &rows, listQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
total := int64(0)
|
|
if len(rows) > 0 {
|
|
total = rows[0].TotalCount
|
|
}
|
|
|
|
items := make([]domain.Product, 0, len(rows))
|
|
for _, row := range rows {
|
|
items = append(items, row.Product)
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
// SearchProducts returns products with distance from buyer, ordered by expiration date.
|
|
// Tenant info is anonymized (only city/state shown, not company name/ID).
|
|
func (r *Repository) SearchProducts(ctx context.Context, filter domain.ProductSearchFilter) ([]domain.ProductWithDistance, int64, error) {
|
|
baseQuery := `FROM products p INNER JOIN companies c ON p.seller_id = c.id`
|
|
var args []any
|
|
var clauses []string
|
|
|
|
if filter.Search != "" {
|
|
clauses = append(clauses, fmt.Sprintf("p.name ILIKE $%d", len(args)+1))
|
|
args = append(args, "%"+filter.Search+"%")
|
|
}
|
|
if filter.MinPriceCents != nil {
|
|
clauses = append(clauses, fmt.Sprintf("p.price_cents >= $%d", len(args)+1))
|
|
args = append(args, *filter.MinPriceCents)
|
|
}
|
|
if filter.MaxPriceCents != nil {
|
|
clauses = append(clauses, fmt.Sprintf("p.price_cents <= $%d", len(args)+1))
|
|
args = append(args, *filter.MaxPriceCents)
|
|
}
|
|
if filter.ExpiresAfter != nil {
|
|
clauses = append(clauses, fmt.Sprintf("p.expires_at >= $%d", len(args)+1))
|
|
args = append(args, *filter.ExpiresAfter)
|
|
}
|
|
if filter.ExpiresBefore != nil {
|
|
clauses = append(clauses, fmt.Sprintf("p.expires_at <= $%d", len(args)+1))
|
|
args = append(args, *filter.ExpiresBefore)
|
|
}
|
|
if filter.ExcludeSellerID != nil {
|
|
clauses = append(clauses, fmt.Sprintf("p.seller_id != $%d", len(args)+1))
|
|
args = append(args, *filter.ExcludeSellerID)
|
|
}
|
|
|
|
// Always filter only available products
|
|
clauses = append(clauses, "p.stock > 0")
|
|
|
|
where := ""
|
|
if len(clauses) > 0 {
|
|
where = " WHERE " + strings.Join(clauses, " AND ")
|
|
}
|
|
|
|
var total int64
|
|
countQuery := "SELECT count(*) " + baseQuery + where
|
|
if err := r.db.GetContext(ctx, &total, countQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if filter.Limit <= 0 {
|
|
filter.Limit = 20
|
|
}
|
|
args = append(args, filter.Limit, filter.Offset)
|
|
|
|
// Select products with tenant location info (anonymous: no company name/ID)
|
|
listQuery := fmt.Sprintf(`
|
|
SELECT p.id, p.seller_id, p.name, p.description, p.batch, p.expires_at,
|
|
p.price_cents, p.stock, p.created_at, p.updated_at,
|
|
c.city, c.state, c.latitude, c.longitude
|
|
%s%s
|
|
ORDER BY p.expires_at ASC
|
|
LIMIT $%d OFFSET $%d`, baseQuery, where, len(args)-1, len(args))
|
|
|
|
type productRow struct {
|
|
domain.Product
|
|
City string `db:"city"`
|
|
State string `db:"state"`
|
|
Latitude float64 `db:"latitude"`
|
|
Longitude float64 `db:"longitude"`
|
|
}
|
|
|
|
var rows []productRow
|
|
if err := r.db.SelectContext(ctx, &rows, listQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Calculate distance and build response
|
|
results := make([]domain.ProductWithDistance, 0, len(rows))
|
|
for _, row := range rows {
|
|
dist := domain.HaversineDistance(filter.BuyerLat, filter.BuyerLng, row.Latitude, row.Longitude)
|
|
|
|
// Filter by max distance if specified
|
|
if filter.MaxDistanceKm != nil && dist > *filter.MaxDistanceKm {
|
|
continue
|
|
}
|
|
|
|
results = append(results, domain.ProductWithDistance{
|
|
Product: row.Product,
|
|
DistanceKm: dist,
|
|
TenantCity: row.City,
|
|
TenantState: row.State,
|
|
})
|
|
}
|
|
|
|
return results, total, 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) UpdateProduct(ctx context.Context, product *domain.Product) error {
|
|
query := `UPDATE products
|
|
SET seller_id = :seller_id, name = :name, description = :description, batch = :batch, expires_at = :expires_at, price_cents = :price_cents, stock = :stock
|
|
WHERE id = :id
|
|
RETURNING updated_at`
|
|
|
|
rows, err := r.db.NamedQueryContext(ctx, query, product)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
if !rows.Next() {
|
|
if err := rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
return errors.New("product not found")
|
|
}
|
|
if err := rows.Scan(&product.UpdatedAt); err != nil {
|
|
return err
|
|
}
|
|
return rows.Err()
|
|
}
|
|
|
|
func (r *Repository) DeleteProduct(ctx context.Context, id uuid.UUID) error {
|
|
var count int
|
|
if err := r.db.GetContext(ctx, &count, `SELECT COUNT(*) FROM order_items WHERE product_id = $1`, id); err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return errors.New("product has related orders")
|
|
}
|
|
|
|
tx, err := r.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM inventory_adjustments WHERE product_id = $1`, id); err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM cart_items WHERE product_id = $1`, id); err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
|
|
res, err := tx.ExecContext(ctx, `DELETE FROM products WHERE id = $1`, id)
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
if rows == 0 {
|
|
_ = tx.Rollback()
|
|
return errors.New("product not found")
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (r *Repository) CreateOrder(ctx context.Context, order *domain.Order) error {
|
|
now := time.Now().UTC()
|
|
order.CreatedAt = now
|
|
order.UpdatedAt = now
|
|
|
|
tx, err := r.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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, $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.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()
|
|
return err
|
|
}
|
|
|
|
itemQuery := `INSERT INTO order_items (id, order_id, product_id, quantity, unit_cents, batch, expires_at) VALUES ($1, $2, $3, $4, $5, $6, $7)`
|
|
for i := range order.Items {
|
|
item := &order.Items[i]
|
|
item.ID = uuid.Must(uuid.NewV7())
|
|
item.OrderID = order.ID
|
|
if _, err := tx.ExecContext(ctx, itemQuery, item.ID, item.OrderID, item.ProductID, item.Quantity, item.UnitCents, item.Batch, item.ExpiresAt); err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
|
|
// Reduce stock
|
|
res, err := tx.ExecContext(ctx, `UPDATE products SET stock = stock - $1, updated_at = $2 WHERE id = $3 AND stock >= $1`, item.Quantity, now, item.ProductID)
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
if rows == 0 {
|
|
_ = tx.Rollback()
|
|
return fmt.Errorf("insufficient stock for product %s", item.ProductID)
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (r *Repository) ListOrders(ctx context.Context, filter domain.OrderFilter) ([]domain.Order, int64, error) {
|
|
baseQuery := `FROM orders`
|
|
var args []any
|
|
var clauses []string
|
|
|
|
if filter.BuyerID != nil {
|
|
clauses = append(clauses, fmt.Sprintf("buyer_id = $%d", len(args)+1))
|
|
args = append(args, *filter.BuyerID)
|
|
}
|
|
if filter.SellerID != nil {
|
|
clauses = append(clauses, fmt.Sprintf("seller_id = $%d", len(args)+1))
|
|
args = append(args, *filter.SellerID)
|
|
}
|
|
if filter.Status != "" {
|
|
clauses = append(clauses, fmt.Sprintf("status = $%d", len(args)+1))
|
|
args = append(args, filter.Status)
|
|
}
|
|
|
|
where := ""
|
|
if len(clauses) > 0 {
|
|
where = " WHERE " + strings.Join(clauses, " AND ")
|
|
}
|
|
|
|
var total int64
|
|
if err := r.db.GetContext(ctx, &total, "SELECT count(*) "+baseQuery+where, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if filter.Limit <= 0 {
|
|
filter.Limit = 20
|
|
}
|
|
args = append(args, filter.Limit, filter.Offset)
|
|
listQuery := fmt.Sprintf(`SELECT id, buyer_id, seller_id, status, total_cents, COALESCE(shipping_recipient_name, '') as shipping_recipient_name, COALESCE(shipping_street, '') as shipping_street, COALESCE(shipping_number, '') as shipping_number, COALESCE(shipping_complement, '') as shipping_complement, COALESCE(shipping_district, '') as shipping_district, COALESCE(shipping_city, '') as shipping_city, COALESCE(shipping_state, '') as shipping_state, COALESCE(shipping_zip_code, '') as shipping_zip_code, COALESCE(shipping_country, '') as shipping_country, created_at, updated_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d`, baseQuery, where, len(args)-1, len(args))
|
|
|
|
var rows []struct {
|
|
ID uuid.UUID `db:"id"`
|
|
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"`
|
|
}
|
|
|
|
if err := r.db.SelectContext(ctx, &rows, listQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
orders := make([]domain.Order, 0, len(rows))
|
|
itemQuery := `SELECT id, order_id, product_id, quantity, unit_cents, batch, expires_at FROM order_items WHERE order_id = $1`
|
|
for _, row := range rows {
|
|
var items []domain.OrderItem
|
|
if err := r.db.SelectContext(ctx, &items, itemQuery, row.ID); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
orders = append(orders, domain.Order{
|
|
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 orders, total, nil
|
|
}
|
|
|
|
func (r *Repository) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) {
|
|
var row struct {
|
|
ID uuid.UUID `db:"id"`
|
|
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, COALESCE(shipping_recipient_name, '') as shipping_recipient_name, COALESCE(shipping_street, '') as shipping_street, COALESCE(shipping_number, '') as shipping_number, COALESCE(shipping_complement, '') as shipping_complement, COALESCE(shipping_district, '') as shipping_district, COALESCE(shipping_city, '') as shipping_city, COALESCE(shipping_state, '') as shipping_state, COALESCE(shipping_zip_code, '') as shipping_zip_code, COALESCE(shipping_country, '') as 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
|
|
}
|
|
|
|
var items []domain.OrderItem
|
|
itemQuery := `SELECT id, order_id, product_id, quantity, unit_cents, batch, expires_at FROM order_items WHERE order_id = $1`
|
|
if err := r.db.SelectContext(ctx, &items, itemQuery, id); err != nil {
|
|
return nil, err
|
|
}
|
|
order := &domain.Order{
|
|
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 {
|
|
query := `UPDATE orders SET status = $1, updated_at = $2 WHERE id = $3`
|
|
res, err := r.db.ExecContext(ctx, query, status, time.Now().UTC(), id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rows == 0 {
|
|
return errors.New("order not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) DeleteOrder(ctx context.Context, id uuid.UUID) error {
|
|
tx, err := r.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM reviews WHERE order_id = $1`, id); err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM shipments WHERE order_id = $1`, id); err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM order_items WHERE order_id = $1`, id); err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
|
|
res, err := tx.ExecContext(ctx, `DELETE FROM orders WHERE id = $1`, id)
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
if rows == 0 {
|
|
_ = tx.Rollback()
|
|
return errors.New("order not found")
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
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) {
|
|
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, int64, error) {
|
|
baseQuery := `FROM products`
|
|
args := []any{}
|
|
clauses := []string{}
|
|
if filter.ExpiringBefore != nil {
|
|
clauses = append(clauses, fmt.Sprintf("expires_at <= $%d", len(args)+1))
|
|
args = append(args, *filter.ExpiringBefore)
|
|
}
|
|
if filter.SellerID != nil {
|
|
clauses = append(clauses, fmt.Sprintf("seller_id = $%d", len(args)+1))
|
|
args = append(args, *filter.SellerID)
|
|
}
|
|
|
|
where := ""
|
|
if len(clauses) > 0 {
|
|
where = " WHERE " + strings.Join(clauses, " AND ")
|
|
}
|
|
|
|
var total int64
|
|
if err := r.db.GetContext(ctx, &total, "SELECT count(*) "+baseQuery+where, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if filter.Limit <= 0 {
|
|
filter.Limit = 20
|
|
}
|
|
args = append(args, filter.Limit, filter.Offset)
|
|
listQuery := fmt.Sprintf(`SELECT id AS product_id, seller_id, name, batch, expires_at, stock AS quantity, price_cents, updated_at %s%s ORDER BY expires_at ASC LIMIT $%d OFFSET $%d`, baseQuery, where, len(args)-1, len(args))
|
|
|
|
var items []domain.InventoryItem
|
|
if err := r.db.SelectContext(ctx, &items, listQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func (r *Repository) CreateUser(ctx context.Context, user *domain.User) error {
|
|
now := time.Now().UTC()
|
|
user.CreatedAt = now
|
|
user.UpdatedAt = now
|
|
|
|
query := `INSERT INTO users (id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at)
|
|
VALUES (:id, :company_id, :role, :name, :username, :email, :email_verified, :password_hash, :created_at, :updated_at)`
|
|
|
|
_, err := r.db.NamedExecContext(ctx, query, user)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) ListUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) {
|
|
baseQuery := `FROM users`
|
|
var args []any
|
|
var clauses []string
|
|
|
|
if filter.CompanyID != nil {
|
|
clauses = append(clauses, fmt.Sprintf("company_id = $%d", len(args)+1))
|
|
args = append(args, *filter.CompanyID)
|
|
}
|
|
|
|
where := ""
|
|
if len(clauses) > 0 {
|
|
where = " WHERE " + strings.Join(clauses, " AND ")
|
|
}
|
|
|
|
countQuery := "SELECT count(*) " + baseQuery + where
|
|
var total int64
|
|
if err := r.db.GetContext(ctx, &total, countQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
args = append(args, filter.Limit, filter.Offset)
|
|
listQuery := fmt.Sprintf("SELECT id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, where, len(args)-1, len(args))
|
|
|
|
var users []domain.User
|
|
if err := r.db.SelectContext(ctx, &users, listQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return users, total, nil
|
|
}
|
|
|
|
func (r *Repository) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error) {
|
|
var user domain.User
|
|
query := `SELECT id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at FROM users WHERE id = $1`
|
|
if err := r.db.GetContext(ctx, &user, query, id); err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
func (r *Repository) GetUserByUsername(ctx context.Context, username string) (*domain.User, error) {
|
|
var user domain.User
|
|
query := `SELECT id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at FROM users WHERE username = $1`
|
|
if err := r.db.GetContext(ctx, &user, query, username); err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
func (r *Repository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
|
var user domain.User
|
|
query := `SELECT id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at FROM users WHERE email = $1`
|
|
if err := r.db.GetContext(ctx, &user, query, email); err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
func (r *Repository) UpdateUser(ctx context.Context, user *domain.User) error {
|
|
user.UpdatedAt = time.Now().UTC()
|
|
|
|
query := `UPDATE users
|
|
SET company_id = :company_id, role = :role, name = :name, username = :username, email = :email, email_verified = :email_verified, password_hash = :password_hash, updated_at = :updated_at
|
|
WHERE id = :id`
|
|
|
|
res, err := r.db.NamedExecContext(ctx, query, user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rows == 0 {
|
|
return errors.New("user not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) DeleteUser(ctx context.Context, id uuid.UUID) error {
|
|
res, err := r.db.ExecContext(ctx, "DELETE FROM users WHERE id = $1", id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rows == 0 {
|
|
return errors.New("user not found")
|
|
}
|
|
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
|
|
}
|
|
|
|
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::text[])`, sellerID, pq.Array(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::text[])
|
|
GROUP BY oi.product_id, p.name
|
|
ORDER BY total_quantity DESC
|
|
LIMIT 5`
|
|
if err := r.db.SelectContext(ctx, &topProducts, topQuery, sellerID, pq.Array(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::text[])`, pq.Array([]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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (r *Repository) ListReviews(ctx context.Context, filter domain.ReviewFilter) ([]domain.Review, int64, error) {
|
|
baseQuery := `FROM reviews`
|
|
var args []any
|
|
|
|
// Add where clauses if needed in future
|
|
|
|
var total int64
|
|
if err := r.db.GetContext(ctx, &total, "SELECT count(*) "+baseQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if filter.Limit <= 0 {
|
|
filter.Limit = 20
|
|
}
|
|
args = append(args, filter.Limit, filter.Offset)
|
|
|
|
listQuery := fmt.Sprintf("SELECT id, order_id, buyer_id, seller_id, rating, comment, created_at %s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, len(args)-1, len(args))
|
|
|
|
var reviews []domain.Review
|
|
if err := r.db.SelectContext(ctx, &reviews, listQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return reviews, total, nil
|
|
}
|
|
|
|
func (r *Repository) ListShipments(ctx context.Context, filter domain.ShipmentFilter) ([]domain.Shipment, int64, error) {
|
|
baseQuery := `FROM shipments`
|
|
var args []any
|
|
|
|
// Add where clauses if needed in future
|
|
|
|
var total int64
|
|
if err := r.db.GetContext(ctx, &total, "SELECT count(*) "+baseQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if filter.Limit <= 0 {
|
|
filter.Limit = 20
|
|
}
|
|
args = append(args, filter.Limit, filter.Offset)
|
|
|
|
listQuery := fmt.Sprintf("SELECT id, order_id, carrier, tracking_code, external_tracking, status, created_at, updated_at %s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, len(args)-1, len(args))
|
|
|
|
var shipments []domain.Shipment
|
|
if err := r.db.SelectContext(ctx, &shipments, listQuery, args...); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return shipments, total, nil
|
|
}
|
|
|
|
func (r *Repository) GetShippingSettings(ctx context.Context, vendorID uuid.UUID) (*domain.ShippingSettings, error) {
|
|
var settings domain.ShippingSettings
|
|
err := r.db.GetContext(ctx, &settings, `SELECT * FROM shipping_settings WHERE vendor_id = $1`, vendorID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &settings, nil
|
|
}
|
|
|
|
func (r *Repository) UpsertShippingSettings(ctx context.Context, settings *domain.ShippingSettings) error {
|
|
now := time.Now().UTC()
|
|
settings.UpdatedAt = now
|
|
|
|
// Create if new
|
|
if settings.CreatedAt.IsZero() {
|
|
settings.CreatedAt = now
|
|
}
|
|
|
|
query := `
|
|
INSERT INTO shipping_settings (
|
|
vendor_id, active, max_radius_km, price_per_km_cents, min_fee_cents,
|
|
free_shipping_threshold_cents, pickup_active, pickup_address, pickup_hours,
|
|
latitude, longitude, created_at, updated_at
|
|
) VALUES (
|
|
:vendor_id, :active, :max_radius_km, :price_per_km_cents, :min_fee_cents,
|
|
:free_shipping_threshold_cents, :pickup_active, :pickup_address, :pickup_hours,
|
|
:latitude, :longitude, :created_at, :updated_at
|
|
)
|
|
ON CONFLICT (vendor_id) DO UPDATE SET
|
|
active = :active,
|
|
max_radius_km = :max_radius_km,
|
|
price_per_km_cents = :price_per_km_cents,
|
|
min_fee_cents = :min_fee_cents,
|
|
free_shipping_threshold_cents = :free_shipping_threshold_cents,
|
|
pickup_active = :pickup_active,
|
|
pickup_address = :pickup_address,
|
|
pickup_hours = :pickup_hours,
|
|
latitude = :latitude,
|
|
longitude = :longitude,
|
|
updated_at = :updated_at
|
|
`
|
|
|
|
_, err := r.db.NamedExecContext(ctx, query, settings)
|
|
return err
|
|
}
|