Skip to main content

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

ColumnTypeDescription
idUUID (PK)Auto-generated
businessIdUUID (FK)Business scope
createdByUUID (FK)User who created
updatedByUUID (FK, nullable)User who last updated
isActivebooleanSubscription status
documentNumberstring (nullable)Auto-generated document number
saleDatetimestampDate of sale
customerIdUUID (FK, nullable)Associated customer
locationIdUUID (FK, nullable)Associated location
locationNamestring (nullable)Denormalized location name
taxId, taxName, taxAddressstring (nullable)Customer tax info snapshot
currencyIdUUID (FK)Payment currency
currencyCode, minorUnit, currencyCurrency metadata
exchangeRatedecimalFX rate to base currency
totalAmountnumericTotal in payment currency
totalBaseAmountnumeric (nullable)Total in base currency
startDatetimestampSubscription start
endDatetimestampSubscription end
billingCyclestringFrequency (e.g., "monthly")
subscriptionDetailjsonbLine items array
paymentDetailjsonbPayment method breakdown

API Endpoints

MethodPathDescription
POST/subscriptionsCreate a subscription
GET/subscriptionsList subscriptions (paginated, filterable)
GET/subscriptions/:idGet subscription by ID
PATCH/subscriptions/:idPartially update a subscription
DELETE/subscriptions/:id?businessId=Delete a subscription

Query Parameters (GET /subscriptions)

ParamTypeRequiredDescription
businessIdUUIDNoFilter by business
locationIdUUIDNoFilter by location
customerIdUUIDNoFilter by customer
searchstringNoText search across taxId, taxName, taxAddress, locationName, billingCycle
sizenumberNoPage size (default: 10)
pagenumberNoPage number (default: 1)
orderBystringNoSort field (taxId, taxName, taxAddress, locationName, billingCycle)
orderstringNoSort 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

  1. Multi-tenancy via filter — The findAll query accepts an optional filter object built by the controller. businessId, locationId, and customerId are applied as WHERE conditions when present.

  2. JSON detail columnssubscriptionDetail and paymentDetail use JSONB for flexibility. Line items are stored as a nested items array rather than separate tables, matching the pattern used by sales and orders.

  3. Injection token for repository — The service depends on ISubscriptionsRepository via the SUBSCRIPTIONS_REPOSITORY injection token, not the concrete class. This enables easy mocking in tests.

  4. Denormalized fieldslocationName, taxId, taxName, taxAddress, currencyCode, and currency are snapshots captured at creation time to preserve historical accuracy.