Skip to main content

Safe Module

Overview

The Safe module manages cash safes — physical cash storage units within business locations. Safes are referenced by the cash movement system to track money flow between registers, drawers, and safes.

Architecture

The module follows hexagonal (ports & adapters) architecture:

safe/
├── safe.module.ts # NestJS module definition
├── domain/
│ └── safe-repository.domain.ts # ISafeRepository port + SAFE_REPOSITORY token
├── application/
│ └── safe.service.ts # Business logic / use cases
├── infrastructure/
│ └── safe.repository.ts # Kysely DB adapter (implements ISafeRepository)
└── interfaces/
├── safe.controller.ts # REST controller
└── dtos/
├── create-safe.dto.ts
├── update-safe.dto.ts
└── safe-query.dto.ts

Dependency flow: Controller → Service → ISafeRepository (port) ← SafeRepository (adapter)

Domain Concepts

ConceptDescription
SafeA named cash storage unit within a business location
ActivationSafes can be active/inactive without deletion
Cash Movement LinkSafes are referenced by cash_movement.safeId; deletion is blocked if movements exist

Business Rules

  • Safe names must be unique per location (enforced by DB constraint + application validation)
  • Safes with linked cash movements cannot be deleted
  • Every safe belongs to exactly one business + location

Database Schema

Table: safe

ColumnTypeNullableDescription
idUUIDPKAuto-generated
businessIdUUIDNOT NULLFK → business.id
locationIdUUIDNOT NULLFK → location.id
nameVARCHARNOT NULLSafe display name
isActiveBOOLEANNOT NULLDefault: true
createdAtTIMESTAMPTZNOT NULLAuto-set
createdByUUIDNOT NULLFK → user.id
updatedAtTIMESTAMPTZnullableSet on update
updatedByUUIDnullableFK → user.id

Indexes: (businessId, locationId), (locationId, isActive) Unique constraint: (locationId, name)

API Endpoints

All endpoints require Firebase Bearer token authentication.

MethodPathDescription
POST/safesCreate a new safe
GET/safesList safes (optional filters, optional pagination)
GET/safes/:idGet a safe by ID
PATCH/safes/:idUpdate a safe
DELETE/safes/:idDelete a safe (blocked if cash movements exist)
POST/safes/:id/activateSet isActive = true
POST/safes/:id/deactivateSet isActive = false

List endpoint behavior

  • Without page/size: returns a flat array of all matching safes
  • With page/size: returns { total, results } (requires businessId)

Query parameters (GET /safes)

ParamTypeRequiredDescription
businessIdUUIDFor paginationFilter by business
locationIdUUIDNoFilter by location
isActivebooleanNoFilter by active status
pageintegerNoPage number (1-based)
sizeintegerNoPage size

Cross-Module Usage

  • CashRegisterModule imports SafeModule and uses SafeService to validate safe existence and active status before creating cash movements.
  • SafeService.getSafeById(id) returns undefined when not found (for external callers that handle absence themselves).

Design Decisions

  1. Hard delete — Safes are permanently deleted (not soft-deleted) since the isActive flag serves the deactivation use case.
  2. Cash movement check via direct query — The repository queries the cashMovement table directly (hasCashMovements) rather than depending on the cash-register module, avoiding a circular dependency.
  3. Firebase user override — When a Firebase user is present in the request, their db_user_id overrides the DTO's createdBy/updatedBy fields.