@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.
- package/README.md +307 -33
- package/dist/billing-state/index.d.ts +1 -0
- package/dist/billing-state/index.js +1 -0
- package/dist/billing-state/index.js.map +1 -1
- package/dist/billing-state/past-due-banner.d.ts +4 -0
- package/dist/billing-state/past-due-banner.js +25 -0
- package/dist/billing-state/past-due-banner.js.map +1 -0
- package/dist/config/runtime.d.ts +6 -1
- package/dist/config/runtime.js +50 -1
- package/dist/config/runtime.js.map +1 -1
- package/dist/crons/recompute-billing-state.js +4 -1
- package/dist/crons/recompute-billing-state.js.map +1 -1
- package/dist/data-api/platformOwner/index.d.ts +1 -0
- package/dist/data-api/platformOwner/index.js +1 -0
- package/dist/data-api/platformOwner/index.js.map +1 -1
- package/dist/data-api/platformOwner/invitations.d.ts +22 -0
- package/dist/data-api/platformOwner/invitations.js +121 -0
- package/dist/data-api/platformOwner/invitations.js.map +1 -0
- package/dist/data-api/platformOwner/invoice-options.js +3 -2
- package/dist/data-api/platformOwner/invoice-options.js.map +1 -1
- package/dist/data-api/platformOwner/workspaces.d.ts +3 -0
- package/dist/data-api/platformOwner/workspaces.js +64 -12
- package/dist/data-api/platformOwner/workspaces.js.map +1 -1
- package/dist/data-api/tenant/tenant-invoices.js +2 -10
- package/dist/data-api/tenant/tenant-invoices.js.map +1 -1
- package/dist/hooks/register.js +2 -1
- package/dist/hooks/register.js.map +1 -1
- package/dist/implementations/dms-saas/billing.d.ts +2 -1
- package/dist/implementations/dms-saas/billing.js +6 -0
- package/dist/implementations/dms-saas/billing.js.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/invoice-line-items/billing-window.js +2 -2
- package/dist/invoice-line-items/billing-window.js.map +1 -1
- package/dist/invoice-line-items/injection.d.ts +10 -0
- package/dist/invoice-line-items/injection.js +7 -3
- package/dist/invoice-line-items/injection.js.map +1 -1
- package/dist/operator-actions/db/lifecycle-delivery.model.js +1 -3
- package/dist/operator-actions/db/lifecycle-delivery.model.js.map +1 -1
- package/dist/operator-actions/db/lifecycle-delivery.table.d.ts +1 -1
- package/dist/operator-actions/db/lifecycle-delivery.table.js.map +1 -1
- package/dist/operator-actions/db/operator-action.model.js +1 -3
- package/dist/operator-actions/db/operator-action.model.js.map +1 -1
- package/dist/operator-actions/db/operator-action.table.d.ts +1 -1
- package/dist/operator-actions/db/operator-action.table.js.map +1 -1
- package/dist/operator-actions/index.d.ts +1 -0
- package/dist/operator-actions/index.js +1 -0
- package/dist/operator-actions/index.js.map +1 -1
- package/dist/operator-actions/invitation-commands.d.ts +12 -0
- package/dist/operator-actions/invitation-commands.js +46 -0
- package/dist/operator-actions/invitation-commands.js.map +1 -0
- package/dist/operator-actions/types.d.ts +1 -1
- package/dist/pages/platform/dashboard.js +2 -4
- package/dist/pages/platform/dashboard.js.map +1 -1
- package/dist/pages/platform/workspaces/detail.js +40 -1
- package/dist/pages/platform/workspaces/detail.js.map +1 -1
- package/dist/pages/platform/workspaces/index.d.ts +2 -8
- package/dist/pages/platform/workspaces/index.js +27 -111
- package/dist/pages/platform/workspaces/index.js.map +1 -1
- package/dist/pages/public/legal/privacy-policy.js +2 -1
- package/dist/pages/public/legal/privacy-policy.js.map +1 -1
- package/dist/pages/public/legal/terms-and-conditions.js +2 -1
- package/dist/pages/public/legal/terms-and-conditions.js.map +1 -1
- package/dist/pages/public/legal/terms-of-use.js +2 -1
- package/dist/pages/public/legal/terms-of-use.js.map +1 -1
- package/dist/plans/index.d.ts +1 -0
- package/dist/plans/index.js +1 -0
- package/dist/plans/index.js.map +1 -1
- package/dist/plans/platform-support.d.ts +2 -0
- package/dist/plans/platform-support.js +19 -0
- package/dist/plans/platform-support.js.map +1 -0
- package/dist/plans/seat-capacity.d.ts +9 -2
- package/dist/plans/seat-capacity.js +59 -12
- package/dist/plans/seat-capacity.js.map +1 -1
- package/dist/plans/seat-hooks.js +34 -13
- package/dist/plans/seat-hooks.js.map +1 -1
- package/dist/routes/platformOwner/credit-notes-issue.js +2 -1
- package/dist/routes/platformOwner/credit-notes-issue.js.map +1 -1
- package/dist/routes/platformOwner/dashboard.d.ts +8 -6
- package/dist/routes/platformOwner/dashboard.js +31 -17
- package/dist/routes/platformOwner/dashboard.js.map +1 -1
- package/dist/routes/platformOwner/index.d.ts +1 -0
- package/dist/routes/platformOwner/index.js +1 -0
- package/dist/routes/platformOwner/index.js.map +1 -1
- package/dist/routes/platformOwner/workspace-invitations.d.ts +13 -0
- package/dist/routes/platformOwner/workspace-invitations.js +73 -0
- package/dist/routes/platformOwner/workspace-invitations.js.map +1 -0
- package/dist/routes/public/register.d.ts +7 -3
- package/dist/routes/public/register.js +42 -27
- package/dist/routes/public/register.js.map +1 -1
- package/dist/routes/tenant/tenant-billing.d.ts +7 -11
- package/dist/routes/tenant/tenant-billing.js +34 -26
- package/dist/routes/tenant/tenant-billing.js.map +1 -1
- package/dist/routes/tenant/tenant-plan-ops.js +2 -2
- package/dist/routes/tenant/tenant-plan-ops.js.map +1 -1
- package/dist/routes/tenant/tenant-plan.d.ts +1 -0
- package/dist/routes/tenant/tenant-plan.js +27 -2
- package/dist/routes/tenant/tenant-plan.js.map +1 -1
- package/dist/routes/tenant/tenant-seats.d.ts +2 -4
- package/dist/routes/tenant/tenant-seats.js.map +1 -1
- package/dist/routes/tenant/workspaces.d.ts +2 -0
- package/dist/routes/tenant/workspaces.js +10 -4
- package/dist/routes/tenant/workspaces.js.map +1 -1
- package/dist/stripe/upcoming-invoice.d.ts +21 -0
- package/dist/stripe/upcoming-invoice.js +42 -0
- package/dist/stripe/upcoming-invoice.js.map +1 -0
- package/dist/stripe/webhook-dispatch.js +2 -0
- package/dist/stripe/webhook-dispatch.js.map +1 -1
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.js +7 -0
- package/dist/types/config.js.map +1 -1
- package/dist/upcoming-invoice/db/preview-cache.model.d.ts +38 -0
- package/dist/upcoming-invoice/db/preview-cache.model.js +57 -0
- package/dist/upcoming-invoice/db/preview-cache.model.js.map +1 -0
- package/dist/upcoming-invoice/db/preview-cache.table.d.ts +10 -0
- package/dist/upcoming-invoice/db/preview-cache.table.js +42 -0
- package/dist/upcoming-invoice/db/preview-cache.table.js.map +1 -0
- package/dist/upcoming-invoice/eligibility.d.ts +17 -0
- package/dist/upcoming-invoice/eligibility.js +52 -0
- package/dist/upcoming-invoice/eligibility.js.map +1 -0
- package/dist/upcoming-invoice/invalidation.d.ts +2 -0
- package/dist/upcoming-invoice/invalidation.js +39 -0
- package/dist/upcoming-invoice/invalidation.js.map +1 -0
- package/dist/upcoming-invoice/mapping.d.ts +8 -0
- package/dist/upcoming-invoice/mapping.js +82 -0
- package/dist/upcoming-invoice/mapping.js.map +1 -0
- package/dist/upcoming-invoice/outcomes.d.ts +13 -0
- package/dist/upcoming-invoice/outcomes.js +15 -0
- package/dist/upcoming-invoice/outcomes.js.map +1 -0
- package/dist/upcoming-invoice/preview.d.ts +3 -0
- package/dist/upcoming-invoice/preview.js +80 -0
- package/dist/upcoming-invoice/preview.js.map +1 -0
- package/dist/upcoming-invoice/pricing.d.ts +3 -0
- package/dist/upcoming-invoice/pricing.js +84 -0
- package/dist/upcoming-invoice/pricing.js.map +1 -0
- package/dist/upcoming-invoice/usage.d.ts +14 -0
- package/dist/upcoming-invoice/usage.js +53 -0
- package/dist/upcoming-invoice/usage.js.map +1 -0
- package/dist/workspaces/billing-identity.d.ts +19 -0
- package/dist/workspaces/billing-identity.js +51 -0
- package/dist/workspaces/billing-identity.js.map +1 -0
- package/dist/workspaces/default-plan.d.ts +5 -0
- package/dist/workspaces/default-plan.js +110 -0
- package/dist/workspaces/default-plan.js.map +1 -0
- package/dist/workspaces/index.d.ts +2 -0
- package/dist/workspaces/index.js +2 -0
- package/dist/workspaces/index.js.map +1 -1
- package/dist/workspaces/invitations.d.ts +26 -0
- package/dist/workspaces/invitations.js +112 -0
- package/dist/workspaces/invitations.js.map +1 -0
- package/dist/workspaces/provisioning.d.ts +3 -3
- package/dist/workspaces/provisioning.js +49 -11
- package/dist/workspaces/provisioning.js.map +1 -1
- package/frontend-vue/app/components/BillingIdentityFields.vue +179 -0
- package/frontend-vue/app/components/PastDueBanner.vue +81 -0
- package/frontend-vue/app/components/PaymentMethodCard.vue +94 -38
- package/frontend-vue/app/components/PlanComparisonModal.vue +62 -26
- package/frontend-vue/app/components/PlanUpgradeModal.vue +181 -0
- package/frontend-vue/app/components/PlatformNotes.vue +4 -0
- package/frontend-vue/app/components/RefundButton.vue +10 -1
- package/frontend-vue/app/components/Register.vue +49 -92
- package/frontend-vue/app/components/SeatQuotaBanner.vue +28 -0
- package/frontend-vue/app/components/TenantBillingForm.vue +52 -128
- package/frontend-vue/app/components/TenantPlanCard.vue +68 -10
- package/frontend-vue/app/components/UserSummaryCard.vue +12 -10
- package/frontend-vue/app/components/WorkspaceAdminCreateModal.vue +202 -0
- package/frontend-vue/app/components/WorkspaceCreateModal.vue +46 -9
- package/frontend-vue/app/components/WorkspaceInvitationLink.vue +106 -0
- package/frontend-vue/app/components/WorkspaceSummaryCard.vue +39 -15
- package/frontend-vue/app/components/WorkspaceSuspended.vue +3 -5
- package/frontend-vue/app/components/WorkspaceSwitcherWidget.vue +8 -5
- package/frontend-vue/app/composables/useBillingCountries.ts +25 -5
- package/frontend-vue/app/composables/useBillingDocumentType.ts +9 -5
- package/frontend-vue/app/composables/useBillingIdentity.ts +141 -0
- package/frontend-vue/app/composables/useInvitationLink.ts +69 -0
- package/frontend-vue/app/composables/usePastDueBannerAction.ts +20 -0
- package/frontend-vue/app/composables/usePlanComparison.ts +15 -0
- package/frontend-vue/app/composables/usePlanFeatureFormat.ts +312 -20
- package/frontend-vue/app/composables/usePlanFeatureLabel.ts +24 -0
- package/frontend-vue/app/composables/useSaasRegistration.ts +110 -223
- package/frontend-vue/app/composables/useSeatQuota.ts +8 -0
- package/frontend-vue/app/composables/useStripePaymentElement.ts +3 -3
- package/frontend-vue/app/composables/useWorkspaceAccessCache.ts +29 -2
- package/frontend-vue/app/composables/useWorkspaceFinalization.ts +7 -32
- package/frontend-vue/app/custom-pages/auth/no-workspace.vue +49 -194
- package/frontend-vue/app/plugins/auth-links.ts +0 -4
- package/frontend-vue/app/plugins/billing-data-types.ts +4 -2
- package/frontend-vue/app/types/runtime-config.ts +13 -0
- package/frontend-vue/dms.frontend.ts +10 -4
- package/frontend-vue/i18n/locales/saas-en-GB.json +161 -36
- package/frontend-vue/i18n/locales/saas-fr-FR.json +162 -37
- package/frontend-vue/package.json +0 -1
- package/frontend-vue/pnpm-lock.yaml +1 -125
- package/package.json +5 -5
- 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
|
|
21
|
-
|
|
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
|
|
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
|
|
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 —
|
|
203
|
-
|
|
204
|
-
|
|
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 {
|
|
209
|
-
|
|
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`.
|
|
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
|
|
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
|
|
500
|
-
|
|
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.
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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":";;;;;;;;;;;;;;;;
|
|
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"}
|
package/dist/config/runtime.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import type
|
|
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;
|
package/dist/config/runtime.js
CHANGED
|
@@ -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
|
|
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":";;
|
|
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,
|
|
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":";;;;;
|
|
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);
|