mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(companies): require and deliver a staff message on suspend/reactivate
Staff could suspend or reactivate a customer role with one silent click: no reason captured, nothing stored, and the customer was never told. The API now rejects a suspend or reactivate without a non-empty note, keeps the note in reviewNote while suspended, and sends the customer an SMS/email/in-app notification quoting the staff message. In the backoffice the reject-note modal is generalised into a decision modal shared by reject, suspend and reactivate, so all three force a message. EDRFREIGHT-188
This commit is contained in:
@@ -1090,6 +1090,23 @@ export class CompaniesService {
|
||||
if (!existing)
|
||||
throw new NotFoundException(`Company profile ${profileId} not found`);
|
||||
|
||||
// Suspension and reactivation must carry a staff explanation — the customer
|
||||
// sees it, so "why" can never be left blank. Reactivation is the
|
||||
// active-write that leaves Suspended; a first approval stays note-free.
|
||||
const reactivating =
|
||||
status === ProfileStatus.Active &&
|
||||
existing.status === ProfileStatus.Suspended;
|
||||
if (
|
||||
(status === ProfileStatus.Suspended || reactivating) &&
|
||||
!note?.trim()
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
status === ProfileStatus.Suspended
|
||||
? "A message explaining the suspension is required — the customer will see it."
|
||||
: "A message explaining the reactivation is required — the customer will see it.",
|
||||
);
|
||||
}
|
||||
|
||||
// A self-registered company is only reviewable once its owner submits the
|
||||
// onboarding wizard (markOnboardingComplete) — until then its profiles are
|
||||
// half-filled drafts and approving one would mint a reference against an
|
||||
@@ -1176,9 +1193,13 @@ export class CompaniesService {
|
||||
);
|
||||
}
|
||||
|
||||
// Track the review outcome. Rejection keeps the note so the customer knows
|
||||
// why; approval clears it. Any decision stamps the reviewer + time.
|
||||
if (status === ProfileStatus.Rejected) {
|
||||
// Track the review outcome. Rejection and suspension keep the note so the
|
||||
// customer knows why; approval/reactivation clears it. Any decision stamps
|
||||
// the reviewer + time.
|
||||
if (
|
||||
status === ProfileStatus.Rejected ||
|
||||
status === ProfileStatus.Suspended
|
||||
) {
|
||||
patch.reviewNote = note ?? null;
|
||||
} else if (status === ProfileStatus.Active) {
|
||||
patch.reviewNote = null;
|
||||
@@ -1192,6 +1213,23 @@ export class CompaniesService {
|
||||
if (!updated)
|
||||
throw new NotFoundException(`Company profile ${existing.id} not found`);
|
||||
|
||||
// Suspension and reactivation lock/unlock a role the customer relies on —
|
||||
// tell them, and carry the staff message so they know why.
|
||||
const reactivated =
|
||||
status === ProfileStatus.Active &&
|
||||
existing.status === ProfileStatus.Suspended;
|
||||
if (status === ProfileStatus.Suspended || reactivated) {
|
||||
const company = await this.companiesRepo.findById(updated.companyId);
|
||||
if (company) {
|
||||
this.companyNotifier.profileStatusChanged(
|
||||
company,
|
||||
updated.type,
|
||||
status === ProfileStatus.Suspended ? "suspended" : "reactivated",
|
||||
note ?? "",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Approving any profile promotes a pending company to active, so the
|
||||
// customer can start working as soon as their first profile is cleared.
|
||||
if (status === ProfileStatus.Active) {
|
||||
|
||||
@@ -89,6 +89,43 @@ export class CompanyNotifierService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the customer one of their operational roles was suspended or
|
||||
* reactivated, quoting the staff message — the service layer requires one for
|
||||
* both transitions, so the customer always learns why, not just what.
|
||||
*/
|
||||
profileStatusChanged(
|
||||
company: Company,
|
||||
profileType: string,
|
||||
change: "suspended" | "reactivated",
|
||||
staffMessage: string,
|
||||
): void {
|
||||
const title = `${profileType} role ${change}`;
|
||||
const consequence =
|
||||
change === "suspended"
|
||||
? `You will not be able to operate under this role until it is reactivated; ` +
|
||||
`your other roles are unaffected.`
|
||||
: `You can operate under this role again.`;
|
||||
const body =
|
||||
`Your company's ${profileType} role has been ${change}. ` +
|
||||
`${consequence} Message from EDR staff: ${staffMessage}`;
|
||||
|
||||
this.logger.log(
|
||||
`PROFILE_${change.toUpperCase()} — ${company.id} / ${profileType}`,
|
||||
);
|
||||
void this.notifyContact(company, `${title}. ${body}`);
|
||||
void this.inbox.notify({
|
||||
recipients: { companyId: company.id },
|
||||
audience: NotificationAudience.PORTAL,
|
||||
type: NotificationType.ACCOUNT_STATUS,
|
||||
title,
|
||||
body,
|
||||
link: "/settings",
|
||||
data: { companyId: company.id, profileType, change, staffMessage },
|
||||
priority: NotificationPriority.HIGH,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Backoffice-facing: work has arrived back in the review queue ────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user