mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
Merge branch 'main' into fix/name-input-invalid-characters
This commit is contained in:
@@ -3,6 +3,7 @@ import { SUPPORT_EMAIL } from '@documenso/lib/constants/app';
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION, SKIP_QUERY_BATCH_META } from '@documenso/lib/constants/trpc';
|
||||
import { INTERNAL_CLAIM_ID } from '@documenso/lib/types/subscription';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@@ -38,12 +39,18 @@ export const OrganisationQuotaBanner = () => {
|
||||
quotaFlags?.isDocumentQuotaExceeded || quotaFlags?.isEmailQuotaExceeded || quotaFlags?.isApiQuotaExceeded,
|
||||
);
|
||||
|
||||
// Every member of the organisation sees the banner when a quota is exhausted.
|
||||
const isAnyQuotaNearing = Boolean(
|
||||
quotaFlags?.isDocumentQuotaNearing || quotaFlags?.isEmailQuotaNearing || quotaFlags?.isApiQuotaNearing,
|
||||
);
|
||||
|
||||
// Every member of the organisation sees the banner when a quota is exhausted or
|
||||
// nearing its limit. When both states apply, "exceeded" wins for the banner copy
|
||||
// and the dialog lists both exceeded and nearing items.
|
||||
// Note: Skipping free plan banner for now because their quota can incorrectly show as exceeded.
|
||||
if (
|
||||
!organisation ||
|
||||
!quotaFlags ||
|
||||
!isAnyQuotaExceeded ||
|
||||
(!isAnyQuotaExceeded && !isAnyQuotaNearing) ||
|
||||
organisation.organisationClaim.originalSubscriptionClaimId === INTERNAL_CLAIM_ID.FREE
|
||||
) {
|
||||
return null;
|
||||
@@ -51,17 +58,29 @@ export const OrganisationQuotaBanner = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-yellow-200 text-yellow-900 dark:bg-yellow-400">
|
||||
<div
|
||||
className={cn({
|
||||
'bg-yellow-200 text-yellow-900 dark:bg-yellow-400': !isAnyQuotaExceeded,
|
||||
'bg-destructive text-destructive-foreground': isAnyQuotaExceeded,
|
||||
})}
|
||||
>
|
||||
<div className="mx-auto flex max-w-screen-xl items-center justify-center gap-x-4 px-4 py-2 font-medium text-sm">
|
||||
<div className="flex items-center">
|
||||
<AlertTriangle className="mr-2.5 h-5 w-5" />
|
||||
|
||||
<Trans>Your organisation has exceeded a fair use limit</Trans>
|
||||
{isAnyQuotaExceeded ? (
|
||||
<Trans>Your organisation has exceeded a fair use limit</Trans>
|
||||
) : (
|
||||
<Trans>Your organisation is approaching a fair use limit</Trans>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-yellow-900 hover:bg-yellow-100 dark:hover:bg-yellow-500"
|
||||
className={cn({
|
||||
'text-yellow-900 hover:bg-yellow-100 dark:hover:bg-yellow-500': !isAnyQuotaExceeded,
|
||||
'text-destructive-foreground hover:bg-destructive hover:text-white': isAnyQuotaExceeded,
|
||||
})}
|
||||
onClick={() => setIsOpen(true)}
|
||||
size="sm"
|
||||
>
|
||||
@@ -74,17 +93,27 @@ export const OrganisationQuotaBanner = () => {
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Fair use limit exceeded</Trans>
|
||||
{isAnyQuotaExceeded ? <Trans>Fair use limit exceeded</Trans> : <Trans>Approaching fair use limit</Trans>}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Your organisation has exceeded a fair use limit. Please contact{' '}
|
||||
<a className="text-primary" href={`mailto:${SUPPORT_EMAIL}`}>
|
||||
support
|
||||
</a>{' '}
|
||||
to review your plan's limits.
|
||||
</Trans>
|
||||
{isAnyQuotaExceeded ? (
|
||||
<Trans>
|
||||
Your organisation has exceeded a fair use limit. Please contact{' '}
|
||||
<a className="text-primary" href={`mailto:${SUPPORT_EMAIL}`}>
|
||||
support
|
||||
</a>{' '}
|
||||
to review your plan's limits.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Your organisation is approaching a fair use limit. If you expect to need higher limits, please contact{' '}
|
||||
<a className="text-primary" href={`mailto:${SUPPORT_EMAIL}`}>
|
||||
support
|
||||
</a>{' '}
|
||||
to review your plan's limits.
|
||||
</Trans>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -106,6 +135,21 @@ export const OrganisationQuotaBanner = () => {
|
||||
<Trans>API requests have been temporarily paused</Trans>
|
||||
</li>
|
||||
)}
|
||||
{quotaFlags.isDocumentQuotaNearing && (
|
||||
<li className="list-disc">
|
||||
<Trans>Document usage is approaching fair use limits</Trans>
|
||||
</li>
|
||||
)}
|
||||
{quotaFlags.isEmailQuotaNearing && (
|
||||
<li className="list-disc">
|
||||
<Trans>Email usage is approaching fair use limits</Trans>
|
||||
</li>
|
||||
)}
|
||||
{quotaFlags.isApiQuotaNearing && (
|
||||
<li className="list-disc">
|
||||
<Trans>API usage is approaching fair use limits</Trans>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -298,10 +298,10 @@ const EnvelopeEditPage = ({ embedAuthoringOptions }: EnvelopeEditPageProps) => {
|
||||
presignToken: token,
|
||||
mode: 'edit' as const,
|
||||
onUpdate: async (envelope: TEditorEnvelope) => updateEmbeddedEnvelope(envelope),
|
||||
brandingLogo,
|
||||
customBrandingLogo: Boolean(brandingLogo),
|
||||
user: embedAuthoringOptions.user,
|
||||
}),
|
||||
[token],
|
||||
[token, brandingLogo, embedAuthoringOptions.user],
|
||||
);
|
||||
|
||||
const editorConfig = useMemo(() => {
|
||||
|
||||
@@ -106,5 +106,5 @@
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "2.11.0"
|
||||
"version": "2.12.0"
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "2.11.0",
|
||||
"version": "2.12.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "2.11.0",
|
||||
"version": "2.12.0",
|
||||
"hasInstallScript": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
@@ -406,7 +406,7 @@
|
||||
},
|
||||
"apps/remix": {
|
||||
"name": "@documenso/remix",
|
||||
"version": "2.11.0",
|
||||
"version": "2.12.0",
|
||||
"dependencies": {
|
||||
"@cantoo/pdf-lib": "^2.5.3",
|
||||
"@documenso/api": "*",
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"version": "2.11.0",
|
||||
"version": "2.12.0",
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"build": "turbo run build",
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { INTERNAL_CLAIM_ID } from '@documenso/lib/types/subscription';
|
||||
import { generateDatabaseId } from '@documenso/lib/universal/id';
|
||||
import { currentMonthlyPeriod } from '@documenso/lib/universal/monthly-period';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
const BANNER_EXCEEDED_TEXT = 'Your organisation has exceeded a fair use limit';
|
||||
const BANNER_NEARING_TEXT = 'Your organisation is approaching a fair use limit';
|
||||
|
||||
type SeedQuotaStateOptions = {
|
||||
organisationId: string;
|
||||
organisationClaimId: string;
|
||||
/**
|
||||
* The `originalSubscriptionClaimId` to set on the claim. The banner is suppressed
|
||||
* for `INTERNAL_CLAIM_ID.FREE`, so use a non-free value to make it render.
|
||||
*/
|
||||
subscriptionClaimId: string;
|
||||
documentQuota: number | null;
|
||||
documentCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Point the organisation's document quota and current-period usage at a known
|
||||
* state. Only the document counter is touched; email/api quotas stay `null`
|
||||
* (unlimited) so the document counter is the sole driver of the banner.
|
||||
*/
|
||||
const seedQuotaState = async ({
|
||||
organisationId,
|
||||
organisationClaimId,
|
||||
subscriptionClaimId,
|
||||
documentQuota,
|
||||
documentCount,
|
||||
}: SeedQuotaStateOptions) => {
|
||||
await prisma.organisationClaim.update({
|
||||
where: {
|
||||
id: organisationClaimId,
|
||||
},
|
||||
data: {
|
||||
originalSubscriptionClaimId: subscriptionClaimId,
|
||||
documentQuota,
|
||||
},
|
||||
});
|
||||
|
||||
const period = currentMonthlyPeriod();
|
||||
|
||||
await prisma.organisationMonthlyStat.upsert({
|
||||
where: {
|
||||
organisationId_period: {
|
||||
organisationId,
|
||||
period,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
documentCount,
|
||||
},
|
||||
create: {
|
||||
id: generateDatabaseId('org_monthly_stat'),
|
||||
organisationId,
|
||||
period,
|
||||
documentCount,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
test('[QUOTA BANNER]: shows the approaching state when a quota is nearing', async ({ page }) => {
|
||||
const { user, organisation } = await seedUser({
|
||||
isPersonalOrganisation: false,
|
||||
});
|
||||
|
||||
// ceil(10 * 0.8) = 8 → nearing, but not yet exceeded.
|
||||
await seedQuotaState({
|
||||
organisationId: organisation.id,
|
||||
organisationClaimId: organisation.organisationClaim.id,
|
||||
subscriptionClaimId: INTERNAL_CLAIM_ID.TEAM,
|
||||
documentQuota: 10,
|
||||
documentCount: 8,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/o/${organisation.url}/settings/general`,
|
||||
});
|
||||
|
||||
await expect(page.getByText(BANNER_NEARING_TEXT)).toBeVisible();
|
||||
await expect(page.getByText(BANNER_EXCEEDED_TEXT)).toBeHidden();
|
||||
});
|
||||
|
||||
test('[QUOTA BANNER]: shows the exceeded state when a quota is reached', async ({ page }) => {
|
||||
const { user, organisation } = await seedUser({
|
||||
isPersonalOrganisation: false,
|
||||
});
|
||||
|
||||
// usage >= quota → exceeded.
|
||||
await seedQuotaState({
|
||||
organisationId: organisation.id,
|
||||
organisationClaimId: organisation.organisationClaim.id,
|
||||
subscriptionClaimId: INTERNAL_CLAIM_ID.TEAM,
|
||||
documentQuota: 10,
|
||||
documentCount: 10,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/o/${organisation.url}/settings/general`,
|
||||
});
|
||||
|
||||
await expect(page.getByText(BANNER_EXCEEDED_TEXT)).toBeVisible();
|
||||
await expect(page.getByText(BANNER_NEARING_TEXT)).toBeHidden();
|
||||
});
|
||||
|
||||
test('[QUOTA BANNER]: learn more dialog lists the affected counter', async ({ page }) => {
|
||||
const { user, organisation } = await seedUser({
|
||||
isPersonalOrganisation: false,
|
||||
});
|
||||
|
||||
await seedQuotaState({
|
||||
organisationId: organisation.id,
|
||||
organisationClaimId: organisation.organisationClaim.id,
|
||||
subscriptionClaimId: INTERNAL_CLAIM_ID.TEAM,
|
||||
documentQuota: 10,
|
||||
documentCount: 10,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/o/${organisation.url}/settings/general`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Learn more' }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
|
||||
await expect(dialog.getByText('Fair use limit exceeded')).toBeVisible();
|
||||
await expect(dialog.getByText('Document creation has been temporarily paused')).toBeVisible();
|
||||
await expect(dialog.getByRole('link', { name: 'support' })).toHaveAttribute('href', /^mailto:/);
|
||||
});
|
||||
|
||||
test('[QUOTA BANNER]: is hidden for free-claim organisations', async ({ page }) => {
|
||||
const { user, organisation } = await seedUser({
|
||||
isPersonalOrganisation: false,
|
||||
});
|
||||
|
||||
// Usage is exceeded, but a free-claim organisation must never see the banner.
|
||||
await seedQuotaState({
|
||||
organisationId: organisation.id,
|
||||
organisationClaimId: organisation.organisationClaim.id,
|
||||
subscriptionClaimId: INTERNAL_CLAIM_ID.FREE,
|
||||
documentQuota: 10,
|
||||
documentCount: 10,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/o/${organisation.url}/settings/general`,
|
||||
});
|
||||
|
||||
// Anchor on a stable element so banner-absence is meaningful (page fully loaded).
|
||||
await expect(page.getByLabel('Organisation Name*')).toBeVisible();
|
||||
|
||||
await expect(page.getByText(BANNER_EXCEEDED_TEXT)).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: 'Learn more' })).toBeHidden();
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import { SUPPORT_EMAIL } from '@documenso/lib/constants/app';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { match } from 'ts-pattern';
|
||||
import { Body, Container, Head, Hr, Html, Preview, Section, Text } from '../components';
|
||||
import { TemplateBrandingLogo } from '../template-components/template-branding-logo';
|
||||
import { TemplateFooter } from '../template-components/template-footer';
|
||||
|
||||
export type OrganisationLimitAlertEmailProps = {
|
||||
assetBaseUrl: string;
|
||||
organisationName: string;
|
||||
counter: 'document' | 'email' | 'api';
|
||||
kind: 'rateLimit' | 'quota' | 'quotaNearing';
|
||||
period: string;
|
||||
};
|
||||
|
||||
export const OrganisationLimitAlertEmailTemplate = ({
|
||||
assetBaseUrl = 'http://localhost:3002',
|
||||
organisationName = 'Organisation Name',
|
||||
counter = 'email',
|
||||
kind = 'quota',
|
||||
period = '2026-05',
|
||||
}: OrganisationLimitAlertEmailProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const previewText = kind === 'quotaNearing' ? msg`Approaching Your Plan Limits` : msg`Organisation Review Required`;
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{_(previewText)}</Preview>
|
||||
|
||||
<Body className="mx-auto my-auto font-sans">
|
||||
<Section className="bg-white text-slate-500">
|
||||
<Container className="mx-auto mt-8 mb-2 max-w-xl rounded-lg border border-slate-200 border-solid p-2 backdrop-blur-sm">
|
||||
<TemplateBrandingLogo assetBaseUrl={assetBaseUrl} className="mb-4 h-6 p-2" />
|
||||
|
||||
<Section className="p-2 text-slate-500">
|
||||
<Text className="text-center font-medium text-black text-lg">
|
||||
{kind === 'quotaNearing' ? (
|
||||
<Trans>Approaching Your Plan Limits</Trans>
|
||||
) : (
|
||||
<Trans>Organisation Review Required</Trans>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 font-medium text-base text-slate-600">
|
||||
{organisationName}
|
||||
</div>
|
||||
|
||||
{match(kind)
|
||||
.with('quota', () => (
|
||||
<Text className="text-center text-base">
|
||||
{match(counter)
|
||||
.with('document', () => (
|
||||
<Trans>
|
||||
We've noticed document activity on your account that exceeds the fair use limits of your
|
||||
current plan. As a precaution, new document activity has been temporarily paused pending
|
||||
review.
|
||||
</Trans>
|
||||
))
|
||||
.with('email', () => (
|
||||
<Trans>
|
||||
We've noticed email sending activity on your account that exceeds the fair use limits of your
|
||||
current plan. As a precaution, new email activity has been temporarily paused pending review.
|
||||
</Trans>
|
||||
))
|
||||
.with('api', () => (
|
||||
<Trans>
|
||||
We've noticed API activity on your account that exceeds the fair use limits of your current
|
||||
plan. As a precaution, new API activity has been temporarily paused pending review.
|
||||
</Trans>
|
||||
))
|
||||
.exhaustive()}
|
||||
</Text>
|
||||
))
|
||||
.with('rateLimit', () => (
|
||||
<Text className="text-center text-base">
|
||||
{match(counter)
|
||||
.with('document', () => (
|
||||
<Trans>
|
||||
Your organisation is generating documents faster than normal, so some requests are being
|
||||
temporarily throttled.
|
||||
</Trans>
|
||||
))
|
||||
.with('email', () => (
|
||||
<Trans>
|
||||
Your organisation is generating emails faster than normal, so some requests are being
|
||||
temporarily throttled.
|
||||
</Trans>
|
||||
))
|
||||
.with('api', () => (
|
||||
<Trans>
|
||||
Your organisation is generating API requests faster than normal, so some requests are being
|
||||
temporarily throttled.
|
||||
</Trans>
|
||||
))
|
||||
.exhaustive()}
|
||||
</Text>
|
||||
))
|
||||
.with('quotaNearing', () => (
|
||||
<Text className="text-center text-base">
|
||||
{match(counter)
|
||||
.with('document', () => (
|
||||
<Trans>
|
||||
Your organisation is nearing its fair use limits for creating documents on your current plan.
|
||||
Once the limit is reached, new document activity will be temporarily paused.
|
||||
</Trans>
|
||||
))
|
||||
.with('email', () => (
|
||||
<Trans>
|
||||
Your organisation is nearing its fair use limits for sending email on your current plan. Once
|
||||
the limit is reached, new email activity will be temporarily paused.
|
||||
</Trans>
|
||||
))
|
||||
.with('api', () => (
|
||||
<Trans>
|
||||
Your organisation is nearing its fair use limits for making API requests on your current plan.
|
||||
Once the limit is reached, new API activity will be temporarily paused.
|
||||
</Trans>
|
||||
))
|
||||
.exhaustive()}
|
||||
</Text>
|
||||
))
|
||||
.exhaustive()}
|
||||
|
||||
<Text className="text-center text-base">
|
||||
{kind === 'quotaNearing' ? (
|
||||
<Trans>
|
||||
If you expect to need higher limits, please contact support at {SUPPORT_EMAIL} and we will review
|
||||
your account.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>Please contact support at {SUPPORT_EMAIL} and we will review your account.</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
|
||||
<Hr className="mx-auto mt-12 max-w-xl" />
|
||||
|
||||
<Container className="mx-auto max-w-xl">
|
||||
<TemplateFooter isDocument={false} />
|
||||
</Container>
|
||||
</Section>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganisationLimitAlertEmailTemplate;
|
||||
@@ -1,109 +0,0 @@
|
||||
import { SUPPORT_EMAIL } from '@documenso/lib/constants/app';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { match } from 'ts-pattern';
|
||||
import { Body, Container, Head, Hr, Html, Preview, Section, Text } from '../components';
|
||||
import { TemplateBrandingLogo } from '../template-components/template-branding-logo';
|
||||
import { TemplateFooter } from '../template-components/template-footer';
|
||||
|
||||
export type OrganisationLimitExceededEmailProps = {
|
||||
assetBaseUrl: string;
|
||||
organisationName: string;
|
||||
counter: 'document' | 'email' | 'api';
|
||||
kind: 'rateLimit' | 'quota';
|
||||
period: string;
|
||||
};
|
||||
|
||||
export const OrganisationLimitExceededEmailTemplate = ({
|
||||
assetBaseUrl = 'http://localhost:3002',
|
||||
organisationName = 'Organisation Name',
|
||||
counter = 'email',
|
||||
kind = 'quota',
|
||||
period = '2026-05',
|
||||
}: OrganisationLimitExceededEmailProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const previewText = msg`Organisation Review Required`;
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{_(previewText)}</Preview>
|
||||
|
||||
<Body className="mx-auto my-auto font-sans">
|
||||
<Section className="bg-white text-slate-500">
|
||||
<Container className="mx-auto mt-8 mb-2 max-w-xl rounded-lg border border-slate-200 border-solid p-2 backdrop-blur-sm">
|
||||
<TemplateBrandingLogo assetBaseUrl={assetBaseUrl} className="mb-4 h-6 p-2" />
|
||||
|
||||
<Section className="p-2 text-slate-500">
|
||||
<Text className="text-center font-medium text-black text-lg">
|
||||
<Trans>Organisation Review Required</Trans>
|
||||
</Text>
|
||||
|
||||
{kind === 'quota' ? (
|
||||
<Text className="text-center text-base">
|
||||
{match(counter)
|
||||
.with('document', () => (
|
||||
<Trans>
|
||||
We've noticed document activity on your account that exceeds the fair use limits of your current
|
||||
plan. As a precaution, new document activity has been temporarily paused pending review.
|
||||
</Trans>
|
||||
))
|
||||
.with('email', () => (
|
||||
<Trans>
|
||||
We've noticed email sending activity on your account that exceeds the fair use limits of your
|
||||
current plan. As a precaution, new email activity has been temporarily paused pending review.
|
||||
</Trans>
|
||||
))
|
||||
.with('api', () => (
|
||||
<Trans>
|
||||
We've noticed API activity on your account that exceeds the fair use limits of your current
|
||||
plan. As a precaution, new API activity has been temporarily paused pending review.
|
||||
</Trans>
|
||||
))
|
||||
.exhaustive()}
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="text-center text-base">
|
||||
{match(counter)
|
||||
.with('document', () => (
|
||||
<Trans>
|
||||
Your organisation is generating documents faster than normal, so some requests are being
|
||||
temporarily throttled.
|
||||
</Trans>
|
||||
))
|
||||
.with('email', () => (
|
||||
<Trans>
|
||||
Your organisation is generating emails faster than normal, so some requests are being
|
||||
temporarily throttled.
|
||||
</Trans>
|
||||
))
|
||||
.with('api', () => (
|
||||
<Trans>
|
||||
Your organisation is generating API requests faster than normal, so some requests are being
|
||||
temporarily throttled.
|
||||
</Trans>
|
||||
))
|
||||
.exhaustive()}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text className="text-center text-base">
|
||||
<Trans>Please contact support at {SUPPORT_EMAIL} and we will review your account.</Trans>
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
|
||||
<Hr className="mx-auto mt-12 max-w-xl" />
|
||||
|
||||
<Container className="mx-auto max-w-xl">
|
||||
<TemplateFooter isDocument={false} />
|
||||
</Container>
|
||||
</Section>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganisationLimitExceededEmailTemplate;
|
||||
@@ -4,7 +4,7 @@ import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/emails/sen
|
||||
import { SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-cancelled-emails';
|
||||
import { SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-completed-emails';
|
||||
import { SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION } from './definitions/emails/send-document-created-from-direct-template-email';
|
||||
import { SEND_ORGANISATION_LIMIT_EXCEEDED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-limit-exceeded-email';
|
||||
import { SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-limit-alert-email';
|
||||
import { SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-joined-email';
|
||||
import { SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-left-email';
|
||||
import { SEND_OWNER_RECIPIENT_EXPIRED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-owner-recipient-expired-email';
|
||||
@@ -37,7 +37,7 @@ export const jobsClient = new JobClient([
|
||||
SEND_CONFIRMATION_EMAIL_JOB_DEFINITION,
|
||||
SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION,
|
||||
SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION,
|
||||
SEND_ORGANISATION_LIMIT_EXCEEDED_EMAIL_JOB_DEFINITION,
|
||||
SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION,
|
||||
SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION,
|
||||
SEAL_DOCUMENT_JOB_DEFINITION,
|
||||
SEAL_DOCUMENT_SWEEP_JOB_DEFINITION,
|
||||
|
||||
+50
-22
@@ -1,21 +1,25 @@
|
||||
import OrganisationLimitExceededEmailTemplate from '@documenso/email/templates/organisation-limit-exceeded';
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import OrganisationLimitAlertEmailTemplate from '@documenso/email/templates/organisation-limit-alert';
|
||||
import { SUPPORT_EMAIL } from '@documenso/lib/constants/app';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { createElement } from 'react';
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { DOCUMENSO_INTERNAL_EMAIL } from '../../../constants/email';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../../constants/organisations';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { INTERNAL_CLAIM_ID } from '../../../types/subscription';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendOrganisationLimitExceededEmailJobDefinition } from './send-organisation-limit-exceeded-email';
|
||||
import type { TSendOrganisationLimitAlertEmailJobDefinition } from './send-organisation-limit-alert-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSendOrganisationLimitExceededEmailJobDefinition;
|
||||
payload: TSendOrganisationLimitAlertEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const organisation = await prisma.organisation.findFirstOrThrow({
|
||||
@@ -24,6 +28,16 @@ export const run = async ({
|
||||
},
|
||||
include: {
|
||||
organisationClaim: true,
|
||||
monthlyStats: {
|
||||
where: {
|
||||
period: payload.period,
|
||||
},
|
||||
select: {
|
||||
documentCount: true,
|
||||
emailCount: true,
|
||||
apiCount: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
where: {
|
||||
organisationGroupMembers: {
|
||||
@@ -60,16 +74,19 @@ export const run = async ({
|
||||
// Do not send emails for "free" claims.
|
||||
if (organisation.organisationClaim.originalSubscriptionClaimId === INTERNAL_CLAIM_ID.FREE) {
|
||||
io.logger.info({
|
||||
msg: 'Skipping organisation limit exceeded email for "free" claim',
|
||||
msg: 'Skipping organisation limit alert email for "free" claim',
|
||||
organisationId: organisation.id,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const memberSubject =
|
||||
payload.kind === 'quotaNearing' ? msg`Approaching Your Plan Limits` : msg`Organisation Review Required`;
|
||||
|
||||
for (const member of organisation.members) {
|
||||
await io.runTask(`send-organisation-limit-exceeded-email-${member.id}`, async () => {
|
||||
const emailContent = createElement(OrganisationLimitExceededEmailTemplate, {
|
||||
await io.runTask(`send-organisation-limit-alert-email-${member.id}`, async () => {
|
||||
const emailContent = createElement(OrganisationLimitAlertEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
organisationName: organisation.name,
|
||||
counter: payload.counter,
|
||||
@@ -87,7 +104,7 @@ export const run = async ({
|
||||
await emailTransport.sendMail({
|
||||
to: member.user.email,
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`Organisation Review Required`),
|
||||
subject: i18n._(memberSubject),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
@@ -95,20 +112,31 @@ export const run = async ({
|
||||
}
|
||||
|
||||
// Todo: Logging
|
||||
// Todo: Decide if we want to send an email or alert via another software.
|
||||
// const i18n = await getI18nInstance('en');
|
||||
const i18n = await getI18nInstance('en');
|
||||
|
||||
// // Email our support team.
|
||||
// await mailer.sendMail({
|
||||
// to: SUPPORT_EMAIL,
|
||||
// from: senderEmail,
|
||||
// subject: i18n._(msg`An organisation has exceeded their fair use limits`),
|
||||
// text: `
|
||||
// Organisation: ${organisation.name}
|
||||
// Organisation ID: ${organisation.id}
|
||||
// Counter: ${payload.counter}
|
||||
// Kind: ${payload.kind}
|
||||
// Period: ${payload.period}
|
||||
// `,
|
||||
// });
|
||||
const supportSubject =
|
||||
payload.kind === 'quotaNearing'
|
||||
? msg`An organisation is nearing their fair use limits`
|
||||
: msg`An organisation has exceeded their fair use limits`;
|
||||
|
||||
// Email our support team. Purposefully sent from the internal email since the
|
||||
// global mailer is not authorized to send from custom per-plan transport addresses.
|
||||
await io.runTask('send-organisation-limit-alert-support-email', async () => {
|
||||
await mailer.sendMail({
|
||||
to: SUPPORT_EMAIL,
|
||||
from: DOCUMENSO_INTERNAL_EMAIL,
|
||||
subject: i18n._(supportSubject),
|
||||
text: `
|
||||
Organisation: ${organisation.name}
|
||||
Organisation ID: ${organisation.id}
|
||||
Organisation Claim Original ID: ${organisation.organisationClaim.originalSubscriptionClaimId}
|
||||
Email Quota: ${organisation.monthlyStats[0]?.emailCount || 0}/${organisation.organisationClaim.emailQuota ?? 'Unlimited'}
|
||||
API Quota: ${organisation.monthlyStats[0]?.apiCount || 0}/${organisation.organisationClaim.apiQuota ?? 'Unlimited'}
|
||||
Document Quota: ${organisation.monthlyStats[0]?.documentCount || 0}/${organisation.organisationClaim.documentQuota ?? 'Unlimited'}
|
||||
Counter: ${payload.counter}
|
||||
Kind: ${payload.kind}
|
||||
Period: ${payload.period}
|
||||
`,
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION_ID = 'send.organisation-limit-alert.email';
|
||||
|
||||
const SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
organisationId: z.string(),
|
||||
counter: z.enum(['document', 'email', 'api']),
|
||||
kind: z.enum(['rateLimit', 'quota', 'quotaNearing']),
|
||||
period: z.string(),
|
||||
});
|
||||
|
||||
export type TSendOrganisationLimitAlertEmailJobDefinition = z.infer<
|
||||
typeof SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Organisation Limit Alert Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-organisation-limit-alert-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendOrganisationLimitAlertEmailJobDefinition
|
||||
>;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_ORGANISATION_LIMIT_EXCEEDED_EMAIL_JOB_DEFINITION_ID = 'send.organisation-limit-exceeded.email';
|
||||
|
||||
const SEND_ORGANISATION_LIMIT_EXCEEDED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
organisationId: z.string(),
|
||||
counter: z.enum(['document', 'email', 'api']),
|
||||
kind: z.enum(['rateLimit', 'quota']),
|
||||
period: z.string(),
|
||||
});
|
||||
|
||||
export type TSendOrganisationLimitExceededEmailJobDefinition = z.infer<
|
||||
typeof SEND_ORGANISATION_LIMIT_EXCEEDED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_ORGANISATION_LIMIT_EXCEEDED_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_ORGANISATION_LIMIT_EXCEEDED_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Organisation Limit Exceeded Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_ORGANISATION_LIMIT_EXCEEDED_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_ORGANISATION_LIMIT_EXCEEDED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-organisation-limit-exceeded-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_ORGANISATION_LIMIT_EXCEEDED_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendOrganisationLimitExceededEmailJobDefinition
|
||||
>;
|
||||
@@ -3,6 +3,7 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { jobsClient } from '../../jobs/client';
|
||||
import { generateDatabaseId } from '../../universal/id';
|
||||
import { currentMonthlyPeriod } from '../../universal/monthly-period';
|
||||
import { getQuotaAlertKind } from './get-quota-alert-kind';
|
||||
import type { LimitCounter } from './types';
|
||||
|
||||
type CheckMonthlyQuotaOptions = {
|
||||
@@ -56,29 +57,33 @@ export const checkMonthlyQuota = async (opts: CheckMonthlyQuotaOptions): Promise
|
||||
const newCount = latestMonthlyStat[column];
|
||||
const previousCount = newCount - opts.count;
|
||||
|
||||
const isOverQuota = newCount > opts.quota;
|
||||
// Returns 'quota' on the single request that reached (or jumped past) the quota,
|
||||
// 'quotaNearing' on the single request that reached the warning threshold,
|
||||
// otherwise null. See getQuotaAlertKind for the exactly-once guarantee.
|
||||
const alertKind = getQuotaAlertKind({
|
||||
previousCount,
|
||||
newCount,
|
||||
quota: opts.quota,
|
||||
});
|
||||
|
||||
// Only notify on the single request that crossed the threshold: the count was
|
||||
// at/under quota before this request and over it after. Because the DB
|
||||
// serializes the atomic increment, the post-increment values are distinct and
|
||||
// monotonic, so exactly one request's (previousCount, newCount] interval
|
||||
// contains the quota boundary — guaranteeing the notification fires once.
|
||||
const didCrossQuota = isOverQuota && previousCount <= opts.quota;
|
||||
|
||||
if (didCrossQuota) {
|
||||
// Trigger the alert before the over-quota check — the 'quota' alert usually fires
|
||||
// on the successful request that consumes the last unit of allowance, but when a
|
||||
// batch jumps past the boundary it fires on this rejected request. Either way it
|
||||
// will never fire again this period, so it must be enqueued before any throw.
|
||||
if (alertKind) {
|
||||
await jobsClient
|
||||
.triggerJob({
|
||||
name: 'send.organisation-limit-exceeded.email',
|
||||
name: 'send.organisation-limit-alert.email',
|
||||
payload: {
|
||||
organisationId: opts.organisationId,
|
||||
counter: opts.counter,
|
||||
kind: 'quota',
|
||||
kind: alertKind,
|
||||
period,
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error({
|
||||
msg: 'Failed to send organisation limit exceeded email',
|
||||
msg: 'Failed to send organisation limit alert email',
|
||||
error,
|
||||
});
|
||||
|
||||
@@ -86,7 +91,7 @@ export const checkMonthlyQuota = async (opts: CheckMonthlyQuotaOptions): Promise
|
||||
});
|
||||
}
|
||||
|
||||
if (isOverQuota) {
|
||||
if (newCount > opts.quota) {
|
||||
throw new AppError(AppErrorCode.TOO_MANY_REQUESTS, {
|
||||
message:
|
||||
'Your request could not be completed at this time due to your account exceeding the fair use limits of your current plan. Please contact support.',
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { QUOTA_WARNING_THRESHOLD } from './get-quota-alert-kind';
|
||||
|
||||
export type QuotaFlags = {
|
||||
isDocumentQuotaExceeded: boolean;
|
||||
isEmailQuotaExceeded: boolean;
|
||||
isApiQuotaExceeded: boolean;
|
||||
isDocumentQuotaNearing: boolean;
|
||||
isEmailQuotaNearing: boolean;
|
||||
isApiQuotaNearing: boolean;
|
||||
};
|
||||
|
||||
type ComputeQuotaFlagsOptions = {
|
||||
@@ -20,11 +25,6 @@ type ComputeQuotaFlagsOptions = {
|
||||
/**
|
||||
* A quota of `null` means unlimited (never exceeded). A quota of `0` means
|
||||
* blocked (always exceeded). Otherwise usage `>=` quota is exceeded.
|
||||
*
|
||||
* Note: this `>=` is intentionally the "reached" signal for the banner and is
|
||||
* distinct from enforcement in `check-monthly-quota.ts`, which blocks the
|
||||
* action that crosses the boundary using a strict `>` on the post-increment
|
||||
* count. Do not "align" them — they answer different questions.
|
||||
*/
|
||||
const isQuotaExceeded = (quota: number | null, usage: number): boolean => {
|
||||
if (quota === null) {
|
||||
@@ -38,10 +38,30 @@ const isQuotaExceeded = (quota: number | null, usage: number): boolean => {
|
||||
return usage >= quota;
|
||||
};
|
||||
|
||||
/**
|
||||
* A counter is "nearing" its quota once usage reaches the warning threshold
|
||||
* (80% of the quota, rounded up) but has not yet been exceeded. Nearing and
|
||||
* exceeded are mutually exclusive per counter.
|
||||
*/
|
||||
const isQuotaNearing = (quota: number | null, usage: number): boolean => {
|
||||
if (quota === null || quota === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isQuotaExceeded(quota, usage)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return usage >= Math.ceil(quota * QUOTA_WARNING_THRESHOLD);
|
||||
};
|
||||
|
||||
export const computeQuotaFlags = ({ quotas, usage }: ComputeQuotaFlagsOptions): QuotaFlags => {
|
||||
return {
|
||||
isDocumentQuotaExceeded: isQuotaExceeded(quotas.documentQuota, usage?.documentCount ?? 0),
|
||||
isEmailQuotaExceeded: isQuotaExceeded(quotas.emailQuota, usage?.emailCount ?? 0),
|
||||
isApiQuotaExceeded: isQuotaExceeded(quotas.apiQuota, usage?.apiCount ?? 0),
|
||||
isDocumentQuotaNearing: isQuotaNearing(quotas.documentQuota, usage?.documentCount ?? 0),
|
||||
isEmailQuotaNearing: isQuotaNearing(quotas.emailQuota, usage?.emailCount ?? 0),
|
||||
isApiQuotaNearing: isQuotaNearing(quotas.apiQuota, usage?.apiCount ?? 0),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
export const QUOTA_WARNING_THRESHOLD = 0.8;
|
||||
|
||||
export type QuotaAlertKind = 'quota' | 'quotaNearing';
|
||||
|
||||
type GetQuotaAlertKindOptions = {
|
||||
previousCount: number;
|
||||
newCount: number;
|
||||
quota: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether the request that moved the counter from `previousCount` to
|
||||
* `newCount` crossed an alert threshold.
|
||||
*
|
||||
* - 'quota': this request reached (or jumped past) the monthly quota.
|
||||
* - 'quotaNearing': this request reached the warning threshold (80% of quota).
|
||||
* - null: no threshold crossed by this request.
|
||||
*
|
||||
* Precondition: callers must handle `quota === null` (unlimited) and `quota === 0`
|
||||
* (blocked) before calling — this function assumes a positive quota.
|
||||
*/
|
||||
export const getQuotaAlertKind = (opts: GetQuotaAlertKindOptions): QuotaAlertKind | null => {
|
||||
const { previousCount, newCount, quota } = opts;
|
||||
|
||||
if (newCount >= quota) {
|
||||
// Only the single request that reached the quota boundary should alert. If the
|
||||
// same request also skipped past the warning threshold, the quota alert
|
||||
// supersedes the warning.
|
||||
return previousCount < quota ? 'quota' : null;
|
||||
}
|
||||
|
||||
// From here newCount < quota, so for tiny quotas (1-4) where the rounded-up
|
||||
// warning threshold equals the quota itself, the warning can never fire — the
|
||||
// exhausting request is handled by the quota branch above.
|
||||
const warningCount = Math.ceil(quota * QUOTA_WARNING_THRESHOLD);
|
||||
|
||||
const didCrossWarning = newCount >= warningCount && previousCount < warningCount;
|
||||
|
||||
return didCrossWarning ? 'quotaNearing' : null;
|
||||
};
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2026-06-09 05:23\n"
|
||||
"PO-Revision-Date: 2026-06-12 07:37\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -642,8 +642,8 @@ msgstr "{visibleRows, plural, one {Eine # Ergebnis wird angezeigt.} other {# Erg
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
msgid "<0>\"{0}\"</0>is no longer available to sign"
|
||||
msgstr "<0>\"{0}\"</0> steht nicht mehr zur Unterschrift zur Verfügung"
|
||||
msgid "<0>\"{0}\"</0> is no longer available to sign"
|
||||
msgstr "<0>„{0}“</0> ist nicht mehr zum Unterschreiben verfügbar"
|
||||
|
||||
#: packages/email/templates/organisation-account-link-confirmation.tsx
|
||||
msgid "<0>{organisationName}</0> has requested to create an account on your behalf."
|
||||
@@ -5778,6 +5778,11 @@ msgctxt "Subscription status"
|
||||
msgid "Free"
|
||||
msgstr "Kostenlos"
|
||||
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgctxt "Subscription status"
|
||||
msgid "Free (Pending)"
|
||||
msgstr "Kostenlos (ausstehend)"
|
||||
|
||||
#: packages/lib/utils/fields.ts
|
||||
#: packages/ui/primitives/document-flow/types.ts
|
||||
msgid "Free Signature"
|
||||
@@ -6146,6 +6151,7 @@ msgstr "Eingabefelder werden identifiziert"
|
||||
msgid "Identifying recipients"
|
||||
msgstr "Empfänger werden identifiziert"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "If there is any issue with your subscription, please contact us at <0>{SUPPORT_EMAIL}</0>."
|
||||
msgstr "Bei einem Problem mit Ihrem Abo kontaktieren Sie uns bitte unter <0>{SUPPORT_EMAIL}</0>."
|
||||
@@ -6780,6 +6786,7 @@ msgstr "Vorlage verwalten und anzeigen"
|
||||
msgid "Manage billing"
|
||||
msgstr "Rechnungsmanagement"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgid "Manage Billing"
|
||||
@@ -7974,6 +7981,11 @@ msgstr "Überfällig"
|
||||
msgid "Payment overdue"
|
||||
msgstr "Zahlung überfällig"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "Payment required"
|
||||
msgstr "Zahlung erforderlich"
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "PDF-Dokument"
|
||||
@@ -11461,6 +11473,10 @@ msgstr "Dieses Element kann nicht gelöscht werden"
|
||||
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
|
||||
msgstr "Dieser Link ist ungültig oder abgelaufen. Bitte kontaktieren Sie Ihr Team, um eine neue Bestätigungsanfrage zu senden."
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "This organisation is awaiting payment. Complete checkout to unlock it."
|
||||
msgstr "Für diese Organisation steht eine Zahlung aus. Schließen Sie den Checkout ab, um sie freizuschalten."
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "This organisation will have administrative control over your account. You can revoke this access later, but they will retain access to any data they've already collected."
|
||||
msgstr "Diese Organisation wird administrative Kontrolle über Ihr Konto haben. Sie können den Zugriff später widerrufen, aber sie behalten den Zugriff auf alle bereits gesammelten Daten."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: es\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2026-06-09 05:23\n"
|
||||
"PO-Revision-Date: 2026-06-12 07:37\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -642,7 +642,7 @@ msgstr "{visibleRows, plural, one {Mostrando # resultado.} other {Mostrando # re
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
msgid "<0>\"{0}\"</0>is no longer available to sign"
|
||||
msgid "<0>\"{0}\"</0> is no longer available to sign"
|
||||
msgstr "<0>\"{0}\"</0> ya no está disponible para firmar"
|
||||
|
||||
#: packages/email/templates/organisation-account-link-confirmation.tsx
|
||||
@@ -5778,6 +5778,11 @@ msgctxt "Subscription status"
|
||||
msgid "Free"
|
||||
msgstr "Gratis"
|
||||
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgctxt "Subscription status"
|
||||
msgid "Free (Pending)"
|
||||
msgstr "Gratis (pendiente)"
|
||||
|
||||
#: packages/lib/utils/fields.ts
|
||||
#: packages/ui/primitives/document-flow/types.ts
|
||||
msgid "Free Signature"
|
||||
@@ -6146,6 +6151,7 @@ msgstr "Identificando campos de entrada"
|
||||
msgid "Identifying recipients"
|
||||
msgstr "Identificando destinatarios"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "If there is any issue with your subscription, please contact us at <0>{SUPPORT_EMAIL}</0>."
|
||||
msgstr "Si tienes algún problema con tu suscripción, por favor contáctanos en <0>{SUPPORT_EMAIL}</0>."
|
||||
@@ -6780,6 +6786,7 @@ msgstr "Gestionar y ver plantilla"
|
||||
msgid "Manage billing"
|
||||
msgstr "Gestionar la facturación"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgid "Manage Billing"
|
||||
@@ -7974,6 +7981,11 @@ msgstr "Vencida"
|
||||
msgid "Payment overdue"
|
||||
msgstr "Pago atrasado"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "Payment required"
|
||||
msgstr "Pago requerido"
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "Documento PDF"
|
||||
@@ -11461,6 +11473,10 @@ msgstr "Este elemento no se puede eliminar"
|
||||
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
|
||||
msgstr "Este enlace es inválido o ha expirado. Por favor, contacta a tu equipo para reenviar una verificación."
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "This organisation is awaiting payment. Complete checkout to unlock it."
|
||||
msgstr "Esta organización está a la espera de pago. Completa el proceso de pago para desbloquearla."
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "This organisation will have administrative control over your account. You can revoke this access later, but they will retain access to any data they've already collected."
|
||||
msgstr "Esta organización tendrá control administrativo sobre su cuenta. Puede revocar este acceso más tarde, pero conservarán el acceso a cualquier dato que ya hayan recopilado."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: fr\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2026-06-09 05:23\n"
|
||||
"PO-Revision-Date: 2026-06-12 07:37\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
@@ -642,8 +642,8 @@ msgstr "{visibleRows, plural, one {Affichage de # résultat.} other {Affichage d
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
msgid "<0>\"{0}\"</0>is no longer available to sign"
|
||||
msgstr "<0>\"{0}\"</0> n'est plus disponible pour signer"
|
||||
msgid "<0>\"{0}\"</0> is no longer available to sign"
|
||||
msgstr "<0>\"{0}\"</0> n’est plus disponible pour signature"
|
||||
|
||||
#: packages/email/templates/organisation-account-link-confirmation.tsx
|
||||
msgid "<0>{organisationName}</0> has requested to create an account on your behalf."
|
||||
@@ -5778,6 +5778,11 @@ msgctxt "Subscription status"
|
||||
msgid "Free"
|
||||
msgstr "Gratuit"
|
||||
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgctxt "Subscription status"
|
||||
msgid "Free (Pending)"
|
||||
msgstr "Gratuit (en attente)"
|
||||
|
||||
#: packages/lib/utils/fields.ts
|
||||
#: packages/ui/primitives/document-flow/types.ts
|
||||
msgid "Free Signature"
|
||||
@@ -6146,6 +6151,7 @@ msgstr "Identification des champs de saisie"
|
||||
msgid "Identifying recipients"
|
||||
msgstr "Identification des destinataires"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "If there is any issue with your subscription, please contact us at <0>{SUPPORT_EMAIL}</0>."
|
||||
msgstr "Si vous rencontrez un problème avec votre abonnement, veuillez nous contacter à <0>{SUPPORT_EMAIL}</0>."
|
||||
@@ -6780,6 +6786,7 @@ msgstr "Gérer et afficher le modèle"
|
||||
msgid "Manage billing"
|
||||
msgstr "Gérer la facturation"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgid "Manage Billing"
|
||||
@@ -7974,6 +7981,11 @@ msgstr "En retard de paiement"
|
||||
msgid "Payment overdue"
|
||||
msgstr "Paiement en retard"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "Payment required"
|
||||
msgstr "Paiement requis"
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "Document PDF"
|
||||
@@ -11461,6 +11473,10 @@ msgstr "Cet élément ne peut pas être supprimé"
|
||||
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
|
||||
msgstr "Ce lien est invalide ou a expiré. Veuillez contacter votre équipe pour renvoyer une vérification."
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "This organisation is awaiting payment. Complete checkout to unlock it."
|
||||
msgstr "Cette organisation est en attente de paiement. Terminez le paiement pour la déverrouiller."
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "This organisation will have administrative control over your account. You can revoke this access later, but they will retain access to any data they've already collected."
|
||||
msgstr "Cette organisation aura un contrôle administratif sur votre compte. Vous pourrez révoquer cet accès plus tard, mais ils conserveront l'accès à toutes les données qu'ils ont déjà collectées."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: it\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2026-06-09 05:23\n"
|
||||
"PO-Revision-Date: 2026-06-12 07:37\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -642,8 +642,8 @@ msgstr "{visibleRows, plural, one {Visualizzazione di # risultato.} other {Visua
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
msgid "<0>\"{0}\"</0>is no longer available to sign"
|
||||
msgstr "<0>\"{0}\"</0>non è più disponibile per la firma"
|
||||
msgid "<0>\"{0}\"</0> is no longer available to sign"
|
||||
msgstr "<0>\"{0}\"</0> non è più disponibile per la firma"
|
||||
|
||||
#: packages/email/templates/organisation-account-link-confirmation.tsx
|
||||
msgid "<0>{organisationName}</0> has requested to create an account on your behalf."
|
||||
@@ -5778,6 +5778,11 @@ msgctxt "Subscription status"
|
||||
msgid "Free"
|
||||
msgstr "Gratuito"
|
||||
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgctxt "Subscription status"
|
||||
msgid "Free (Pending)"
|
||||
msgstr "Gratuito (in sospeso)"
|
||||
|
||||
#: packages/lib/utils/fields.ts
|
||||
#: packages/ui/primitives/document-flow/types.ts
|
||||
msgid "Free Signature"
|
||||
@@ -6146,6 +6151,7 @@ msgstr "Identificazione dei campi di input in corso"
|
||||
msgid "Identifying recipients"
|
||||
msgstr "Identificazione dei destinatari in corso"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "If there is any issue with your subscription, please contact us at <0>{SUPPORT_EMAIL}</0>."
|
||||
msgstr "Se ci sono problemi con il tuo abbonamento, contattaci a <0>{SUPPORT_EMAIL}</0>."
|
||||
@@ -6780,6 +6786,7 @@ msgstr "Gestisci e visualizza il modello"
|
||||
msgid "Manage billing"
|
||||
msgstr "Gestisci la fatturazione"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgid "Manage Billing"
|
||||
@@ -7974,6 +7981,11 @@ msgstr "In ritardo"
|
||||
msgid "Payment overdue"
|
||||
msgstr "Pagamento scaduto"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "Payment required"
|
||||
msgstr "Pagamento richiesto"
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "Documento PDF"
|
||||
@@ -11461,6 +11473,10 @@ msgstr "Questo elemento non può essere eliminato"
|
||||
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
|
||||
msgstr "Questo link è invalido o è scaduto. Si prega di contattare il tuo team per inviare nuovamente una verifica."
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "This organisation is awaiting payment. Complete checkout to unlock it."
|
||||
msgstr "Questa organizzazione è in attesa di pagamento. Completa il checkout per sbloccarla."
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "This organisation will have administrative control over your account. You can revoke this access later, but they will retain access to any data they've already collected."
|
||||
msgstr "Questa organizzazione avrà il controllo amministrativo sul tuo account. Puoi revocare questo accesso in seguito, ma conserveranno l'accesso a qualsiasi dato che hanno già raccolto."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ja\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2026-06-09 05:23\n"
|
||||
"PO-Revision-Date: 2026-06-12 07:37\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Japanese\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -642,8 +642,8 @@ msgstr "{visibleRows, plural, other {# 件の結果を表示中}}"
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
msgid "<0>\"{0}\"</0>is no longer available to sign"
|
||||
msgstr "<0>\"{0}\"</0>は署名できなくなりました"
|
||||
msgid "<0>\"{0}\"</0> is no longer available to sign"
|
||||
msgstr "<0>\"{0}\"</0> は署名できなくなりました"
|
||||
|
||||
#: packages/email/templates/organisation-account-link-confirmation.tsx
|
||||
msgid "<0>{organisationName}</0> has requested to create an account on your behalf."
|
||||
@@ -5778,6 +5778,11 @@ msgctxt "Subscription status"
|
||||
msgid "Free"
|
||||
msgstr "無料"
|
||||
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgctxt "Subscription status"
|
||||
msgid "Free (Pending)"
|
||||
msgstr "無料(保留中)"
|
||||
|
||||
#: packages/lib/utils/fields.ts
|
||||
#: packages/ui/primitives/document-flow/types.ts
|
||||
msgid "Free Signature"
|
||||
@@ -6146,6 +6151,7 @@ msgstr "入力フィールドを特定しています"
|
||||
msgid "Identifying recipients"
|
||||
msgstr "受信者を特定しています"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "If there is any issue with your subscription, please contact us at <0>{SUPPORT_EMAIL}</0>."
|
||||
msgstr "サブスクリプションに問題がある場合は、<0>{SUPPORT_EMAIL}</0> までお問い合わせください。"
|
||||
@@ -6780,6 +6786,7 @@ msgstr "テンプレートを管理・閲覧"
|
||||
msgid "Manage billing"
|
||||
msgstr "請求を管理"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgid "Manage Billing"
|
||||
@@ -7974,6 +7981,11 @@ msgstr "支払い遅延"
|
||||
msgid "Payment overdue"
|
||||
msgstr "支払い期限超過"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "Payment required"
|
||||
msgstr "支払いが必要です"
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "PDF文書"
|
||||
@@ -11461,6 +11473,10 @@ msgstr "この項目は削除できません"
|
||||
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
|
||||
msgstr "このリンクは無効か有効期限が切れています。チームに連絡して、認証を再送してもらってください。"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "This organisation is awaiting payment. Complete checkout to unlock it."
|
||||
msgstr "この組織は支払い待ちの状態です。ロックを解除するにはチェックアウトを完了してください。"
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "This organisation will have administrative control over your account. You can revoke this access later, but they will retain access to any data they've already collected."
|
||||
msgstr "この組織はあなたのアカウントに対して管理権限を持ちます。このアクセスは後で取り消せますが、すでに収集済みのデータには引き続きアクセスできます。"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ko\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2026-06-09 05:23\n"
|
||||
"PO-Revision-Date: 2026-06-12 07:37\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Korean\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -642,7 +642,7 @@ msgstr "{visibleRows, plural, other {총 #개 결과 중 표시 중입니다.}}"
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
msgid "<0>\"{0}\"</0>is no longer available to sign"
|
||||
msgid "<0>\"{0}\"</0> is no longer available to sign"
|
||||
msgstr "<0>\"{0}\"</0>은(는) 더 이상 서명할 수 없습니다"
|
||||
|
||||
#: packages/email/templates/organisation-account-link-confirmation.tsx
|
||||
@@ -5778,6 +5778,11 @@ msgctxt "Subscription status"
|
||||
msgid "Free"
|
||||
msgstr "무료"
|
||||
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgctxt "Subscription status"
|
||||
msgid "Free (Pending)"
|
||||
msgstr "무료(보류 중)"
|
||||
|
||||
#: packages/lib/utils/fields.ts
|
||||
#: packages/ui/primitives/document-flow/types.ts
|
||||
msgid "Free Signature"
|
||||
@@ -6146,6 +6151,7 @@ msgstr "입력 필드 식별 중"
|
||||
msgid "Identifying recipients"
|
||||
msgstr "수신자 식별 중"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "If there is any issue with your subscription, please contact us at <0>{SUPPORT_EMAIL}</0>."
|
||||
msgstr "구독 관련 문제가 있는 경우 <0>{SUPPORT_EMAIL}</0>로 연락해 주세요."
|
||||
@@ -6780,6 +6786,7 @@ msgstr "템플릿 관리 및 보기"
|
||||
msgid "Manage billing"
|
||||
msgstr "결제 관리"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgid "Manage Billing"
|
||||
@@ -7974,6 +7981,11 @@ msgstr "연체"
|
||||
msgid "Payment overdue"
|
||||
msgstr "결제 지연"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "Payment required"
|
||||
msgstr "결제가 필요합니다"
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "PDF 문서"
|
||||
@@ -11461,6 +11473,10 @@ msgstr "이 항목은 삭제할 수 없습니다"
|
||||
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
|
||||
msgstr "이 링크는 유효하지 않거나 만료되었습니다. 팀에 문의해 인증 메일을 다시 보내 달라고 요청해 주세요."
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "This organisation is awaiting payment. Complete checkout to unlock it."
|
||||
msgstr "이 조직은 결제를 기다리고 있습니다. 잠금 해제를 위해 결제를 완료하세요."
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "This organisation will have administrative control over your account. You can revoke this access later, but they will retain access to any data they've already collected."
|
||||
msgstr "이 조직은 귀하의 계정을 관리할 수 있는 권한을 갖게 됩니다. 나중에 이 액세스를 취소할 수 있지만, 이미 수집한 데이터에는 계속 액세스할 수 있습니다."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: nl\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2026-06-09 05:23\n"
|
||||
"PO-Revision-Date: 2026-06-12 07:37\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -642,8 +642,8 @@ msgstr "{visibleRows, plural, one {# resultaat wordt getoond.} other {# resultat
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
msgid "<0>\"{0}\"</0>is no longer available to sign"
|
||||
msgstr "<0>\"{0}\"</0>is niet langer beschikbaar om te ondertekenen"
|
||||
msgid "<0>\"{0}\"</0> is no longer available to sign"
|
||||
msgstr "<0>\"{0}\"</0> is niet langer beschikbaar om te ondertekenen"
|
||||
|
||||
#: packages/email/templates/organisation-account-link-confirmation.tsx
|
||||
msgid "<0>{organisationName}</0> has requested to create an account on your behalf."
|
||||
@@ -5778,6 +5778,11 @@ msgctxt "Subscription status"
|
||||
msgid "Free"
|
||||
msgstr "Gratis"
|
||||
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgctxt "Subscription status"
|
||||
msgid "Free (Pending)"
|
||||
msgstr "Gratis (in behandeling)"
|
||||
|
||||
#: packages/lib/utils/fields.ts
|
||||
#: packages/ui/primitives/document-flow/types.ts
|
||||
msgid "Free Signature"
|
||||
@@ -6146,6 +6151,7 @@ msgstr "Invoervelden identificeren"
|
||||
msgid "Identifying recipients"
|
||||
msgstr "Ontvangers identificeren"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "If there is any issue with your subscription, please contact us at <0>{SUPPORT_EMAIL}</0>."
|
||||
msgstr "Als er een probleem is met je abonnement, neem dan contact met ons op via <0>{SUPPORT_EMAIL}</0>."
|
||||
@@ -6780,6 +6786,7 @@ msgstr "Sjabloon beheren en bekijken"
|
||||
msgid "Manage billing"
|
||||
msgstr "Facturering beheren"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgid "Manage Billing"
|
||||
@@ -7974,6 +7981,11 @@ msgstr "Achterstallig"
|
||||
msgid "Payment overdue"
|
||||
msgstr "Betaling achterstallig"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "Payment required"
|
||||
msgstr "Betaling vereist"
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "PDF-document"
|
||||
@@ -11461,6 +11473,10 @@ msgstr "Dit item kan niet worden verwijderd"
|
||||
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
|
||||
msgstr "Deze link is ongeldig of verlopen. Neem contact op met je team om de verificatie opnieuw te laten verzenden."
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "This organisation is awaiting payment. Complete checkout to unlock it."
|
||||
msgstr "Deze organisatie wacht op betaling. Rond de betaling af om deze te ontgrendelen."
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "This organisation will have administrative control over your account. You can revoke this access later, but they will retain access to any data they've already collected."
|
||||
msgstr "Deze organisatie krijgt administratieve controle over uw account. U kunt deze toegang later intrekken, maar zij behouden toegang tot alle gegevens die zij al hebben verzameld."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: pl\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2026-06-09 05:23\n"
|
||||
"PO-Revision-Date: 2026-06-12 07:37\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Polish\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
@@ -642,8 +642,8 @@ msgstr "{visibleRows, plural, one {# wynik} few {# wyniki} many {# wyników} oth
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
msgid "<0>\"{0}\"</0>is no longer available to sign"
|
||||
msgstr "Dokument <0>„{0}”</0>nie jest już dostępny do podpisu"
|
||||
msgid "<0>\"{0}\"</0> is no longer available to sign"
|
||||
msgstr "<0>„{0}”</0> nie jest już dostępny do podpisu"
|
||||
|
||||
#: packages/email/templates/organisation-account-link-confirmation.tsx
|
||||
msgid "<0>{organisationName}</0> has requested to create an account on your behalf."
|
||||
@@ -5778,6 +5778,11 @@ msgctxt "Subscription status"
|
||||
msgid "Free"
|
||||
msgstr "Darmowa"
|
||||
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgctxt "Subscription status"
|
||||
msgid "Free (Pending)"
|
||||
msgstr "Darmowy (oczekujący)"
|
||||
|
||||
#: packages/lib/utils/fields.ts
|
||||
#: packages/ui/primitives/document-flow/types.ts
|
||||
msgid "Free Signature"
|
||||
@@ -6146,6 +6151,7 @@ msgstr "Identyfikowanie pól formularzy"
|
||||
msgid "Identifying recipients"
|
||||
msgstr "Identyfikowanie odbiorców"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "If there is any issue with your subscription, please contact us at <0>{SUPPORT_EMAIL}</0>."
|
||||
msgstr "Jeśli masz problemy z subskrypcją, skontaktuj się z nami pod adresem <0>{SUPPORT_EMAIL}</0>."
|
||||
@@ -6780,6 +6786,7 @@ msgstr "Zarządzanie szablonem"
|
||||
msgid "Manage billing"
|
||||
msgstr "Zarządzaj płatnościami"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgid "Manage Billing"
|
||||
@@ -7974,6 +7981,11 @@ msgstr "Przeterminowana"
|
||||
msgid "Payment overdue"
|
||||
msgstr "Zaległa płatność"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "Payment required"
|
||||
msgstr "Wymagana płatność"
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "Dokument PDF"
|
||||
@@ -11461,6 +11473,10 @@ msgstr "Nie można usunąć elementu"
|
||||
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
|
||||
msgstr "Link jest nieprawidłowy lub wygasł. Skontaktuj się ze swoim zespołem, aby ponownie wysłać wiadomość weryfikacyjną."
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "This organisation is awaiting payment. Complete checkout to unlock it."
|
||||
msgstr "Ta organizacja oczekuje na płatność. Dokończ proces zakupu, aby ją odblokować."
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "This organisation will have administrative control over your account. You can revoke this access later, but they will retain access to any data they've already collected."
|
||||
msgstr "Organizacja będzie miała kontrolę administracyjną nad Twoim kontem. Możesz później cofnąć ten dostęp, ale nadal będą mieli dostęp do danych, które już zgromadzili."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: zh\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2026-06-09 05:23\n"
|
||||
"PO-Revision-Date: 2026-06-12 07:37\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -642,8 +642,8 @@ msgstr "{visibleRows, plural, other {正在显示 # 条结果。}}"
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
msgid "<0>\"{0}\"</0>is no longer available to sign"
|
||||
msgstr "<0>\"{0}\"</0>已不再可供签署"
|
||||
msgid "<0>\"{0}\"</0> is no longer available to sign"
|
||||
msgstr "<0>“{0}”</0>已不再可供签署"
|
||||
|
||||
#: packages/email/templates/organisation-account-link-confirmation.tsx
|
||||
msgid "<0>{organisationName}</0> has requested to create an account on your behalf."
|
||||
@@ -5778,6 +5778,11 @@ msgctxt "Subscription status"
|
||||
msgid "Free"
|
||||
msgstr "免费"
|
||||
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgctxt "Subscription status"
|
||||
msgid "Free (Pending)"
|
||||
msgstr "免费(待处理)"
|
||||
|
||||
#: packages/lib/utils/fields.ts
|
||||
#: packages/ui/primitives/document-flow/types.ts
|
||||
msgid "Free Signature"
|
||||
@@ -6146,6 +6151,7 @@ msgstr "正在识别输入字段"
|
||||
msgid "Identifying recipients"
|
||||
msgstr "正在识别收件人"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "If there is any issue with your subscription, please contact us at <0>{SUPPORT_EMAIL}</0>."
|
||||
msgstr "如果您的订阅出现任何问题,请通过 <0>{SUPPORT_EMAIL}</0> 联系我们。"
|
||||
@@ -6780,6 +6786,7 @@ msgstr "管理并查看模板"
|
||||
msgid "Manage billing"
|
||||
msgstr "管理计费"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
msgid "Manage Billing"
|
||||
@@ -7974,6 +7981,11 @@ msgstr "逾期"
|
||||
msgid "Payment overdue"
|
||||
msgstr "付款逾期"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "Payment required"
|
||||
msgstr "需要付款"
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "PDF 文档"
|
||||
@@ -11461,6 +11473,10 @@ msgstr "无法删除此条目"
|
||||
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
|
||||
msgstr "此链接无效或已过期。请联系你的团队重新发送验证邮件。"
|
||||
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
msgid "This organisation is awaiting payment. Complete checkout to unlock it."
|
||||
msgstr "此组织正在等待付款。完成结账以解锁。"
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "This organisation will have administrative control over your account. You can revoke this access later, but they will retain access to any data they've already collected."
|
||||
msgstr "此组织将对您的账户拥有管理控制权。您可以稍后撤销此访问权限,但他们将保留对已收集数据的访问权。"
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { getInternalClaimPlans } from '@documenso/ee/server-only/stripe/get-internal-claim-plans';
|
||||
import { getSubscription } from '@documenso/ee/server-only/stripe/get-subscription';
|
||||
import { syncStripeCustomerSubscription } from '@documenso/ee/server-only/stripe/sync-stripe-customer-subscription';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { Stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { Logger } from 'pino';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZGetSubscriptionRequestSchema } from './get-subscription.types';
|
||||
@@ -26,9 +33,17 @@ export const getSubscriptionRoute = authenticatedProcedure
|
||||
}
|
||||
|
||||
const [subscription, plans] = await Promise.all([
|
||||
// If the subscription is not found or there's an error, we return null to
|
||||
// avoid failing the entire request.
|
||||
getSubscription({
|
||||
organisationId,
|
||||
userId,
|
||||
}).catch(async (e) => {
|
||||
ctx.logger.error(`Failed to get subscription for organisation ${organisationId}`, e);
|
||||
|
||||
await reconcileMissingStripeSubscription({ logger: ctx.logger, organisationId, userId, error: e });
|
||||
|
||||
return null;
|
||||
}),
|
||||
getInternalClaimPlans(),
|
||||
]);
|
||||
@@ -38,3 +53,51 @@ export const getSubscriptionRoute = authenticatedProcedure
|
||||
plans,
|
||||
};
|
||||
});
|
||||
|
||||
type ReconcileMissingStripeSubscriptionOptions = {
|
||||
logger: Logger;
|
||||
organisationId: string;
|
||||
userId: number;
|
||||
error: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* When the Stripe subscription no longer exists (e.g. deleted by Stripe's
|
||||
* test-mode retention policy, or removed manually), fire-and-forget a reconcile
|
||||
* so the stale local subscription row and any billing banner converge on the
|
||||
* next load. Reconcile failures must never break the read path that calls this.
|
||||
*/
|
||||
const reconcileMissingStripeSubscription = async ({
|
||||
logger,
|
||||
organisationId,
|
||||
userId,
|
||||
error,
|
||||
}: ReconcileMissingStripeSubscriptionOptions) => {
|
||||
if (!(error instanceof Stripe.errors.StripeInvalidRequestError) || error.code !== 'resource_missing') {
|
||||
return;
|
||||
}
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
select: {
|
||||
customerId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation?.customerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
void syncStripeCustomerSubscription({
|
||||
customerId: organisation.customerId,
|
||||
}).catch((syncError) => {
|
||||
logger.error(
|
||||
`Failed to reconcile subscription after resource_missing for organisation ${organisationId}`,
|
||||
syncError,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -12,6 +12,9 @@ export const ZGetOrganisationQuotaFlagsResponseSchema = z.object({
|
||||
isDocumentQuotaExceeded: z.boolean(),
|
||||
isEmailQuotaExceeded: z.boolean(),
|
||||
isApiQuotaExceeded: z.boolean(),
|
||||
isDocumentQuotaNearing: z.boolean(),
|
||||
isEmailQuotaNearing: z.boolean(),
|
||||
isApiQuotaNearing: z.boolean(),
|
||||
});
|
||||
|
||||
export type TGetOrganisationQuotaFlagsResponse = z.infer<typeof ZGetOrganisationQuotaFlagsResponseSchema>;
|
||||
|
||||
Reference in New Issue
Block a user