backbone-pos 0.5.32

Point of sale — cashier session, ticket + multi-tender; orchestrates billing (revenue) + payment (settlement), posts no GL directly
Documentation
//! PosManagerPin DTOs (Data Transfer Objects)
//!
//! 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.
//!
//! This entity stores an ARGON2 CREDENTIAL HASH, so its DTO surface is deliberately credential-blind:
//! NO DTO — request or response — carries `pin_hash`, and none carries the attempt-source address.
//! The hash never leaves the server, and no generic CRUD surface can forge or overwrite one: the
//! Create/Update/Patch DTOs accept only the identity dimension (the manager). The only way to
//! set or change a PIN is the guarded `set_pin` verb, which hashes server-side and checks authority.
//!
//! The generated CRUD handlers still compile against these DTOs, but a CRUD create produces a row
//! whose hash is a non-verifying placeholder — a row that can never authenticate anyone.
//!
//! Tenancy is composition-installed (ADR-0029): no tenancy field on any DTO — the composing
//! service's scope middleware owns the fence.

use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};

#[cfg(feature = "openapi")]
use utoipa::ToSchema;

#[cfg(feature = "validation")]
use validator::Validate;

use crate::domain::entity::PosManagerPin;
use crate::domain::entity::AuditMetadata;

/// A PHC-formatted string that is structurally present but can never verify — the placeholder a
/// credential-blind CRUD create lands on. `PasswordHash::new` rejects it (no algorithm header), so
/// the verify path fails closed on any row created this way.
const NEVER_VERIFIES: &str = "(set via the guarded set-pin verb)";

// =============================================================================
// Create DTO
// =============================================================================

/// Request DTO for creating a new PosManagerPin row.
///
/// Credential-blind: accepts only the identity dimension. It cannot install a hash — use the
/// guarded `set_pin` verb (server-side argon2 + authority proof) to give a manager a working PIN.
#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "validation", derive(Validate))]
#[serde(rename_all = "camelCase")]
pub struct CreatePosManagerPinDto {
    #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
    #[serde(alias = "employee_party_id")]
    pub employee_party_id: Uuid,
}

// =============================================================================
// Update DTO
// =============================================================================

/// Request DTO for full update of a PosManagerPin row.
///
/// Credential-blind: an update can re-point the identity dimension but can never touch the hash,
/// the failure counter, or the lockout — those change only through the verify path.
#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "validation", derive(Validate))]
#[serde(rename_all = "camelCase")]
pub struct UpdatePosManagerPinDto {
    #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
    #[serde(alias = "employee_party_id")]
    pub employee_party_id: Uuid,
}

// =============================================================================
// Patch DTO
// =============================================================================

/// Request DTO for partial update of a PosManagerPin row. Credential-blind, as above.
#[derive(Debug, Clone, Default, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "validation", derive(Validate))]
#[serde(rename_all = "camelCase")]
pub struct PatchPosManagerPinDto {
    #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
    #[serde(skip_serializing_if = "Option::is_none", alias = "employee_party_id")]
    pub employee_party_id: Option<Uuid>,
}

impl PatchPosManagerPinDto {
    /// Check if any field is set
    pub fn has_changes(&self) -> bool {
        self.employee_party_id.is_some()
    }
}

// =============================================================================
// Response DTO
// =============================================================================

/// Response DTO for PosManagerPin entity — the lockout state only, never the hash and never the
/// attempt-source address. This is what an admin surface shows: WHICH managers hold a credential
/// and whether one is locked, nothing that helps guess one.
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct PosManagerPinResponseDto {
    #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
    pub id: Uuid,
    #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
    pub employee_party_id: Uuid,
    #[cfg_attr(feature = "openapi", schema(example = 42))]
    pub failed_attempts: i32,
    pub locked_until: Option<DateTime<Utc>>,
    pub last_attempt_at: Option<DateTime<Utc>>,
    pub metadata: AuditMetadata,
}

// =============================================================================
// List Response DTO
// =============================================================================

/// Paginated list response for PosManagerPin entities
///
/// Used in API responses for list endpoints.
/// Includes pagination metadata.
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct PosManagerPinListResponseDto {
    /// List of items
    pub items: Vec<PosManagerPinResponseDto>,
    /// Total number of items (across all pages)
    pub total: u64,
    /// Current page number (1-indexed)
    pub page: u32,
    /// Number of items per page
    pub per_page: u32,
    /// Total number of pages
    pub total_pages: u32,
    /// Whether there is a next page
    pub has_next: bool,
    /// Whether there is a previous page
    pub has_prev: bool,
}

impl PosManagerPinListResponseDto {
    /// Create a new list response from items and pagination info
    pub fn new(items: Vec<PosManagerPinResponseDto>, total: u64, page: u32, per_page: u32) -> Self {
        let total_pages = if per_page > 0 {
            ((total as f64) / (per_page as f64)).ceil() as u32
        } else {
            0
        };
        Self {
            items,
            total,
            page,
            per_page,
            total_pages,
            has_next: page < total_pages,
            has_prev: page > 1,
        }
    }
}

/// Summary DTO for list views (compact version) — identity only, no lockout detail, no hash.
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct PosManagerPinSummaryDto {
    pub id: Uuid,
    pub employee_party_id: Uuid,
    pub created_at: Option<DateTime<Utc>>,
}

// =============================================================================
// Conversions
// =============================================================================

impl From<PosManagerPin> for PosManagerPinResponseDto {
    fn from(entity: PosManagerPin) -> Self {
        Self {
            id: entity.id,
            employee_party_id: entity.employee_party_id,
            failed_attempts: entity.failed_attempts,
            locked_until: entity.locked_until,
            last_attempt_at: entity.last_attempt_at,
            metadata: entity.metadata,
        }
    }
}

impl From<PosManagerPin> for PosManagerPinSummaryDto {
    fn from(entity: PosManagerPin) -> Self {
        let created_at = backbone_core::PersistentEntity::created_at(&entity);
        Self {
            id: entity.id,
            employee_party_id: entity.employee_party_id,
            created_at,
        }
    }
}

impl From<CreatePosManagerPinDto> for PosManagerPin {
    fn from(dto: CreatePosManagerPinDto) -> Self {
        // The placeholder can never verify — a credential-blind create cannot mint access.
        Self {
            id: Uuid::new_v4(),
            employee_party_id: dto.employee_party_id,
            pin_hash: NEVER_VERIFIES.to_string(),
            failed_attempts: 0,
            locked_until: None,
            last_attempt_at: None,
            last_attempt_ip: None,
            metadata: AuditMetadata::default(),
        }
    }
}

impl From<&PosManagerPin> for PosManagerPinResponseDto {
    fn from(entity: &PosManagerPin) -> Self {
        Self {
            id: entity.id,
            employee_party_id: entity.employee_party_id,
            failed_attempts: entity.failed_attempts,
            locked_until: entity.locked_until,
            last_attempt_at: entity.last_attempt_at,
            metadata: entity.metadata.clone(),
        }
    }
}

impl backbone_core::FromCreateDto<CreatePosManagerPinDto> for PosManagerPin {
    fn from_create_dto(dto: CreatePosManagerPinDto) -> backbone_core::ServiceResult<Self> {
        Ok(PosManagerPin::from(dto))
    }
}

impl backbone_core::ApplyUpdateDto<UpdatePosManagerPinDto> for PosManagerPin {
    fn apply_update(mut self, dto: UpdatePosManagerPinDto) -> backbone_core::ServiceResult<Self> {
        // Only the identity dimension moves; the credential + lockout state are verify-path-owned.
        self.employee_party_id = dto.employee_party_id;
        Ok(self)
    }
}

// =============================================================================
// Custom DTOs
// =============================================================================

// <<< CUSTOM DTOs
// Add custom DTOs specific to PosManagerPin here.
// This section will be preserved during regeneration.
// >>> END CUSTOM DTOs