backbone-catalog 0.7.0

Canonical product/service identity: Item, Item Group, UOM (Indonesia-first)
Documentation
//! Repository for Brand entities
//!
//! Originally generated by metaphor-schema; now **user-owned** — this exact path is declared under
//! `user_owned` in `metaphor.codegen.yaml`, so the generator skips it wholesale. The custom methods
//! below hold the catalog write service's brand SQL (4-layer rule: services orchestrate, repos hold
//! SQL).
//!
//! Tenant-agnostic (ADR-0029): no statement here names a tenancy column. When the composing
//! host mounts a request-scoped org fence, plain pool reads ride the request-dedicated
//! scoped connection and see only in-scope rows; undecorated, they see the whole table.
//!
//! Thin newtype over `backbone_orm::GenericCrudRepository<Brand, backbone_orm::SoftDelete>`.
//! All standard CRUD methods are available via `Deref`.

use sqlx::PgPool;
use uuid::Uuid;

use crate::domain::entity::Brand;

/// Table name for Brand entities
pub const TABLE_NAME: &str = "catalog.brands";

/// Repository for Brand entities.
///
/// All standard CRUD, soft-delete, pagination, and bulk methods are
/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
pub struct BrandRepository(
    backbone_orm::GenericCrudRepository<Brand, backbone_orm::SoftDelete>,
);

impl std::ops::Deref for BrandRepository {
    type Target = backbone_orm::GenericCrudRepository<Brand, backbone_orm::SoftDelete>;
    fn deref(&self) -> &Self::Target { &self.0 }
}

impl BrandRepository {
    /// Create a new repository instance.
    pub fn new(pool: PgPool) -> Self {
        Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
    }
}

/// The exact row a validated brand insert writes.
pub struct NewBrandRow<'a> {
    pub id: Uuid,
    pub code: &'a str,
    pub name: &'a str,
    pub short_description: Option<&'a str>,
    pub description: Option<&'a str>,
    pub logo_url: Option<&'a str>,
    pub sort_order: i32,
}

/// Catalog brand SQL. Lives here (not in the service) per the module's 4-layer rule.
impl BrandRepository {
    /// `EXISTS` probe for a live row (replaces the prior string-built `exists_in` helper
    /// in the write service). Used for optional `brand_id` FK validation on create-item.
    pub async fn exists_id(&self, executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>, id: Uuid) -> Result<bool, sqlx::Error> {
        let found: Option<Uuid> = sqlx::query_scalar(
            "SELECT id FROM catalog.brands \
             WHERE id = $1 AND (metadata->>'deleted_at') IS NULL",
        )
        .bind(id)
        .fetch_optional(executor)
        .await?;
        Ok(found.is_some())
    }

    /// Insert a validated brand row. Unique-constraint errors propagate as `sqlx::Error`
    /// so the service can disambiguate code duplicates.
    pub async fn insert_brand(
        &self,
        executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
        r: &NewBrandRow<'_>,
    ) -> Result<(), sqlx::Error> {
        sqlx::query(
            r#"INSERT INTO catalog.brands
                (id, code, name, short_description, description, logo_url, sort_order, status)
               VALUES ($1,$2,$3,$4,$5,$6,$7,'active'::catalog_status)"#,
        )
        .bind(r.id)
        .bind(r.code)
        .bind(r.name)
        .bind(r.short_description)
        .bind(r.description)
        .bind(r.logo_url)
        .bind(r.sort_order)
        .execute(executor)
        .await?;
        Ok(())
    }
}

backbone_core::impl_crud_repository!(BrandRepository, Brand, soft_delete);