Saltar al contenido principal

Audit Log Module

Overview

The audit-log module provides a write-once, append-only audit trail for entity mutations across the FlowPOS system. It records who changed what, when, and captures the before/after state of any tracked entity.


Architecture

The module follows hexagonal architecture with strict layer separation:

audit-log/
├── audit-log.module.ts
├── domain/
│ ├── audit-log.domain.ts # CreateAuditLogInput type
│ └── audit-log-repository.domain.ts # IAuditLogRepository port + sortable keys
├── application/
│ └── audit-log.service.ts # Use cases: log, getByEntity, getAll
├── infrastructure/
│ └── audit-log.repository.ts # Kysely adapter (implements IAuditLogRepository)
└── interfaces/
├── audit-log.controller.ts # HTTP layer (GET endpoints)
└── query/
└── paginate-audit-log.query.ts # Query params DTO

Layer Responsibilities

LayerResponsibility
domain/Defines CreateAuditLogInput (pure TS) and IAuditLogRepository contract. No framework imports.
application/Orchestrates use cases: create entry, query by entity, paginate all entries.
infrastructure/Kysely implementation of IAuditLogRepository. All SQL lives here.
interfaces/Thin HTTP controller. Maps query params to service calls, handles errors uniformly.

Database Schema

Table: audit_log

ColumnTypeDescription
idUUIDPrimary key (auto-generated)
entitytextEntity type, e.g. accounts_payable_bill
entity_idUUIDID of the audited record
actiontextAction performed: create, update, delete, auto_paid, etc.
oldjsonbSnapshot of the entity before the action (null for creates)
newjsonbSnapshot of the entity after the action (null for deletes)
user_idUUIDFK to user.id (nullable, ON DELETE SET NULL)
created_attimestamptzTimestamp of the event

Indexes:

  • idx_audit_log_entity_entity_id — composite (entity, entity_id) for fast entity history queries
  • idx_audit_log_created_at — for time-range queries and default sort

Domain Concepts

CreateAuditLogInput

Input shape consumed by AuditLogService.log():

interface CreateAuditLogInput {
entity: string; // Entity type string
entityId: string; // UUID of the entity instance
action: string; // Action label: create | update | delete | ...
oldValue?: JsonValue; // Before-state snapshot (null for creates)
newValue?: JsonValue; // After-state snapshot (null for deletes)
userId?: string; // Actor UUID (nullable for system actions)
}

Immutability

Audit log records are never updated or deleted. The repository only exposes create, findByEntity, and findAll. This is intentional — audit data must be tamper-proof.


Use Cases

1. Log an entity mutation

Used by other modules to record a change. Typically called inside a Kysely transaction:

// Inside a transactional service method
await this.auditLogService.log(
{
entity: "accounts_payable_bill",
entityId: bill.id,
action: "update",
oldValue: previousSnapshot,
newValue: updatedSnapshot,
userId: actorId,
},
trx, // Pass the Kysely transaction for atomicity
);

2. Query entity history

Returns all audit entries for a specific record, newest first:

const history = await this.auditLogService.getByEntity(
"accounts_payable_bill",
billId,
);

3. Paginated global query

Returns a filtered, sorted, paginated list:

const page = await this.auditLogService.getAll({
page: 1,
size: 20,
orderBy: "createdAt",
order: SortOrder.Desc,
filter: { "auditLog.entity": "accounts_payable_bill" },
});

API Endpoints

Base path: /audit-log

GET /audit-log

Returns a paginated list of audit entries.

Query parameters:

ParameterTypeRequiredDescription
entitystringNoFilter by entity type
entityIdUUIDNoFilter by entity instance UUID
userIdUUIDNoFilter by actor UUID
searchstringNoSearch entity type and action fields
pagenumberNoPage number (default: 1)
sizenumberNoPage size (default: 10; 0 = no limit)
orderByenumNocreatedAt | entity | action
orderenumNoasc | desc (default: asc)

Example request:

GET /audit-log?entity=accounts_payable_bill&page=1&size=20&orderBy=createdAt&order=desc
Authorization: Bearer <token>

Response: IOffsetPagination<SelectableAuditLog>


GET /audit-log/:entity/:entityId

Returns all audit entries for a specific entity instance.

Path parameters:

ParameterDescription
entityEntity type string, e.g. accounts_payable_bill
entityIdUUID of the entity instance

Example request:

GET /audit-log/accounts_payable_bill/3fa85f64-5717-4562-b3fc-2c963f66afa6
Authorization: Bearer <token>

Response: SelectableAuditLog[] ordered by createdAt desc


Transaction Safety

The log() method accepts an optional trx (Kysely transaction) parameter. Always pass the active transaction when the audit log is part of a mutation:

await this.database.transaction().execute(async (trx) => {
const updated = await this.billRepository.update({ id, entity, trx });
await this.auditLogService.log({ ...auditPayload, userId }, trx);
});

This guarantees that if the entity update fails, the audit record is also rolled back — preventing orphaned audit entries.


Currently Audited Entities

ModuleEntity KeyActions
Accounts Payable Billsaccounts_payable_billcreate, update, auto_paid
Accounts Payable Paymentsaccounts_payable_paymentcreate, update
Accounts Receivable Receiptsaccounts_receivable_receiptcreate, update

To add auditing to a new module, import AuditLogModule and inject AuditLogService.


Bruno API Collection

Located at: api-client/flowpos/collections/audit-log/

FileDescription
audit logs.ymlPaginated list with all optional filters
audit log by entity.ymlEntity-specific history (uses {{auditEntityType}} and {{auditEntityId}} vars)

Set auditEntityType and auditEntityId in your Bruno environment variables before running the entity history request.