@antelopejs/dms-saas 0.2.0 → 0.3.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.
Files changed (195) hide show
  1. package/README.md +307 -33
  2. package/dist/billing-state/index.d.ts +1 -0
  3. package/dist/billing-state/index.js +1 -0
  4. package/dist/billing-state/index.js.map +1 -1
  5. package/dist/billing-state/past-due-banner.d.ts +4 -0
  6. package/dist/billing-state/past-due-banner.js +25 -0
  7. package/dist/billing-state/past-due-banner.js.map +1 -0
  8. package/dist/config/runtime.d.ts +6 -1
  9. package/dist/config/runtime.js +50 -1
  10. package/dist/config/runtime.js.map +1 -1
  11. package/dist/crons/recompute-billing-state.js +4 -1
  12. package/dist/crons/recompute-billing-state.js.map +1 -1
  13. package/dist/data-api/platformOwner/index.d.ts +1 -0
  14. package/dist/data-api/platformOwner/index.js +1 -0
  15. package/dist/data-api/platformOwner/index.js.map +1 -1
  16. package/dist/data-api/platformOwner/invitations.d.ts +22 -0
  17. package/dist/data-api/platformOwner/invitations.js +121 -0
  18. package/dist/data-api/platformOwner/invitations.js.map +1 -0
  19. package/dist/data-api/platformOwner/invoice-options.js +3 -2
  20. package/dist/data-api/platformOwner/invoice-options.js.map +1 -1
  21. package/dist/data-api/platformOwner/workspaces.d.ts +3 -0
  22. package/dist/data-api/platformOwner/workspaces.js +64 -12
  23. package/dist/data-api/platformOwner/workspaces.js.map +1 -1
  24. package/dist/data-api/tenant/tenant-invoices.js +2 -10
  25. package/dist/data-api/tenant/tenant-invoices.js.map +1 -1
  26. package/dist/hooks/register.js +2 -1
  27. package/dist/hooks/register.js.map +1 -1
  28. package/dist/implementations/dms-saas/billing.d.ts +2 -1
  29. package/dist/implementations/dms-saas/billing.js +6 -0
  30. package/dist/implementations/dms-saas/billing.js.map +1 -1
  31. package/dist/index.js +5 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/invoice-line-items/billing-window.js +2 -2
  34. package/dist/invoice-line-items/billing-window.js.map +1 -1
  35. package/dist/invoice-line-items/injection.d.ts +10 -0
  36. package/dist/invoice-line-items/injection.js +7 -3
  37. package/dist/invoice-line-items/injection.js.map +1 -1
  38. package/dist/operator-actions/db/lifecycle-delivery.model.js +1 -3
  39. package/dist/operator-actions/db/lifecycle-delivery.model.js.map +1 -1
  40. package/dist/operator-actions/db/lifecycle-delivery.table.d.ts +1 -1
  41. package/dist/operator-actions/db/lifecycle-delivery.table.js.map +1 -1
  42. package/dist/operator-actions/db/operator-action.model.js +1 -3
  43. package/dist/operator-actions/db/operator-action.model.js.map +1 -1
  44. package/dist/operator-actions/db/operator-action.table.d.ts +1 -1
  45. package/dist/operator-actions/db/operator-action.table.js.map +1 -1
  46. package/dist/operator-actions/index.d.ts +1 -0
  47. package/dist/operator-actions/index.js +1 -0
  48. package/dist/operator-actions/index.js.map +1 -1
  49. package/dist/operator-actions/invitation-commands.d.ts +12 -0
  50. package/dist/operator-actions/invitation-commands.js +46 -0
  51. package/dist/operator-actions/invitation-commands.js.map +1 -0
  52. package/dist/operator-actions/types.d.ts +1 -1
  53. package/dist/pages/platform/dashboard.js +2 -4
  54. package/dist/pages/platform/dashboard.js.map +1 -1
  55. package/dist/pages/platform/workspaces/detail.js +40 -1
  56. package/dist/pages/platform/workspaces/detail.js.map +1 -1
  57. package/dist/pages/platform/workspaces/index.d.ts +2 -8
  58. package/dist/pages/platform/workspaces/index.js +27 -111
  59. package/dist/pages/platform/workspaces/index.js.map +1 -1
  60. package/dist/pages/public/legal/privacy-policy.js +2 -1
  61. package/dist/pages/public/legal/privacy-policy.js.map +1 -1
  62. package/dist/pages/public/legal/terms-and-conditions.js +2 -1
  63. package/dist/pages/public/legal/terms-and-conditions.js.map +1 -1
  64. package/dist/pages/public/legal/terms-of-use.js +2 -1
  65. package/dist/pages/public/legal/terms-of-use.js.map +1 -1
  66. package/dist/plans/index.d.ts +1 -0
  67. package/dist/plans/index.js +1 -0
  68. package/dist/plans/index.js.map +1 -1
  69. package/dist/plans/platform-support.d.ts +2 -0
  70. package/dist/plans/platform-support.js +19 -0
  71. package/dist/plans/platform-support.js.map +1 -0
  72. package/dist/plans/seat-capacity.d.ts +9 -2
  73. package/dist/plans/seat-capacity.js +59 -12
  74. package/dist/plans/seat-capacity.js.map +1 -1
  75. package/dist/plans/seat-hooks.js +34 -13
  76. package/dist/plans/seat-hooks.js.map +1 -1
  77. package/dist/routes/platformOwner/credit-notes-issue.js +2 -1
  78. package/dist/routes/platformOwner/credit-notes-issue.js.map +1 -1
  79. package/dist/routes/platformOwner/dashboard.d.ts +8 -6
  80. package/dist/routes/platformOwner/dashboard.js +31 -17
  81. package/dist/routes/platformOwner/dashboard.js.map +1 -1
  82. package/dist/routes/platformOwner/index.d.ts +1 -0
  83. package/dist/routes/platformOwner/index.js +1 -0
  84. package/dist/routes/platformOwner/index.js.map +1 -1
  85. package/dist/routes/platformOwner/workspace-invitations.d.ts +13 -0
  86. package/dist/routes/platformOwner/workspace-invitations.js +73 -0
  87. package/dist/routes/platformOwner/workspace-invitations.js.map +1 -0
  88. package/dist/routes/public/register.d.ts +7 -3
  89. package/dist/routes/public/register.js +42 -27
  90. package/dist/routes/public/register.js.map +1 -1
  91. package/dist/routes/tenant/tenant-billing.d.ts +7 -11
  92. package/dist/routes/tenant/tenant-billing.js +34 -26
  93. package/dist/routes/tenant/tenant-billing.js.map +1 -1
  94. package/dist/routes/tenant/tenant-plan-ops.js +2 -2
  95. package/dist/routes/tenant/tenant-plan-ops.js.map +1 -1
  96. package/dist/routes/tenant/tenant-plan.d.ts +1 -0
  97. package/dist/routes/tenant/tenant-plan.js +27 -2
  98. package/dist/routes/tenant/tenant-plan.js.map +1 -1
  99. package/dist/routes/tenant/tenant-seats.d.ts +2 -4
  100. package/dist/routes/tenant/tenant-seats.js.map +1 -1
  101. package/dist/routes/tenant/workspaces.d.ts +2 -0
  102. package/dist/routes/tenant/workspaces.js +10 -4
  103. package/dist/routes/tenant/workspaces.js.map +1 -1
  104. package/dist/stripe/upcoming-invoice.d.ts +21 -0
  105. package/dist/stripe/upcoming-invoice.js +42 -0
  106. package/dist/stripe/upcoming-invoice.js.map +1 -0
  107. package/dist/stripe/webhook-dispatch.js +2 -0
  108. package/dist/stripe/webhook-dispatch.js.map +1 -1
  109. package/dist/types/config.d.ts +9 -0
  110. package/dist/types/config.js +7 -0
  111. package/dist/types/config.js.map +1 -1
  112. package/dist/upcoming-invoice/db/preview-cache.model.d.ts +38 -0
  113. package/dist/upcoming-invoice/db/preview-cache.model.js +57 -0
  114. package/dist/upcoming-invoice/db/preview-cache.model.js.map +1 -0
  115. package/dist/upcoming-invoice/db/preview-cache.table.d.ts +10 -0
  116. package/dist/upcoming-invoice/db/preview-cache.table.js +42 -0
  117. package/dist/upcoming-invoice/db/preview-cache.table.js.map +1 -0
  118. package/dist/upcoming-invoice/eligibility.d.ts +17 -0
  119. package/dist/upcoming-invoice/eligibility.js +52 -0
  120. package/dist/upcoming-invoice/eligibility.js.map +1 -0
  121. package/dist/upcoming-invoice/invalidation.d.ts +2 -0
  122. package/dist/upcoming-invoice/invalidation.js +39 -0
  123. package/dist/upcoming-invoice/invalidation.js.map +1 -0
  124. package/dist/upcoming-invoice/mapping.d.ts +8 -0
  125. package/dist/upcoming-invoice/mapping.js +82 -0
  126. package/dist/upcoming-invoice/mapping.js.map +1 -0
  127. package/dist/upcoming-invoice/outcomes.d.ts +13 -0
  128. package/dist/upcoming-invoice/outcomes.js +15 -0
  129. package/dist/upcoming-invoice/outcomes.js.map +1 -0
  130. package/dist/upcoming-invoice/preview.d.ts +3 -0
  131. package/dist/upcoming-invoice/preview.js +80 -0
  132. package/dist/upcoming-invoice/preview.js.map +1 -0
  133. package/dist/upcoming-invoice/pricing.d.ts +3 -0
  134. package/dist/upcoming-invoice/pricing.js +84 -0
  135. package/dist/upcoming-invoice/pricing.js.map +1 -0
  136. package/dist/upcoming-invoice/usage.d.ts +14 -0
  137. package/dist/upcoming-invoice/usage.js +53 -0
  138. package/dist/upcoming-invoice/usage.js.map +1 -0
  139. package/dist/workspaces/billing-identity.d.ts +19 -0
  140. package/dist/workspaces/billing-identity.js +51 -0
  141. package/dist/workspaces/billing-identity.js.map +1 -0
  142. package/dist/workspaces/default-plan.d.ts +5 -0
  143. package/dist/workspaces/default-plan.js +110 -0
  144. package/dist/workspaces/default-plan.js.map +1 -0
  145. package/dist/workspaces/index.d.ts +2 -0
  146. package/dist/workspaces/index.js +2 -0
  147. package/dist/workspaces/index.js.map +1 -1
  148. package/dist/workspaces/invitations.d.ts +26 -0
  149. package/dist/workspaces/invitations.js +112 -0
  150. package/dist/workspaces/invitations.js.map +1 -0
  151. package/dist/workspaces/provisioning.d.ts +3 -3
  152. package/dist/workspaces/provisioning.js +49 -11
  153. package/dist/workspaces/provisioning.js.map +1 -1
  154. package/frontend-vue/app/components/BillingIdentityFields.vue +179 -0
  155. package/frontend-vue/app/components/PastDueBanner.vue +81 -0
  156. package/frontend-vue/app/components/PaymentMethodCard.vue +94 -38
  157. package/frontend-vue/app/components/PlanComparisonModal.vue +62 -26
  158. package/frontend-vue/app/components/PlanUpgradeModal.vue +181 -0
  159. package/frontend-vue/app/components/PlatformNotes.vue +4 -0
  160. package/frontend-vue/app/components/RefundButton.vue +10 -1
  161. package/frontend-vue/app/components/Register.vue +49 -92
  162. package/frontend-vue/app/components/SeatQuotaBanner.vue +28 -0
  163. package/frontend-vue/app/components/TenantBillingForm.vue +52 -128
  164. package/frontend-vue/app/components/TenantPlanCard.vue +68 -10
  165. package/frontend-vue/app/components/UserSummaryCard.vue +12 -10
  166. package/frontend-vue/app/components/WorkspaceAdminCreateModal.vue +202 -0
  167. package/frontend-vue/app/components/WorkspaceCreateModal.vue +46 -9
  168. package/frontend-vue/app/components/WorkspaceInvitationLink.vue +106 -0
  169. package/frontend-vue/app/components/WorkspaceSummaryCard.vue +39 -15
  170. package/frontend-vue/app/components/WorkspaceSuspended.vue +3 -5
  171. package/frontend-vue/app/components/WorkspaceSwitcherWidget.vue +8 -5
  172. package/frontend-vue/app/composables/useBillingCountries.ts +25 -5
  173. package/frontend-vue/app/composables/useBillingDocumentType.ts +9 -5
  174. package/frontend-vue/app/composables/useBillingIdentity.ts +141 -0
  175. package/frontend-vue/app/composables/useInvitationLink.ts +69 -0
  176. package/frontend-vue/app/composables/usePastDueBannerAction.ts +20 -0
  177. package/frontend-vue/app/composables/usePlanComparison.ts +15 -0
  178. package/frontend-vue/app/composables/usePlanFeatureFormat.ts +312 -20
  179. package/frontend-vue/app/composables/usePlanFeatureLabel.ts +24 -0
  180. package/frontend-vue/app/composables/useSaasRegistration.ts +110 -223
  181. package/frontend-vue/app/composables/useSeatQuota.ts +8 -0
  182. package/frontend-vue/app/composables/useStripePaymentElement.ts +3 -3
  183. package/frontend-vue/app/composables/useWorkspaceAccessCache.ts +29 -2
  184. package/frontend-vue/app/composables/useWorkspaceFinalization.ts +7 -32
  185. package/frontend-vue/app/custom-pages/auth/no-workspace.vue +49 -194
  186. package/frontend-vue/app/plugins/auth-links.ts +0 -4
  187. package/frontend-vue/app/plugins/billing-data-types.ts +4 -2
  188. package/frontend-vue/app/types/runtime-config.ts +13 -0
  189. package/frontend-vue/dms.frontend.ts +10 -4
  190. package/frontend-vue/i18n/locales/saas-en-GB.json +161 -36
  191. package/frontend-vue/i18n/locales/saas-fr-FR.json +162 -37
  192. package/frontend-vue/package.json +0 -1
  193. package/frontend-vue/pnpm-lock.yaml +1 -125
  194. package/package.json +5 -5
  195. package/frontend-vue/app/middleware/workspace-suspended.global.ts +0 -59
package/README.md CHANGED
@@ -17,9 +17,8 @@ See the design documents in `dms/saas/` (sibling repo) for the full architectura
17
17
  The module registers `frontend-vue/dms.frontend.ts` with the Vue 3 Inertia
18
18
  adapter. It preserves the `DmsSaas*` component names, the `auth/no-workspace`
19
19
  page override, client-only displays, global guards, and English/French catalogs.
20
- Public module configuration moves from `public.cmsSaas` to `public.dmsSaas` —
21
- a breaking change for any frontend reading the old key. Stripe secrets are
22
- never exposed to the frontend.
20
+ Public module configuration is exposed under `public.dmsSaas`. Stripe secrets
21
+ are never exposed to the frontend.
23
22
 
24
23
  The backend integration requires `@antelopejs/dms`. Frontend source
25
24
  verification runs through `@antelopejs/dms-frontend` and the DMS frontend
@@ -41,7 +40,8 @@ environment or in `playground/.env`.
41
40
  An account can reach the product before it owns anything to log into: an OAuth
42
41
  sign-in for an unknown e-mail, or any entry point the core answers with
43
42
  `requires_tenant_assignment`. The `auth/no-workspace` page is where that
44
- account buys its way in — a workspace, a plan, a card — and
43
+ account opens its first workspace — on the free plan, with a card when
44
+ `registration.paymentMethod` asks for one — and
45
45
  `POST /api/saas/register/finalize` provisions it and answers with a token pair
46
46
  for the workspace it just created, so the visitor lands signed in instead of
47
47
  back on the login screen.
@@ -59,8 +59,8 @@ declare the finalize endpoint in the frontend server's environment:
59
59
  DMS_AUTH_ESTABLISH_ENDPOINTS=/api/saas/register/finalize
60
60
  ```
61
61
 
62
- Without it the loader answers `403` and a visitor who has paid stays signed
63
- out. A replacement completion screen keeps the same requirement, and can build
62
+ Without it the loader answers `403` and a visitor who has just registered stays
63
+ signed out. A replacement completion screen keeps the same requirement, and can build
64
64
  its request with the auto-imported `buildSessionEstablishRequest()` helper
65
65
  rather than restating the endpoint and the payload shape.
66
66
 
@@ -73,6 +73,11 @@ permissions, regardless of assigned roles. Permissions outside the plan are
73
73
  removed even when a role grants them. Other members retain only the intersection
74
74
  of their incoming permissions and the resolved plan.
75
75
 
76
+ A plan grants exactly the ids it lists, nothing nested under them: the plan
77
+ editor's permission tree adds a node's descendants and ancestors when it is
78
+ ticked, so plans built from the UI list every component id, and plans seeded by
79
+ code must list them too.
80
+
76
81
  Tenant ownership never grants `*`, even if a plan contains it. An incoming `*`
77
82
  (including the DMS platform owner's wildcard) retains the existing behavior:
78
83
  the wildcard survives, and other incoming permissions are intersected with the
@@ -131,6 +136,164 @@ accepted, each is promoted before the tenant message is persisted, and failed
131
136
  writes move promoted files back to staging. Ticket-specific metadata routes
132
137
  verify that a key belongs to the requested thread before issuing its read URL.
133
138
 
139
+ ## Registration
140
+
141
+ Public registration is short: an account (name, e-mail, password) and the
142
+ acceptance of the terms, plus a card when the deployment asks for one. The
143
+ workspace it opens lands on the catalogue's first free plan — the lowest
144
+ `order` among active plans priced 0 and open to individuals — under a default
145
+ name its owner renames from the workspace settings. Customer type, billing
146
+ address, VAT number and plan choice belong to the upgrade flow. Registration
147
+ answers `409 saas.errors.plan.no_free_plan` while the catalogue has no such
148
+ plan.
149
+
150
+ `POST /api/saas/register` holds the password to the DMS password policy
151
+ (`isPasswordCompliant` from `@antelopejs/interface-dms/auth/password`) before
152
+ it looks the e-mail up or creates anything, and answers
153
+ `400 saas.errors.registration.password_policy` otherwise. The OAuth completion
154
+ carries no password.
155
+
156
+ Two module options shape it:
157
+
158
+ ```json
159
+ {
160
+ "modules": {
161
+ "dms-saas": {
162
+ "config": {
163
+ "admissionMode": "open",
164
+ "registration": { "paymentMethod": "required" }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ ```
170
+
171
+ `registration.paymentMethod` decides whether registration asks for a card:
172
+
173
+ | Value | Behaviour |
174
+ | --- | --- |
175
+ | `required` (default) | The card step is shown and must be completed. The workspace gets a Stripe customer and a free subscription on that card, and the free-workspace-per-card cap applies. |
176
+ | `optional` | The card step is shown, and the visitor may choose to add a card later. Without a card the workspace is card-less, as under `none`. |
177
+ | `none` | The card step is never shown, `GET /api/saas/register/setup-intent` answers `400 saas.errors.registration.payment_method_disabled`, and Stripe is never called: the workspace gets a local free subscription with no Stripe customer, which the upgrade checkout creates when the owner first pays. A card sent anyway is ignored. |
178
+
179
+ Under `optional` and `none`, a card-less workspace has no card fingerprint, so
180
+ the free-workspace-per-card cap does not apply to it: the only remaining limit
181
+ on free workspaces is one account per e-mail address.
182
+
183
+ The option covers both public entry points — `POST /api/saas/register` and the
184
+ `auth/no-workspace` completion — and nothing else: invitation sign-up never
185
+ asks for a plan or a card, whatever `admissionMode` and
186
+ `registration.paymentMethod` say. Tenant-side workspace creation keeps
187
+ requiring a card.
188
+
189
+ `admissionMode: "invitation-only"` closes public registration: every
190
+ `/api/saas/register` route answers `403 saas.errors.registration_closed` before
191
+ looking at the card policy, the registration screens render a "registration by
192
+ invitation only" state instead of the form, and the login page drops its
193
+ sign-up link. Invitations keep working.
194
+
195
+ Both values reach the browser through the frontend module options
196
+ (`useDmsRuntimeConfig().public.dmsSaas.admissionMode` and
197
+ `.registrationPaymentMethod`); `useSaasRegistration()` already reads them.
198
+
199
+ The Stripe SetupIntent behind the card step is restricted to cards on the
200
+ server (`payment_method_types: ["card"]`); the Payment Element is created from
201
+ its client secret alone, since Stripe refuses `paymentMethodTypes` next to a
202
+ `clientSecret`.
203
+
204
+ ## Default plan
205
+
206
+ A workspace is never without a plan. The default plan is the catalogue's
207
+ first free plan — the lowest `order` among active plans priced 0 and open to
208
+ individuals — unless `defaultPlanSlug` names another such plan:
209
+
210
+ ```json
211
+ { "modules": { "dms-saas": { "config": { "defaultPlanSlug": "free" } } } }
212
+ ```
213
+
214
+ It is what registration opens, and it is attached, as an active card-less
215
+ subscription, to every workspace that has no subscription at all — the
216
+ platform's `default` tenant and workspaces created outside dms-saas included.
217
+ The backfill runs at startup and with the billing-state recompute every 15
218
+ minutes, and the billing page covers a workspace on first read; each pass is
219
+ idempotent and never touches a workspace that holds any subscription. While
220
+ the catalogue has no free plan, nothing is attached and a warning is logged.
221
+
222
+ Once attached, the plan's permissions cap the workspace's members like any
223
+ other plan (see [Tenant owner permissions](#tenant-owner-permissions)).
224
+
225
+ ## Plan feature labels and values
226
+
227
+ A feature row stores one display name and one tooltip. The tenant plan pages
228
+ (the plan card summary and the plan comparison table) render both through the
229
+ DMS display string convention: a value starting with `$` is an i18n key,
230
+ anything else is shown as written. A module declaring features either stores
231
+ plain text or stores `$<key>` and ships that key in its own frontend locale
232
+ files (`frontend-vue/i18n/locales/<name>-<locale>.json`):
233
+
234
+ ```ts
235
+ {
236
+ displayName: "$cloud.plan_features.egress.label",
237
+ tooltip: "$cloud.plan_features.egress.tooltip",
238
+ }
239
+ ```
240
+
241
+ A key missing in the viewer's locale falls back to the fallback locale; a
242
+ tooltip key missing everywhere hides the tooltip.
243
+
244
+ Values are formatted from the feature's `valueType` and `unit`: `-1` reads as
245
+ unlimited, booleans as ✓/—, numbers are grouped in the viewer's locale. A
246
+ `per <unit>` unit makes the value a price in the plan's currency, and
247
+ `currency units` an amount of it. Known units are scaled to something a person
248
+ reads at a glance; any other unit is shown verbatim after the grouped number.
249
+ A text value follows the same convention as labels: a `$<key>` value is
250
+ translated in the declaring module's locales, anything else is shown as stored.
251
+
252
+ | Stored unit | Quantity reads as | `per <unit>` price reads as |
253
+ |-------------|-------------------|-----------------------------|
254
+ | `byte(s)` | bytes, KB, MB, GB or TB (powers of 1000) | per GB |
255
+ | `minute(s)` | minutes | per minute |
256
+
257
+ A module declaring features in its own units teaches the plan pages how they
258
+ read with `registerPlanFeatureUnit`, auto-imported like the other dms-saas
259
+ composables. Call it from a frontend plugin that runs on the server and in the
260
+ browser, so server-rendered and hydrated values agree. Each scale says how
261
+ many stored units it holds; a quantity takes the largest scale it fills, and a
262
+ `per <unit>` price reads per `priceScale`. Labels are full i18n keys the module
263
+ ships in its own locales, each a node holding a `quantity` message
264
+ (`{value}`, pluralised on the scaled value) and a `price` message (`{price}`):
265
+
266
+ ```ts
267
+ // frontend-vue/app/plugins/plan-feature-units.ts
268
+ const MINUTES_PER_HOUR = 60;
269
+ const vcpuHour = { label: "cloud.plan.units.vcpu_hour", size: MINUTES_PER_HOUR };
270
+
271
+ export default defineDmsPlugin(() => {
272
+ registerPlanFeatureUnit(["vCPU-minute", "vCPU-minutes"], {
273
+ scales: [vcpuHour],
274
+ priceScale: vcpuHour,
275
+ });
276
+ });
277
+ ```
278
+
279
+ ```json
280
+ {
281
+ "cloud": {
282
+ "plan": {
283
+ "units": {
284
+ "vcpu_hour": {
285
+ "quantity": "{value} vCPU-hour | {value} vCPU-hours",
286
+ "price": "{price} / vCPU-hour"
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+ ```
293
+
294
+ Units match case-insensitively, and a registered unit takes precedence over a
295
+ built-in one of the same name.
296
+
134
297
  ## Extension points
135
298
 
136
299
  ### Consuming this module
@@ -144,7 +307,7 @@ boots the registration graph:
144
307
  | Subpath | Surface |
145
308
  | --- | --- |
146
309
  | `@antelopejs/interface-dms-saas` | every extension point except `db`: billing, data API, pages, plans, provisioning and registration flat, plus the `invoiceLineItems` and `workspaceLifecycle` namespaces |
147
- | `@antelopejs/interface-dms-saas/billing` | customer balance and complimentary subscription state |
310
+ | `@antelopejs/interface-dms-saas/billing` | customer balance, upcoming invoice preview and complimentary subscription state |
148
311
  | `@antelopejs/interface-dms-saas/data-api` | hidden-value data API filters |
149
312
  | `@antelopejs/interface-dms-saas/db` | canonical SaaS tables and data models |
150
313
  | `@antelopejs/interface-dms-saas/hidden-filter` | the `HiddenStringFilter` decorator on its own |
@@ -199,19 +362,31 @@ races the bundled one instead of overriding it:
199
362
 
200
363
  Then register your own page on the `register` slug and build it on the
201
364
  `useSaasRegistration()` composable the Vue adapter auto-imports. It holds the
202
- whole flow — plan loading and filtering by customer type, the Stripe setup
203
- intent and card confirmation, the order the requirements are validated in, the
204
- provisioning call and the landing route — and renders nothing:
365
+ whole flow — the card policy, the Stripe setup intent and card confirmation,
366
+ the order the requirements are validated in, the provisioning call and the
367
+ landing route — and renders nothing, so a re-themed screen keeps the same
368
+ steps:
205
369
 
206
370
  ```vue
207
371
  <script setup lang="ts">
208
- const { form, plans, errorMessage, isSubmitting, formatPlanPrice, submit } =
209
- useSaasRegistration({ redirectTo: "/welcome" });
372
+ const {
373
+ form, // name, email, password, hasAcceptedLegal, skipsPaymentMethod
374
+ paymentElementId,
375
+ canSkipPaymentMethod, // true under `optional`
376
+ isPaymentStepVisible, // bind with v-show so the Stripe element stays mounted
377
+ passwordStrength, // { strength, score, color } for <DmsPasswordStrength>
378
+ isRegistrationClosed, // render the invitation-only state instead of the form
379
+ errorMessage,
380
+ isSubmitting,
381
+ submit,
382
+ } = useSaasRegistration({ redirectTo: "/welcome" });
210
383
  </script>
211
384
  ```
212
385
 
213
386
  Nothing it returns throws: a failure surfaces as a translated `errorMessage`,
214
- and `submit()` resolves to the new tenant id or `null`. Every screen stays
387
+ and `submit()` resolves to the new tenant id or `null`. A password outside the
388
+ DMS policy stops `submit()` before the card is confirmed. The workspace name is
389
+ the localised `saas.register.default_workspace_name`. Every screen stays
215
390
  enabled by default, so an existing project needs no configuration change.
216
391
 
217
392
  ### Registration extras and the provisioning hook
@@ -354,6 +529,77 @@ export async function stop(): Promise<void> {
354
529
  why it is only reported at error level once dms-saas is attached. Two modules
355
530
  registering the same `id` before that collapse into one silently, so pick an
356
531
  `id` no other module could plausibly choose.
532
+ - Resolvers are also called to price the [upcoming invoice
533
+ preview](#upcoming-invoice-preview). Such a call carries `isPreview: true`,
534
+ a synthetic `upcoming_<subscription id>` `invoiceId`, and a `periodEnd` set
535
+ to the time of the request: the running cycle's usage so far. Its lines are
536
+ only quoted to Stripe, never invoiced. A resolver that throws or returns an
537
+ invalid line then makes the preview `unavailable` rather than short.
538
+
539
+ ### Upcoming invoice preview
540
+
541
+ `GetUpcomingInvoicePreview` returns what Stripe will bill the current
542
+ workspace next, tax included, so a page can show the exact VAT and total before
543
+ the invoice exists. Stripe computes every figure (Stripe Tax: country, VAT
544
+ number, reverse charge); dms-saas copies them without recomputing any.
545
+
546
+ ```ts
547
+ import { GetUpcomingInvoicePreview } from "@antelopejs/interface-dms-saas/billing";
548
+
549
+ const preview = await GetUpcomingInvoicePreview({ tenantId, userId });
550
+ if (preview.status === "available") {
551
+ const { taxMinorUnits, totalMinorUnits, currency } = preview;
552
+ render({ tax: taxMinorUnits, total: totalMinorUnits, currency });
553
+ }
554
+ ```
555
+
556
+ The scope must come from a trusted authentication boundary: the call rejects
557
+ with `403` a user who is not a member of the tenant or is refused by the tenant
558
+ access gate (a workspace awaiting its first payment, or suspended).
559
+
560
+ The result is always one of three states, and Stripe errors never throw:
561
+
562
+ | `status` | Meaning | `reason` |
563
+ | --- | --- | --- |
564
+ | `available` | Stripe priced the next invoice | — |
565
+ | `absent` | Nothing to bill next | `free_plan`, `complimentary`, `customer_not_configured`, `subscription_not_configured` (no plan, or no Stripe subscription yet), `no_upcoming_invoice` (the subscription ends with the current cycle) |
566
+ | `unavailable` | An invoice is due but cannot be priced exactly right now; the cause is logged | `stripe_not_configured` (placeholder keys), `tax_not_configured` (Stripe Tax inactive), `tax_location_invalid`, `tax_location_required`, `tax_calculation_failed`, `usage_unavailable` (a line items provider failed), `provider_error` (any other Stripe failure) |
567
+
568
+ A missing payment method does not prevent a preview: Stripe prices the invoice
569
+ all the same.
570
+
571
+ An `available` preview carries:
572
+
573
+ - `currency` (uppercase ISO 4217) and integer amounts in its minor units:
574
+ `subtotalMinorUnits` (lines before discounts and tax),
575
+ `totalExcludingTaxMinorUnits`, `taxMinorUnits`, `totalMinorUnits` (tax
576
+ included) and `amountDueMinorUnits` (after the customer balance).
577
+ - `taxes`: one entry per tax Stripe applied, with its amount, taxable amount,
578
+ `ratePercentage`, `country`, `taxType` (`vat`…), `taxabilityReason` and
579
+ `isReverseCharge`. `taxCountry` and `isReverseCharge` summarize them; a
580
+ reverse-charged invoice has a zero tax amount and `isReverseCharge: true`.
581
+ - `lines`: `subscription` for the plan, `usage` for the lines quoted by the
582
+ [invoice line items](#invoice-line-items) providers (with their
583
+ `usageLineKey`), `invoice_item` for any other pending Stripe item. Each has
584
+ its pre-tax amount, its tax and its period. `hasMoreLines` flags a list
585
+ Stripe truncated; the totals are always complete.
586
+ - `periodStart` / `periodEnd` (the cycle the invoice closes), `billingDate`
587
+ (when Stripe issues it), `usageThrough` (how far the quoted usage runs, null
588
+ without usage lines) and `computedAt`. Dates are ISO 8601 strings.
589
+
590
+ Usage is quoted up to the time of the request with the same providers, paid
591
+ coverage window and line validation as the renewal invoice, so the preview's
592
+ tax covers plan and usage together. It is an estimate of a cycle still running:
593
+ the renewal invoice bills the whole cycle.
594
+
595
+ Previews are cached per workspace for an hour, shared by every instance through
596
+ the database. Set `upcomingInvoicePreviewCacheTtlSeconds` to change that
597
+ lifetime, or to `0` to price every request. A cached preview is dropped as soon
598
+ as the workspace's subscription (plan change, status) or billing identity
599
+ (address, VAT number) changes locally, and when Stripe reports an invoice
600
+ created, finalized, paid or voided, a subscription updated or deleted, or a
601
+ customer updated. `unavailable` results are never cached. Display
602
+ `computedAt` to tell the reader how current the figures are.
357
603
 
358
604
  ### Workspace settings pages
359
605
 
@@ -464,7 +710,7 @@ jobs never use timed takeover; startup marks interrupted jobs as
464
710
  `reconciliation_required`, retaining their per-workspace evidence and pending
465
711
  subscription intents instead of blindly replaying effects.
466
712
 
467
- The historical `migrate-and-delete` endpoint now requests migration only.
713
+ The `migrate-and-delete` endpoint requests a migration only.
468
714
  Inspect `GET /api/saas/plans-deletion/migrations/:migrationId` for the snapshot,
469
715
  outcomes, and reconciliation requirements. **Snapshot completion does not retire
470
716
  the source plan.** The source remains available even at zero observed references;
@@ -496,9 +742,8 @@ the terminal receipt, and does not promise exactly-once delivery or external ord
496
742
 
497
743
  Stripe webhook dispatch admits an event once and fences completion with its
498
744
  admitted revision. An old `pending` record never permits timed takeover. Handler
499
- failures become `reconciliation_required`; legacy `failed` records also block
500
- replay because they may contain partial effects. Redelivery receives an error
501
- while unresolved, rather than a successful acknowledgement or another execution.
745
+ failures become `reconciliation_required`, which blocks replay because the
746
+ attempt may have partial effects. Redelivery receives an error while unresolved, rather than a successful acknowledgement or another execution.
502
747
 
503
748
  Inspect `stripe_webhook_events` by Stripe event ID for its result, revision, and
504
749
  error message. Recovery requires establishing the prior executor's quiescence
@@ -509,12 +754,9 @@ remain indefinitely. The existing retention policy still removes old terminal
509
754
 
510
755
  Revision mutations require the published database interface `0.1.6` or later
511
756
  and an adapter implementing that contract. The MongoDB test adapter uses `1.3.1`,
512
- and any DMS release carrying this module satisfies the requirement. New records
513
- initialize revisions; legacy PostgreSQL rows with a declared revision column
514
- require an explicit revision backfill
515
- before these mutations can proceed. An absent revision is not equivalent to
516
- `null`, and unsupported bootstrap fails closed. Do not run this backfill against
517
- shared data without a separately approved migration and quiescent writers.
757
+ and any DMS release carrying this module satisfies the requirement. Every
758
+ record is written with a revision, and these mutations refuse a record without
759
+ one; see [Upgrading](#upgrading) for rows written by earlier builds.
518
760
 
519
761
  Lifecycle-dependent modules register a required consumer through the public
520
762
  interface:
@@ -635,10 +877,7 @@ interrupted pre-payment preparation requires payment/provisioning recovery;
635
877
  it never emits `created` merely because the tenant exists. HTTP creation requests
636
878
  are not made idempotent by the delivery operation ID.
637
879
 
638
- These additions require a dms-saas package built from this source revision (or
639
- a later release containing it), including its lifecycle table fields and worker.
640
- The previously published `0.2.5` package does not provide the gate or `created`
641
- contract. Register consumers before startup reconciliation; use the existing
880
+ Register consumers before startup reconciliation; use the existing
642
881
  tenant inventory and gate for bootstrap/backfill rather than treating `created`
643
882
  as a historical inventory API.
644
883
 
@@ -666,7 +905,6 @@ uses the existing database operation receipt to authorize publication. Published
666
905
  prerequisites are the database interface `0.1.6`, the storage interface `0.1.3`,
667
906
  and a compatible provider: local `0.1.4` or S3 `0.1.3`, all of which any DMS
668
907
  release carrying this module provides. Tests use MongoDB `1.3.1`.
669
- There is no legacy move/existence fallback.
670
908
 
671
909
  Source admissions remain insert-only and permanent. Rejected work cannot publish,
672
910
  even if a delayed promotion finishes. Owner-initiated paginated support recovery
@@ -684,8 +922,44 @@ cleanup pass, so storage expiry/operational cleanup must account for them. Do no
684
922
  delete receipts to free a source for another request or garbage-collect a file
685
923
  that a committed support message still references.
686
924
 
687
- The earlier draft seal schema has no production installations and is replaced,
688
- not migrated online. If retaining development fixtures from that draft, stop
689
- writers and reconcile or discard those disposable support fixtures and their
690
- private files offline before adopting this schema. Do not reinterpret old seal
691
- records as confirmed promotion ownership.
925
+ ## Upgrading
926
+
927
+ ### Rows written by earlier builds
928
+
929
+ Billing no longer infers state from rows that predate the current fields. Rows
930
+ imported from the pre-DMS codebase or written by earlier builds must be fixed
931
+ in the database, with writers stopped, before upgrading:
932
+
933
+ - **`tenant_subscriptions.isComplimentary`** is required. Only
934
+ `isComplimentary: true` makes a subscription complimentary; a missing value
935
+ is read as not complimentary. Set `isComplimentary: true` on admin gifts that
936
+ lack it (typically rows with no `stripeCustomerId`, no `stripeSubscriptionId`
937
+ and a status other than `pending_payment`), otherwise they are billed and
938
+ shown as paid plans. Backfill `isComplimentary: false` on every other row
939
+ that lacks the field, including workspaces created through paid provisioning
940
+ before this change, so the stored rows match the table type.
941
+ - **`invoices.documentType`** is required. Invoice lists and reads only match
942
+ `documentType: "invoice"`; a row without it is neither listed nor readable.
943
+ Set `documentType: "invoice"` on every invoice row that lacks it.
944
+ - **`stripe_webhook_events.result`** no longer accepts `failed`. Such attempts
945
+ may carry partial effects: establish their outcome as described for
946
+ `reconciliation_required` above, then set their result to
947
+ `reconciliation_required` or delete the record once resolved.
948
+ - **`revision`** is required on `tenant_subscriptions`, `segments`,
949
+ `user_segments`, `plan_migrations`, `stripe_webhook_events`,
950
+ `saas_lifecycle_deliveries` and `saas_operator_actions`. Revision-fenced
951
+ writes no longer adopt a row without one: they refuse it, so the workflow
952
+ holding that row stalls. Give every row lacking the field (absent or `null`)
953
+ an initial revision, any unique string such as a fresh UUID. Segments created
954
+ through the platform data API by earlier builds are the usual case.
955
+ - **`tenant_subscriptions.paidUsagePeriods`** is required. It is the only
956
+ source of usage coverage: a renewal invoice bills usage only inside a period
957
+ recorded for its own Stripe subscription, and a missing ledger is no longer
958
+ read as "bill the whole window". Rows lacking it (or holding `null`) are
959
+ typically default-plan, free-plan and pending-checkout subscriptions, and
960
+ workspaces created through paid provisioning. Set it by kind:
961
+ - a row with a `stripeSubscriptionId`, not complimentary:
962
+ `[{ stripeSubscriptionId, start, end: null }]`, where `start` is the Stripe
963
+ subscription's `start_date` (or any earlier date), so renewals keep billing
964
+ their whole window;
965
+ - every other row (no Stripe subscription, or `isComplimentary: true`): `[]`.
@@ -1,4 +1,5 @@
1
1
  import { type BillingState, type TenantSubscription } from "../db";
2
+ export * from "./past-due-banner";
2
3
  export * from "./recovery";
3
4
  export declare function deriveBillingState(subscription: TenantSubscription | undefined): BillingState;
4
5
  export declare function recomputeTenantBillingState(tenantId: string): Promise<void>;
@@ -20,6 +20,7 @@ exports.recomputeAllTenantBillingStates = recomputeAllTenantBillingStates;
20
20
  const interface_database_decorators_1 = require("@antelopejs/interface-database-decorators");
21
21
  const db_1 = require("@antelopejs/interface-dms/db");
22
22
  const db_2 = require("../db");
23
+ __exportStar(require("./past-due-banner"), exports);
23
24
  __exportStar(require("./recovery"), exports);
24
25
  const ACTIVE_STATUS = "active";
25
26
  const FREE_STATE = "free";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/billing-state/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAeA,gDAWC;AAED,kEAwBC;AAED,0EAMC;AA5DD,6FAAqE;AACrE,qDAA2D;AAC3D,8BAKe;AAEf,6CAA2B;AAE3B,MAAM,aAAa,GAAG,QAAQ,CAAC;AAC/B,MAAM,UAAU,GAAiB,MAAM,CAAC;AACxC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAE/B,SAAgB,kBAAkB,CAChC,YAA4C;IAE5C,IAAI,CAAC,YAAY;QAAE,OAAO,UAAU,CAAC;IACrC,IACE,YAAY,CAAC,MAAM,KAAK,aAAa;QACrC,CAAC,YAAY,CAAC,oBAAoB,EAClC,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,OAAO,YAAY,CAAC,MAAM,CAAC;AAC7B,CAAC;AAEM,KAAK,UAAU,2BAA2B,CAC/C,QAAgB;IAEhB,MAAM,KAAK,GAAG,IAAA,wCAAQ,EAAC,4BAAuB,CAAC,CAAC;IAChD,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,oBAAoB,EAAE,OAAO,EAAE,EAAE,CAAC;QAChE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,QAAQ,EAAE,SAAS;YAAE,OAAO;QAChC,MAAM,YAAY,GAAG,MAAM,IAAA,wCAAQ,EACjC,4BAAuB,EACvB,QAAQ,CACT,CAAC,OAAO,EAAE,CAAC;QACZ,IAAI,YAAY,EAAE,iBAAiB;YAAE,OAAO;QAC5C,IACE,MAAM,KAAK,CAAC,eAAe,CACzB,QAAQ,EACR,kBAAkB,CAAC,YAAY,CAAC,EAChC,QAAQ,CACT;YAED,OAAO;IACX,CAAC;IACD,MAAM,IAAI,KAAK,CACb,gEAAgE,CACjE,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,+BAA+B;IACnD,MAAM,OAAO,GAAG,MAAM,IAAA,wCAAQ,EAAC,gBAAW,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC;IACrE,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC,MAAM,CAAC,GAAG;YAAE,SAAS;QAC1B,MAAM,2BAA2B,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/billing-state/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAgBA,gDAWC;AAED,kEAwBC;AAED,0EAMC;AA7DD,6FAAqE;AACrE,qDAA2D;AAC3D,8BAKe;AAEf,oDAAkC;AAClC,6CAA2B;AAE3B,MAAM,aAAa,GAAG,QAAQ,CAAC;AAC/B,MAAM,UAAU,GAAiB,MAAM,CAAC;AACxC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAE/B,SAAgB,kBAAkB,CAChC,YAA4C;IAE5C,IAAI,CAAC,YAAY;QAAE,OAAO,UAAU,CAAC;IACrC,IACE,YAAY,CAAC,MAAM,KAAK,aAAa;QACrC,CAAC,YAAY,CAAC,oBAAoB,EAClC,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,OAAO,YAAY,CAAC,MAAM,CAAC;AAC7B,CAAC;AAEM,KAAK,UAAU,2BAA2B,CAC/C,QAAgB;IAEhB,MAAM,KAAK,GAAG,IAAA,wCAAQ,EAAC,4BAAuB,CAAC,CAAC;IAChD,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,oBAAoB,EAAE,OAAO,EAAE,EAAE,CAAC;QAChE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,QAAQ,EAAE,SAAS;YAAE,OAAO;QAChC,MAAM,YAAY,GAAG,MAAM,IAAA,wCAAQ,EACjC,4BAAuB,EACvB,QAAQ,CACT,CAAC,OAAO,EAAE,CAAC;QACZ,IAAI,YAAY,EAAE,iBAAiB;YAAE,OAAO;QAC5C,IACE,MAAM,KAAK,CAAC,eAAe,CACzB,QAAQ,EACR,kBAAkB,CAAC,YAAY,CAAC,EAChC,QAAQ,CACT;YAED,OAAO;IACX,CAAC;IACD,MAAM,IAAI,KAAK,CACb,gEAAgE,CACjE,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,+BAA+B;IACnD,MAAM,OAAO,GAAG,MAAM,IAAA,wCAAQ,EAAC,gBAAW,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC;IACrE,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC,MAAM,CAAC,GAAG;YAAE,SAAS;QAC1B,MAAM,2BAA2B,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChD,CAAC;AACH,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { type LayoutBannerComponentInfo, type LayoutBannerContext } from "@antelopejs/interface-dms/layout-banners";
2
+ export declare function isPastDueBannerVisible({ user, tenantId, }: LayoutBannerContext): Promise<boolean>;
3
+ export declare const PAST_DUE_BANNER: LayoutBannerComponentInfo;
4
+ export declare function registerPastDueBanner(): void;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PAST_DUE_BANNER = void 0;
4
+ exports.isPastDueBannerVisible = isPastDueBannerVisible;
5
+ exports.registerPastDueBanner = registerPastDueBanner;
6
+ const interface_database_decorators_1 = require("@antelopejs/interface-database-decorators");
7
+ const layout_banners_1 = require("@antelopejs/interface-dms/layout-banners");
8
+ const db_1 = require("../db");
9
+ const PAST_DUE_STATE = "past_due";
10
+ async function isPastDueBannerVisible({ user, tenantId, }) {
11
+ if (!user)
12
+ return false;
13
+ const state = await (0, interface_database_decorators_1.GetModel)(db_1.TenantBillingStateModel).findByTenant(tenantId);
14
+ return !state?.deletedAt && state?.billingState === PAST_DUE_STATE;
15
+ }
16
+ exports.PAST_DUE_BANNER = {
17
+ key: "dms-saas:past-due",
18
+ variant: layout_banners_1.LayoutBannerVariant.ERROR,
19
+ component: "DmsSaasPastDueBanner",
20
+ visible: isPastDueBannerVisible,
21
+ };
22
+ function registerPastDueBanner() {
23
+ (0, layout_banners_1.RegisterLayoutBanner)(exports.PAST_DUE_BANNER);
24
+ }
25
+ //# sourceMappingURL=past-due-banner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"past-due-banner.js","sourceRoot":"","sources":["../../src/billing-state/past-due-banner.ts"],"names":[],"mappings":";;;AAgBA,wDAOC;AASD,sDAEC;AAlCD,6FAAqE;AACrE,6EAKkD;AAClD,8BAAmE;AAEnE,MAAM,cAAc,GAAiB,UAAU,CAAC;AAOzC,KAAK,UAAU,sBAAsB,CAAC,EAC3C,IAAI,EACJ,QAAQ,GACY;IACpB,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,MAAM,KAAK,GAAG,MAAM,IAAA,wCAAQ,EAAC,4BAAuB,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC7E,OAAO,CAAC,KAAK,EAAE,SAAS,IAAI,KAAK,EAAE,YAAY,KAAK,cAAc,CAAC;AACrE,CAAC;AAEY,QAAA,eAAe,GAA8B;IACxD,GAAG,EAAE,mBAAmB;IACxB,OAAO,EAAE,oCAAmB,CAAC,KAAK;IAClC,SAAS,EAAE,sBAAsB;IACjC,OAAO,EAAE,sBAAsB;CAChC,CAAC;AAEF,SAAgB,qBAAqB;IACnC,IAAA,qCAAoB,EAAC,uBAAe,CAAC,CAAC;AACxC,CAAC"}
@@ -1,5 +1,10 @@
1
- import type { DmsSaasConfig } from "../types";
1
+ import { type DmsSaasConfig, type RegistrationPaymentMethodPolicy } from "../types";
2
2
  export declare function setRuntimeConfig(config: DmsSaasConfig): void;
3
+ export declare function getRegistrationPaymentMethodPolicy(): RegistrationPaymentMethodPolicy;
4
+ export declare function assertRegistrationCardAccepted(): void;
5
+ export declare function resolveRegistrationPaymentMethodId(paymentMethodId: string | undefined): string | undefined;
6
+ export declare function getDefaultPlanSlug(): string | undefined;
7
+ export declare function getUpcomingInvoicePreviewCacheTtlMs(): number;
3
8
  export declare function getAllowedRedirectHosts(): string[];
4
9
  export declare function getSupportStorage(): string;
5
10
  export declare function assertAdmissionOpen(isPlatformOwner?: boolean): void;
@@ -1,24 +1,73 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setRuntimeConfig = setRuntimeConfig;
4
+ exports.getRegistrationPaymentMethodPolicy = getRegistrationPaymentMethodPolicy;
5
+ exports.assertRegistrationCardAccepted = assertRegistrationCardAccepted;
6
+ exports.resolveRegistrationPaymentMethodId = resolveRegistrationPaymentMethodId;
7
+ exports.getDefaultPlanSlug = getDefaultPlanSlug;
8
+ exports.getUpcomingInvoicePreviewCacheTtlMs = getUpcomingInvoicePreviewCacheTtlMs;
4
9
  exports.getAllowedRedirectHosts = getAllowedRedirectHosts;
5
10
  exports.getSupportStorage = getSupportStorage;
6
11
  exports.assertAdmissionOpen = assertAdmissionOpen;
7
12
  exports.isAllowedRedirectUrl = isAllowedRedirectUrl;
8
13
  const interface_api_util_1 = require("@antelopejs/interface-api-util");
14
+ const types_1 = require("../types");
9
15
  const dev_mode_1 = require("./dev-mode");
10
16
  const REDIRECT_PROTOCOLS = ["http:", "https:"];
11
17
  const LOOPBACK_HOSTNAMES = ["localhost", "127.0.0.1", "::1", "[::1]"];
18
+ const HTTP_BAD_REQUEST = 400;
12
19
  const HTTP_UNAVAILABLE = 503;
13
20
  const ADMISSION_MODES = ["open", "invitation-only"];
21
+ const DEFAULT_UPCOMING_INVOICE_PREVIEW_CACHE_TTL_SECONDS = 3600;
22
+ const MS_PER_SECOND = 1000;
14
23
  let runtimeConfig = null;
15
- function setRuntimeConfig(config) {
24
+ function assertValidAdmissionMode(config) {
16
25
  if (config.admissionMode !== undefined &&
17
26
  !ADMISSION_MODES.includes(config.admissionMode)) {
18
27
  throw new Error("Invalid dms-saas admissionMode: expected open or invitation-only");
19
28
  }
29
+ }
30
+ function assertValidRegistrationPaymentMethod(config) {
31
+ const policy = config.registration?.paymentMethod;
32
+ if (policy !== undefined &&
33
+ !types_1.REGISTRATION_PAYMENT_METHOD_POLICIES.includes(policy)) {
34
+ throw new Error("Invalid dms-saas registration.paymentMethod: expected required, optional or none");
35
+ }
36
+ }
37
+ function assertValidUpcomingInvoicePreviewCacheTtl(config) {
38
+ const ttl = config.upcomingInvoicePreviewCacheTtlSeconds;
39
+ if (ttl === undefined || (Number.isFinite(ttl) && ttl >= 0))
40
+ return;
41
+ throw new Error("Invalid dms-saas upcomingInvoicePreviewCacheTtlSeconds: expected a non-negative number");
42
+ }
43
+ function setRuntimeConfig(config) {
44
+ assertValidAdmissionMode(config);
45
+ assertValidRegistrationPaymentMethod(config);
46
+ assertValidUpcomingInvoicePreviewCacheTtl(config);
20
47
  runtimeConfig = config;
21
48
  }
49
+ function getRegistrationPaymentMethodPolicy() {
50
+ return (runtimeConfig?.registration?.paymentMethod ??
51
+ types_1.DEFAULT_REGISTRATION_PAYMENT_METHOD_POLICY);
52
+ }
53
+ function assertRegistrationCardAccepted() {
54
+ (0, interface_api_util_1.assert)(getRegistrationPaymentMethodPolicy() !== "none", HTTP_BAD_REQUEST, "saas.errors.registration.payment_method_disabled");
55
+ }
56
+ function resolveRegistrationPaymentMethodId(paymentMethodId) {
57
+ const policy = getRegistrationPaymentMethodPolicy();
58
+ if (policy === "none")
59
+ return undefined;
60
+ (0, interface_api_util_1.assert)(policy === "optional" || paymentMethodId, HTTP_BAD_REQUEST, "saas.errors.registration.payment_method_required");
61
+ return paymentMethodId || undefined;
62
+ }
63
+ function getDefaultPlanSlug() {
64
+ return runtimeConfig?.defaultPlanSlug;
65
+ }
66
+ function getUpcomingInvoicePreviewCacheTtlMs() {
67
+ const seconds = runtimeConfig?.upcomingInvoicePreviewCacheTtlSeconds ??
68
+ DEFAULT_UPCOMING_INVOICE_PREVIEW_CACHE_TTL_SECONDS;
69
+ return seconds * MS_PER_SECOND;
70
+ }
22
71
  function getAllowedRedirectHosts() {
23
72
  return runtimeConfig?.allowedRedirectHosts ?? [];
24
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/config/runtime.ts"],"names":[],"mappings":";;AAWA,4CAUC;AAED,0DAEC;AAGD,8CAQC;AAGD,kDAOC;AAuBD,oDAKC;AA1ED,uEAAwD;AAExD,yCAAuC;AAEvC,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AAC/C,MAAM,kBAAkB,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;AACtE,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAEpD,IAAI,aAAa,GAAyB,IAAI,CAAC;AAE/C,SAAgB,gBAAgB,CAAC,MAAqB;IACpD,IACE,MAAM,CAAC,aAAa,KAAK,SAAS;QAClC,CAAC,eAAe,CAAC,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,EAC/C,CAAC;QACD,MAAM,IAAI,KAAK,CACb,kEAAkE,CACnE,CAAC;IACJ,CAAC;IACD,aAAa,GAAG,MAAM,CAAC;AACzB,CAAC;AAED,SAAgB,uBAAuB;IACrC,OAAO,aAAa,EAAE,oBAAoB,IAAI,EAAE,CAAC;AACnD,CAAC;AAGD,SAAgB,iBAAiB;IAC/B,MAAM,OAAO,GAAG,aAAa,EAAE,cAAc,CAAC;IAC9C,IAAA,2BAAM,EACJ,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EACxD,gBAAgB,EAChB,yCAAyC,CAC1C,CAAC;IACF,OAAO,OAAO,CAAC;AACjB,CAAC;AAGD,SAAgB,mBAAmB,CAAC,eAAe,GAAG,KAAK;IACzD,MAAM,cAAc,GAAG,GAAG,CAAC;IAC3B,IAAA,2BAAM,EACJ,eAAe,IAAI,aAAa,EAAE,aAAa,KAAK,iBAAiB,EACrE,cAAc,EACd,iCAAiC,CAClC,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAc;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/B,OAAO,kBAAkB,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,GAAQ;IAC7B,OAAO,kBAAkB,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AACnD,CAAC;AAUD,SAAgB,oBAAoB,CAAC,MAAc;IACjD,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,IAAI,IAAA,oBAAS,GAAE,IAAI,aAAa,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,OAAO,uBAAuB,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACzD,CAAC"}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/config/runtime.ts"],"names":[],"mappings":"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"}
@@ -7,11 +7,14 @@ exports.scheduleRecomputeBillingState = scheduleRecomputeBillingState;
7
7
  const logging_1 = require("@antelopejs/interface-core/logging");
8
8
  const node_cron_1 = __importDefault(require("node-cron"));
9
9
  const billing_state_1 = require("../billing-state");
10
+ const default_plan_1 = require("../workspaces/default-plan");
10
11
  const CRON_NAME = "recompute-billing-state";
11
12
  const CRON_SCHEDULE = "*/15 * * * *";
12
13
  function scheduleRecomputeBillingState() {
13
14
  return node_cron_1.default.schedule(CRON_SCHEDULE, () => {
14
- void (0, billing_state_1.recomputeAllTenantBillingStates)().catch((error) => {
15
+ void (0, default_plan_1.backfillDefaultSubscriptions)()
16
+ .then(billing_state_1.recomputeAllTenantBillingStates)
17
+ .catch((error) => {
15
18
  logging_1.Logging.Error(`[dms-saas:cron:${CRON_NAME}] failed`, error);
16
19
  });
17
20
  });
@@ -1 +1 @@
1
- {"version":3,"file":"recompute-billing-state.js","sourceRoot":"","sources":["../../src/crons/recompute-billing-state.ts"],"names":[],"mappings":";;;;;AAOA,sEAMC;AAbD,gEAA6D;AAC7D,0DAAqD;AACrD,oDAAmE;AAEnE,MAAM,SAAS,GAAG,yBAAyB,CAAC;AAC5C,MAAM,aAAa,GAAG,cAAc,CAAC;AAErC,SAAgB,6BAA6B;IAC3C,OAAO,mBAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;QACvC,KAAK,IAAA,+CAA+B,GAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YAC9D,iBAAO,CAAC,KAAK,CAAC,kBAAkB,SAAS,UAAU,EAAE,KAAK,CAAC,CAAC;QAC9D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"recompute-billing-state.js","sourceRoot":"","sources":["../../src/crons/recompute-billing-state.ts"],"names":[],"mappings":";;;;;AAQA,sEAUC;AAlBD,gEAA6D;AAC7D,0DAAqD;AACrD,oDAAmE;AACnE,6DAA0E;AAE1E,MAAM,SAAS,GAAG,yBAAyB,CAAC;AAC5C,MAAM,aAAa,GAAG,cAAc,CAAC;AAErC,SAAgB,6BAA6B;IAC3C,OAAO,mBAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;QAGvC,KAAK,IAAA,2CAA4B,GAAE;aAChC,IAAI,CAAC,+CAA+B,CAAC;aACrC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACxB,iBAAO,CAAC,KAAK,CAAC,kBAAkB,SAAS,UAAU,EAAE,KAAK,CAAC,CAAC;QAC9D,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -2,6 +2,7 @@ export * from "./admin-user-invoices";
2
2
  export * from "./admin-user-workspaces";
3
3
  export * from "./credit-notes";
4
4
  export * from "./features";
5
+ export * from "./invitations";
5
6
  export * from "./invoices";
6
7
  export * from "./members";
7
8
  export * from "./plan-migrations";
@@ -18,6 +18,7 @@ __exportStar(require("./admin-user-invoices"), exports);
18
18
  __exportStar(require("./admin-user-workspaces"), exports);
19
19
  __exportStar(require("./credit-notes"), exports);
20
20
  __exportStar(require("./features"), exports);
21
+ __exportStar(require("./invitations"), exports);
21
22
  __exportStar(require("./invoices"), exports);
22
23
  __exportStar(require("./members"), exports);
23
24
  __exportStar(require("./plan-migrations"), exports);