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
| Concept | Description |
|---|---|
| Safe | A named cash storage unit within a business location |
| Activation | Safes can be active/inactive without deletion |
| Cash Movement Link | Safes 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
| Column | Type | Nullable | Description |
|---|---|---|---|
| id | UUID | PK | Auto-generated |
| businessId | UUID | NOT NULL | FK → business.id |
| locationId | UUID | NOT NULL | FK → location.id |
| name | VARCHAR | NOT NULL | Safe display name |
| isActive | BOOLEAN | NOT NULL | Default: true |
| createdAt | TIMESTAMPTZ | NOT NULL | Auto-set |
| createdBy | UUID | NOT NULL | FK → user.id |
| updatedAt | TIMESTAMPTZ | nullable | Set on update |
| updatedBy | UUID | nullable | FK → user.id |
Indexes: (businessId, locationId), (locationId, isActive)
Unique constraint: (locationId, name)
API Endpoints
All endpoints require Firebase Bearer token authentication.
| Method | Path | Description |
|---|---|---|
| POST | /safes | Create a new safe |
| GET | /safes | List safes (optional filters, optional pagination) |
| GET | /safes/:id | Get a safe by ID |
| PATCH | /safes/:id | Update a safe |
| DELETE | /safes/:id | Delete a safe (blocked if cash movements exist) |
| POST | /safes/:id/activate | Set isActive = true |
| POST | /safes/:id/deactivate | Set isActive = false |
List endpoint behavior
- Without
page/size: returns a flat array of all matching safes - With
page/size: returns{ total, results }(requiresbusinessId)
Query parameters (GET /safes)
| Param | Type | Required | Description |
|---|---|---|---|
| businessId | UUID | For pagination | Filter by business |
| locationId | UUID | No | Filter by location |
| isActive | boolean | No | Filter by active status |
| page | integer | No | Page number (1-based) |
| size | integer | No | Page size |
Cross-Module Usage
- CashRegisterModule imports
SafeModuleand usesSafeServiceto validate safe existence and active status before creating cash movements. SafeService.getSafeById(id)returnsundefinedwhen not found (for external callers that handle absence themselves).
Design Decisions
- Hard delete — Safes are permanently deleted (not soft-deleted) since the
isActiveflag serves the deactivation use case. - Cash movement check via direct query — The repository queries the
cashMovementtable directly (hasCashMovements) rather than depending on the cash-register module, avoiding a circular dependency. - Firebase user override — When a Firebase user is present in the request, their
db_user_idoverrides the DTO'screatedBy/updatedByfields.