Merge remote-tracking branch 'origin/main' into pr-2559

# Conflicts:
#	packages/lib/translations/de/web.po
#	packages/lib/translations/en/web.po
#	packages/lib/translations/es/web.po
#	packages/lib/translations/fr/web.po
#	packages/lib/translations/it/web.po
#	packages/lib/translations/ja/web.po
#	packages/lib/translations/ko/web.po
#	packages/lib/translations/nl/web.po
#	packages/lib/translations/pl/web.po
#	packages/lib/translations/pt-BR/web.po
#	packages/lib/translations/zh/web.po
This commit is contained in:
ephraimduncan
2026-05-14 15:44:00 +00:00
88 changed files with 7290 additions and 1741 deletions
@@ -0,0 +1,54 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import { ZDeleteOrganisationRequestSchema, ZDeleteOrganisationResponseSchema } from './delete-organisation.types';
export const deleteOrganisationRoute = adminProcedure
.input(ZDeleteOrganisationRequestSchema)
.output(ZDeleteOrganisationResponseSchema)
.mutation(async ({ input, ctx }) => {
const { organisationId, organisationName, sendEmailToOwner } = input;
const { user } = ctx;
ctx.logger.info({
input: {
organisationId,
sendEmailToOwner,
},
});
const organisation = await prisma.organisation.findUnique({
where: {
id: organisationId,
},
select: {
id: true,
name: true,
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Organisation not found',
});
}
if (organisation.name !== organisationName) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Organisation name does not match',
});
}
// The deletion itself is offloaded to a background job because orphaning
// potentially-large numbers of envelopes can take a while.
await jobs.triggerJob({
name: 'internal.admin-delete-organisation',
payload: {
organisationId: organisation.id,
sendEmailToOwner,
requestedByUserId: user.id,
},
});
});
@@ -0,0 +1,20 @@
import { z } from 'zod';
export const ZDeleteOrganisationRequestSchema = z.object({
organisationId: z.string().min(1),
/**
* The organisation name as typed by the admin in the confirmation dialog.
* Must exactly match the persisted organisation's name for the deletion
* to proceed.
*/
organisationName: z.string().min(1),
/**
* Whether to email the organisation owner notifying them of the deletion.
*/
sendEmailToOwner: z.boolean(),
});
export const ZDeleteOrganisationResponseSchema = z.void();
export type TDeleteOrganisationRequest = z.infer<typeof ZDeleteOrganisationRequestSchema>;
export type TDeleteOrganisationResponse = z.infer<typeof ZDeleteOrganisationResponseSchema>;
@@ -3,6 +3,7 @@ import { createAdminOrganisationRoute } from './create-admin-organisation';
import { createStripeCustomerRoute } from './create-stripe-customer';
import { createSubscriptionClaimRoute } from './create-subscription-claim';
import { deleteDocumentRoute } from './delete-document';
import { deleteOrganisationRoute } from './delete-organisation';
import { deleteAdminOrganisationMemberRoute } from './delete-organisation-member';
import { deleteSubscriptionClaimRoute } from './delete-subscription-claim';
import { deleteAdminTeamMemberRoute } from './delete-team-member';
@@ -41,6 +42,7 @@ export const adminRouter = router({
get: getAdminOrganisationRoute,
create: createAdminOrganisationRoute,
update: updateAdminOrganisationRoute,
delete: deleteOrganisationRoute,
swapSubscription: swapOrganisationSubscriptionRoute,
},
organisationMember: {
@@ -1,5 +1,6 @@
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { convertToPdf } from '@documenso/lib/server-only/document-conversion';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
@@ -35,7 +36,7 @@ export const createDocumentRoute = authenticatedProcedure
attachments,
} = payload;
let pdf = Buffer.from(await file.arrayBuffer());
let pdf = await convertToPdf(file, ctx.logger);
if (formValues) {
// eslint-disable-next-line require-atomic-updates
@@ -37,5 +37,6 @@ export const createEmbeddingEnvelopeRoute = procedure
bypassDefaultRecipients: true,
},
apiRequestMetadata: ctx.metadata,
logger: ctx.logger,
});
});
@@ -1,11 +1,13 @@
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { convertToPdf } from '@documenso/lib/server-only/document-conversion';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { extractPdfPlaceholders } from '@documenso/lib/server-only/pdf/auto-place-fields';
import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { EnvelopeType } from '@prisma/client';
import type { Logger } from 'pino';
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
import { authenticatedProcedure } from '../trpc';
@@ -32,6 +34,7 @@ export const createEnvelopeRoute = authenticatedProcedure
teamId: ctx.teamId,
input,
apiRequestMetadata: ctx.metadata,
logger: ctx.logger,
});
});
@@ -48,6 +51,12 @@ type CreateEnvelopeRouteOptions = {
input: TCreateEnvelopeRequest;
apiRequestMetadata: ApiRequestMetadata;
/**
* Optional pino logger threaded from the calling tRPC context. Passed to
* downstream helpers (e.g. `convertToPdf`) for structured logging.
*/
logger?: Logger;
options?: {
bypassDefaultRecipients?: boolean;
};
@@ -58,6 +67,7 @@ export const createEnvelopeRouteCaller = async ({
teamId,
input,
apiRequestMetadata,
logger,
options = {},
}: CreateEnvelopeRouteOptions) => {
const { payload, files } = input;
@@ -96,17 +106,10 @@ export const createEnvelopeRouteCaller = async ({
});
}
if (files.some((file) => !file.type.startsWith('application/pdf'))) {
throw new AppError('INVALID_DOCUMENT_FILE', {
message: 'You cannot upload non-PDF files',
statusCode: 400,
});
}
// For each file: normalize, extract & clean placeholders, then upload.
// For each file: convert to PDF if needed, normalize, extract & clean placeholders, then upload.
const envelopeItems = await Promise.all(
files.map(async (file) => {
let pdf = Buffer.from(await file.arrayBuffer());
let pdf = await convertToPdf(file, logger);
if (formValues) {
// eslint-disable-next-line require-atomic-updates
@@ -3,6 +3,7 @@ import {
ORGANISATION_USER_ACCOUNT_TYPE,
} from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { orphanEnvelopes } from '@documenso/lib/server-only/envelope/orphan-envelopes';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
@@ -42,6 +43,11 @@ export const deleteOrganisationRoute = authenticatedProcedure
id: true,
},
},
subscription: {
select: {
planId: true,
},
},
},
});
@@ -68,4 +74,18 @@ export const deleteOrganisationRoute = authenticatedProcedure
},
});
});
// If the organisation has a Stripe subscription, schedule it to be
// cancelled at the end of the current billing period. The job runs
// asynchronously so a Stripe outage doesn't block deletion, and is
// retried by the job runner if Stripe is temporarily unavailable.
if (organisation.subscription) {
await jobs.triggerJob({
name: 'internal.cancel-organisation-subscription',
payload: {
stripeSubscriptionId: organisation.subscription.planId,
organisationId: organisation.id,
},
});
}
});
+13 -3
View File
@@ -3,6 +3,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { convertToPdf } from '@documenso/lib/server-only/document-conversion';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { duplicateEnvelope } from '@documenso/lib/server-only/envelope/duplicate-envelope';
@@ -269,9 +270,18 @@ export const templateRouter = router({
attachments,
} = payload;
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide(file, {
flattenForm: false,
});
const pdf = await convertToPdf(file, ctx.logger);
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide(
{
name: file.name,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdf),
},
{
flattenForm: false,
},
);
ctx.logger.info({
input: {