Skip to content

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

ts
class PolicyTypes {
  static readonly TRIAL = '000_TRIAL';
  static readonly SUBSCRIPTION = '100_SUBSCRIPTION';
  static readonly PERPETUAL = '200_PERPETUAL';
}
LoạiCodeCó hết hạnRenew đượcDùng điển hình
Trial000_TRIALKhông (phát hành mới)Kỳ đánh giá miễn phí
Subscription100_SUBSCRIPTIONCó (mở rộng expiresAt)License có giới hạn thời gian trả phí
Perpetual200_PERPETUALKhông (durationnull)KhôngCấp một lần, không hết hạn

Giá trị PolicyTypes thuần là một nhãn — LicenseManagementService.renew() từ chối bất kỳ license nào có policy không duration (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.

ts
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()renew() để tính expiresAt.

UnitConstantMilliseconds
millisecondMILLISECOND1
secondSECOND1,000
minuteMINUTE60,000
hourHOUR3,600,000
dayDAY86,400,000
weekWEEK604,800,000
monthMONTH30 ngày = 2,592,000,000
yearYEAR365 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ả expiresAtgraceExpiresAtnull.

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.

json
{
  "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:

PhaDurationKết quả validation
Activengày 0–365valid: true, code: VALID
Gracengày 365–372valid: true, code: GRACE_PERIOD
Expiredngà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.

ts
interface IActivationConfig {
  limit: number;
}
TrườngHành vi
limitSố 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.activationnull (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:

json
{
  "override": {
    "activation": { "limit": 10 },
    "features": {
      "max_products": 1000,
      "custom_branding": true
    }
  }
}
  • LicensingBaseService.resolveActivation() trả license.override.activation ?? policy.activation ?? null.
  • LicensingBaseService.resolveFeatures() merge policy 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

ts
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:

dataTypeNguồn giá trị activeGiá trị deactivated
BOOLEANboValue ?? truefalse
NUMBERnValue ?? 00
TEXTtValue ?? ''''
JSONjValue ?? nullnull

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

dataTypeCộtTypeScriptVí dụ
BOOLEANboValue (boolean)booleantrue
NUMBERnValue (numeric)number100
TEXTtValue (text)string"professional"
JSONjValue (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:

http
POST /v1/api/licensing/policy-features
json
{
  "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 }):

  1. policyFeatureRepository.find({ where: { policyId } }) — nạp tất cả feature (cả activateddeactivated)
  2. Cho mỗi feature, gọi resolveFeatureValue():
    • Nếu status !== activated, trả giá trị DATA_TYPE_DEFAULTS cho kiểu của nó
    • Ngược lại, trả cột <x>Value khớp (với fallback theo kiểu ở trên)
  3. Dựng một Record<string, unknown> khoá theo feature.code
  4. 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ó statusACTIVATED, sắp theo sequence ASC, với feature ACTIVATED của mỗi policy inline (cũng sắp theo sequence ASC):

http
GET /v1/api/licensing/policies/catalogs
json
{
  "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 PolicyPolicyFeature nằm trong Domain Model. Điểm chính cho trang này: Policy soft-delete được; PolicyFeature không (cột deleted bị tắt) và cascade-delete với Policy cha; (policyId, code) là duy nhất; giá trị feature nằm trong cột có kiểu khớp dataType (boValue/nValue/tValue/jValue).

Trang liên quan

TrangMô tả
Domain ModelSchema đầy đủ Policy + PolicyFeature
License LifecycleCách license di chuyển qua các trạng thái
Validation & ActivationCách feature và activation được giải lúc validation
ConfigurationPolicy FREE_TRIAL được seed

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.