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) } if filter.IsVerified != nil { clauses = append(clauses, fmt.Sprintf("is_verified = $%d", len(args)+1)) args = append(args, *filter.IsVerified) } 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 { // Removed batch, expires_at, stock query := `INSERT INTO products (id, seller_id, ean_code, name, description, manufacturer, category, subcategory, price_cents, observations, internal_code, factory_price_cents, pmc_cents, commercial_discount_cents, tax_substitution_cents, invoice_price_cents) VALUES (:id, :seller_id, :ean_code, :name, :description, :manufacturer, :category, :subcategory, :price_cents, :observations, :internal_code, :factory_price_cents, :pmc_cents, :commercial_discount_cents, :tax_substitution_cents, :invoice_price_cents) 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) BatchCreateProducts(ctx context.Context, products []domain.Product) error { if len(products) == 0 { return nil } tx, err := r.db.BeginTxx(ctx, nil) if err != nil { return err } query := `INSERT INTO products (id, seller_id, ean_code, name, description, manufacturer, category, subcategory, price_cents, observations, created_at, updated_at) VALUES (:id, :seller_id, :ean_code, :name, :description, :manufacturer, :category, :subcategory, :price_cents, :observations, :created_at, :updated_at)` for _, p := range products { if _, err := tx.NamedExecContext(ctx, query, p); err != nil { _ = tx.Rollback() return err } } return tx.Commit() } 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) // REmoved batch, expires_at, stock columns from SELECT listQuery := fmt.Sprintf("SELECT id, seller_id, ean_code, name, description, manufacturer, category, subcategory, price_cents, 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) // Removed batch, expires_at, stock from SELECT list listQuery := fmt.Sprintf(`SELECT id, seller_id, ean_code, name, description, manufacturer, category, subcategory, price_cents, 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, payment_method, 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, $17)` if _, err := tx.ExecContext(ctx, orderQuery, order.ID, order.BuyerID, order.SellerID, order.Status, order.TotalCents, order.PaymentMethod, 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, payment_method, 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"` PaymentMethod domain.PaymentMethod `db:"payment_method"` 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, PaymentMethod: row.PaymentMethod, 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"` PaymentMethod domain.PaymentMethod `db:"payment_method"` 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, payment_method, 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, PaymentMethod: row.PaymentMethod, 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 // } // Updated to use inventory_items // var item domain.InventoryItem // Finding an arbitrary inventory item for this product/batch? // The current AdjustInventory signature is simplistic (ProductID only), // assuming 1:1 or we need to find ANY item? // Realistically, AdjustInventory should take an InventoryItemID or (ProductID + Batch). // For now, let's assume it updates the TOTAL stock for a product if we don't have batch? // OR, IF the user is refactoring, we might need to disable this function or fix it properly. // Since I don't have the full context of how AdjustInventory is called (handler just passes ID), // I will just STUB it or try to find an item. // Let's try to find an existing inventory item for this ProductID (Dictionary) + SellerID (from context? No seller in args). // This function seems broken for the new model without SellerID. // I will return an error acting as "Not Implemented" for now to satisfy compilation. return nil, errors.New("AdjustInventory temporarily disabled during refactor") } func (r *Repository) ListInventory(ctx context.Context, filter domain.InventoryFilter) ([]domain.InventoryItem, int64, error) { baseQuery := `FROM inventory_items i JOIN products p ON i.product_id = p.id` args := []any{} clauses := []string{} if filter.ExpiringBefore != nil { clauses = append(clauses, fmt.Sprintf("i.expires_at <= $%d", len(args)+1)) args = append(args, *filter.ExpiringBefore) } if filter.SellerID != nil { clauses = append(clauses, fmt.Sprintf("i.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) // Select columns matching InventoryItem struct db tags + product_name listQuery := fmt.Sprintf(` SELECT i.id, i.product_id, i.seller_id, i.sale_price_cents, i.stock_quantity, i.batch, i.expires_at, i.observations, i.created_at, i.updated_at, p.name AS product_name %s%s ORDER BY i.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, superadmin, nome_social, cpf, created_at, updated_at) VALUES (:id, :company_id, :role, :name, :username, :email, :email_verified, :password_hash, :superadmin, :nome_social, :cpf, :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 var clauses []string 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, order_id, buyer_id, seller_id, rating, comment, created_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, where, 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 s` var args []any var clauses []string if filter.SellerID != nil { baseQuery += ` JOIN orders o ON s.order_id = o.id` clauses = append(clauses, fmt.Sprintf("o.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 s.id, s.order_id, s.carrier, s.tracking_code, s.external_tracking, s.status, s.created_at, s.updated_at %s%s ORDER BY s.created_at DESC LIMIT $%d OFFSET $%d", baseQuery, where, 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 } // GetPaymentGatewayConfig retrieves admin config for a provider (e.g. stripe). func (r *Repository) GetPaymentGatewayConfig(ctx context.Context, provider string) (*domain.PaymentGatewayConfig, error) { var cfg domain.PaymentGatewayConfig query := `SELECT provider, active, credentials, environment, commission, updated_at FROM payment_gateway_configs WHERE provider = $1` if err := r.db.GetContext(ctx, &cfg, query, provider); err != nil { return nil, err } return &cfg, nil } // UpsertPaymentGatewayConfig updates or creates global gateway settings. func (r *Repository) UpsertPaymentGatewayConfig(ctx context.Context, config *domain.PaymentGatewayConfig) error { config.UpdatedAt = time.Now().UTC() query := `INSERT INTO payment_gateway_configs (provider, active, credentials, environment, commission, updated_at) VALUES (:provider, :active, :credentials, :environment, :commission, :updated_at) ON CONFLICT (provider) DO UPDATE SET active = EXCLUDED.active, credentials = EXCLUDED.credentials, environment = EXCLUDED.environment, commission = EXCLUDED.commission, updated_at = EXCLUDED.updated_at` _, err := r.db.NamedExecContext(ctx, query, config) return err } // GetSellerPaymentAccount retrieves seller's connect account. func (r *Repository) GetSellerPaymentAccount(ctx context.Context, sellerID uuid.UUID) (*domain.SellerPaymentAccount, error) { var acc domain.SellerPaymentAccount query := `SELECT seller_id, gateway, account_id, account_type, status, created_at FROM seller_payment_accounts WHERE seller_id = $1` if err := r.db.GetContext(ctx, &acc, query, sellerID); err != nil { return nil, err } return &acc, nil } // UpsertSellerPaymentAccount updates or creates seller's connect account. func (r *Repository) UpsertSellerPaymentAccount(ctx context.Context, account *domain.SellerPaymentAccount) error { query := `INSERT INTO seller_payment_accounts (seller_id, gateway, account_id, account_type, status, created_at) VALUES (:seller_id, :gateway, :account_id, :account_type, :status, :created_at) ON CONFLICT (seller_id, gateway) DO UPDATE SET account_id = EXCLUDED.account_id, account_type = EXCLUDED.account_type, status = EXCLUDED.status` _, err := r.db.NamedExecContext(ctx, query, account) return err } func (r *Repository) CreateAddress(ctx context.Context, address *domain.Address) error { query := `INSERT INTO addresses (id, entity_id, title, zip_code, street, number, complement, district, city, state, created_at, updated_at) VALUES (:id, :entity_id, :title, :zip_code, :street, :number, :complement, :district, :city, :state, :created_at, :updated_at)` _, err := r.db.NamedExecContext(ctx, query, address) return err } func (r *Repository) ListManufacturers(ctx context.Context) ([]string, error) { query := `SELECT DISTINCT manufacturer FROM products WHERE manufacturer IS NOT NULL AND manufacturer != '' ORDER BY manufacturer ASC` var manufacturers []string if err := r.db.SelectContext(ctx, &manufacturers, query); err != nil { return nil, err } return manufacturers, nil } func (r *Repository) ListCategories(ctx context.Context) ([]string, error) { query := `SELECT DISTINCT category FROM products WHERE category IS NOT NULL AND category != '' ORDER BY category ASC` var categories []string if err := r.db.SelectContext(ctx, &categories, query); err != nil { return nil, err } return categories, nil } func (r *Repository) GetProductByEAN(ctx context.Context, ean string) (*domain.Product, error) { var product domain.Product query := `SELECT id, seller_id, ean_code, name, description, manufacturer, category, subcategory, price_cents, internal_code, factory_price_cents, pmc_cents, commercial_discount_cents, tax_substitution_cents, invoice_price_cents, observations, created_at, updated_at FROM products WHERE ean_code = $1 LIMIT 1` if err := r.db.GetContext(ctx, &product, query, ean); err != nil { return nil, err } return &product, nil } func (r *Repository) CreateInventoryItem(ctx context.Context, item *domain.InventoryItem) error { query := `INSERT INTO inventory_items (id, product_id, seller_id, sale_price_cents, stock_quantity, batch, expires_at, observations, created_at, updated_at) VALUES (:id, :product_id, :seller_id, :sale_price_cents, :stock_quantity, :batch, :expires_at, :observations, :created_at, :updated_at)` if item.ID == uuid.Nil { item.ID = uuid.Must(uuid.NewV7()) } item.CreatedAt = time.Now().UTC() item.UpdatedAt = time.Now().UTC() _, err := r.db.NamedExecContext(ctx, query, item) return err } func (r *Repository) UpdateInventoryItem(ctx context.Context, item *domain.InventoryItem) error { query := `UPDATE inventory_items SET sale_price_cents = :sale_price_cents, stock_quantity = :stock_quantity, updated_at = :updated_at WHERE id = :id AND seller_id = :seller_id` item.UpdatedAt = time.Now().UTC() _, err := r.db.NamedExecContext(ctx, query, item) return err }