@antelopejs/interface-dms-saas 0.4.0 → 0.5.0
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 +77 -87
- package/dist/db/models/billingSettings.model.d.ts +1 -0
- package/dist/db/models/creditNotes.model.d.ts +1 -0
- package/dist/db/models/features.model.d.ts +1 -0
- package/dist/db/models/invoices.model.d.ts +1 -0
- package/dist/db/models/legalDocuments.model.d.ts +1 -0
- package/dist/db/models/planMigrations.model.d.ts +1 -0
- package/dist/db/models/plans.model.d.ts +1 -0
- package/dist/db/models/platformNotes.model.d.ts +1 -0
- package/dist/db/models/refunds.model.d.ts +1 -0
- package/dist/db/models/segments.model.d.ts +1 -0
- package/dist/db/models/stripeWebhookEvents.model.d.ts +1 -0
- package/dist/db/models/tenantBillingInfo.model.d.ts +1 -0
- package/dist/db/models/tenantBillingState.model.d.ts +1 -0
- package/dist/db/models/tenantSubscriptions.model.d.ts +1 -0
- package/dist/db/models/trialConsumptions.model.d.ts +1 -0
- package/dist/db/models/userSegments.model.d.ts +1 -0
- package/dist/db/tables/billingSettings.table.d.ts +1 -1
- package/dist/db/tables/billingSettings.table.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/tenant-lifecycle.d.ts +27 -0
- package/dist/tenant-lifecycle.js +30 -0
- package/dist/tenant-lifecycle.js.map +1 -0
- package/docs/1.index.md +47 -0
- package/docs/2.billing.md +66 -0
- package/docs/3.lifecycle-and-usage.md +97 -0
- package/docs/4.pages-plans-data.md +64 -0
- package/docs/tsconfig.json +11 -0
- package/package.json +24 -11
- package/dist/workspace-lifecycle.d.ts +0 -27
- package/dist/workspace-lifecycle.js +0 -30
- package/dist/workspace-lifecycle.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,95 +1,85 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# @antelopejs/interface-dms-saas
|
|
2
4
|
|
|
3
|
-
<
|
|
4
|
-
<a href="./LICENSE"><img alt="License" src="https://img.shields.io/
|
|
5
|
-
<a href="https://discord.gg/sjK28QHrA7"><img src="https://img.shields.io/badge/Discord-18181B?logo=discord&style=for-the-badge&color=000000"
|
|
6
|
-
<a href="https://antelopejs.com"><img src="https://img.shields.io/badge/Docs-18181B?style=for-the-badge&color=000000"
|
|
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
|
-
|
|
14
|
+
## What it does
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
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 tenant lifecycle events and operator actions, and exposes the canonical SaaS tables and models (`/db`).
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
34
|
-
import { invoiceLineItems, workspaceLifecycle } from "@antelopejs/interface-dms-saas";
|
|
35
|
-
|
|
36
|
-
invoiceLineItems.RegisterInvoiceLineItemsProvider(/* … */);
|
|
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
|
|
37
28
|
```
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
A module may seed plans straight into the `db` plan table.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`@
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
[`@antelopejs/dms-saas` upgrade notes](../dms-saas/README.md#upgrading).
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
The root entry holds every extension point except `db`, with `invoiceLineItems` and `tenantLifecycle` as namespaces:
|
|
33
|
+
|
|
34
|
+
```ts docs-check
|
|
35
|
+
import { invoiceLineItems, tenantLifecycle } from "@antelopejs/interface-dms-saas";
|
|
36
|
+
|
|
37
|
+
export const usageKey = invoiceLineItems.buildInvoiceLineKey("example.compute", "minutes");
|
|
38
|
+
export const isProvisioningCommitted = tenantLifecycle.IsTenantProvisioningCommitted;
|
|
39
|
+
```
|
|
40
|
+
|
|
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 =
|
|
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 =
|
|
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 {
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.
|
|
39
|
+
exports.tenantLifecycle = exports.invoiceLineItems = void 0;
|
|
40
40
|
__exportStar(require("./billing"), exports);
|
|
41
41
|
__exportStar(require("./data-api"), exports);
|
|
42
42
|
__exportStar(require("./pages"), exports);
|
|
@@ -44,5 +44,5 @@ __exportStar(require("./plans"), exports);
|
|
|
44
44
|
__exportStar(require("./provisioning"), exports);
|
|
45
45
|
__exportStar(require("./registration"), exports);
|
|
46
46
|
exports.invoiceLineItems = __importStar(require("./invoice-line-items"));
|
|
47
|
-
exports.
|
|
47
|
+
exports.tenantLifecycle = __importStar(require("./tenant-lifecycle"));
|
|
48
48
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAWA,4CAA0B;AAC1B,6CAA2B;AAC3B,0CAAwB;AACxB,0CAAwB;AACxB,iDAA+B;AAC/B,iDAA+B;AAC/B,yEAAyD;AACzD,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAWA,4CAA0B;AAC1B,6CAA2B;AAC3B,0CAAwB;AACxB,0CAAwB;AACxB,iDAA+B;AAC/B,iDAA+B;AAC/B,yEAAyD;AACzD,sEAAsD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { RegisteringProxy } from "@antelopejs/interface-core";
|
|
2
|
+
export declare const TENANT_LIFECYCLE_TRANSITIONS: readonly ["created", "suspended", "reactivation_requested"];
|
|
3
|
+
export type TenantLifecycleTransition = (typeof TENANT_LIFECYCLE_TRANSITIONS)[number];
|
|
4
|
+
export interface TenantLifecycleMessage {
|
|
5
|
+
tenantId: string;
|
|
6
|
+
operationId: string;
|
|
7
|
+
transition: TenantLifecycleTransition;
|
|
8
|
+
requestedAt: Date;
|
|
9
|
+
}
|
|
10
|
+
export interface TenantLifecycleReceipt {
|
|
11
|
+
receiptId: string;
|
|
12
|
+
effectiveAt?: Date;
|
|
13
|
+
}
|
|
14
|
+
export interface TenantLifecycleConsumer {
|
|
15
|
+
name: string;
|
|
16
|
+
transitions: readonly TenantLifecycleTransition[];
|
|
17
|
+
consume(message: TenantLifecycleMessage): Promise<TenantLifecycleReceipt>;
|
|
18
|
+
}
|
|
19
|
+
export interface TenantLifecycleConsumerRegistration {
|
|
20
|
+
unregister(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare const IsTenantProvisioningCommitted: (tenantId: string) => Promise<boolean>;
|
|
23
|
+
export declare namespace internal {
|
|
24
|
+
const RegisterTenantLifecycleConsumer: RegisteringProxy<(name: string, consumer: TenantLifecycleConsumer) => void>;
|
|
25
|
+
}
|
|
26
|
+
export declare function UnregisterTenantLifecycleConsumer(name: string): void;
|
|
27
|
+
export declare function RegisterTenantLifecycleConsumer(consumer: TenantLifecycleConsumer): TenantLifecycleConsumerRegistration;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.internal = exports.IsTenantProvisioningCommitted = exports.TENANT_LIFECYCLE_TRANSITIONS = void 0;
|
|
4
|
+
exports.UnregisterTenantLifecycleConsumer = UnregisterTenantLifecycleConsumer;
|
|
5
|
+
exports.RegisterTenantLifecycleConsumer = RegisterTenantLifecycleConsumer;
|
|
6
|
+
const interface_core_1 = require("@antelopejs/interface-core");
|
|
7
|
+
const CONSUMER_NAME_PATTERN = /^[a-z][a-z0-9._-]{2,63}$/;
|
|
8
|
+
exports.TENANT_LIFECYCLE_TRANSITIONS = [
|
|
9
|
+
"created",
|
|
10
|
+
"suspended",
|
|
11
|
+
"reactivation_requested",
|
|
12
|
+
];
|
|
13
|
+
exports.IsTenantProvisioningCommitted = (0, interface_core_1.InterfaceFunction)();
|
|
14
|
+
var internal;
|
|
15
|
+
(function (internal) {
|
|
16
|
+
internal.RegisterTenantLifecycleConsumer = new interface_core_1.RegisteringProxy();
|
|
17
|
+
})(internal || (exports.internal = internal = {}));
|
|
18
|
+
function UnregisterTenantLifecycleConsumer(name) {
|
|
19
|
+
internal.RegisterTenantLifecycleConsumer.unregister(name);
|
|
20
|
+
}
|
|
21
|
+
function RegisterTenantLifecycleConsumer(consumer) {
|
|
22
|
+
if (!CONSUMER_NAME_PATTERN.test(consumer.name)) {
|
|
23
|
+
throw new Error("Invalid workspace lifecycle consumer name");
|
|
24
|
+
}
|
|
25
|
+
internal.RegisterTenantLifecycleConsumer.register(consumer.name, consumer);
|
|
26
|
+
return {
|
|
27
|
+
unregister: () => UnregisterTenantLifecycleConsumer(consumer.name),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=tenant-lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-lifecycle.js","sourceRoot":"","sources":["../src/tenant-lifecycle.ts"],"names":[],"mappings":";;;AA+DA,8EAEC;AAWD,0EAUC;AAtFD,+DAGoC;AAEpC,MAAM,qBAAqB,GAAG,0BAA0B,CAAC;AAG5C,QAAA,4BAA4B,GAAG;IAC1C,SAAS;IACT,WAAW;IACX,wBAAwB;CAChB,CAAC;AAwCE,QAAA,6BAA6B,GACxC,IAAA,kCAAiB,GAA0C,CAAC;AAG9D,IAAiB,QAAQ,CAIxB;AAJD,WAAiB,QAAQ;IACV,wCAA+B,GAAG,IAAI,iCAAgB,EAEhE,CAAC;AACN,CAAC,EAJgB,QAAQ,wBAAR,QAAQ,QAIxB;AAGD,SAAgB,iCAAiC,CAAC,IAAY;IAC5D,QAAQ,CAAC,+BAA+B,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;AAC5D,CAAC;AAWD,SAAgB,+BAA+B,CAC7C,QAAiC;IAEjC,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,CAAC;IACD,QAAQ,CAAC,+BAA+B,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3E,OAAO;QACL,UAAU,EAAE,GAAG,EAAE,CAAC,iCAAiC,CAAC,QAAQ,CAAC,IAAI,CAAC;KACnE,CAAC;AACJ,CAAC"}
|
package/docs/1.index.md
ADDED
|
@@ -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 `tenantLifecycle` 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
|
+
| `/tenant-lifecycle` | [Durable lifecycle consumers and provisioning gate](./3.lifecycle-and-usage.md#tenant-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 `tenantLifecycle` 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, tenantLifecycle } 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 tenantLifecycle.IsTenantProvisioningCommitted(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,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Lifecycle and usage
|
|
3
|
+
description: Registration extras, durable tenant 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 `tenantLifecycle` 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
|
+
## Tenant lifecycle
|
|
53
|
+
|
|
54
|
+
Import from `/tenant-lifecycle`. `TENANT_LIFECYCLE_TRANSITIONS` contains `created`, `suspended` and `reactivation_requested`; `TenantLifecycleTransition` is their union.
|
|
55
|
+
|
|
56
|
+
**Breaking migration:** Update `@antelopejs/interface-dms-saas` and `@antelopejs/dms-saas` together. The old `RegisterWorkspaceLifecycleConsumer`, `UnregisterWorkspaceLifecycleConsumer`, `WorkspaceLifecycle*` types, `WORKSPACE_LIFECYCLE_TRANSITIONS`, `IsWorkspaceProvisioningCommitted`, `/workspace-lifecycle` subpath and root `workspaceLifecycle` namespace are removed, without compatibility aliases. Use the tenant-named API below and root `tenantLifecycle` namespace instead. Persisted consumer IDs, transition values and operation IDs remain unchanged; keep existing consumer names and deduplication keys.
|
|
57
|
+
|
|
58
|
+
```ts [Contract]
|
|
59
|
+
import type { TenantLifecycleTransition } from "@antelopejs/interface-dms-saas/tenant-lifecycle";
|
|
60
|
+
|
|
61
|
+
interface TenantLifecycleMessage {
|
|
62
|
+
tenantId: string;
|
|
63
|
+
operationId: string;
|
|
64
|
+
transition: TenantLifecycleTransition;
|
|
65
|
+
requestedAt: Date;
|
|
66
|
+
}
|
|
67
|
+
interface TenantLifecycleReceipt {
|
|
68
|
+
receiptId: string;
|
|
69
|
+
effectiveAt?: Date;
|
|
70
|
+
}
|
|
71
|
+
interface TenantLifecycleConsumer {
|
|
72
|
+
name: string;
|
|
73
|
+
transitions: readonly TenantLifecycleTransition[];
|
|
74
|
+
consume(message: TenantLifecycleMessage): Promise<TenantLifecycleReceipt>;
|
|
75
|
+
}
|
|
76
|
+
interface TenantLifecycleConsumerRegistration { unregister(): void; }
|
|
77
|
+
|
|
78
|
+
declare function RegisterTenantLifecycleConsumer(
|
|
79
|
+
consumer: TenantLifecycleConsumer,
|
|
80
|
+
): TenantLifecycleConsumerRegistration;
|
|
81
|
+
declare function UnregisterTenantLifecycleConsumer(name: string): void;
|
|
82
|
+
declare function IsTenantProvisioningCommitted(tenantId: string): Promise<boolean>;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
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.
|
|
88
|
+
|
|
89
|
+
`IsTenantProvisioningCommitted` 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.
|
|
90
|
+
|
|
91
|
+
## Registration and provisioning
|
|
92
|
+
|
|
93
|
+
`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>`.
|
|
94
|
+
|
|
95
|
+
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.
|
|
96
|
+
|
|
97
|
+
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.
|
package/package.json
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/interface-dms-saas",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
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.
|
|
61
|
-
"@antelopejs/interface-dms": ">=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.
|
|
65
|
-
"@antelopejs/interface-database": ">=0.1.
|
|
66
|
-
"@antelopejs/interface-database-decorators": ">=0.1.
|
|
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.
|
|
79
|
-
"@antelopejs/interface-database": ">=0.1.
|
|
80
|
-
"@antelopejs/interface-database-decorators": ">=0.1.
|
|
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": {}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { RegisteringProxy } from "@antelopejs/interface-core";
|
|
2
|
-
export declare const WORKSPACE_LIFECYCLE_TRANSITIONS: readonly ["created", "suspended", "reactivation_requested"];
|
|
3
|
-
export type WorkspaceLifecycleTransition = (typeof WORKSPACE_LIFECYCLE_TRANSITIONS)[number];
|
|
4
|
-
export interface WorkspaceLifecycleMessage {
|
|
5
|
-
tenantId: string;
|
|
6
|
-
operationId: string;
|
|
7
|
-
transition: WorkspaceLifecycleTransition;
|
|
8
|
-
requestedAt: Date;
|
|
9
|
-
}
|
|
10
|
-
export interface WorkspaceLifecycleReceipt {
|
|
11
|
-
receiptId: string;
|
|
12
|
-
effectiveAt?: Date;
|
|
13
|
-
}
|
|
14
|
-
export interface WorkspaceLifecycleConsumer {
|
|
15
|
-
name: string;
|
|
16
|
-
transitions: readonly WorkspaceLifecycleTransition[];
|
|
17
|
-
consume(message: WorkspaceLifecycleMessage): Promise<WorkspaceLifecycleReceipt>;
|
|
18
|
-
}
|
|
19
|
-
export interface WorkspaceLifecycleConsumerRegistration {
|
|
20
|
-
unregister(): void;
|
|
21
|
-
}
|
|
22
|
-
export declare const IsWorkspaceProvisioningCommitted: (tenantId: string) => Promise<boolean>;
|
|
23
|
-
export declare namespace internal {
|
|
24
|
-
const RegisterWorkspaceLifecycleConsumer: RegisteringProxy<(name: string, consumer: WorkspaceLifecycleConsumer) => void>;
|
|
25
|
-
}
|
|
26
|
-
export declare function UnregisterWorkspaceLifecycleConsumer(name: string): void;
|
|
27
|
-
export declare function RegisterWorkspaceLifecycleConsumer(consumer: WorkspaceLifecycleConsumer): WorkspaceLifecycleConsumerRegistration;
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.internal = exports.IsWorkspaceProvisioningCommitted = exports.WORKSPACE_LIFECYCLE_TRANSITIONS = void 0;
|
|
4
|
-
exports.UnregisterWorkspaceLifecycleConsumer = UnregisterWorkspaceLifecycleConsumer;
|
|
5
|
-
exports.RegisterWorkspaceLifecycleConsumer = RegisterWorkspaceLifecycleConsumer;
|
|
6
|
-
const interface_core_1 = require("@antelopejs/interface-core");
|
|
7
|
-
const CONSUMER_NAME_PATTERN = /^[a-z][a-z0-9._-]{2,63}$/;
|
|
8
|
-
exports.WORKSPACE_LIFECYCLE_TRANSITIONS = [
|
|
9
|
-
"created",
|
|
10
|
-
"suspended",
|
|
11
|
-
"reactivation_requested",
|
|
12
|
-
];
|
|
13
|
-
exports.IsWorkspaceProvisioningCommitted = (0, interface_core_1.InterfaceFunction)();
|
|
14
|
-
var internal;
|
|
15
|
-
(function (internal) {
|
|
16
|
-
internal.RegisterWorkspaceLifecycleConsumer = new interface_core_1.RegisteringProxy();
|
|
17
|
-
})(internal || (exports.internal = internal = {}));
|
|
18
|
-
function UnregisterWorkspaceLifecycleConsumer(name) {
|
|
19
|
-
internal.RegisterWorkspaceLifecycleConsumer.unregister(name);
|
|
20
|
-
}
|
|
21
|
-
function RegisterWorkspaceLifecycleConsumer(consumer) {
|
|
22
|
-
if (!CONSUMER_NAME_PATTERN.test(consumer.name)) {
|
|
23
|
-
throw new Error("Invalid workspace lifecycle consumer name");
|
|
24
|
-
}
|
|
25
|
-
internal.RegisterWorkspaceLifecycleConsumer.register(consumer.name, consumer);
|
|
26
|
-
return {
|
|
27
|
-
unregister: () => UnregisterWorkspaceLifecycleConsumer(consumer.name),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
//# sourceMappingURL=workspace-lifecycle.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-lifecycle.js","sourceRoot":"","sources":["../src/workspace-lifecycle.ts"],"names":[],"mappings":";;;AAiEA,oFAEC;AAWD,gFAUC;AAxFD,+DAGoC;AAEpC,MAAM,qBAAqB,GAAG,0BAA0B,CAAC;AAG5C,QAAA,+BAA+B,GAAG;IAC7C,SAAS;IACT,WAAW;IACX,wBAAwB;CAChB,CAAC;AA0CE,QAAA,gCAAgC,GAC3C,IAAA,kCAAiB,GAA0C,CAAC;AAG9D,IAAiB,QAAQ,CAIxB;AAJD,WAAiB,QAAQ;IACV,2CAAkC,GAAG,IAAI,iCAAgB,EAEnE,CAAC;AACN,CAAC,EAJgB,QAAQ,wBAAR,QAAQ,QAIxB;AAGD,SAAgB,oCAAoC,CAAC,IAAY;IAC/D,QAAQ,CAAC,kCAAkC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;AAC/D,CAAC;AAWD,SAAgB,kCAAkC,CAChD,QAAoC;IAEpC,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,CAAC;IACD,QAAQ,CAAC,kCAAkC,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC9E,OAAO;QACL,UAAU,EAAE,GAAG,EAAE,CAAC,oCAAoC,CAAC,QAAQ,CAAC,IAAI,CAAC;KACtE,CAAC;AACJ,CAAC"}
|