Subscriptions Module
Overview
The subscriptions module manages recurring billing subscriptions for customers. Each subscription tracks billing cycles, line items, payment details, and is scoped to a business and location for multi-tenancy.
Subscriptions are also linked to the addon system — an association_addon record can reference a subscriptionId to grant addon access via a subscription.
Architecture
subscriptions/
├── subscriptions.module.ts # NestJS module definition
├── application/
│ └── subscriptions.service.ts # Use cases (business logic orchestration)
├── domain/
│ └── subscriptions-repository.domain.ts # Repository port (interface) + domain constants
├── infrastructure/
│ └── subscriptions.repository.ts # Kysely repository adapter
└── interfaces/
├── subscriptions.controller.ts # REST controller
├── dtos/
│ ├── create-subscription.dto.ts # Create request validation
│ └── update-subscription.dto.ts # Partial update validation
└── query/
└── paginate-subscriptions.query.ts # List query with pagination, sort, filter
Dependency Flow
Controller → Service → ISubscriptionsRepository (port)
↑
SubscriptionsRepository (adapter)
The service depends on the ISubscriptionsRepository interface (injected via SUBSCRIPTIONS_REPOSITORY token), not the concrete class — enabling testability and respecting hexagonal boundaries.
Database Schema
Table: subscription
| Column | Type | Description |
|---|---|---|
| id | UUID (PK) | Auto-generated |
| businessId | UUID (FK) | Business scope |
| createdBy | UUID (FK) | User who created |
| updatedBy | UUID (FK, nullable) | User who last updated |
| isActive | boolean | Subscription status |
| documentNumber | string (nullable) | Auto-generated document number |
| saleDate | timestamp | Date of sale |
| customerId | UUID (FK, nullable) | Associated customer |
| locationId | UUID (FK, nullable) | Associated location |
| locationName | string (nullable) | Denormalized location name |
| taxId, taxName, taxAddress | string (nullable) | Customer tax info snapshot |
| currencyId | UUID (FK) | Payment currency |
| currencyCode, minorUnit, currency | Currency metadata | |
| exchangeRate | decimal | FX rate to base currency |
| totalAmount | numeric | Total in payment currency |
| totalBaseAmount | numeric (nullable) | Total in base currency |
| startDate | timestamp | Subscription start |
| endDate | timestamp | Subscription end |
| billingCycle | string | Frequency (e.g., "monthly") |
| subscriptionDetail | jsonb | Line items array |
| paymentDetail | jsonb | Payment method breakdown |
API Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /subscriptions | Create a subscription |
| GET | /subscriptions | List subscriptions (paginated, filterable) |
| GET | /subscriptions/:id | Get subscription by ID |
| PATCH | /subscriptions/:id | Partially update a subscription |
| DELETE | /subscriptions/:id?businessId= | Delete a subscription |
Query Parameters (GET /subscriptions)
| Param | Type | Required | Description |
|---|---|---|---|
| businessId | UUID | No | Filter by business |
| locationId | UUID | No | Filter by location |
| customerId | UUID | No | Filter by customer |
| search | string | No | Text search across taxId, taxName, taxAddress, locationName, billingCycle |
| size | number | No | Page size (default: 10) |
| page | number | No | Page number (default: 1) |
| orderBy | string | No | Sort field (taxId, taxName, taxAddress, locationName, billingCycle) |
| order | string | No | Sort direction (asc/desc) |
Example: Create Subscription
POST /subscriptions
{
"businessId": "uuid",
"isActive": true,
"createdBy": "uuid",
"saleDate": "2026-01-15",
"customerId": "uuid",
"taxId": "17195594",
"taxName": "LUIS RANGEL",
"taxAddress": "CIUDAD",
"locationId": "uuid",
"locationName": "Main Store",
"totalAmount": 112.00,
"exchangeRate": 7.90,
"totalBaseAmount": 100.00,
"currencyId": "uuid",
"startDate": "2026-01-01",
"endDate": "2026-01-31",
"billingCycle": "monthly",
"subscriptionDetail": {
"items": [
{
"productId": "uuid",
"productName": "Service Plan",
"quantity": 1,
"price": 112.00,
"total": 112.00
}
]
},
"paymentDetail": {}
}
Example: Update Subscription
PATCH /subscriptions/:id
{
"isActive": false,
"updatedBy": "uuid"
}
Example: Delete Subscription
DELETE /subscriptions/:id?businessId=uuid
Design Decisions
-
Multi-tenancy via filter — The
findAllquery accepts an optionalfilterobject built by the controller.businessId,locationId, andcustomerIdare applied as WHERE conditions when present. -
JSON detail columns —
subscriptionDetailandpaymentDetailuse JSONB for flexibility. Line items are stored as a nesteditemsarray rather than separate tables, matching the pattern used by sales and orders. -
Injection token for repository — The service depends on
ISubscriptionsRepositoryvia theSUBSCRIPTIONS_REPOSITORYinjection token, not the concrete class. This enables easy mocking in tests. -
Denormalized fields —
locationName,taxId,taxName,taxAddress,currencyCode, andcurrencyare snapshots captured at creation time to preserve historical accuracy.