saveinmed/backend/internal/repository/postgres/postgres.go
2025-12-18 11:42:23 -03:00

282 lines
8.3 KiB
Go

package postgres
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/gofrs/uuid/v5"
"github.com/jmoiron/sqlx"
"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, role, cnpj, corporate_name, sanitary_license, created_at, updated_at)
VALUES (:id, :role, :cnpj, :corporate_name, :sanitary_license, :created_at, :updated_at)`
_, err := r.db.NamedExecContext(ctx, query, company)
return err
}
func (r *Repository) ListCompanies(ctx context.Context) ([]domain.Company, error) {
var companies []domain.Company
query := `SELECT id, role, cnpj, corporate_name, sanitary_license, created_at, updated_at FROM companies ORDER BY created_at DESC`
if err := r.db.SelectContext(ctx, &companies, query); err != nil {
return nil, err
}
return companies, nil
}
func (r *Repository) CreateProduct(ctx context.Context, product *domain.Product) error {
now := time.Now().UTC()
product.CreatedAt = now
product.UpdatedAt = now
query := `INSERT INTO products (id, seller_id, name, description, batch, expires_at, price_cents, stock, created_at, updated_at)
VALUES (:id, :seller_id, :name, :description, :batch, :expires_at, :price_cents, :stock, :created_at, :updated_at)`
_, err := r.db.NamedExecContext(ctx, query, product)
return err
}
func (r *Repository) ListProducts(ctx context.Context) ([]domain.Product, error) {
var products []domain.Product
query := `SELECT id, seller_id, name, description, batch, expires_at, price_cents, stock, created_at, updated_at FROM products ORDER BY created_at DESC`
if err := r.db.SelectContext(ctx, &products, query); err != nil {
return nil, err
}
return products, nil
}
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, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)`
if _, err := tx.ExecContext(ctx, orderQuery, order.ID, order.BuyerID, order.SellerID, order.Status, order.TotalCents, 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
}
}
return tx.Commit()
}
func (r *Repository) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) {
var order domain.Order
orderQuery := `SELECT id, buyer_id, seller_id, status, total_cents, created_at, updated_at FROM orders WHERE id = $1`
if err := r.db.GetContext(ctx, &order, 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.Items = items
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) 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, email, password_hash, created_at, updated_at)
VALUES (:id, :company_id, :role, :name, :email, :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, email, 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, email, 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) 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, email = :email, 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
}
// InitSchema applies a minimal schema for development environments.
func (r *Repository) InitSchema(ctx context.Context) error {
schema := `
CREATE TABLE IF NOT EXISTS companies (
id UUID PRIMARY KEY,
role TEXT NOT NULL,
cnpj TEXT NOT NULL UNIQUE,
corporate_name TEXT NOT NULL,
sanitary_license TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY,
company_id UUID NOT NULL REFERENCES companies(id),
role TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS products (
id UUID PRIMARY KEY,
seller_id UUID NOT NULL REFERENCES companies(id),
name TEXT NOT NULL,
description TEXT,
batch TEXT NOT NULL,
expires_at DATE NOT NULL,
price_cents BIGINT NOT NULL,
stock BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS orders (
id UUID PRIMARY KEY,
buyer_id UUID NOT NULL REFERENCES companies(id),
seller_id UUID NOT NULL REFERENCES companies(id),
status TEXT NOT NULL,
total_cents BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS order_items (
id UUID PRIMARY KEY,
order_id UUID NOT NULL REFERENCES orders(id),
product_id UUID NOT NULL REFERENCES products(id),
quantity BIGINT NOT NULL,
unit_cents BIGINT NOT NULL,
batch TEXT NOT NULL,
expires_at DATE NOT NULL
);
`
if _, err := r.db.ExecContext(ctx, schema); err != nil {
return fmt.Errorf("apply schema: %w", err)
}
return nil
}