fix(crm): tell the admin whether a customer's invitation actually went out

Closes #1261.

"Invite customer" is two calls: createDirect, then sendInvite. The mode wiring
is right -- CustomerManagementPage passes mode='invite' and
InlineCustomerCreate does call sendInvite -- so the reported symptom is not a
missed branch. It is that nothing downstream distinguishes the outcomes.

Three things could not be told apart afterwards:

- The success toast claimed "portal invitation sent". sendInvite only queues an
  email_queue row; whether it was delivered is decided minutes later by the
  queue processor. The toast now says queued, and says what sends it.
- When sendInvite failed, the warning read "Invitation email failed -- retry
  from the customer detail page", which sounds like the mail bounced. What
  actually remains is a PASSIVE customer with no invitation at all, so it says
  that instead. A 409 is now separated out: that means an invitation for the
  address is already open and the RE-invite was refused, so the customer is
  invited and telling them to retry sends them the wrong way.
- The customers table rendered a customer whose invitation never went out
  identically to one the admin created as passive on purpose -- both showed
  only "Passive - admin only". Passive customers with an open invitation now
  show "Invitation pending", matched case-insensitively because
  customer_invitations lowercases the address while customer_accounts keeps
  what the admin typed. The invitations list was already being fetched for the
  tab; this only cross-references it.

Active customers are left alone: they have portal access, so a stale
invitation row for their address says nothing about them.

7 tests; the 5 that assert the new behaviour all fail before the change, and
the 2 negative controls pass on both sides.
This commit is contained in:
Paul Nothaft
2026-09-02 13:49:31 +02:00
parent f722bdaf4b
commit 1b8e5f83d7
5 changed files with 254 additions and 15 deletions
@@ -16,6 +16,11 @@
* stays saved (passive) and a warning toast asks the admin to retry
* from the customer detail page.
*
* #1261 — the toasts here say what actually happened rather than what was
* intended. Two calls means three outcomes, and the middle one used to be
* indistinguishable from success: the invitation email is only QUEUED, so
* "invitation sent" was a claim this code cannot make.
*
* Field set mirrors the customer detail page so admins see the same
* shape regardless of where they're editing.
*/
@@ -182,10 +187,17 @@ export const InlineCustomerCreate: React.FC<Props> = ({ onCreated, onCancel, mod
try {
await customerAdminService.sendInvite(customer.id);
toast.success(t('customers.create.savedActiveToast',
'Customer created and portal invitation sent.'));
'Customer created and portal invitation queued. It is sent by the email queue — check System health if it does not arrive.'));
} catch (err: any) {
toast.warn(t('customers.create.inviteFailedToast',
'Customer saved (passive). Invitation email failed — retry from the customer detail page.'));
// A 409 means an invitation for this address is already open, which
// is a different thing from the email failing: the customer is
// invited, and re-inviting is what was refused.
const alreadyInvited = err?.response?.status === 409;
toast.warn(alreadyInvited
? t('customers.create.inviteAlreadyPendingToast',
'Customer saved. An invitation for this address is already open — cancel it on the Invitations tab before sending a new one.')
: t('customers.create.inviteFailedToast',
'Customer saved as PASSIVE — no invitation went out. Retry "Send portal invitation" from the customer detail page.'));
// eslint-disable-next-line no-console
console.warn('sendInvite failed', err);
}