Policies & Features
Một Policy là template mà mỗi License được phát hành từ đó. Nó định nghĩa loại policy, duration, grace period tuỳ chọn, giới hạn activation tuỳ chọn, và túi feature flag (bản ghi PolicyFeature) mà license dẫn xuất từ nó sẽ cấp.
Loại Policy
class PolicyTypes {
static readonly TRIAL = '000_TRIAL';
static readonly SUBSCRIPTION = '100_SUBSCRIPTION';
static readonly PERPETUAL = '200_PERPETUAL';
}| Loại | Code | Có hết hạn | Renew được | Dùng điển hình |
|---|---|---|---|---|
| Trial | 000_TRIAL | Có | Không (phát hành mới) | Kỳ đánh giá miễn phí |
| Subscription | 100_SUBSCRIPTION | Có | Có (mở rộng expiresAt) | License có giới hạn thời gian trả phí |
| Perpetual | 200_PERPETUAL | Không (duration là null) | Không | Cấp một lần, không hết hạn |
Giá trị
PolicyTypesthuần là một nhãn —LicenseManagementService.renew()từ chối bất kỳ license nào có policy khôngduration(tức vĩnh viễn) bất kể code loại.
Cấu hình Policy
Duration
Điều khiển một license đã phát hành ở hợp lệ bao lâu.
interface IDuration {
unit: TDurationUnit; // 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'
value: number;
}DurationMultipliers.toMilliseconds(duration) chuyển một IDuration thành số millisecond, dùng bởi issue() và renew() để tính expiresAt.
| Unit | Constant | Milliseconds |
|---|---|---|
millisecond | MILLISECOND | 1 |
second | SECOND | 1,000 |
minute | MINUTE | 60,000 |
hour | HOUR | 3,600,000 |
day | DAY | 86,400,000 |
week | WEEK | 604,800,000 |
month | MONTH | 30 ngày = 2,592,000,000 |
year | YEAR | 365 ngày = 31,536,000,000 |
Không quan tâm lịch
month hard-code thành 30 ngày và year thành 365 ngày. Không có số học theo lịch, không xử lý năm nhuận, và không xử lý DST. Nếu finance hay billing sau này cần độ chính xác lịch, nó phải được thêm riêng.
Một policy vĩnh viễn lưu duration: null. issue() khi đó để cả expiresAt và graceExpiresAt là null.
Grace Period
Mở rộng tuỳ chọn sau expiresAt. Khi now < graceExpiresAt, validation vẫn trả valid: true nhưng với code: GRACE_PERIOD thay vì VALID.
{
"duration": { "unit": "year", "value": 1 },
"gracePeriod": { "unit": "day", "value": 7 }
}Một license phát hành hôm nay với policy trên:
| Pha | Duration | Kết quả validation |
|---|---|---|
| Active | ngày 0–365 | valid: true, code: VALID |
| Grace | ngày 365–372 | valid: true, code: GRACE_PERIOD |
| Expired | ngày 372+ | valid: false, code: LICENSE_EXPIRED (status auto-lật sang expired ở validation tiếp theo) |
gracePeriod bản thân là một IDuration. Nếu bỏ qua, license chỉ đơn giản chuyển từ active thẳng sang expired.
Config Activation
Giới hạn số fingerprint thiết bị duy nhất có thể kích hoạt với một license.
interface IActivationConfig {
limit: number;
}| Trường | Hành vi |
|---|---|
limit | Số hàng (licenseId, fingerprint) duy nhất tối đa trong bảng Activation |
Không có mode "fixed vs floating"
Không có trường mode trên IActivationConfig. Không có activation floating, không cơ chế heartbeat, và không tự dọn slot. Một slot activation được giữ tới khi ai đó xoá rõ ràng hàng activation (DELETE /activations/{id}) hoặc license cha bị xoá (cascade).
Nếu policy.activation là null (hoặc vắng), activation không giới hạn.
Override theo từng license
License riêng lẻ có thể ghi đè config activation và giá trị feature của policy qua cột JSONB override trên License:
{
"override": {
"activation": { "limit": 10 },
"features": {
"max_products": 1000,
"custom_branding": true
}
}
}LicensingBaseService.resolveActivation()trảlicense.override.activation ?? policy.activation ?? null.LicensingBaseService.resolveFeatures()mergepolicy features → override features, với giá trị override ưu tiên.
Update override không re-publish
Trường override được đặt qua PATCH /licenses/{id}, là một update CRUD thuần — nó không re-publish chứng chỉ cache. Để ép override mới vào Redis (và do đó vào consumer LicenseMiddleware), hiện bạn cần trigger một thao tác vòng đời đổi trạng thái như suspend → reinstate hoặc renew. Endpoint POST /validation/validate luôn đọc tươi từ database, nên nó phản ánh thay đổi ngay.
Feature Flags
Hàng PolicyFeature là flag key/value có kiểu gắn vào một policy.
Statuses
class PolicyFeatureStatuses {
static readonly ACTIVATED = Statuses.ACTIVATED; // 'activated'
static readonly DEACTIVATED = Statuses.DEACTIVATED; // 'deactivated'
}Khi một feature deactivated, LicensingBaseService.resolveFeatureValue() trả giá trị falsy DATA_TYPE_DEFAULTS cho kiểu của nó thay vì giá trị cấu hình:
dataType | Nguồn giá trị active | Giá trị deactivated |
|---|---|---|
BOOLEAN | boValue ?? true | false |
NUMBER | nValue ?? 0 | 0 |
TEXT | tValue ?? '' | '' |
JSON | jValue ?? null | null |
Cái này cho bạn giữ một hàng feature tại chỗ nhưng tắt nó mà không xoá.
Data Types
dataType | Cột | TypeScript | Ví dụ |
|---|---|---|---|
BOOLEAN | boValue (boolean) | boolean | true |
NUMBER | nValue (numeric) | number | 100 |
TEXT | tValue (text) | string | "professional" |
JSON | jValue (jsonb) | Record<string, any> | { "modules": ["pos","crm"] } |
Giá trị dataType đến từ hằng DataTypes dùng chung trong @venizia/ignis-helpers.
Thêm feature
Không có method PolicyService.addFeature() riêng — feature quản lý qua endpoint CRUD chuẩn PolicyFeatureController:
POST /v1/api/licensing/policy-features{
"policyId": "<policy-id>",
"code": "max_products",
"name": { "en": "Maximum Products", "vi": "Sản phẩm tối đa" },
"description": { "en": "Max products allowed", "vi": "Số sản phẩm tối đa" },
"dataType": "NUMBER",
"nValue": 500,
"status": "activated",
"sequence": 10
}Cặp (policyId, code) là duy nhất. Xoá một policy cascade và xoá mọi feature của nó.
Giải Feature
LicensingBaseService.resolveFeatures({ policyId, featuresOverride }):
policyFeatureRepository.find({ where: { policyId } })— nạp tất cả feature (cảactivatedvàdeactivated)- Cho mỗi feature, gọi
resolveFeatureValue():- Nếu
status !== activated, trả giá trịDATA_TYPE_DEFAULTScho kiểu của nó - Ngược lại, trả cột
<x>Valuekhớp (với fallback theo kiểu ở trên)
- Nếu
- Dựng một
Record<string, unknown>khoá theofeature.code - Nếu cung cấp
featuresOverride(từlicense.override.features), spread nó lên trên để override thắng
Bản ghi kết quả là cái hiện trong response validation dưới features, và bên trong trường features của payload chứng chỉ.
/policies/catalogs
Endpoint catalog công khai expose bởi PolicyController. Nó nạp mọi Policy có status là ACTIVATED, sắp theo sequence ASC, với feature ACTIVATED của mỗi policy inline (cũng sắp theo sequence ASC):
GET /v1/api/licensing/policies/catalogs{
"data": [
{
"id": "...",
"name": "BANA Professional — Yearly",
"type": "100_SUBSCRIPTION",
"duration": { "unit": "year", "value": 1 },
"gracePeriod": { "unit": "day", "value": 14 },
"activation": { "limit": 5 },
"features": [
{ "code": "max_products", "dataType": "NUMBER", "nValue": 500 },
{ "code": "custom_branding","dataType": "BOOLEAN", "boValue": true }
]
}
]
}Route vẫn có cổng bởi permission Policy.find — controller không bypass authentication.
Data Model
Tham chiếu cột đầy đủ, index, và ràng buộc cho
PolicyvàPolicyFeaturenằm trong Domain Model. Điểm chính cho trang này:Policysoft-delete được;PolicyFeaturekhông (cột deleted bị tắt) và cascade-delete vớiPolicycha;(policyId, code)là duy nhất; giá trị feature nằm trong cột có kiểu khớpdataType(boValue/nValue/tValue/jValue).
Trang liên quan
| Trang | Mô tả |
|---|---|
| Domain Model | Schema đầy đủ Policy + PolicyFeature |
| License Lifecycle | Cách license di chuyển qua các trạng thái |
| Validation & Activation | Cách feature và activation được giải lúc validation |
| Configuration | Policy FREE_TRIAL được seed |