From d448e3f8f6a3cf4d16f6af0b16f78fb93823daf4 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 1 Oct 2026 14:01:57 +1000 Subject: [PATCH] fix: send notification on limits change (#3414) --- .../admin+/organisations.$id.tsx | 59 +++++++++++++--- .../templates/organisation-limits-updated.tsx | 70 +++++++++++++++++++ packages/lib/jobs/client.ts | 2 + ...-organisation-limit-alert-email.handler.ts | 1 + ...ganisation-limits-updated-email.handler.ts | 65 +++++++++++++++++ .../send-organisation-limits-updated-email.ts | 31 ++++++++ .../universal/organisation-limit-changes.ts | 26 +++++++ .../admin-router/update-admin-organisation.ts | 29 +++++++- .../update-admin-organisation.types.ts | 5 +- 9 files changed, 277 insertions(+), 11 deletions(-) create mode 100644 packages/email/templates/organisation-limits-updated.tsx create mode 100644 packages/lib/jobs/definitions/emails/send-organisation-limits-updated-email.handler.ts create mode 100644 packages/lib/jobs/definitions/emails/send-organisation-limits-updated-email.ts create mode 100644 packages/lib/universal/organisation-limit-changes.ts diff --git a/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx b/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx index a2f01d6c5..cf976d3d4 100644 --- a/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx +++ b/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx @@ -37,7 +37,7 @@ import { useMemo } from 'react'; import { useForm } from 'react-hook-form'; import { Link, useNavigate } from 'react-router'; import { match } from 'ts-pattern'; -import type { z } from 'zod'; +import { z } from 'zod'; import { AdminOrganisationDeleteDialog } from '~/components/dialogs/admin-organisation-delete-dialog'; import { AdminOrganisationMemberDeleteDialog } from '~/components/dialogs/admin-organisation-member-delete-dialog'; @@ -563,11 +563,15 @@ const GenericOrganisationAdminForm = ({ organisation }: OrganisationAdminFormOpt ); }; -const ZUpdateOrganisationBillingFormSchema = ZUpdateAdminOrganisationRequestSchema.shape.data.pick({ - claims: true, - customerId: true, - originalSubscriptionClaimId: true, -}); +const ZUpdateOrganisationBillingFormSchema = ZUpdateAdminOrganisationRequestSchema.shape.data + .pick({ + claims: true, + customerId: true, + originalSubscriptionClaimId: true, + }) + .extend({ + notifyOrganisation: z.boolean(), + }); type TUpdateOrganisationBillingFormSchema = z.infer; @@ -614,19 +618,31 @@ const OrganisationAdminForm = ({ organisation, licenseFlags }: OrganisationAdmin emailTransportId: organisation.organisationClaim.emailTransportId ?? null, }, originalSubscriptionClaimId: organisation.organisationClaim.originalSubscriptionClaimId || '', + notifyOrganisation: true, }, }); - const onSubmit = async (values: TUpdateOrganisationBillingFormSchema) => { + const onSubmit = async ({ notifyOrganisation, ...values }: TUpdateOrganisationBillingFormSchema) => { try { - await updateOrganisation({ + const { isNotificationSent } = await updateOrganisation({ organisationId: organisation.id, data: values, + notifyOrganisation, }); + form.setValue('notifyOrganisation', false); + + const description = match({ notifyOrganisation, isNotificationSent }) + .with({ isNotificationSent: true }, () => t`Organisation has been updated and its owner has been notified`) + .with( + { notifyOrganisation: true }, + () => t`Organisation has been updated. No limits changed, so no notification was sent`, + ) + .otherwise(() => t`Organisation has been updated successfully`); + toast({ title: t`Success`, - description: t`Organisation has been updated successfully`, + description, duration: 5000, }); } catch (err) { @@ -919,6 +935,31 @@ const OrganisationAdminForm = ({ organisation, licenseFlags }: OrganisationAdmin )} /> + ( + +
+ + + + + +
+ + + Emails the organisation owner to let them know their limits have been updated. No email is sent if no + limits changed. + + + +
+ )} + /> +