Saltar al contenido principal

Invites Module

Overview

The invites module manages user invitations to businesses within the FlowPOS multi-tenant platform. A business admin creates an invite for a specific email address and role. The invitee receives a notification email and can then accept or reject the invite via the API.

Module path: apps/backend/src/invites/


Architecture

The module follows Hexagonal Architecture with strict layer boundaries.

invites/
├── invites.module.ts # NestJS module wiring
├── domain/
│ └── invites-repository.domain.ts # IInvitesRepository port + INVITES_REPOSITORY token
├── application/
│ ├── invites.service.ts # Use cases (CRUD + accept/reject/resend)
│ └── events/
│ ├── on-create-invite.event.ts # invite.create event payload
│ └── on-resend-invite.event.ts # invite.resend event payload
├── infrastructure/
│ └── invites.repository.ts # Kysely DB adapter (implements port)
└── interfaces/
├── invites.controller.ts # HTTP adapter
├── dtos/
│ ├── create-invite.dto.ts
│ └── update-invite.dto.ts
└── query/
└── paginate-invites.query.ts

Layer responsibilities

LayerResponsibility
domain/Repository port (IInvitesRepository) and INVITES_REPOSITORY injection token.
application/InvitesService — orchestrates CRUD, accept/reject/resend use cases, and event emission.
infrastructure/InvitesRepository — Kysely queries against the invite table. Implements IInvitesRepository.
interfaces/InvitesController — maps HTTP requests to service calls. Thin layer with no business logic.

Dependency rule

interfaces → application → domain ← infrastructure

The service also depends on BusinessUsersService for the acceptInvite use case.


Domain Concepts

Invite

A pending invitation linking an email address to a business and a role.

FieldTypeDescription
idUUIDPrimary key (auto-generated)
emailstringInvitee email address (required)
namestringInvitee display name (optional)
phonestringInvitee phone number (optional)
invitedByNamestringName of the person who sent the invite (optional)
uniqueRoleNamestringRole to assign on acceptance
businessIdUUIDFK to business
statusInviteStatuspending / accepted / rejected / cancelled
isActivebooleanDefaults to true
resendCountintegerHow many times the invite has been resent
lastResentAttimestamptzTimestamp of the most recent resend
createdByUUIDFK to user
updatedByUUIDFK to user (nullable)
createdAttimestamptzAuto-set on creation
updatedAttimestamptzSet on update

Status transitions

pending ──accept──→ accepted
pending ──reject──→ rejected
pending ──cancel──→ cancelled

Only pending invites should be acted upon. The API does not enforce this guard today — callers are responsible for checking status before accepting or rejecting.


Use Cases

1. Create invite

Creates an invite record and emits invite.create to trigger the invitation email.

2. Accept invite

Authenticated user accepts the invite:

  1. Marks invite status as accepted.
  2. Creates a business_user record linking the user to the business with the invited role.
  3. Firebase custom claims are synced by BusinessUsersService.

3. Reject invite

Marks invite status as rejected. No side effects.

4. Resend invite

Increments resendCount, sets lastResentAt, and emits invite.resend to re-trigger the email.

5. List / search invites

Paginated list of active invites, filterable by businessId and searchable by email.

6. Get invites by email

Returns all active invites for an email address, joined with businessName for display.


API Endpoints

All endpoints require Bearer token authentication and are scoped to PolicyResource.Invite for RBAC.

POST /invites

Create a new invite.

Body:

{
"email": "jane@example.com",
"name": "Jane Doe",
"invitedByName": "John Smith",
"uniqueRoleName": "manager",
"businessId": "<uuid>",
"status": "pending",
"isActive": true,
"createdBy": "<uuid>"
}

Responses: 201 Created | 401 Unauthorized | 422 Validation error

GET /invites

List invites with pagination, search, and sorting.

Query parameters:

ParamRequiredDescription
businessIdNoUUID of the business (filter)
pageNoPage number (default: 1)
sizeNoPage size (default: 10)
searchNoCase-insensitive email search
orderByNoSort field (email)
orderNoasc or desc

Responses: 200 Paginated list | 401 Unauthorized

GET /invites/by-email

Get all active invites for an email address, enriched with businessName.

Query parameters: email (required)

Responses: 200 List of invites | 400 Missing email | 401 Unauthorized

GET /invites/:id

Get a single invite by UUID.

Responses: 200 Found | 404 Not found | 401 Unauthorized

PATCH /invites/:id

Partially update an invite.

Body:

{
"isActive": false,
"updatedBy": "<uuid>"
}

Responses: 200 Updated | 404 Not found | 401 Unauthorized | 422 Validation error

DELETE /invites/:id

Hard-delete an invite record.

Responses: 200 Deleted | 404 Not found | 401 Unauthorized

POST /invites/:id/accept

Accept an invite. The authenticated user's identity is resolved from the Firebase token — no body required.

Responses: 201 Accepted | 404 Not found | 401 Unauthorized

PATCH /invites/:id/reject

Reject an invite. No body required.

Responses: 200 Rejected | 404 Not found | 401 Unauthorized

POST /invites/:id/resend

Resend the invitation email. Increments resend counter. Requires Firebase token.

Responses: 201 Resent (returns updated invite) | 404 Not found | 401 Unauthorized


Event System

The module emits two domain events via EventEmitter2:

Event nameClassEmitted on
invite.createOnCreateInviteEventSuccessful invite creation
invite.resendOnResendInviteEventSuccessful resend

Listeners (email handlers) are defined in the communications module.


Design Decisions

Dependency inversion via Symbol token

The repository is injected through INVITES_REPOSITORY Symbol token, matching the project convention (brands, colors, sizes). The service depends on IInvitesRepository (port), not the concrete Kysely implementation.

RBAC enforcement

The controller uses @UseGuards(RolesGuard) with @PermissionResource(PolicyResource.Invite) and per-endpoint @PermissionAction(...) decorators to enforce role-based access control.

Accept is an application-layer use case

The acceptInvite flow updates the invite status and creates a business_user in a single service call. This preserves SRP and makes the use case testable independently of HTTP.

findByFilter joins business name

The GET /invites/by-email endpoint returns invites enriched with businessName via an INNER JOIN on the business table. The general paginated list does not include this join to keep the query lightweight.

Event-driven communication

The invites module does not know about email sending. The communications module listens to events and sends notifications asynchronously.


Bruno API Collection

Requests are located at:

api-client/flowpos/collections/invites/
FileDescription
invites.ymlGET list (paginated, filterable by businessId)
invites by email.ymlGET by email
invite by Id.ymlGET by ID
invite.ymlPOST create
invite_1.ymlPATCH update
invite_2.ymlDELETE
accept invite.ymlPOST accept
reject invite.ymlPATCH reject
Resend an invite.ymlPOST resend

  • BusinessUsersacceptInvite creates a business_user record
  • Communications — listens to invite.create and invite.resend events to send emails/SMS
  • Firebase — token verification for accept/resend endpoints
  • Users — user lookup during accept/resend flows