Files
documenso/packages/lib/utils/billing.ts
T
Lucas Smith 3887aa67c8 fix: rework stripe webhooks into idempotent subscription sync (#2977)
Replace per-event webhook handlers with a single sync function that
fetches the current state from Stripe and converges the local
subscription, claim, and organisation type.

- Create organisations upfront before checkout, restricted as
  "pending payment" until the first payment syncs
- Add rate-limited subscription sync route, triggered on checkout
  success so the UI doesn't wait on webhooks
- Surface pending payment state in banner, billing table, and limits
2026-06-12 16:01:03 +10:00

59 lines
1.6 KiB
TypeScript

import type { Subscription } from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
import { OrganisationType } from '@prisma/client';
import { IS_BILLING_ENABLED } from '../constants/app';
import { AppError, AppErrorCode } from '../errors/app-error';
import { INTERNAL_CLAIM_ID } from '../types/subscription';
/**
* Throws an error if billing is enabled and no subscription is found.
*/
export const validateIfSubscriptionIsRequired = (subscription?: Subscription | null) => {
const isBillingEnabled = IS_BILLING_ENABLED();
if (!isBillingEnabled) {
return;
}
if (isBillingEnabled && !subscription) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Subscription not found',
});
}
return subscription;
};
type PendingPaymentOrganisation = {
type: OrganisationType;
subscription?: unknown;
organisationClaim: {
originalSubscriptionClaimId: string | null;
};
};
/**
* Whether the organisation was created ahead of a paid checkout and is still awaiting
* its first successful payment.
*
* Such organisations have no subscription row and still carry the copied "free" claim,
* and must be treated as restricted until the Stripe webhook sync activates them.
*
* Always returns false when billing is disabled (self-hosted).
*/
export const isOrganisationPendingPayment = (organisation: PendingPaymentOrganisation) => {
if (!IS_BILLING_ENABLED()) {
return false;
}
if (organisation.type !== OrganisationType.ORGANISATION) {
return false;
}
if (organisation.subscription) {
return false;
}
return organisation.organisationClaim.originalSubscriptionClaimId === INTERNAL_CLAIM_ID.FREE;
};