mirror of
https://github.com/documenso/documenso.git
synced 2026-07-27 10:25:00 +10:00
15549a6758
## Description - Add banners when plans near fair use limits - Automated email alerts to our support when nearing fair use limits
170 lines
5.0 KiB
TypeScript
170 lines
5.0 KiB
TypeScript
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();
|
|
});
|