Category.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 jakarta.persistence.Table;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;

/**
 * Representa uma categoria de produtos no sistema.
 *
 * <p>
 * Esta entidade é utilizada para classificar e organizar os produtos,
 * permitindo agrupamentos lógicos que facilitam a navegação, busca e
 * gerenciamento no sistema.
 * </p>
 *
 * <p>
 * <b>Regras e invariantes:</b>
 * </p>
 * <ul>
 * <li>O nome é obrigatório e único, com até 80 caracteres (restrições do
 * mapeamento); a descrição é opcional, com até 255 caracteres</li>
 * <li>A categoria possui um indicador {@code active}, que a entidade apenas
 * armazena: ela não impõe nenhuma restrição com base nele. Uma instância nova
 * nasce inativa ({@code false}); quem a cria define o valor</li>
 * <li>A validação de formato do nome e da descrição não é feita nesta
 * entidade; ocorre 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_category}</li>
 * <li>Relacionamento muitos-para-muitos com {@link Product}, no qual
 * {@code Product} é o lado dono; nesta entidade o lado é apenas inverso
 * ({@code mappedBy = "categories"})</li>
 * <li>Datas de auditoria preenchidas por callbacks JPA
 * ({@link #prePersist()} e {@link #preUpdate()})</li>
 * </ul>
 */
@Entity
@Table(name = "tb_category")
public class Category implements Serializable {

  private static final long serialVersionUID = 1L;

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

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

  /**
   * Descrição detalhada da categoria.
   *
   * <p>
   * Pode ser utilizada para fornecer mais contexto sobre o tipo de produtos
   * associados.
   * </p>
   */
  @Column(length = 255)
  private String description;

  /**
   * Indica se a categoria está ativa.
   *
   * <p>
   * A entidade apenas armazena o valor: não filtra 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;

  /**
   * Data de criação do registro.
   *
   * <p>
   * Preenchida automaticamente no momento da persistência (callback
   * {@link #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>
   * Atualizada pelo callback {@link #preUpdate()} quando uma alteração é
   * sincronizada com o banco. Não é preenchida na criação: permanece
   * {@code null} até a primeira atualização.
   * </p>
   */
  @Column(columnDefinition = "TIMESTAMP WITHOUT TIME ZONE")
  private Instant updatedAt;

  /**
   * Conjunto de produtos associados a esta categoria.
   *
   * <p>
   * Representa a relação de muitos para muitos entre categorias e produtos.
   * </p>
   *
   * <p>
   * É o lado inverso do relacionamento ({@code mappedBy}): a tabela de junção é
   * mantida por {@link Product#getCategories()}, portanto adicionar ou remover
   * produtos apenas neste conjunto não altera o vínculo persistido.
   * </p>
   */
  @ManyToMany(mappedBy = "categories")
  private Set<Product> products = new HashSet<>();

  /**
   * Cria uma categoria vazia: textos nulos, {@code active} igual a
   * {@code false} e nenhum produto associado. Construtor sem argumentos exigido
   * pela JPA.
   */
  public Category() {
  }

  /**
   * Cria uma categoria com identificador definido.
   *
   * <p>
   * Nenhum argumento é validado, e {@code createdAt}/{@code updatedAt} não são
   * preenchidos aqui (apenas pelos callbacks JPA).
   * </p>
   *
   * @param id          identificador da categoria
   * @param name        nome da categoria
   * @param description descrição da categoria
   * @param active      indica se a categoria está ativa
   */
  public Category(Long id, String name, String description, boolean active) {
    this.id = id;
    this.name = name;
    this.description = description;
    this.active = active;
  }

  /**
   * Cria uma categoria ainda sem identificador (o banco o gera na persistência).
   *
   * <p>
   * Nenhum argumento é validado, e {@code createdAt}/{@code updatedAt} não são
   * preenchidos aqui (apenas pelos callbacks JPA).
   * </p>
   *
   * @param name        nome da categoria
   * @param description descrição da categoria
   * @param active      indica se a categoria está ativa
   */
  public Category(String name, String description, boolean active) {
    this.name = name;
    this.description = description;
    this.active = active;
  }

  /**
   * @return identificador único da categoria
   */
  public Long getId() {
    return id;
  }

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

  /**
   * @return nome da categoria
   */
  public String getName() {
    return name;
  }

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

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

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

  /**
   * @return {@code true} se a categoria estiver ativa, {@code false} caso
   *         contrário
   */
  public boolean isActive() {
    return active;
  }

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

  /**
   * @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;
  }

  /**
   * Método executado automaticamente antes da persistência da entidade.
   *
   * <p>
   * Responsável por definir a data de criação ({@code createdAt}); a data de
   * atualização ({@code updatedAt}) não é definida neste momento.
   * </p>
   */
  @PrePersist
  public void prePersist() {
    createdAt = Instant.now();
  }

  /**
   * Método executado automaticamente antes da atualização da entidade.
   *
   * <p>
   * Responsável por atualizar a data de modificação.
   * </p>
   */
  @PreUpdate
  public void preUpdate() {
    updatedAt = Instant.now();
  }

  /**
   * Retorna o próprio conjunto interno (não uma cópia). Por ser o lado inverso
   * do relacionamento, alterações feitas nele não modificam o vínculo
   * persistido; ver {@link Product#getCategories()}.
   *
   * @return conjunto de produtos associados a esta categoria
   */
  public Set<Product> getProducts() {
    return products;
  }

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

  /**
   * Compara duas categorias com base no identificador.
   *
   * <p>
   * Duas categorias são consideradas 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;
    Category other = (Category) obj;
    if (id == null) {
      if (other.id != null)
        return false;
    } else if (!id.equals(other.id))
      return false;
    return true;
  }
}