Product.java

package com.albertsilva.dev.asjcatalog.domain.catalog;

import java.io.Serializable;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;

import com.albertsilva.dev.asjcatalog.domain.Identifiable;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinTable;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;

/**
 * Representa um produto no sistema.
 *
 * <p>
 * Esta entidade contém as informações essenciais de um produto,
 * incluindo dados descritivos, preço e relacionamento com categorias.
 * </p>
 *
 * <p>
 * <b>Regras e invariantes:</b>
 * </p>
 * <ul>
 * <li>O nome é obrigatório e único (restrições do mapeamento)</li>
 * <li>A entidade <b>não</b> valida o preço: o campo é um {@link Double}
 * opcional, e a exigência de valor positivo é feita no DTO de entrada
 * ({@code @Positive}), não aqui</li>
 * <li>O indicador {@code active} é apenas armazenado: a entidade não impõe
 * restrições com base nele. Uma instância nova nasce inativa ({@code false})
 * até que o valor seja definido</li>
 * <li>Um produto pode pertencer a várias categorias; a entidade não exige pelo
 * menos uma categoria (essa exigência está nos DTOs de entrada)</li>
 * <li>A identidade é definida somente pelo {@code id} (ver
 * {@link #equals(Object)})</li>
 * </ul>
 *
 * <p>
 * <b>Mapeamento:</b>
 * </p>
 * <ul>
 * <li>Tabela: {@code tb_product}</li>
 * <li>Relacionamento muitos-para-muitos com {@link Category}, no qual
 * {@code Product} é o lado dono (tabela de junção
 * {@code tb_product_category})</li>
 * <li>Datas de auditoria preenchidas por callbacks JPA privados
 * ({@code prePersist} e {@code preUpdate})</li>
 * <li>Implementa {@link Identifiable}, o que permite tratá-lo de forma genérica
 * pelo identificador</li>
 * </ul>
 */
@Entity
@Table(name = "tb_product")
public class Product implements Serializable, Identifiable<Long> {
  private static final long serialVersionUID = 1L;

  /**
   * Identificador único do produto.
   *
   * <p>
   * Gerado automaticamente pelo banco de dados.
   * </p>
   */
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  /**
   * Nome do produto.
   *
   * <p>
   * Utilizado para identificação e exibição.
   * </p>
   */
  @Column(nullable = false, unique = true)
  private String name;

  /**
   * Descrição detalhada do produto.
   *
   * <p>
   * Armazenada como texto longo no banco de dados.
   * </p>
   */
  @Column(columnDefinition = "TEXT")
  private String description;

  /**
   * Preço do produto.
   *
   * <p>
   * Valor do produto. É um {@link Double} sem restrição de nulidade ou de faixa
   * na entidade (a validação ocorre nos DTOs de entrada); por ser ponto
   * flutuante, não oferece precisão decimal exata.
   * </p>
   */
  private Double price;

  /**
   * URL da imagem do produto.
   *
   * <p>
   * Utilizada para exibição visual em interfaces.
   * </p>
   */
  private String imgUrl;

  /**
   * Data de criação do registro.
   *
   * <p>
   * Preenchida automaticamente no momento da persistência (callback
   * {@code prePersist}); permanece {@code null} em instâncias ainda não
   * persistidas.
   * </p>
   */
  @Column(columnDefinition = "TIMESTAMP WITHOUT TIME ZONE")
  private Instant createdAt;

  /**
   * Data da última atualização do registro.
   *
   * <p>
   * Definida na criação (com o mesmo instante de {@code createdAt}) e
   * atualizada pelo callback {@code preUpdate} quando uma alteração é
   * sincronizada com o banco. Permanece {@code null} em instâncias ainda não
   * persistidas.
   * </p>
   */
  @Column(columnDefinition = "TIMESTAMP WITHOUT TIME ZONE")
  private Instant updatedAt;

  /**
   * Indica se o produto está ativo.
   *
   * <p>
   * A entidade apenas armazena o valor: não oculta nem bloqueia nada com base
   * nele. Vale {@code false} em uma instância recém-criada até que seja
   * definido (por {@link #setActive(boolean)} ou pelos construtores que
   * recebem o parâmetro).
   * </p>
   */
  private boolean active;

  /**
   * Categorias associadas ao produto.
   *
   * <p>
   * Relacionamento muitos-para-muitos onde:
   * </p>
   * <ul>
   * <li>Um produto pode pertencer a várias categorias</li>
   * <li>Uma categoria pode conter vários produtos</li>
   * </ul>
   *
   * <p>
   * Este relacionamento é gerenciado pela tabela intermediária
   * {@code tb_product_category}.
   * </p>
   *
   * <p>
   * {@code Product} é o lado dono: adicionar ou remover categorias neste
   * conjunto altera o vínculo persistido. Não há {@code cascade}, portanto as
   * categorias precisam já existir (uma categoria nova não é criada ao salvar
   * o produto), e não há {@code fetch} explícito (vale o padrão da JPA para
   * coleções, carregamento tardio).
   * </p>
   */
  @ManyToMany
  @JoinTable(name = "tb_product_category", joinColumns = @JoinColumn(name = "product_id"), inverseJoinColumns = @JoinColumn(name = "category_id"))
  private Set<Category> categories = new HashSet<>();

  /**
   * Cria um produto vazio: campos nulos, {@code active} igual a {@code false} e
   * nenhuma categoria associada. Construtor sem argumentos exigido pela JPA.
   */
  public Product() {
  }

  /**
   * Cria um produto com identificador definido.
   *
   * <p>
   * Nenhum argumento é validado; as categorias começam vazias e
   * {@code createdAt}/{@code updatedAt} não são preenchidos aqui (apenas pelos
   * callbacks JPA).
   * </p>
   *
   * @param id          identificador do produto
   * @param name        nome do produto
   * @param description descrição do produto
   * @param price       preço do produto
   * @param imgUrl      URL da imagem do produto
   * @param active      indica se o produto está ativo
   */
  public Product(Long id, String name, String description, Double price, String imgUrl, boolean active) {
    this.id = id;
    this.name = name;
    this.description = description;
    this.price = price;
    this.imgUrl = imgUrl;
    this.active = active;
  }

  /**
   * Cria um produto ainda sem identificador (o banco o gera na persistência).
   *
   * <p>
   * Nenhum argumento é validado; as categorias começam vazias e
   * {@code createdAt}/{@code updatedAt} não são preenchidos aqui (apenas pelos
   * callbacks JPA).
   * </p>
   *
   * @param name        nome do produto
   * @param description descrição do produto
   * @param price       preço do produto
   * @param imgUrl      URL da imagem do produto
   * @param active      indica se o produto está ativo
   */
  public Product(String name, String description, Double price, String imgUrl, boolean active) {
    this.name = name;
    this.description = description;
    this.price = price;
    this.imgUrl = imgUrl;
    this.active = active;
  }

  /**
   * @return identificador único do produto
   */
  @Override
  public Long getId() {
    return id;
  }

  /**
   * @param id identificador único do produto
   */
  public void setId(Long id) {
    this.id = id;
  }

  /**
   * @return nome do produto
   */
  public String getName() {
    return name;
  }

  /**
   * @param name nome do produto
   */
  public void setName(String name) {
    this.name = name;
  }

  /**
   * @return descrição do produto
   */
  public String getDescription() {
    return description;
  }

  /**
   * @param description descrição detalhada do produto
   */
  public void setDescription(String description) {
    this.description = description;
  }

  /**
   * @return preço do produto
   */
  public Double getPrice() {
    return price;
  }

  /**
   * @param price valor monetário do produto
   */
  public void setPrice(Double price) {
    this.price = price;
  }

  /**
   * @return URL da imagem do produto
   */
  public String getImgUrl() {
    return imgUrl;
  }

  /**
   * @param imgUrl URL da imagem do produto
   */
  public void setImgUrl(String imgUrl) {
    this.imgUrl = imgUrl;
  }

  /**
   * @return data de criação do registro
   */
  public Instant getCreatedAt() {
    return createdAt;
  }

  /**
   * @return data da última atualização do registro
   */
  public Instant getUpdatedAt() {
    return updatedAt;
  }

  /**
   * Retorna o próprio conjunto interno (não uma cópia). Como {@code Product} é
   * o lado dono do relacionamento, adicionar ou remover categorias aqui altera
   * o vínculo persistido em {@code tb_product_category}. Não existe método
   * auxiliar de vínculo na entidade.
   *
   * @return conjunto de categorias associadas ao produto
   */
  public Set<Category> getCategories() {
    return categories;
  }

  /**
   * @return {@code true} se o produto estiver ativo
   */
  public boolean isActive() {
    return active;
  }

  /**
   * Callback JPA executado antes da primeira persistência: define
   * {@code createdAt} e {@code updatedAt} com o mesmo instante atual.
   */
  @PrePersist
  private void prePersist() {
    Instant now = Instant.now();

    createdAt = now;
    updatedAt = now;
  }

  /**
   * Callback JPA executado antes de uma atualização: define {@code updatedAt}
   * com o instante atual.
   */
  @PreUpdate
  private void preUpdate() {
    updatedAt = Instant.now();
  }

  /**
   * @param active define se o produto estará ativo ou não
   */
  public void setActive(boolean active) {
    this.active = active;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((id == null) ? 0 : id.hashCode());
    return result;
  }

  /**
   * Compara dois produtos com base no identificador.
   *
   * <p>
   * Dois produtos são considerados iguais quando possuem o mesmo ID. Duas
   * instâncias sem ID (ainda não persistidas) são consideradas iguais entre
   * si, e {@link #hashCode()} depende apenas do ID. A comparação exige a mesma
   * classe exata ({@code getClass()}), e não apenas compatibilidade de tipo.
   * </p>
   */
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Product other = (Product) obj;
    if (id == null) {
      if (other.id != null)
        return false;
    } else if (!id.equals(other.id))
      return false;
    return true;
  }
}