@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.
- package/README.md +262 -13
- package/dist/auth/subscription-access-gate.d.ts +1 -1
- 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/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/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/notifications/subjects.d.ts +19 -19
- 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/categories.d.ts +6 -6
- package/dist/pages/platform/credit-notes.d.ts +1 -1
- package/dist/pages/platform/dashboard.d.ts +2 -2
- package/dist/pages/platform/dashboard.js +2 -4
- package/dist/pages/platform/dashboard.js.map +1 -1
- package/dist/pages/platform/features.d.ts +1 -1
- package/dist/pages/platform/invoices.d.ts +1 -1
- package/dist/pages/platform/plans/edit.d.ts +2 -2
- package/dist/pages/platform/plans/index.d.ts +1 -1
- package/dist/pages/platform/plans/migrations.d.ts +1 -1
- package/dist/pages/platform/segments/index.d.ts +1 -1
- package/dist/pages/platform/settings.d.ts +1 -1
- package/dist/pages/platform/users/detail.d.ts +1 -1
- package/dist/pages/platform/users/index.d.ts +1 -1
- package/dist/pages/platform/workspaces/detail.d.ts +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 +4 -10
- 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/pages/tenant/billing.d.ts +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/billing-settings.d.ts +2 -2
- 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/plan-deletion.d.ts +1 -1
- package/dist/routes/platformOwner/plans.d.ts +5 -5
- package/dist/routes/platformOwner/segments.d.ts +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/legal-documents.d.ts +2 -2
- 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.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 +3 -1
- 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/utils/invoice-lines-type.d.ts +1 -1
- 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 +109 -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 +39 -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/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/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 +15 -15
- 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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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">{{
|
|
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
|
|
41
|
-
//
|
|
42
|
-
//
|
|
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
|
|
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:
|
|
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
|
+
}
|