@antelopejs/dms-saas 0.1.8 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/README.md +262 -13
  2. package/dist/auth/subscription-access-gate.d.ts +1 -1
  3. package/dist/billing-state/index.d.ts +1 -0
  4. package/dist/billing-state/index.js +1 -0
  5. package/dist/billing-state/index.js.map +1 -1
  6. package/dist/billing-state/past-due-banner.d.ts +4 -0
  7. package/dist/billing-state/past-due-banner.js +25 -0
  8. package/dist/billing-state/past-due-banner.js.map +1 -0
  9. package/dist/config/runtime.d.ts +6 -1
  10. package/dist/config/runtime.js +50 -1
  11. package/dist/config/runtime.js.map +1 -1
  12. package/dist/crons/recompute-billing-state.js +4 -1
  13. package/dist/crons/recompute-billing-state.js.map +1 -1
  14. package/dist/data-api/platformOwner/index.d.ts +1 -0
  15. package/dist/data-api/platformOwner/index.js +1 -0
  16. package/dist/data-api/platformOwner/index.js.map +1 -1
  17. package/dist/data-api/platformOwner/invitations.d.ts +22 -0
  18. package/dist/data-api/platformOwner/invitations.js +121 -0
  19. package/dist/data-api/platformOwner/invitations.js.map +1 -0
  20. package/dist/data-api/platformOwner/workspaces.d.ts +3 -0
  21. package/dist/data-api/platformOwner/workspaces.js +64 -12
  22. package/dist/data-api/platformOwner/workspaces.js.map +1 -1
  23. package/dist/data-api/tenant/tenant-invoices.js +2 -10
  24. package/dist/data-api/tenant/tenant-invoices.js.map +1 -1
  25. package/dist/hooks/register.js +2 -1
  26. package/dist/hooks/register.js.map +1 -1
  27. package/dist/implementations/dms-saas/billing.d.ts +2 -1
  28. package/dist/implementations/dms-saas/billing.js +6 -0
  29. package/dist/implementations/dms-saas/billing.js.map +1 -1
  30. package/dist/index.js +5 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/invoice-line-items/injection.d.ts +10 -0
  33. package/dist/invoice-line-items/injection.js +7 -3
  34. package/dist/invoice-line-items/injection.js.map +1 -1
  35. package/dist/notifications/subjects.d.ts +19 -19
  36. package/dist/operator-actions/index.d.ts +1 -0
  37. package/dist/operator-actions/index.js +1 -0
  38. package/dist/operator-actions/index.js.map +1 -1
  39. package/dist/operator-actions/invitation-commands.d.ts +12 -0
  40. package/dist/operator-actions/invitation-commands.js +46 -0
  41. package/dist/operator-actions/invitation-commands.js.map +1 -0
  42. package/dist/operator-actions/types.d.ts +1 -1
  43. package/dist/pages/platform/categories.d.ts +6 -6
  44. package/dist/pages/platform/credit-notes.d.ts +1 -1
  45. package/dist/pages/platform/dashboard.d.ts +2 -2
  46. package/dist/pages/platform/dashboard.js +2 -4
  47. package/dist/pages/platform/dashboard.js.map +1 -1
  48. package/dist/pages/platform/features.d.ts +1 -1
  49. package/dist/pages/platform/invoices.d.ts +1 -1
  50. package/dist/pages/platform/plans/edit.d.ts +2 -2
  51. package/dist/pages/platform/plans/index.d.ts +1 -1
  52. package/dist/pages/platform/plans/migrations.d.ts +1 -1
  53. package/dist/pages/platform/segments/index.d.ts +1 -1
  54. package/dist/pages/platform/settings.d.ts +1 -1
  55. package/dist/pages/platform/users/detail.d.ts +1 -1
  56. package/dist/pages/platform/users/index.d.ts +1 -1
  57. package/dist/pages/platform/workspaces/detail.d.ts +1 -1
  58. package/dist/pages/platform/workspaces/detail.js +40 -1
  59. package/dist/pages/platform/workspaces/detail.js.map +1 -1
  60. package/dist/pages/platform/workspaces/index.d.ts +4 -10
  61. package/dist/pages/platform/workspaces/index.js +27 -111
  62. package/dist/pages/platform/workspaces/index.js.map +1 -1
  63. package/dist/pages/public/legal/privacy-policy.js +2 -1
  64. package/dist/pages/public/legal/privacy-policy.js.map +1 -1
  65. package/dist/pages/public/legal/terms-and-conditions.js +2 -1
  66. package/dist/pages/public/legal/terms-and-conditions.js.map +1 -1
  67. package/dist/pages/public/legal/terms-of-use.js +2 -1
  68. package/dist/pages/public/legal/terms-of-use.js.map +1 -1
  69. package/dist/pages/tenant/billing.d.ts +1 -1
  70. package/dist/plans/index.d.ts +1 -0
  71. package/dist/plans/index.js +1 -0
  72. package/dist/plans/index.js.map +1 -1
  73. package/dist/plans/platform-support.d.ts +2 -0
  74. package/dist/plans/platform-support.js +19 -0
  75. package/dist/plans/platform-support.js.map +1 -0
  76. package/dist/plans/seat-capacity.d.ts +9 -2
  77. package/dist/plans/seat-capacity.js +59 -12
  78. package/dist/plans/seat-capacity.js.map +1 -1
  79. package/dist/plans/seat-hooks.js +34 -13
  80. package/dist/plans/seat-hooks.js.map +1 -1
  81. package/dist/routes/platformOwner/billing-settings.d.ts +2 -2
  82. package/dist/routes/platformOwner/dashboard.d.ts +8 -6
  83. package/dist/routes/platformOwner/dashboard.js +31 -17
  84. package/dist/routes/platformOwner/dashboard.js.map +1 -1
  85. package/dist/routes/platformOwner/index.d.ts +1 -0
  86. package/dist/routes/platformOwner/index.js +1 -0
  87. package/dist/routes/platformOwner/index.js.map +1 -1
  88. package/dist/routes/platformOwner/plan-deletion.d.ts +1 -1
  89. package/dist/routes/platformOwner/plans.d.ts +5 -5
  90. package/dist/routes/platformOwner/segments.d.ts +1 -1
  91. package/dist/routes/platformOwner/workspace-invitations.d.ts +13 -0
  92. package/dist/routes/platformOwner/workspace-invitations.js +73 -0
  93. package/dist/routes/platformOwner/workspace-invitations.js.map +1 -0
  94. package/dist/routes/public/legal-documents.d.ts +2 -2
  95. package/dist/routes/public/register.d.ts +7 -3
  96. package/dist/routes/public/register.js +42 -27
  97. package/dist/routes/public/register.js.map +1 -1
  98. package/dist/routes/tenant/tenant-billing.d.ts +7 -11
  99. package/dist/routes/tenant/tenant-billing.js +34 -26
  100. package/dist/routes/tenant/tenant-billing.js.map +1 -1
  101. package/dist/routes/tenant/tenant-plan.d.ts +1 -0
  102. package/dist/routes/tenant/tenant-plan.js +27 -2
  103. package/dist/routes/tenant/tenant-plan.js.map +1 -1
  104. package/dist/routes/tenant/tenant-seats.d.ts +2 -4
  105. package/dist/routes/tenant/tenant-seats.js.map +1 -1
  106. package/dist/routes/tenant/workspaces.d.ts +3 -1
  107. package/dist/routes/tenant/workspaces.js +10 -4
  108. package/dist/routes/tenant/workspaces.js.map +1 -1
  109. package/dist/stripe/upcoming-invoice.d.ts +21 -0
  110. package/dist/stripe/upcoming-invoice.js +42 -0
  111. package/dist/stripe/upcoming-invoice.js.map +1 -0
  112. package/dist/stripe/webhook-dispatch.js +2 -0
  113. package/dist/stripe/webhook-dispatch.js.map +1 -1
  114. package/dist/types/config.d.ts +9 -0
  115. package/dist/types/config.js +7 -0
  116. package/dist/types/config.js.map +1 -1
  117. package/dist/upcoming-invoice/db/preview-cache.model.d.ts +38 -0
  118. package/dist/upcoming-invoice/db/preview-cache.model.js +57 -0
  119. package/dist/upcoming-invoice/db/preview-cache.model.js.map +1 -0
  120. package/dist/upcoming-invoice/db/preview-cache.table.d.ts +10 -0
  121. package/dist/upcoming-invoice/db/preview-cache.table.js +42 -0
  122. package/dist/upcoming-invoice/db/preview-cache.table.js.map +1 -0
  123. package/dist/upcoming-invoice/eligibility.d.ts +17 -0
  124. package/dist/upcoming-invoice/eligibility.js +52 -0
  125. package/dist/upcoming-invoice/eligibility.js.map +1 -0
  126. package/dist/upcoming-invoice/invalidation.d.ts +2 -0
  127. package/dist/upcoming-invoice/invalidation.js +39 -0
  128. package/dist/upcoming-invoice/invalidation.js.map +1 -0
  129. package/dist/upcoming-invoice/mapping.d.ts +8 -0
  130. package/dist/upcoming-invoice/mapping.js +82 -0
  131. package/dist/upcoming-invoice/mapping.js.map +1 -0
  132. package/dist/upcoming-invoice/outcomes.d.ts +13 -0
  133. package/dist/upcoming-invoice/outcomes.js +15 -0
  134. package/dist/upcoming-invoice/outcomes.js.map +1 -0
  135. package/dist/upcoming-invoice/preview.d.ts +3 -0
  136. package/dist/upcoming-invoice/preview.js +80 -0
  137. package/dist/upcoming-invoice/preview.js.map +1 -0
  138. package/dist/upcoming-invoice/pricing.d.ts +3 -0
  139. package/dist/upcoming-invoice/pricing.js +84 -0
  140. package/dist/upcoming-invoice/pricing.js.map +1 -0
  141. package/dist/upcoming-invoice/usage.d.ts +14 -0
  142. package/dist/upcoming-invoice/usage.js +53 -0
  143. package/dist/upcoming-invoice/usage.js.map +1 -0
  144. package/dist/utils/invoice-lines-type.d.ts +1 -1
  145. package/dist/workspaces/billing-identity.d.ts +19 -0
  146. package/dist/workspaces/billing-identity.js +51 -0
  147. package/dist/workspaces/billing-identity.js.map +1 -0
  148. package/dist/workspaces/default-plan.d.ts +5 -0
  149. package/dist/workspaces/default-plan.js +109 -0
  150. package/dist/workspaces/default-plan.js.map +1 -0
  151. package/dist/workspaces/index.d.ts +2 -0
  152. package/dist/workspaces/index.js +2 -0
  153. package/dist/workspaces/index.js.map +1 -1
  154. package/dist/workspaces/invitations.d.ts +26 -0
  155. package/dist/workspaces/invitations.js +112 -0
  156. package/dist/workspaces/invitations.js.map +1 -0
  157. package/dist/workspaces/provisioning.d.ts +3 -3
  158. package/dist/workspaces/provisioning.js +39 -11
  159. package/dist/workspaces/provisioning.js.map +1 -1
  160. package/frontend-vue/app/components/BillingIdentityFields.vue +179 -0
  161. package/frontend-vue/app/components/PastDueBanner.vue +81 -0
  162. package/frontend-vue/app/components/PaymentMethodCard.vue +94 -38
  163. package/frontend-vue/app/components/PlanComparisonModal.vue +62 -26
  164. package/frontend-vue/app/components/PlanUpgradeModal.vue +181 -0
  165. package/frontend-vue/app/components/PlatformNotes.vue +4 -0
  166. package/frontend-vue/app/components/RefundButton.vue +10 -1
  167. package/frontend-vue/app/components/Register.vue +49 -92
  168. package/frontend-vue/app/components/SeatQuotaBanner.vue +28 -0
  169. package/frontend-vue/app/components/TenantBillingForm.vue +52 -128
  170. package/frontend-vue/app/components/TenantPlanCard.vue +68 -10
  171. package/frontend-vue/app/components/UserSummaryCard.vue +12 -10
  172. package/frontend-vue/app/components/WorkspaceAdminCreateModal.vue +202 -0
  173. package/frontend-vue/app/components/WorkspaceCreateModal.vue +46 -9
  174. package/frontend-vue/app/components/WorkspaceInvitationLink.vue +106 -0
  175. package/frontend-vue/app/components/WorkspaceSummaryCard.vue +39 -15
  176. package/frontend-vue/app/components/WorkspaceSuspended.vue +3 -5
  177. package/frontend-vue/app/components/WorkspaceSwitcherWidget.vue +8 -5
  178. package/frontend-vue/app/composables/useBillingCountries.ts +25 -5
  179. package/frontend-vue/app/composables/useBillingIdentity.ts +141 -0
  180. package/frontend-vue/app/composables/useInvitationLink.ts +69 -0
  181. package/frontend-vue/app/composables/usePastDueBannerAction.ts +20 -0
  182. package/frontend-vue/app/composables/usePlanComparison.ts +15 -0
  183. package/frontend-vue/app/composables/usePlanFeatureFormat.ts +312 -20
  184. package/frontend-vue/app/composables/usePlanFeatureLabel.ts +24 -0
  185. package/frontend-vue/app/composables/useSaasRegistration.ts +110 -223
  186. package/frontend-vue/app/composables/useSeatQuota.ts +8 -0
  187. package/frontend-vue/app/composables/useStripePaymentElement.ts +3 -3
  188. package/frontend-vue/app/composables/useWorkspaceAccessCache.ts +29 -2
  189. package/frontend-vue/app/composables/useWorkspaceFinalization.ts +7 -32
  190. package/frontend-vue/app/custom-pages/auth/no-workspace.vue +49 -194
  191. package/frontend-vue/app/plugins/auth-links.ts +0 -4
  192. package/frontend-vue/app/types/runtime-config.ts +13 -0
  193. package/frontend-vue/dms.frontend.ts +10 -4
  194. package/frontend-vue/i18n/locales/saas-en-GB.json +161 -36
  195. package/frontend-vue/i18n/locales/saas-fr-FR.json +162 -37
  196. package/frontend-vue/package.json +0 -1
  197. package/frontend-vue/pnpm-lock.yaml +1 -125
  198. package/package.json +15 -15
  199. package/frontend-vue/app/middleware/workspace-suspended.global.ts +0 -59
@@ -9,10 +9,17 @@ interface WorkspaceSummaryData {
9
9
  currency: string;
10
10
  mrr: number;
11
11
  membersCount: number;
12
+ platformSupportCount: number;
13
+ pendingInvitationsCount: number;
12
14
  createdAt: string;
13
15
  subscription: { stripeCustomerId: string | null } | null;
14
16
  }
15
17
 
18
+ interface PeopleCount {
19
+ key: string;
20
+ count: number;
21
+ }
22
+
16
23
  type BadgeColor =
17
24
  | "primary"
18
25
  | "secondary"
@@ -34,7 +41,16 @@ const props = defineProps<{
34
41
  routeParams?: Record<string, string>;
35
42
  }>();
36
43
 
44
+ const PEOPLE_SEPARATOR = " · ";
45
+ const MISSING_DATE = "—";
46
+ const DAY_FORMAT: Intl.DateTimeFormatOptions = {
47
+ day: "2-digit",
48
+ month: "short",
49
+ year: "numeric",
50
+ };
51
+
37
52
  const { $authFetch } = useAuthFetch();
53
+ const { t, locale } = useI18n();
38
54
 
39
55
  const tenantId = computed(() => props.routeParams?.id ?? "");
40
56
  const { triggerRef } = useDetailRefresh(tenantId.value);
@@ -55,13 +71,27 @@ const formattedMrr = computed(() => {
55
71
  }).format(workspace.value.mrr);
56
72
  });
57
73
 
58
- function formatDate(value: string | null): string {
59
- if (!value) return "—";
60
- return new Date(value).toLocaleDateString("fr-FR", {
61
- day: "2-digit",
62
- month: "short",
63
- year: "numeric",
64
- });
74
+ /** Seated members, then platform support and pending invitations, if any. */
75
+ const peopleLabel = computed(() => {
76
+ if (!workspace.value) return "";
77
+ const { membersCount, platformSupportCount, pendingInvitationsCount } =
78
+ workspace.value;
79
+ const optionalCounts: PeopleCount[] = [
80
+ { key: "platform_support", count: platformSupportCount },
81
+ { key: "pending_invitations", count: pendingInvitationsCount },
82
+ ];
83
+ return [
84
+ t("saas.workspaces.summary.members", { count: membersCount }, membersCount),
85
+ ...optionalCounts
86
+ .filter(({ count }) => count > 0)
87
+ .map(({ key, count }) =>
88
+ t(`saas.workspaces.summary.${key}`, { count }, count),
89
+ ),
90
+ ].join(PEOPLE_SEPARATOR);
91
+ });
92
+
93
+ function formatDay(value: string | null): string {
94
+ return formatDate(value, locale.value, DAY_FORMAT) ?? MISSING_DATE;
65
95
  }
66
96
 
67
97
  async function load(): Promise<void> {
@@ -110,13 +140,7 @@ watch(triggerRef, () => {
110
140
  <div class="mt-2 flex flex-col gap-1 text-sm text-muted">
111
141
  <div class="flex items-center gap-2">
112
142
  <UIcon name="i-ph-users" />
113
- <span>
114
- {{
115
- $t("saas.workspaces.summary.members", {
116
- count: workspace.membersCount,
117
- })
118
- }}
119
- </span>
143
+ <span>{{ peopleLabel }}</span>
120
144
  </div>
121
145
  </div>
122
146
  </div>
@@ -145,7 +169,7 @@ watch(triggerRef, () => {
145
169
  <dt class="text-muted">
146
170
  {{ $t("saas.workspaces.column.created_at") }}
147
171
  </dt>
148
- <dd class="mt-0.5">{{ formatDate(workspace.createdAt) }}</dd>
172
+ <dd class="mt-0.5">{{ formatDay(workspace.createdAt) }}</dd>
149
173
  </div>
150
174
  <div>
151
175
  <dt class="text-muted">
@@ -37,11 +37,9 @@ async function loadAccess(): Promise<void> {
37
37
  const result = await $authFetch<WorkspaceAccess>(ACCESS_ENDPOINT);
38
38
  access.value = result;
39
39
  if (!result.blocked) {
40
- // Drop the middleware's cached blocked state before navigating,
41
- // otherwise it redirects straight back here and the recovery path
42
- // never resolves. The shared billing payloads cached while blocked go
43
- // with it: an SPA navigation keeps useState alive, and the billing page
44
- // would keep announcing a suspension the user just settled.
40
+ // Drop the access and billing payloads cached while blocked before
41
+ // navigating: an SPA navigation keeps useState alive, and the billing
42
+ // page would keep announcing a suspension the user just settled.
45
43
  cachedAccess.value = null;
46
44
  billingStatusState.value = null;
47
45
  tenantPlanState.value = null;
@@ -5,10 +5,6 @@ const props = defineProps<{
5
5
  collapsed?: boolean
6
6
  }>()
7
7
 
8
- interface DmsSaasPublicRuntimeConfig {
9
- admissionMode?: 'open' | 'invitation-only'
10
- }
11
-
12
8
  const toast = useToast()
13
9
  const { t } = useI18n()
14
10
  const { resolveApiError } = useApiErrorMessage()
@@ -102,6 +98,10 @@ function openCreateModal(): void {
102
98
  isCreateOpen.value = true
103
99
  }
104
100
 
101
+ function closeCreateModal(): void {
102
+ isCreateOpen.value = false
103
+ }
104
+
105
105
  async function onWorkspaceCreated(tenantId: string): Promise<void> {
106
106
  isCreateOpen.value = false
107
107
  try {
@@ -300,7 +300,10 @@ onMounted(load)
300
300
  :description="$t('saas.workspaces.create.self_serve.description')"
301
301
  >
302
302
  <template #body>
303
- <DmsSaasWorkspaceCreateModal :on-success-callback="onWorkspaceCreated" />
303
+ <DmsSaasWorkspaceCreateModal
304
+ :on-success-callback="onWorkspaceCreated"
305
+ :on-cancel-callback="closeCreateModal"
306
+ />
304
307
  </template>
305
308
  </UModal>
306
309
  </template>
@@ -1,10 +1,12 @@
1
+ import { computed, type ComputedRef } from "vue";
2
+
1
3
  export interface BillingCountryItem {
2
4
  value: string;
3
5
  label: string;
4
6
  }
5
7
 
6
8
  export interface BillingCountries {
7
- countryItems: BillingCountryItem[];
9
+ countryItems: ComputedRef<BillingCountryItem[]>;
8
10
  }
9
11
 
10
12
  const BILLING_COUNTRY_CODES = [
@@ -44,11 +46,29 @@ const BILLING_COUNTRY_CODES = [
44
46
  "NZ",
45
47
  ];
46
48
 
49
+ /**
50
+ * Country choices labelled with their full name in the given locale, sorted
51
+ * alphabetically for that locale. The stored value stays the ISO code Stripe
52
+ * expects.
53
+ *
54
+ * @param locale BCP 47 locale the labels are rendered in
55
+ */
56
+ export function buildBillingCountryItems(locale: string): BillingCountryItem[] {
57
+ const names = new Intl.DisplayNames([locale], { type: "region" });
58
+ return BILLING_COUNTRY_CODES.map((code) => ({
59
+ value: code,
60
+ label: names.of(code) ?? code,
61
+ })).sort((left, right) => left.label.localeCompare(right.label, locale));
62
+ }
63
+
64
+ /** Full country name for an ISO code, or the code itself when unknown. */
65
+ export function formatBillingCountry(code: string, locale: string): string {
66
+ return new Intl.DisplayNames([locale], { type: "region" }).of(code) ?? code;
67
+ }
68
+
47
69
  export function useBillingCountries(): BillingCountries {
70
+ const { locale } = useI18n();
48
71
  return {
49
- countryItems: BILLING_COUNTRY_CODES.map((code) => ({
50
- value: code,
51
- label: code,
52
- })),
72
+ countryItems: computed(() => buildBillingCountryItems(locale.value)),
53
73
  };
54
74
  }
@@ -0,0 +1,141 @@
1
+ const BILLING_INFO_ENDPOINT = "/api/saas/tenant/billing-info";
2
+ const BUSINESS_CUSTOMER_TYPE = "business";
3
+ const COUNTRY_CODE_PATTERN = /^[A-Z]{2}$/;
4
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5
+
6
+ export type CustomerType = "individual" | "business";
7
+ export type VatVerificationStatus = "pending" | "verified" | "unverified";
8
+
9
+ export type BillingIdentityField =
10
+ | "customerType"
11
+ | "companyName"
12
+ | "country"
13
+ | "line1"
14
+ | "postalCode"
15
+ | "city"
16
+ | "billingEmail";
17
+
18
+ /** The editable billing identity, as the form holds it. */
19
+ export interface BillingIdentityDraft {
20
+ customerType: CustomerType | null;
21
+ companyName: string;
22
+ vatNumber: string;
23
+ billingEmail: string;
24
+ country: string;
25
+ line1: string;
26
+ postalCode: string;
27
+ city: string;
28
+ }
29
+
30
+ interface BillingAddress {
31
+ line1: string | null;
32
+ line2: string | null;
33
+ postalCode: string | null;
34
+ city: string | null;
35
+ state: string | null;
36
+ country: string | null;
37
+ }
38
+
39
+ export interface BillingInfoResponse {
40
+ customerType: CustomerType | null;
41
+ companyName: string | null;
42
+ vatNumber: string | null;
43
+ vatVerificationStatus: VatVerificationStatus | null;
44
+ billingEmail: string | null;
45
+ address: BillingAddress | null;
46
+ missingFields: BillingIdentityField[];
47
+ }
48
+
49
+ type FieldCheck = (draft: BillingIdentityDraft) => boolean;
50
+
51
+ function isFilled(value: string): boolean {
52
+ return value.trim().length > 0;
53
+ }
54
+
55
+ // Mirrors the server-side rules (src/workspaces/billing-identity.ts): the
56
+ // form flags a field before the request, the server refuses it regardless.
57
+ const FIELD_CHECKS: Record<BillingIdentityField, FieldCheck> = {
58
+ customerType: (draft) => draft.customerType !== null,
59
+ companyName: (draft) =>
60
+ draft.customerType !== BUSINESS_CUSTOMER_TYPE ||
61
+ isFilled(draft.companyName),
62
+ country: (draft) => COUNTRY_CODE_PATTERN.test(draft.country.trim()),
63
+ line1: (draft) => isFilled(draft.line1),
64
+ postalCode: (draft) => isFilled(draft.postalCode),
65
+ city: (draft) => isFilled(draft.city),
66
+ billingEmail: (draft) => EMAIL_PATTERN.test(draft.billingEmail.trim()),
67
+ };
68
+
69
+ /**
70
+ * Required fields still missing or malformed, in form order; empty when the
71
+ * identity can be saved. The VAT number is optional, even for a business.
72
+ */
73
+ export function findMissingBillingFields(
74
+ draft: BillingIdentityDraft,
75
+ ): BillingIdentityField[] {
76
+ return (Object.keys(FIELD_CHECKS) as BillingIdentityField[]).filter(
77
+ (field) => !FIELD_CHECKS[field](draft),
78
+ );
79
+ }
80
+
81
+ export function emptyBillingIdentityDraft(): BillingIdentityDraft {
82
+ return {
83
+ customerType: null,
84
+ companyName: "",
85
+ vatNumber: "",
86
+ billingEmail: "",
87
+ country: "",
88
+ line1: "",
89
+ postalCode: "",
90
+ city: "",
91
+ };
92
+ }
93
+
94
+ export function toBillingIdentityDraft(
95
+ info: BillingInfoResponse,
96
+ ): BillingIdentityDraft {
97
+ return {
98
+ customerType: info.customerType,
99
+ companyName: info.companyName ?? "",
100
+ vatNumber: info.vatNumber ?? "",
101
+ billingEmail: info.billingEmail ?? "",
102
+ country: info.address?.country ?? "",
103
+ line1: info.address?.line1 ?? "",
104
+ postalCode: info.address?.postalCode ?? "",
105
+ city: info.address?.city ?? "",
106
+ };
107
+ }
108
+
109
+ function toRequestBody(draft: BillingIdentityDraft) {
110
+ const isBusiness = draft.customerType === BUSINESS_CUSTOMER_TYPE;
111
+ return {
112
+ customerType: draft.customerType,
113
+ companyName: isBusiness ? draft.companyName : null,
114
+ vatNumber: isBusiness ? draft.vatNumber : null,
115
+ billingEmail: draft.billingEmail,
116
+ address: {
117
+ country: draft.country,
118
+ line1: draft.line1,
119
+ postalCode: draft.postalCode,
120
+ city: draft.city,
121
+ },
122
+ };
123
+ }
124
+
125
+ /** Reads and saves the workspace billing identity. */
126
+ export function useBillingIdentity() {
127
+ const { $authFetch } = useAuthFetch();
128
+
129
+ function load(): Promise<BillingInfoResponse> {
130
+ return $authFetch<BillingInfoResponse>(BILLING_INFO_ENDPOINT);
131
+ }
132
+
133
+ function save(draft: BillingIdentityDraft): Promise<BillingInfoResponse> {
134
+ return $authFetch<BillingInfoResponse>(BILLING_INFO_ENDPOINT, {
135
+ method: "PUT",
136
+ body: toRequestBody(draft),
137
+ });
138
+ }
139
+
140
+ return { load, save };
141
+ }
@@ -0,0 +1,69 @@
1
+ export interface InvitationLink {
2
+ link: string;
3
+ expiresAt: string;
4
+ }
5
+
6
+ export interface InvitationRef {
7
+ tenantId: string;
8
+ inviteId: string;
9
+ }
10
+
11
+ const INVITATIONS_ENDPOINT = "/api/saas/workspaces";
12
+
13
+ /**
14
+ * Retrieves an invitation's signup link (each retrieval is journaled server
15
+ * side) and puts it on the clipboard, reporting the outcome with a toast.
16
+ */
17
+ export function useInvitationLink() {
18
+ const { $authFetch } = useAuthFetch();
19
+ const nuxtApp = useDmsApp();
20
+ const toast = useToast();
21
+ const { resolveApiError } = useApiErrorMessage();
22
+
23
+ function fetchInvitationLink(ref: InvitationRef): Promise<InvitationLink> {
24
+ return $authFetch<InvitationLink>(
25
+ `${INVITATIONS_ENDPOINT}/${ref.tenantId}/invitations/${ref.inviteId}/link`,
26
+ { method: "POST" },
27
+ );
28
+ }
29
+
30
+ /** Returns whether the link reached the clipboard. */
31
+ async function copyLink(link: string): Promise<boolean> {
32
+ const t = nuxtApp.$i18n.t;
33
+ try {
34
+ await navigator.clipboard.writeText(link);
35
+ toast.add({
36
+ title: t("saas.workspaces.invitations.link.copied"),
37
+ color: "success",
38
+ icon: "i-ph-check-circle",
39
+ });
40
+ return true;
41
+ } catch {
42
+ // The clipboard can be refused (permissions, insecure origin): the link
43
+ // stays readable so it can still be copied by hand.
44
+ toast.add({
45
+ title: t("saas.workspaces.invitations.link.copy_failed"),
46
+ description: link,
47
+ color: "warning",
48
+ icon: "i-ph-warning",
49
+ duration: 0,
50
+ });
51
+ return false;
52
+ }
53
+ }
54
+
55
+ async function fetchAndCopy(ref: InvitationRef): Promise<void> {
56
+ try {
57
+ const { link } = await fetchInvitationLink(ref);
58
+ await copyLink(link);
59
+ } catch (error) {
60
+ toast.add({
61
+ title: resolveApiError(error, "saas.workspaces.invitations.link.error"),
62
+ color: "error",
63
+ icon: "i-ph-warning-circle",
64
+ });
65
+ }
66
+ }
67
+
68
+ return { fetchInvitationLink, copyLink, fetchAndCopy };
69
+ }
@@ -0,0 +1,20 @@
1
+ import type { BillingStatusResponse } from "./useBillingStatus";
2
+
3
+ /**
4
+ * What the past-due banner offers: the invoice itself when Stripe hands out
5
+ * its payment page, else the billing page to the owner, who can settle from
6
+ * there; a member only gets the notice, since the owner pays.
7
+ */
8
+ export type PastDueBannerAction = "settle" | "open_billing" | "none";
9
+
10
+ type PastDueBannerStatus = Pick<
11
+ BillingStatusResponse,
12
+ "isTenantOwner" | "unpaidInvoice"
13
+ >;
14
+
15
+ export function resolvePastDueBannerAction(
16
+ status: PastDueBannerStatus | null,
17
+ ): PastDueBannerAction {
18
+ if (status?.unpaidInvoice?.hostedInvoiceUrl) return "settle";
19
+ return status?.isTenantOwner ? "open_billing" : "none";
20
+ }
@@ -0,0 +1,15 @@
1
+ const OPEN_STATE_KEY = "saas-plan-comparison-open";
2
+
3
+ /**
4
+ * Open state of the plan comparison modal hosted by the plan card, shared so
5
+ * other billing blocks (the payment method card's "add a card") can open it.
6
+ */
7
+ export function usePlanComparison() {
8
+ const isOpen = useDmsState<boolean>(OPEN_STATE_KEY, () => false);
9
+
10
+ function open(): void {
11
+ isOpen.value = true;
12
+ }
13
+
14
+ return { isOpen, open };
15
+ }