Skip to content

[ING-68] feat/multi-entity): adapt dunning campaign flow for multi entity billing#5515

Open
lovrocolic wants to merge 3 commits into
mainfrom
feat-dunning-multi-entity
Open

[ING-68] feat/multi-entity): adapt dunning campaign flow for multi entity billing#5515
lovrocolic wants to merge 3 commits into
mainfrom
feat-dunning-multi-entity

Conversation

@lovrocolic
Copy link
Copy Markdown
Collaborator

Context

Currently, one customer can be assigned to only one billing entity. With multi-billing-entities feature, it will be possible to assign any billing entity to customer's billing object, like subscription or wallet.

Description

This PR adapts dunning campaign flow for multi entity billing.

@lovrocolic lovrocolic changed the title Feat dunning multi entity [ING-68] feat/multi-entity): adapt dunning campaign flow for multi entity billing May 15, 2026
Copy link
Copy Markdown
Contributor

@ancorcruz ancorcruz left a comment

Choose a reason for hiding this comment

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

I understand reading the code that we run the dunning for the customer across all billing entities (with a single counter per currency) but we split the amounts billing by BE. is this intended?

If that is the case, looks all good except self billed invoices must be excluded from dunning (pre existing bug I'm afraid).

end

def overdue_invoices
def overdue_invoices_in_currency
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.

If I'm not wrong this also should exclude the self-billed invoices from the overdue ones.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Applied it!


def dunning_campaign_threshold_reached?
overdue_invoices.sum(:total_amount_cents) >= dunning_campaign_threshold.amount_cents
overdue_invoices_in_currency.sum(:total_amount_cents) >= dunning_campaign_threshold.amount_cents
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.

I'm a bit confuse here...

We compute all the overdue invoices of the customer across billing entities with a given currency to check if the threshold has been reached. However, we run the dunning campaign scoped to each billing entity + currency.... then a customer with overdue invoices in EUR, for example 10k in billing_entity_1 and 5k in billing_entity_2, given a dunning threshold of 12k EUR, it will run two dunning processes for this customer one for the 10k overdue in BE_1 and another one with 5k in BE_2. Is this right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I would say yes. BE affects only the billing side, not the tresholds


custom_campaign = customer.applied_dunning_campaign
default_campaign = billing_entity.applied_dunning_campaign
default_campaign = customer.billing_entity.applied_dunning_campaign
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 consider the customer default campaign the one defined as one of its billing entities' default campaing instead of the given billing entity default campaign? Does this make sense?

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.

if this is intentional it would be great to add a quick comment as insight for the next reader...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

A dunning campaign is a customer-level concept — it's resolved as customer.applied_dunning_campaign ||
customer.billing_entity.applied_dunning_campaign. The per-BE iteration only scopes invoices, not which campaign applies. Note BulkProcessService#applicable_dunning_campaign (line 71) uses the exact same resolution, so they're consistent.

Comment on lines +87 to +89
entity_ids = customer.invoices.non_self_billed.payment_overdue
.where(currency: currency).distinct.pluck(:billing_entity_id)
customer.organization.billing_entities.where(id: entity_ids)
Copy link
Copy Markdown
Contributor

@ancorcruz ancorcruz May 15, 2026

Choose a reason for hiding this comment

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

should we filter invoices by ready_for_payment_processing: true as well? We do in ProcessAttemptService#overdue_invoices

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Applied it, thanks!

Comment on lines 52 to 54
def dunning_campaign_threshold_reached?
overdue_invoices.sum(:total_amount_cents) >= dunning_campaign_threshold.amount_cents
overdue_invoices_in_currency.sum(:total_amount_cents) >= dunning_campaign_threshold.amount_cents
end
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.

we run this query for each BE of the customer, it is always the same query as it is across all BE, we can run this one level above and pass the result to the service saving a DB query for each BE.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I improved it a bit with memoization, but IMO not worth passing result around since in most cases, BE is the same and in rare cases we would have max. few billing entities. Let me know if you still think we should improve it more

@lovrocolic lovrocolic force-pushed the feat-dunning-multi-entity branch from 8b1d466 to 46b52f8 Compare May 27, 2026 11:23
@lovrocolic lovrocolic force-pushed the feat-dunning-multi-entity branch from abbe64b to 5a5aaff Compare May 27, 2026 15:46
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