Merge branch 'main' into fix/name-input-invalid-characters

This commit is contained in:
Catalin Pit
2026-06-16 10:03:27 +03:00
committed by GitHub
26 changed files with 789 additions and 230 deletions
@@ -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(() => {
+1 -1
View File
@@ -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"
}
+3 -3
View File
@@ -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
View File
@@ -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;
+2 -2
View File
@@ -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,
@@ -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;
};
+19 -3
View File
@@ -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."
+18 -2
View File
@@ -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."
+19 -3
View File
@@ -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> nest 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."
+19 -3
View File
@@ -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."
+19 -3
View File
@@ -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 "この組織はあなたのアカウントに対して管理権限を持ちます。このアクセスは後で取り消せますが、すでに収集済みのデータには引き続きアクセスできます。"
+18 -2
View File
@@ -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 "이 조직은 귀하의 계정을 관리할 수 있는 권한을 갖게 됩니다. 나중에 이 액세스를 취소할 수 있지만, 이미 수집한 데이터에는 계속 액세스할 수 있습니다."
+19 -3
View File
@@ -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."
+19 -3
View File
@@ -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."
+19 -3
View File
@@ -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>;