@antelopejs/interface-dms-saas 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,95 +1,85 @@
1
+ <div align="center">
2
+
1
3
  # @antelopejs/interface-dms-saas
2
4
 
3
- <div align="center">
4
- <a href="./LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-blue?style=for-the-badge&labelColor=000000"></a>
5
- <a href="https://discord.gg/sjK28QHrA7"><img src="https://img.shields.io/badge/Discord-18181B?logo=discord&style=for-the-badge&color=000000" alt="Discord"></a>
6
- <a href="https://antelopejs.com"><img src="https://img.shields.io/badge/Docs-18181B?style=for-the-badge&color=000000" alt="Documentation"></a>
5
+ <a href="https://www.npmjs.com/package/@antelopejs/interface-dms-saas"><img alt="npm" src="https://img.shields.io/npm/v/@antelopejs/interface-dms-saas?style=for-the-badge&labelColor=000000&color=000000"></a>
6
+ <a href="./LICENSE"><img alt="License" src="https://img.shields.io/npm/l/@antelopejs/interface-dms-saas?style=for-the-badge&labelColor=000000&color=000000"></a>
7
+ <a href="https://discord.gg/sjK28QHrA7"><img alt="Discord" src="https://img.shields.io/badge/Discord-18181B?logo=discord&style=for-the-badge&color=000000"></a>
8
+ <a href="https://dms.antelopejs.com/docs/modules/saas/extending"><img alt="Documentation" src="https://img.shields.io/badge/Docs-18181B?style=for-the-badge&color=000000"></a>
9
+
10
+ The AntelopeJS interface to extend the DMS SaaS module: billing, plans, registration, workspace pages and lifecycle.
11
+
7
12
  </div>
8
13
 
9
- Public interfaces of the AntelopeJS DMS SaaS module.
14
+ ## What it does
10
15
 
11
- A module that extends `@antelopejs/dms-saas` depends on this package rather than
12
- on the runtime, and imports the extension points from it:
16
+ - Adds invoice line items, reads customer balances and previews the upcoming invoice.
17
+ - Builds plan projections and catalogues, and normalizes prices.
18
+ - Extends registration and the provisioning of a new workspace.
19
+ - Adds workspace settings categories and blocks to the tenant billing page.
20
+ - Listens to workspace lifecycle events and operator actions, and exposes the canonical SaaS tables and models (`/db`).
13
21
 
14
- | Subpath | Surface |
15
- | --- | --- |
16
- | `@antelopejs/interface-dms-saas` | every extension point except `db`: billing, data API, pages, plans, provisioning and registration flat, plus the `invoiceLineItems` and `workspaceLifecycle` namespaces |
17
- | `@antelopejs/interface-dms-saas/billing` | customer balance, upcoming invoice preview and complimentary subscription state |
18
- | `@antelopejs/interface-dms-saas/data-api` | hidden-value data API filters |
19
- | `@antelopejs/interface-dms-saas/db` | canonical SaaS tables and data models |
20
- | `@antelopejs/interface-dms-saas/hidden-filter` | the `HiddenStringFilter` decorator on its own |
21
- | `@antelopejs/interface-dms-saas/invoice-line-items` | provider registration, line item types, line key helpers |
22
- | `@antelopejs/interface-dms-saas/page-definitions` | page and module ids, slugs and descriptors |
23
- | `@antelopejs/interface-dms-saas/pages` | workspace settings category and tenant billing page extensions |
24
- | `@antelopejs/interface-dms-saas/plans` | plan projections, catalog builder, and price normalization |
25
- | `@antelopejs/interface-dms-saas/provisioning` | provisioning hook payload |
26
- | `@antelopejs/interface-dms-saas/registration` | public registration capture limits |
27
- | `@antelopejs/interface-dms-saas/workspace-lifecycle` | workspace lifecycle events and operator actions |
28
-
29
- Two of those surfaces reach the root entry as namespaces rather than flat
30
- re-exports, because each exposes an `internal` proxy namespace and two
31
- `internal` names cannot coexist in one barrel:
32
-
33
- ```ts
22
+ A module that extends [`@antelopejs/dms-saas`](https://www.npmjs.com/package/@antelopejs/dms-saas) depends on this package, never on the runtime.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pnpm add @antelopejs/interface-dms-saas
28
+ ```
29
+
30
+ ## Quick start
31
+
32
+ The root entry holds every extension point except `db`, with `invoiceLineItems` and `workspaceLifecycle` as namespaces:
33
+
34
+ ```ts docs-check
34
35
  import { invoiceLineItems, workspaceLifecycle } from "@antelopejs/interface-dms-saas";
35
36
 
36
- invoiceLineItems.RegisterInvoiceLineItemsProvider(/* … */);
37
+ export const usageKey = invoiceLineItems.buildInvoiceLineKey("example.compute", "minutes");
38
+ export const isProvisioningCommitted = workspaceLifecycle.IsWorkspaceProvisioningCommitted;
37
39
  ```
38
40
 
39
- Their own subpaths stay the direct route to the same symbols. `./db` is the one
40
- surface the root entry leaves out: importing it registers the SaaS tables, and a
41
- consumer that only wants types should not pay for that.
42
-
43
- Subpaths resolve through the `exports` map only, so consumers must compile with
44
- `moduleResolution` set to `bundler`, `node16` or `nodenext`; the legacy `node`
45
- (node10) mode is not supported.
46
-
47
- This package is released on its own workflow and must be published before the
48
- runtime module: `@antelopejs/dms-saas` depends on it through
49
- `>=<interface version> <0.<minor + 1>.0`, a range that only resolves once this
50
- package is released. The runtime implements this interface, so it caps the range
51
- below the next minor: a breaking minor of this package never reaches an older
52
- dms-saas.
53
-
54
- ## Writing plans
55
-
56
- A module may seed plans straight into the `db` plan table. It leaves
57
- `Plan.paymentProviderRefs` empty on creation and never rewrites it: dms-saas
58
- links every plan billed through Stripe to a matching Stripe product and price,
59
- at startup and whenever the plan is offered or billed, and records what it
60
- synced in `paymentProviderRefs.stripeSyncedTerms`. See
61
- [Plans and Stripe](../dms-saas/README.md#plans-and-stripe).
62
-
63
- ## Upgrading
64
-
65
- ### Tenant billing page anchors
66
-
67
- The plan, next invoice, payment method and money-back cards of the tenant
68
- billing page now share a two-column grid (`overview`), so `planCard`,
69
- `paymentMethod` and `refundButton` are no longer top-level fields of the page.
70
- The names in `tenantBillingPage.components` are unchanged, and
71
- `RegisterTenantBillingPageExtension` maps each one to its position through
72
- `tenantBillingPageAnchorPaths`: a block registered after `planCard` still lands
73
- right under the plan card, in its column. Upgrade this package together with
74
- `@antelopejs/dms-saas`, since an earlier build of it anchors on the old
75
- top-level fields, which the DMS no longer finds (the block is then appended at
76
- the end of the page). Code that anchors with the DMS's own
77
- `@RegisterPageExtension` must pass the path, as in
78
- `.after(tenantBillingPageAnchorPaths.planCard)`.
79
-
80
- The permission ids of those four cards follow their position
81
- (`settings.workspace.billing.overview.plan.current.planCard`, and so on), so a
82
- plan or role granting them needs the new ids.
83
-
84
- ### Rows written by earlier builds
85
-
86
- The data contract no longer tolerates rows that predate the current fields:
87
- `TenantSubscription.isComplimentary` is a required boolean and
88
- `isComplimentarySubscription` only returns `true` for `isComplimentary: true`,
89
- `InvoiceModel.getAllInvoices` only matches `documentType: "invoice"`,
90
- `WebhookResult` no longer includes `failed`, `TenantSubscription.paidUsagePeriods`
91
- is a required array that alone decides billable usage, and `revision` is
92
- required on `TenantSubscription`, `Segment`, `UserSegment`, `PlanMigration` and
93
- `StripeWebhookEvent`, whose revision-fenced writes refuse a row without one.
94
- Fix such rows before upgrading, as described in the
95
- [`@antelopejs/dms-saas` upgrade notes](../dms-saas/README.md#upgrading).
41
+ Use the [entry-point reference](./docs/1.index.md#entry-points) for supported
42
+ subpaths. Importing `/db` registers SaaS tables; it is not a side-effect-free
43
+ types barrel. After direct subscription writes, call
44
+ [`RecomputeTenantBillingState`](./docs/2.billing.md#recomputetenantbillingstate)
45
+ to refresh derived billing and workspace directory state.
46
+
47
+ A module may seed plans straight into the `db` plan table: it leaves `Plan.paymentProviderRefs` empty and never rewrites it, since dms-saas links every billed plan to Stripe itself. See [Plans and Stripe](https://dms.antelopejs.com/docs/modules/saas/stripe#plans-and-stripe).
48
+
49
+ ## Compatibility
50
+
51
+ The implementation is `@antelopejs/dms-saas >=0.6.0`. The interface declares these dependencies:
52
+
53
+ <!-- docs-compat: ./package.json#dependencies -->
54
+
55
+ | Package | Range |
56
+ | --- | --- |
57
+ | `@antelopejs/interface-data-api` | `>=0.2.2 <1.0.0` |
58
+ | `@antelopejs/interface-dms` | `>=0.7.2 <1.0.0` |
59
+
60
+ Peer dependencies:
61
+
62
+ <!-- docs-compat: ./package.json#peerDependencies -->
63
+
64
+ | Package | Range |
65
+ | --- | --- |
66
+ | `@antelopejs/interface-core` | `>=0.2.4 <1.0.0` |
67
+ | `@antelopejs/interface-database` | `>=0.1.10 <1.0.0` |
68
+ | `@antelopejs/interface-database-decorators` | `>=0.1.9 <1.0.0` |
69
+
70
+ Subpaths resolve through the `exports` map only: compile consumers with `moduleResolution` set to `bundler`, `node16` or `nodenext`. Upgrade this package together with `@antelopejs/dms-saas`; the [upgrade notes](https://dms.antelopejs.com/docs/modules/saas/faq#upgrading) cover the tenant billing page anchors and the rows written by earlier builds.
71
+
72
+ ## Documentation
73
+
74
+ - [Consumer reference](./docs/1.index.md): billing, usage, lifecycle, pages, plans and database boundaries
75
+ - [Extending the SaaS module](https://dms.antelopejs.com/docs/modules/saas/extending): every extension point with examples
76
+ - [Stripe](https://dms.antelopejs.com/docs/modules/saas/stripe)
77
+ - [SaaS module guide](https://dms.antelopejs.com/docs/modules/saas)
78
+
79
+ ## Contributing
80
+
81
+ This package lives in the [`AntelopeJS/dms-saas`](https://github.com/AntelopeJS/dms-saas) pnpm workspace, next to the module that implements it. Read [CONTRIBUTING.md](https://github.com/AntelopeJS/dms-saas/blob/main/CONTRIBUTING.md) and the [repository README](https://github.com/AntelopeJS/dms-saas#contributing) for the development commands. Release this package on its own workflow before the module that implements it.
82
+
83
+ ## License
84
+
85
+ [Apache-2.0](./LICENSE)
@@ -12,6 +12,7 @@ declare const BillingSettingsModel_base: {
12
12
  delete(id: string): Promise<number>;
13
13
  };
14
14
  readonly schemaName: string;
15
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<BillingSettings>;
15
16
  fromPlainData(obj: any): BillingSettings;
16
17
  fromDatabase(obj: any): BillingSettings | undefined;
17
18
  toDatabase(obj: any): Record<string, any>;
@@ -13,6 +13,7 @@ declare const CreditNoteModel_base: {
13
13
  delete(id: string): Promise<number>;
14
14
  };
15
15
  readonly schemaName: string;
16
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<CreditNote>;
16
17
  fromPlainData(obj: any): CreditNote;
17
18
  fromDatabase(obj: any): CreditNote | undefined;
18
19
  toDatabase(obj: any): Record<string, any>;
@@ -12,6 +12,7 @@ declare const FeatureModel_base: {
12
12
  delete(id: string): Promise<number>;
13
13
  };
14
14
  readonly schemaName: string;
15
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<Feature>;
15
16
  fromPlainData(obj: any): Feature;
16
17
  fromDatabase(obj: any): Feature | undefined;
17
18
  toDatabase(obj: any): Record<string, any>;
@@ -13,6 +13,7 @@ declare const InvoiceModel_base: {
13
13
  delete(id: string): Promise<number>;
14
14
  };
15
15
  readonly schemaName: string;
16
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<Invoice>;
16
17
  fromPlainData(obj: any): Invoice;
17
18
  fromDatabase(obj: any): Invoice | undefined;
18
19
  toDatabase(obj: any): Record<string, any>;
@@ -12,6 +12,7 @@ declare const LegalDocumentsModel_base: {
12
12
  delete(id: string): Promise<number>;
13
13
  };
14
14
  readonly schemaName: string;
15
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<LegalDocuments>;
15
16
  fromPlainData(obj: any): LegalDocuments;
16
17
  fromDatabase(obj: any): LegalDocuments | undefined;
17
18
  toDatabase(obj: any): Record<string, any>;
@@ -13,6 +13,7 @@ declare const PlanMigrationModel_base: {
13
13
  delete(id: string): Promise<number>;
14
14
  };
15
15
  readonly schemaName: string;
16
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<PlanMigration>;
16
17
  fromPlainData(obj: any): PlanMigration;
17
18
  fromDatabase(obj: any): PlanMigration | undefined;
18
19
  toDatabase(obj: any): Record<string, any>;
@@ -16,6 +16,7 @@ declare const PlanModel_base: {
16
16
  delete(id: string): Promise<number>;
17
17
  };
18
18
  readonly schemaName: string;
19
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<Plan>;
19
20
  fromPlainData(obj: any): Plan;
20
21
  fromDatabase(obj: any): Plan | undefined;
21
22
  toDatabase(obj: any): Record<string, any>;
@@ -22,6 +22,7 @@ declare const PlatformNoteModel_base: {
22
22
  delete(id: string): Promise<number>;
23
23
  };
24
24
  readonly schemaName: string;
25
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<PlatformNote>;
25
26
  fromPlainData(obj: any): PlatformNote;
26
27
  fromDatabase(obj: any): PlatformNote | undefined;
27
28
  toDatabase(obj: any): Record<string, any>;
@@ -12,6 +12,7 @@ declare const RefundModel_base: {
12
12
  delete(id: string): Promise<number>;
13
13
  };
14
14
  readonly schemaName: string;
15
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<Refund>;
15
16
  fromPlainData(obj: any): Refund;
16
17
  fromDatabase(obj: any): Refund | undefined;
17
18
  toDatabase(obj: any): Record<string, any>;
@@ -18,6 +18,7 @@ declare const SegmentModel_base: {
18
18
  delete(id: string): Promise<number>;
19
19
  };
20
20
  readonly schemaName: string;
21
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<Segment>;
21
22
  fromPlainData(obj: any): Segment;
22
23
  fromDatabase(obj: any): Segment | undefined;
23
24
  toDatabase(obj: any): Record<string, any>;
@@ -13,6 +13,7 @@ declare const StripeWebhookEventModel_base: {
13
13
  delete(id: string): Promise<number>;
14
14
  };
15
15
  readonly schemaName: string;
16
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<StripeWebhookEvent>;
16
17
  fromPlainData(obj: any): StripeWebhookEvent;
17
18
  fromDatabase(obj: any): StripeWebhookEvent | undefined;
18
19
  toDatabase(obj: any): Record<string, any>;
@@ -12,6 +12,7 @@ declare const TenantBillingInfoModel_base: {
12
12
  delete(id: string): Promise<number>;
13
13
  };
14
14
  readonly schemaName: string;
15
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<TenantBillingInfo>;
15
16
  fromPlainData(obj: any): TenantBillingInfo;
16
17
  fromDatabase(obj: any): TenantBillingInfo | undefined;
17
18
  toDatabase(obj: any): Record<string, any>;
@@ -12,6 +12,7 @@ declare const TenantBillingStateModel_base: {
12
12
  delete(id: string): Promise<number>;
13
13
  };
14
14
  readonly schemaName: string;
15
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<TenantBillingState>;
15
16
  fromPlainData(obj: any): TenantBillingState;
16
17
  fromDatabase(obj: any): TenantBillingState | undefined;
17
18
  toDatabase(obj: any): Record<string, any>;
@@ -14,6 +14,7 @@ declare const TenantSubscriptionModel_base: {
14
14
  delete(id: string): Promise<number>;
15
15
  };
16
16
  readonly schemaName: string;
17
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<TenantSubscription>;
17
18
  fromPlainData(obj: any): TenantSubscription;
18
19
  fromDatabase(obj: any): TenantSubscription | undefined;
19
20
  toDatabase(obj: any): Record<string, any>;
@@ -12,6 +12,7 @@ declare const TrialConsumptionModel_base: {
12
12
  delete(id: string): Promise<number>;
13
13
  };
14
14
  readonly schemaName: string;
15
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<TrialConsumption>;
15
16
  fromPlainData(obj: any): TrialConsumption;
16
17
  fromDatabase(obj: any): TrialConsumption | undefined;
17
18
  toDatabase(obj: any): Record<string, any>;
@@ -14,6 +14,7 @@ declare const UserSegmentModel_base: {
14
14
  delete(id: string): Promise<number>;
15
15
  };
16
16
  readonly schemaName: string;
17
+ readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<UserSegment>;
17
18
  fromPlainData(obj: any): UserSegment;
18
19
  fromDatabase(obj: any): UserSegment | undefined;
19
20
  toDatabase(obj: any): Record<string, any>;
@@ -3,7 +3,7 @@ export declare const billingSettingsTableName = "billing_settings";
3
3
  export declare const BILLING_SETTINGS_SINGLETON_ID = "singleton";
4
4
  export declare const DEFAULT_AUTO_SUSPEND_DELAY_DAYS = 14;
5
5
  export declare const DEFAULT_STRIPE_TAX_CODE = "txcd_10000000";
6
- export declare const DEFAULT_MAX_FREE_WORKSPACES_PER_CARD = 1;
6
+ export declare const DEFAULT_MAX_FREE_WORKSPACES_PER_CARD = 0;
7
7
  export declare const DEFAULT_DATA_RETENTION_DAYS = 30;
8
8
  export declare const REFUND_PRORATA_MODES: readonly ["full", "prorated"];
9
9
  export type RefundProrataMode = (typeof REFUND_PRORATA_MODES)[number];
@@ -16,7 +16,7 @@ exports.billingSettingsTableName = "billing_settings";
16
16
  exports.BILLING_SETTINGS_SINGLETON_ID = "singleton";
17
17
  exports.DEFAULT_AUTO_SUSPEND_DELAY_DAYS = 14;
18
18
  exports.DEFAULT_STRIPE_TAX_CODE = "txcd_10000000";
19
- exports.DEFAULT_MAX_FREE_WORKSPACES_PER_CARD = 1;
19
+ exports.DEFAULT_MAX_FREE_WORKSPACES_PER_CARD = 0;
20
20
  exports.DEFAULT_DATA_RETENTION_DAYS = 30;
21
21
  exports.REFUND_PRORATA_MODES = ["full", "prorated"];
22
22
  let BillingSettings = class BillingSettings extends interface_database_decorators_1.Table {
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: SaaS interface
3
+ description: Supported backend imports and side-effect boundaries for modules extending DMS SaaS.
4
+ ---
5
+
6
+ # SaaS interface
7
+
8
+ Consumers depend on `@antelopejs/interface-dms-saas`, not the runtime's private services or page controllers. Load `@antelopejs/dms-saas` in the same AntelopeJS project for functions that need an implementation. Compile with `moduleResolution: "bundler"`, `"node16"` or `"nodenext"` so subpaths resolve through the package's exports map.
9
+
10
+ ## Entry points
11
+
12
+ | Import after `@antelopejs/interface-dms-saas` | Contract |
13
+ | --- | --- |
14
+ | Root | Billing, data API, pages, plans, provisioning and registration exports; `invoiceLineItems` and `workspaceLifecycle` namespaces |
15
+ | `/billing` | [Balances, previews and billing-state recomputation](./2.billing.md) |
16
+ | `/invoice-line-items` | [Usage providers, line types and key helpers](./3.lifecycle-and-usage.md#invoice-line-items) |
17
+ | `/workspace-lifecycle` | [Durable lifecycle consumers and provisioning gate](./3.lifecycle-and-usage.md#workspace-lifecycle) |
18
+ | `/provisioning`, `/registration` | [Provisioning payload and registration limits](./3.lifecycle-and-usage.md#registration-and-provisioning) |
19
+ | `/pages`, `/page-definitions` | [Page descriptors and extension registration](./4.pages-plans-data.md#page-extensions) |
20
+ | `/plans` | [Plan projections, localization and pricing helpers](./4.pages-plans-data.md#plan-helpers) |
21
+ | `/data-api`, `/hidden-filter` | [HiddenStringFilter](./4.pages-plans-data.md#hiddenstringfilter) |
22
+ | `/db`, `/db/models`, `/db/tables` | Canonical SaaS models and table classes; see the data boundary below |
23
+
24
+ `invoiceLineItems` and `workspaceLifecycle` are namespaces at the root, not flat exports. Use their direct subpaths for individual functions. Their `internal` proxy namespaces are implementation plumbing, not consumer APIs.
25
+
26
+ ```ts docs-check
27
+ import { invoiceLineItems, workspaceLifecycle } from "@antelopejs/interface-dms-saas";
28
+
29
+ export const usageKey = invoiceLineItems.buildInvoiceLineKey("example.compute", "minutes");
30
+
31
+ export async function mayProvision(tenantId: string): Promise<boolean> {
32
+ // Errors propagate: unavailable storage must not grant eligibility.
33
+ return workspaceLifecycle.IsWorkspaceProvisioningCommitted(tenantId);
34
+ }
35
+ ```
36
+
37
+ ## Database imports have side effects
38
+
39
+ The root does not re-export `db`. Importing `/db` registers the canonical SaaS tables; do not create duplicate table definitions or load the runtime's registration graph to reach them. Prefer `import type` when only a type is needed. The root also exports plan helpers that import database values, so do not assume that a root value import avoids all transitive table registration. Narrow subpaths express which contract the consumer needs.
40
+
41
+ Database exports are persistence primitives, not an authorization or billing-service API. Use tenant-scoped models with the correct tenant, and keep direct writes in trusted import/seed code. Direct writes do not automatically perform all Stripe, lifecycle, cache and directory work of the runtime's normal operations.
42
+
43
+ After seeding or importing subscription records, call [RecomputeTenantBillingState](./2.billing.md#recomputetenantbillingstate) for each affected tenant once writes finish. This republishes derived billing and directory state; it does not make an arbitrary direct write equivalent to a normal billing operation. When seeding plans, leave `Plan.paymentProviderRefs` empty and never overwrite the runtime's Stripe linkage. See the [plan and Stripe guide](/docs/modules/saas/stripe#plans-and-stripe).
44
+
45
+ `workspaceSettingsCategory` belongs to `@antelopejs/interface-dms/page`, not this package. There is no SaaS `GetWorkspaceSettingsCategory` export. Use `/pages` for SaaS billing and platform-workspace extension anchors.
46
+
47
+ The [runtime guide](/docs/modules/saas/extending) covers installation, public screens, UI integration and examples. These pages define the backend consumer contracts.
@@ -0,0 +1,66 @@
1
+ ---
2
+ title: Billing
3
+ description: Authorized customer balances and invoice previews, and republishing derived billing state after direct writes.
4
+ ---
5
+
6
+ # Billing
7
+
8
+ Import these functions and types from `@antelopejs/interface-dms-saas/billing` or the package root.
9
+
10
+ ## GetTenantCustomerBalance
11
+
12
+ `GetTenantCustomerBalance(scope: TenantCustomerBalanceScope): Promise<TenantCustomerBalance>` takes `{ tenantId: string, userId: string }` from a trusted authentication boundary. It requires membership and a successful tenant access gate; it rejects unauthorized callers with 403. It does not trust a supplied user ID as proof of authentication.
13
+
14
+ | Result | Fields |
15
+ | --- | --- |
16
+ | `AvailableTenantCustomerBalance` | `status: "available"`, `balanceMinorUnits: number`, `currency: string` |
17
+ | `AbsentTenantCustomerBalance` | `status: "absent"`, `reason: TenantCustomerBalanceAbsenceReason`, `balanceMinorUnits: null`, `currency: null` |
18
+
19
+ Amounts are Stripe minor units: negative means customer credit, positive means money owed, and zero means settled. Currency is uppercase ISO 4217. Absence reasons are `complimentary`, `customer_not_configured`, `customer_deleted`, and `currency_unavailable`. Stripe read failures reject with 502 and `saas.errors.billing.customer_balance_provider_unavailable`; they are not an absent balance. This call does not charge or refund the customer.
20
+
21
+ `isComplimentarySubscription(subscription: ComplimentarySubscriptionState | null | undefined): boolean` is a synchronous helper. `ComplimentarySubscriptionState` has `isComplimentary?: boolean | null`; only an explicit `true` returns true. A free plan alone is not the complimentary marker.
22
+
23
+ ## GetUpcomingInvoicePreview
24
+
25
+ `GetUpcomingInvoicePreview(scope: UpcomingInvoicePreviewScope): Promise<UpcomingInvoicePreview>` uses the same `{ tenantId, userId }` scope and authorization checks. Stripe failures become an `unavailable` result rather than an invented total; authentication, implementation availability or local storage failures can still reject.
26
+
27
+ | `status` | Result type and reason |
28
+ | --- | --- |
29
+ | `available` | `AvailableUpcomingInvoicePreview`: Stripe priced the next invoice |
30
+ | `absent` | `AbsentUpcomingInvoicePreview`: `complimentary`, `free_plan`, `customer_not_configured`, `subscription_not_configured`, or `no_upcoming_invoice` |
31
+ | `unavailable` | `UnavailableUpcomingInvoicePreview`: `stripe_not_configured`, `tax_not_configured`, `tax_location_invalid`, `tax_location_required`, `tax_calculation_failed`, `usage_unavailable`, or `provider_error` |
32
+
33
+ Both non-available variants have `reason` and `computedAt: string`. The corresponding exported unions are `UpcomingInvoicePreviewAbsenceReason` and `UpcomingInvoicePreviewUnavailableReason`.
34
+
35
+ An available preview includes these fields. All amounts are integers in the minor units of its uppercase `currency`; all dates are ISO 8601 strings.
36
+
37
+ | Fields | Type and meaning |
38
+ | --- | --- |
39
+ | `subtotalMinorUnits`, `totalExcludingTaxMinorUnits`, `taxMinorUnits`, `totalMinorUnits`, `amountDueMinorUnits` | `number`: subtotal before discounts/tax; total after discounts before tax; tax; tax-inclusive total; amount after customer balance |
40
+ | `lines`, `hasMoreLines` | `UpcomingInvoiceLine[]`, `boolean`: line page and truncation flag; totals remain complete |
41
+ | `taxes`, `taxCountry`, `isReverseCharge` | `UpcomingInvoiceTax[]`, `string \| null`, `boolean` |
42
+ | `periodStart`, `periodEnd`, `billingDate`, `computedAt` | `string`: cycle bounds, issue date and computation time |
43
+ | `usageThrough` | `string \| null`: end of quoted usage, null without usage lines |
44
+
45
+ `UpcomingInvoiceLine` has `kind: UpcomingInvoiceLineKind` (`subscription`, `usage`, or `invoice_item`), `description: string | null`, `amountMinorUnits: number`, `taxMinorUnits: number`, `quantity: number | null`, `periodStart: string`, `periodEnd: string`, `isProration: boolean`, `usageLineKey: string | null`, and `productId: string | null`. Usage keys are `<provider id>:<key>`; other line kinds have a null usage key.
46
+
47
+ `UpcomingInvoiceTax` has `amountMinorUnits: number`, `taxableAmountMinorUnits: number | null`, `isInclusive: boolean`, `ratePercentage: number | null`, `isReverseCharge: boolean`, and nullable string fields `country`, `taxType`, `displayName`, `jurisdiction`, and `taxabilityReason`. A reverse charge can produce zero tax with `isReverseCharge: true`.
48
+
49
+ The preview calls usage providers with `isPreview: true` and can populate a tenant-level shared database cache. It never creates an invoice or charges quoted lines. Usage covers the running cycle so far, not its final total; display `computedAt`. A missing payment method does not itself prevent previewing. Cache lifetime, invalidation and Stripe read timeouts are [runtime configuration](/docs/modules/saas/extending#upcoming-invoice-preview).
50
+
51
+ ## RecomputeTenantBillingState
52
+
53
+ `RecomputeTenantBillingState(tenantId: string): Promise<void>` republishes the tenant's billing state and workspace directory fields, including plan, seats, MRR, renewal and owner, from current records. Call it after a seed or import finishes direct writes. Normal runtime billing operations already do this.
54
+
55
+ This is a trusted administrative call with no user scope or membership check. Authorize the tenant before invoking it. It writes derived database state, does not mutate the Stripe subscription, and does not replace lifecycle delivery or preview-cache invalidation. Deleted directory rows and subscriptions whose deletion has started are left alone. Storage errors propagate; repeated concurrent-publication conflicts reject after bounded retries. Await completion and report failures instead of displaying stale directory state as current.
56
+
57
+ ```ts docs-check
58
+ import { RecomputeTenantBillingState } from "@antelopejs/interface-dms-saas/billing";
59
+
60
+ // Invoke only after the import has committed all rows for these authorized tenants.
61
+ export async function refreshImportedTenants(tenantIds: readonly string[]): Promise<void> {
62
+ for (const tenantId of new Set(tenantIds)) {
63
+ await RecomputeTenantBillingState(tenantId);
64
+ }
65
+ }
66
+ ```
@@ -0,0 +1,95 @@
1
+ ---
2
+ title: Lifecycle and usage
3
+ description: Registration extras, durable workspace lifecycle consumers and idempotent invoice usage providers.
4
+ ---
5
+
6
+ # Lifecycle and usage
7
+
8
+ Use the direct subpaths below or the root's `invoiceLineItems` and `workspaceLifecycle` namespaces. Register consumers/providers from the owning module's lifecycle, not a request handler. Registrations replay when SaaS attaches or reloads and are released when their owner unloads.
9
+
10
+ ## Invoice line items
11
+
12
+ Import from `/invoice-line-items`. `RegisterInvoiceLineItemsProvider(provider: InvoiceLineItemsProvider): void` registers `{ id: string, resolve: InvoiceLineItemsResolver }`. `UnregisterInvoiceLineItemsProvider(providerId: string): void` removes it; unknown IDs are ignored. Register during `construct()` and unregister during `stop()`.
13
+
14
+ ```ts [Contract]
15
+ interface InvoiceLineItemsContext {
16
+ tenantId: string;
17
+ invoiceId: string;
18
+ currency: string;
19
+ periodStart: Date;
20
+ periodEnd: Date;
21
+ isPreview?: boolean;
22
+ }
23
+
24
+ interface InvoiceLineItem {
25
+ key: string;
26
+ description: string;
27
+ amountCents: number;
28
+ quantity?: number;
29
+ unit?: string;
30
+ metadata?: Record<string, string>;
31
+ }
32
+
33
+ type InvoiceLineItemsResolver = (
34
+ context: InvoiceLineItemsContext,
35
+ ) => Promise<InvoiceLineItem[]> | InvoiceLineItem[];
36
+ ```
37
+
38
+ The runtime asks providers for `subscription_cycle` renewal invoices, not initial or one-off invoices. The dates cover the cycle just closed, clipped to the subscription's paid usage period. Complimentary/grace usage is excluded; no provider runs for an empty covered window. Read tenant-scoped, pre-aggregated usage because the resolver runs inside the webhook request.
39
+
40
+ `amountCents` is the safe-integer line total in the invoice currency, including negative credits. Quantity and unit are audit metadata, not factors multiplied into the total. Zero amounts are dropped. Empty keys/descriptions, non-safe-integer amounts, keys containing `:`, or combined keys over 150 characters are invalid. Provider IDs must not contain `:`; registration throws if they do. IDs must be globally unique across modules: a collision replaces the earlier provider, and collisions before attachment can collapse silently.
41
+
42
+ Keep each line key stable. The runtime injects at most one line per invoice/provider/key across webhook replays and concurrent workers. A throwing provider is logged and skipped so others can run, but partial/invalid injection ultimately fails the webhook to expose underbilling. Returning lines is not a guarantee that Stripe accepted the invoice items.
43
+
44
+ Preview calls set `isPreview: true`, use a synthetic `upcoming_<subscription id>` invoice ID, and end at the request time. They quote usage without invoicing it. Do not mutate usage accounting inside a resolver. A failed resolver or invalid line makes the preview unavailable.
45
+
46
+ ### Line helpers
47
+
48
+ `LINE_KEY_SEPARATOR` is `":"`. `buildInvoiceLineKey(providerId: string, key: string): string` joins the two fragments; it does not validate them.
49
+
50
+ `selectInvoiceLineItemsToCreate(providerId: string, items: readonly InvoiceLineItem[], alreadyInjectedKeys: ReadonlySet<string>): InvoiceLineItemSelection` is synchronous and has no Stripe side effects. It returns `accepted: PreparedInvoiceLineItem[]` (`{ lineKey, item }`, with trimmed item keys) and `skipped: SkippedInvoiceLineItem[]` (`{ lineKey, reason }`). `SkippedLineItemReason` is `already_invoiced`, `duplicate_key`, `zero_amount`, or `invalid`. The set must contain full provider-prefixed keys. This selection helper does not itself provide transactional idempotency.
51
+
52
+ ## Workspace lifecycle
53
+
54
+ Import from `/workspace-lifecycle`. `WORKSPACE_LIFECYCLE_TRANSITIONS` contains `created`, `suspended` and `reactivation_requested`; `WorkspaceLifecycleTransition` is their union.
55
+
56
+ ```ts [Contract]
57
+ import type { WorkspaceLifecycleTransition } from "@antelopejs/interface-dms-saas/workspace-lifecycle";
58
+
59
+ interface WorkspaceLifecycleMessage {
60
+ tenantId: string;
61
+ operationId: string;
62
+ transition: WorkspaceLifecycleTransition;
63
+ requestedAt: Date;
64
+ }
65
+ interface WorkspaceLifecycleReceipt {
66
+ receiptId: string;
67
+ effectiveAt?: Date;
68
+ }
69
+ interface WorkspaceLifecycleConsumer {
70
+ name: string;
71
+ transitions: readonly WorkspaceLifecycleTransition[];
72
+ consume(message: WorkspaceLifecycleMessage): Promise<WorkspaceLifecycleReceipt>;
73
+ }
74
+ interface WorkspaceLifecycleConsumerRegistration { unregister(): void; }
75
+
76
+ declare function RegisterWorkspaceLifecycleConsumer(
77
+ consumer: WorkspaceLifecycleConsumer,
78
+ ): WorkspaceLifecycleConsumerRegistration;
79
+ declare function UnregisterWorkspaceLifecycleConsumer(name: string): void;
80
+ declare function IsWorkspaceProvisioningCommitted(tenantId: string): Promise<boolean>;
81
+ ```
82
+
83
+ Names must match `/^[a-z][a-z0-9._-]{2,63}$/`; invalid names throw synchronously. Pick a stable unique name and explicitly select transitions. Unregister via the returned handle or by name when the module stops.
84
+
85
+ Delivery is **at least once**. Deduplicate by `operationId`, durably link provisioned resources, then return a durable receipt. A rejection is not an acknowledgement. Suspension closes SaaS access before delivery; reactivation keeps access closed until every registered required consumer returns a receipt. `created` arrives after ownership, billing and pre-provisioning hooks succeed and the first invoice is paid, or no payment is required.
86
+
87
+ `IsWorkspaceProvisioningCommitted` returns false for preparing, unpaid or cancelled self-service workspaces; true for committed creations and legacy/bootstrap tenants without a marker. It is not an existence, membership or access check. Storage and missing-implementation errors propagate; fail closed instead of treating an unavailable check as eligibility.
88
+
89
+ ## Registration and provisioning
90
+
91
+ `TenantBeingProvisionedPayload` from `/provisioning` has `tenantId: string`, `userId: string`, and optional `extras: Record<string, unknown>`. Use it with the DMS `Hook.TENANT_BEING_PROVISIONED` hook; the hook handler returns `Promise<undefined>`, not `Promise<void>`.
92
+
93
+ Listeners run serially inside provisioning, while the per-card lock is held, after workspace records exist but before registration returns. A throwing listener cancels registration and rolls back the runtime's tenant, subscription, Stripe customer and password-path account. Do not assume external side effects of your listener roll back automatically. Keep it to bounded persistence work rather than network provisioning; use durable `created` delivery for post-payment resources.
94
+
95
+ Extras reach password and OAuth completion paths unchanged, are absent when nothing was captured, and remain consumer-owned. The runtime does not persist or return them. `/registration` exports `RegistrationExtrasLimits` and `REGISTRATION_EXTRAS_LIMITS`: `maxSerialisedBytes: 4096`, `maxKeys: 64`, `maxDepth: 4`. Public registration also rejects `__proto__`, `constructor`, and `prototype` keys. Rejected extras produce HTTP 400 `saas.errors.registration.extras_*` errors before listeners run.
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: Pages, plans and data
3
+ description: Stable page extension anchors, plan projections and safe use of the canonical data exports.
4
+ ---
5
+
6
+ # Pages, plans and data
7
+
8
+ These contracts let consumers build backend-declared DMS pages without importing private SaaS controllers.
9
+
10
+ ## Page extensions
11
+
12
+ Import from `/pages`. `tenantBillingPage` identifies `settings.workspace.billing` at `/settings/workspace/billing`. Its `components` exposes `freeAccessBanner`, `pastDueAlert`, `planCard`, `paymentMethod`, `billingForm`, `refundButton`, and `table` anchors. `TenantBillingPageComponentKey` is the union of those keys; `tenantBillingPageAnchorPaths` maps them to the current nested layout. Use keys instead of copying paths.
13
+
14
+ `RegisterTenantBillingPageExtension(extension: TenantBillingPageExtension): TenantBillingPageExtensionRegistration` registers `{ name: string, components: readonly TenantBillingPageExtensionComponent[] }` and returns `{ unregister(): void }`. Registration may precede page loading; call the handle's `unregister()` during `stop()`.
15
+
16
+ Each component has `key: string`, `component: Component`, and optional `order: number` (default 0). An anchored component adds `side: "before" | "after"` and `anchorKey: TenantBillingPageComponentKey`; an appended component has optional `side: "end"` and no anchor. These are exported as `TenantBillingPageExtensionComponentBase`, `TenantBillingPageAnchoredExtensionComponent`, and `TenantBillingPageAppendedExtensionComponent`. The plan/payment anchors resolve inside their column's stack, not to an extra grid column.
17
+
18
+ `platformSaasModule: CategoryInfo` is a synchronous descriptor, not the canonical registered module object. `GetPlatformSaasModule(): Promise<CategoryInfo>` resolves that object after runtime attachment and rejects when unavailable. `platformWorkspaceDetailPage` identifies `modules.saas.customers.workspaces.detail`, with `header`, `summary` and `body` anchors.
19
+
20
+ `RegisterPlatformWorkspaceDetailPageExtension(extension: PlatformWorkspaceDetailPageExtension): PlatformWorkspaceDetailPageExtensionRegistration` accepts `{ name: string, components: readonly PageExtensionComponent[] }` and returns an unregister handle. Use the DMS `PageExtensionComponent` placement contract for these blocks. These registrations do not grant access: the platform detail remains owner-only, and tenant page components need appropriate metadata/permissions.
21
+
22
+ `/page-definitions` exports `MODULES_ROOT_DEFINITION: PageInfo`, `SAAS_MODULE_ID = "saas"`, and `SAAS_MODULE_DEFINITION` (full ID, full slug and module registration metadata). These are descriptors, not runtime registration functions. The workspace settings category itself comes from `@antelopejs/interface-dms/page`.
23
+
24
+ ```ts docs-check
25
+ import { PageController, RegisterPage, workspaceSettingsCategory } from "@antelopejs/interface-dms/page";
26
+
27
+ @RegisterPage()
28
+ export class WorkspaceRegistriesController extends PageController("registries", {
29
+ displayName: "Registries",
30
+ category: workspaceSettingsCategory,
31
+ icon: "i-ph-package",
32
+ order: 10,
33
+ }) {}
34
+ ```
35
+
36
+ ## Plan helpers
37
+
38
+ Import from `/plans`. These helpers do not charge customers or authorize access.
39
+
40
+ | Function | Contract |
41
+ | --- | --- |
42
+ | `monthlyPrice(plan: Plan): number` | Returns the price as a monthly amount; yearly prices divide by 12 |
43
+ | `isDowngrade(current: Plan \| null, target: Plan, seats?: number): boolean` | Compares monthly cost, multiplying seat plans by at least one seat; seats default to 1. No current plan returns false; equal cost is not a downgrade |
44
+ | `localizeFeature(feature: Feature, locale: string): Feature` | Applies localization to `displayName`, `description`, `tooltip`, and `unit`, listed by exported `FEATURE_LOCALIZED_FIELDS`; missing translations fall back to the table locale (`en`) |
45
+ | `buildTenantPlanCatalog(planModel: PlanModel, featureModel: FeatureModel, plans: Plan[], locale: string): Promise<TenantPlanCatalog>` | Reads features, resolves plan inheritance, excludes unknown feature IDs and sorts plans/features by order; model read failures reject |
46
+
47
+ `TenantPlanCatalog` has `plans: TenantPlanView[]` and `features: TenantPlanFeature[]`.
48
+
49
+ - `TenantPlanView`: `_id: string`, `name: string`, `price: number`, `currency: string`, `interval: PlanInterval` (`month` or `year`), `order: number`, `checkoutAvailable: boolean`, and `featureValues: Record<string, unknown>`. Checkout availability reflects a Stripe price reference, not an authorization decision.
50
+ - `TenantPlanFeature`: `featureId: string`, `displayName: string`, `tooltip: string | null`, `unit: string | null`, `valueType: FeatureValueType` (`boolean`, `number`, or `string`), `isDetailRow: boolean`, and `order: number`.
51
+
52
+ Keep caller-supplied plan/model scopes authorized. The catalogue builder does not filter an arbitrary plan list to what a user may buy. Write localized feature values using the canonical model's `localize("*")` convention; see the [feature integration guide](/docs/modules/saas/extending#plan-feature-labels-and-values).
53
+
54
+ ## HiddenStringFilter
55
+
56
+ `HiddenStringFilter(): PropertyDecorator` is exported from `/data-api` and `/hidden-filter`. It registers string comparisons, including `is` for `TableView.routeParamFilters`, without a visible `@Column`. Apply it to the data controller prototype field, such as `_instance` or `userId`.
57
+
58
+ An unregistered filter can be silently dropped by the data API, so declaring a hidden route filter matters for scoping. The decorator itself does not authenticate a user or make a client-supplied tenant ID trustworthy; enforce the scope at the server authorization boundary.
59
+
60
+ ## Canonical database surface
61
+
62
+ `/db` exports the table/model pairs for plans, features, subscriptions, billing info/state/settings, invoices, credit notes, refunds, legal documents, trial consumptions, segments, user segments, platform notes, plan migrations and Stripe webhook events. `/db/models` and `/db/tables` expose those groups separately. For example, `PlanModel` works with `Plan`, `FeatureModel` with `Feature`, and `TenantSubscriptionModel` with `TenantSubscription`; use their shipped TypeScript declarations for persistence fields and methods.
63
+
64
+ These are registered persistence objects, not copies to redeclare. Imports have registration side effects; prefer type-only imports for types. Direct model writes bypass runtime orchestration and require correct tenant scope. See the [database boundary](./1.index.md#database-imports-have-side-effects) before writing seeds or imports, and [recompute derived billing state](./2.billing.md#recomputetenantbillingstate) after writing relevant records.
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "..",
5
+ "composite": false,
6
+ "incremental": false,
7
+ "declaration": false,
8
+ "noEmit": true
9
+ },
10
+ "include": ["../src/**/*.ts", "./**/*.ts"]
11
+ }
package/package.json CHANGED
@@ -1,15 +1,28 @@
1
1
  {
2
2
  "name": "@antelopejs/interface-dms-saas",
3
- "version": "0.4.0",
4
- "description": "Public interfaces for the DMS SaaS module",
3
+ "version": "0.4.1",
4
+ "description": "AntelopeJS interface for the DMS SaaS module: billing, plans, registration, workspace pages and lifecycle extension points.",
5
+ "keywords": [
6
+ "antelopejs",
7
+ "billing",
8
+ "dms",
9
+ "interface",
10
+ "saas"
11
+ ],
12
+ "homepage": "https://dms.antelopejs.com/docs/modules/saas/extending",
13
+ "bugs": {
14
+ "url": "https://github.com/AntelopeJS/dms-saas/issues"
15
+ },
5
16
  "license": "Apache-2.0",
6
17
  "repository": {
7
18
  "type": "git",
8
- "url": "git+https://github.com/AntelopeJS/dms-saas.git"
19
+ "url": "git+https://github.com/AntelopeJS/dms-saas.git",
20
+ "directory": "packages/interface-dms-saas"
9
21
  },
10
22
  "files": [
11
23
  "dist",
12
24
  "!dist/.tsbuildinfo",
25
+ "docs",
13
26
  "package.json",
14
27
  "README.md",
15
28
  "LICENSE"
@@ -57,13 +70,13 @@
57
70
  "prepack": "pnpm run build"
58
71
  },
59
72
  "dependencies": {
60
- "@antelopejs/interface-data-api": ">=0.2.0 <1.0.0",
61
- "@antelopejs/interface-dms": ">=0.5.5 <1.0.0"
73
+ "@antelopejs/interface-data-api": ">=0.2.2 <1.0.0",
74
+ "@antelopejs/interface-dms": ">=0.7.2 <1.0.0"
62
75
  },
63
76
  "devDependencies": {
64
- "@antelopejs/interface-core": ">=0.1.1 <1.0.0",
65
- "@antelopejs/interface-database": ">=0.1.8 <1.0.0",
66
- "@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
77
+ "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
78
+ "@antelopejs/interface-database": ">=0.1.10 <1.0.0",
79
+ "@antelopejs/interface-database-decorators": ">=0.1.9 <1.0.0",
67
80
  "@types/node": "^24.0.0",
68
81
  "knip": "^6.34.0",
69
82
  "oxfmt": "^0.66.0",
@@ -75,9 +88,9 @@
75
88
  "typescript": "^6.0.3"
76
89
  },
77
90
  "peerDependencies": {
78
- "@antelopejs/interface-core": ">=0.1.1 <1.0.0",
79
- "@antelopejs/interface-database": ">=0.1.8 <1.0.0",
80
- "@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0"
91
+ "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
92
+ "@antelopejs/interface-database": ">=0.1.10 <1.0.0",
93
+ "@antelopejs/interface-database-decorators": ">=0.1.9 <1.0.0"
81
94
  },
82
95
  "packageManager": "pnpm@10.2.0",
83
96
  "antelopeJs": {}