Skip to content

fix: Filter out blank recipients for outbound messages [DHIS2-21836] - #25213

Open
enricocolasante wants to merge 2 commits into
masterfrom
DHIS2-21836
Open

enricocolasante wants to merge 2 commits into
masterfrom
DHIS2-21836

Conversation

@enricocolasante

@enricocolasante enricocolasante commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes [DHIS2-21836]: when a program notification is configured for multiple delivery channels (SMS and Email) but the organisation unit contact only has details for one of them, the send used to be all-or-nothing or could be misreported as failed. Delivery is now resolved best-effort per channel — a channel with no deliverable, non-blank recipient is quietly skipped instead of blocking or failing the channels that do have one.

Changes

  • Recipient resolution (EmailDeliveryChannelStrategy, SmsDeliveryChannelStrategy): getOrganisationUnitRecipient is removed; setAttributes now adds the org unit's email/phone directly, guarded by StringUtils.isNotBlank(...) so a blank string is treated the same as a missing one, not just null. DeliveryChannelStrategy's corresponding abstract method is removed to match.
  • Best-effort batch creation (EmailMessageBatchCreatorService, SmsMessageBatchCreatorService): a message with no resolved recipient for that channel is filtered out before an OutboundMessage is built, so no batch is ever created for it — it's discarded, not sent, nor reported as FAILED. Filtering now logs a warn (with the program message UID) so a discarded message still leaves a trace to diagnose why a notification wasn't delivered on a given channel.
  • DefaultOutboundMessageBatchService: reverted to its original form — the earlier attempt at filtering "no recipient" batches at send time was removed since that responsibility now lives upstream in the batch creators (see above).
  • DefaultProgramMessageService: updated the comment on setAttributesBasedOnStrategy to reflect current behavior — the per-channel drop-on-exception path is now only for cases where a recipient can't be resolved at all (e.g. a tracked entity missing the required attribute type), not for an org unit simply missing one channel's contact detail.

@enricocolasante
enricocolasante marked this pull request as ready for review September 15, 2026 07:39
@enricocolasante
enricocolasante requested a review from a team as a code owner September 15, 2026 07:39

@teleivo teleivo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Why do we even add empty recipients in the first place? If we would not have any we would not need to filter/watch out for them
  • If we cannot keep empty recipients out of an OutboundMessage, then we should at least filter them out at that level later one. Right now you are checking if a OutboundMessageBatch has any OutboundMessage with at least one non-empty recipient. You can still have a scenario where the batch has messages with no non-empty recipient

@sonarqubecloud

Copy link
Copy Markdown

@enricocolasante

Copy link
Copy Markdown
Contributor Author
  • Why do we even add empty recipients in the first place? If we would not have any we would not need to filter/watch out for them
  • If we cannot keep empty recipients out of an OutboundMessage, then we should at least filter them out at that level later one. Right now you are checking if a OutboundMessageBatch has any OutboundMessage with at least one non-empty recipient. You can still have a scenario where the batch has messages with no non-empty recipient

You are right, we don't need the empty recipients at all.

// Iterate over a copy: a channel the resolved recipient cannot receive on (e.g. an org unit
// contact with an email but no phone number) is dropped from the message so that the remaining
// deliverable channels are still sent, instead of aborting the whole send.
// Iterate over a copy: a channel whose recipient cannot be resolved at all (e.g. a tracked

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is getting pretty long, do we need all of this?

@Override
public String getOrganisationUnitRecipient(OrganisationUnit orgUnit) {
if (orgUnit.getEmail() == null) {
throw new IllegalQueryException("Organisation unit does not have an email address");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing did remove the undeliverable channel from the persisted deliveryChannels. This still happens when the recipient is a tracked entity missing the attribute but not anymore when its the orgUnit missing an email. Not sure what the contract is for the persisted deliveryChannels.

@teleivo

teleivo commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

List<String> recipientList =
enrollment.getTrackedEntity().getTrackedEntityAttributeValues().stream()
.filter(
av ->
template
.getRecipientProgramAttribute()
.getUid()
.equals(av.getAttribute().getUid()))
.map(TrackedEntityAttributeValue::getValue)
.toList();
if (template.getDeliveryChannels().contains(DeliveryChannel.SMS)) {
recipients.getPhoneNumbers().addAll(recipientList);
} else if (template.getDeliveryChannels().contains(DeliveryChannel.EMAIL)) {
recipients.getEmailAddresses().addAll(recipientList);
}

Can this have the same problem you are trying to fix for TE/OU?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants