Repository navigation
fix(mp): type CommunicationType as MP's real enum — 'Text' and 'Letter' 500 - #92
Merged
chriskehayias merged 2 commits intoSep 29, 2026
Conversation
…r' 500
`CommunicationInfo.CommunicationType` was typed `'Email' | 'Text' | 'Letter'`.
Ministry Platform's `Platform.Messaging.CommunicationType` is
`Unknown | Email | SMS | RssFeed | GlobalMFA`, so two of the three values the
type offered were never members, and MP rejects either with an opaque HTTP 500
rather than a 400:
{"Message":"Error converting value \"Text\" to type
'Platform.Messaging.CommunicationType'. Path 'CommunicationType' ...
Requested value 'Text' was not found."}
The only value the type offered for texting was one that always fails, and the
failure reads as a server fault rather than a bad payload. `'Email'` happens to
be a real member and is the only value anyone had passed, which is why the
other two went unnoticed.
Once `'SMS'` is used, MP additionally requires `TextPhoneNumberId`
(`dp_SMS_Numbers.SMS_Number_ID` of the outbound number) and answers its absence
with a second 500 — `Property 'TextPhoneNumberId' is required and must be
populated`. MP's own Swagger marks the field optional, so the Swagger cannot be
trusted here.
Three changes:
- `COMMUNICATION_TYPES` as a `const [...] as const` with `CommunicationType`
derived from it, so the enum has one source of truth.
- `CommunicationInfo` becomes a discriminated union, so the compiler requires
`TextPhoneNumberId` exactly when the type is `'SMS'`. A runtime 500 becomes a
build error.
- A runtime guard in `CommunicationService.createCommunication`, placed above
`ensureValidToken()` so a doomed payload costs neither a token refresh nor a
round trip. It catches callers arriving through an `as` cast or untyped JSON,
and names the accepted values so nobody needs the Swagger to fix their call.
Verified against a live MP tenant on 2026-09-08 rather than from the Swagger
alone: `dp_Communication_Types` holds `1 Email`, `2 SMS Text`, `3 RSS Feed`,
`4 GlobalMFA` (`Unknown` is the zero value and has no row), and an email plus an
SMS were both delivered end to end — the SMS only after switching `'Text'` to
`'SMS'` and supplying `TextPhoneNumberId`.
Tests: eight cases added to `communication.service.test.ts`. Each rejection test
asserts that *nothing was sent* — not merely that an error came back — since
asserting on the message alone would still pass if the guard were moved below
the POST. The type half is covered by `@ts-expect-error` cases that fail
`tsc --noEmit`, which `next build` also performs.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This was referenced Sep 21, 2026
Closed
Contributor
Author
|
@chriskehayias - just wanted to bring your attention to this bug/pr |
…to Email and SMS Brings this PR up to date with main's CommunicationService rework (buildCommunication, trusted sender, fixed-message validation) and adapts the fix to it: - COMMUNICATION_TYPES is narrowed to ['Email', 'SMS']. Unknown, RssFeed and GlobalMFA are real MP enum members, but the template deliberately does not permit them, in the type or at runtime. - main's runtime allowlist, new Set(['Email', 'Text', 'Letter']), repeated the original bug and actively refused 'SMS'. It is now built from COMMUNICATION_TYPES, and the SMS TextPhoneNumberId requirement moves into buildCommunication in place of assertSendable. Errors keep main's fixed-message form (Invalid CommunicationType / Invalid TextPhoneNumberId), naming the field and never echoing the value. - CommunicationContent, the MPHelper.createCommunication input, uses a distributive Omit. A plain Omit over the union merged its members, so an 'SMS' payload without TextPhoneNumberId compiled again at the public API. - Tests adapted to pass the trusted sender; the rejection cases now cover every excluded value, plus a compile-time case on CommunicationContent. - docs/README.md: main's "Email, Text or Letter" corrected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Contributor
|
Thanks @jonnydcakes — great catch, and the live-tenant verification made this easy to trust. Your commit is unchanged; I merged
Verified: |
chriskehayias
merged commit Sep 29, 2026
2d14533
into
MinistryPlatform-Community:main
5 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
src/lib/providers/ministry-platform/types/provider.types.ts:41MP's
Platform.Messaging.CommunicationTypeisUnknown | Email | SMS | RssFeed | GlobalMFA.'Text'and'Letter'were never members, and MP rejects either with an opaque HTTP 500,not a 400:
So the only value the type offered for texting is one that always fails, and the failure reads
as a server fault rather than a bad payload.
'Email'happens to be a real member — and it isthe only value anyone has ever passed, which is why a two-thirds-wrong union sat in the type
layer unnoticed.
Also: SMS requires
TextPhoneNumberIdOnce
CommunicationType: 'SMS'is used, MP additionally requires it:TextPhoneNumberIdis the outbound number'sdp_SMS_Numbers.SMS_Number_ID. The field alreadyexists on
CommunicationInfobut is optional — it is effectively required whenCommunicationType === 'SMS'. MP's own Swagger marks it optional, so the Swagger cannot betrusted here.
Verified against a live tenant
Not taken from the Swagger alone — re-checked on a production MP instance on 2026-09-08:
dp_Communication_Types1 Email,2 SMS Text,3 RSS Feed,4 GlobalMFA(Unknownis the zero value, no row)dp_SMS_NumbersOriginally found on 2026-06-11 building a passwordless sign-in spike that reuses this
/communicationspattern: the email delivered; the SMS succeeded only after switching'Text'→
'SMS'and addingTextPhoneNumberId.Impact
Latent, not a live outage. Nothing in the template calls
createCommunication— thereferences are the plumbing (
helper.ts,provider.ts,communication.service.ts) and thetests, which use
'Email'. This is a wrong type surface waiting for the first person who triesto send a text, who gets a 500 with no indication their payload was at fault. Every downstream
fork of the template inherits it.
The change
COMMUNICATION_TYPESas aconst [...] as constwithCommunicationTypederived fromit, so the enum has one source of truth and a comment naming what verified it.
CommunicationInfobecomes a discriminated union, so the compiler requiresTextPhoneNumberIdexactly when the type is'SMS'. A runtime 500 becomes a build error.CommunicationService.createCommunication, placed aboveensureValidToken()so a doomed payload costs neither a token refresh nor a round trip. Itcatches callers arriving through an
ascast or untyped JSON, and names the accepted valuesso nobody needs the Swagger to fix their call.
Plus a paragraph in the provider
docs/README.mdCommunications section.provider.types.tsis hand-maintained (generate-types.tsonly writes intomodels/), so thiswill not be undone by
npm run mp:generate:models.Tests
Eight cases added to the existing
communication.service.test.ts(21 in that file now), plus acompile-time block.
Each rejection test asserts that nothing was sent — no
post, nopostFormData, and noensureValidToken— not merely that an error came back. Asserting on the message alone wouldstill pass if the guard were later moved below the POST, which is the failure mode the guard
exists to prevent.
All four mutations were run and each fails the suite it should:
assertSendable(...)callensureValidToken()'Text'back inCOMMUNICATION_TYPEStscTS2578 — unused@ts-expect-errorTextPhoneNumberIdoptional for'SMS'tscTS2578 — unused@ts-expect-errorVerification on this branch:
npx vitest run --coverage→ 59 files / 1023 tests pass, coveragegates green (99.74% statements);
npx tsc --noEmitclean;npx eslint .clean;node scripts/check-lockfile.mjsreports no drift (no dependency changes).