Skip to content

feat(charge_filters): add code to identify filters - #6079

Merged
brunomiguelpinto merged 3 commits into
mainfrom
ing-536-filter-code
Aug 11, 2026
Merged

feat(charge_filters): add code to identify filters#6079
brunomiguelpinto merged 3 commits into
mainfrom
ing-536-filter-code

Conversation

@brunomiguelpinto

@brunomiguelpinto brunomiguelpinto commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Context

A charge filter has no stable identity. It is known by its values, and those move: removing a value
from a billable metric trims it out of every filter using it, so two filters that were distinct can
collapse onto the same values and stop being distinguishable.

{region: [us], tier: [gold]}    @0.02     ->  {region: [us]}  @0.02
{region: [us], tier: [silver]}  @0.015    ->  {region: [us]}  @0.015

Propagating a plan's charge to its overrides pairs filters by those values, so once that happens it
can reprice the wrong one.

Description

Charge filters get a code, derived from the values a filter is created with and frozen from then
on, so a filter keeps its identity even when its values move. An override inherits the code of the
filter it was copied from rather than deriving its own — that inheritance is what records the link
between the two.

A filter that exists only on the override, because the customer negotiated a segment the plan
does not price, derives its own code: there is nothing to inherit from, and it is not orphaned
either. If the plan later gains that predicate it derives the same code, so the two line up by
construction rather than by luck.

Backfilling existing rows

One walk, over the charges belonging to a plan, a job per charge. Each job derives the codes for its
own filters and then hands its overrides on to a second job

The unit of work is a charge on the way in and a parent on the way out, because every override of a
charge answers to the same set of codes.

What stays NULL, on purpose

  • a charge holding two filters on one predicate — whichever kept the code would be the one that
    bills from then on, and that is not a migration's call. Only the colliding filters are skipped;
    their siblings are still filled.
  • an override holding a filter its plan no longer has — nothing left to link it to, either
    because the plan dropped it or because the parent charge was deleted and dependent: :nullify cut
    the link. Damage already done; no backfill undoes it.
  • a filter whose code a sibling already holds, frozen when that sibling's own values were these —
    writing it again would break the unique index on (charge_id, code).
  • organizations with nothing active or pending, and deleted plans — not billing, not worth the work.

rake filters:report_codes reports what is left and why. Cleaning up the first kind and running the
upgrade task again converges, since a cleaned charge goes back to having filters without a code and
the walk selects it again.

Comment thread app/services/charge_filters/cascade_service.rb Outdated
Comment thread lib/tasks/filters.rake Outdated
Comment thread app/models/plan_rate_card.rb
Comment thread app/services/database_migrations/enqueue_charge_filter_code_backfill_service.rb Outdated
Comment thread app/jobs/database_migrations/backfill_charge_filter_codes_job.rb Outdated
@brunomiguelpinto
brunomiguelpinto marked this pull request as ready for review August 7, 2026 15:27
@brunomiguelpinto brunomiguelpinto changed the title feat: add code to charge filters feat(charge_filters): add code to identify filters Aug 7, 2026
Comment thread lib/tasks/upgrade_verification.rake Outdated
Comment thread lib/tasks/upgrade_verification.rake
Comment thread lib/tasks/upgrade_verification.rake
## Context

A charge filter has no stable identity. It is known by its values, and
those move: removing a value from a billable metric trims it out of every
filter using it, so two filters that were distinct can collapse onto the
same values and stop being distinguishable.

Propagating a plan's charge to its overrides pairs filters by those
values, so once that happens it can reprice the wrong one.

## Description

Charge filters get a code, derived from the values a filter is created
with and frozen from then on, so a filter keeps its identity even when
its values move. An override inherits the code of the filter it was
copied from rather than deriving its own, and that inheritance is what
records the link between the two. A filter that exists only on the
override derives its own code, since there is nothing to inherit from.

A partial unique index keeps the code unique per charge among the
filters that are not deleted.

Existing rows are backfilled by one walk over the charges belonging to a
plan, a job per charge, each handing its own overrides on to a second job
as it finishes. That second job pairs a copy back to the filter it came
from by a signature the database computes: one entry per billable metric
filter a charge filter names, holding that filter's values sorted. Two
filters are the same filter when their signatures are equal.

Three kinds of filter are left without a code on purpose: a charge
holding two filters on one predicate, because whichever kept the code
would be the one that bills from then on; an override holding a filter
its plan no longer has, because there is nothing left to link it to; and
one whose code a sibling already holds, frozen when that sibling's own
values were these. The upgrade task therefore waits on the count of
codeless filters to stop moving rather than on it reaching zero, and
filters:report_codes reports what was left and why.
Comment thread app/jobs/database_migrations/backfill_child_charge_filter_codes_job.rb Outdated
Comment thread lib/tasks/filters.rake

@annvelents annvelents 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.

Agree on @tiagolupepic's comments, but LGTM! 🚀

@brunomiguelpinto
brunomiguelpinto merged commit 72baba6 into main Aug 11, 2026
12 checks passed
@brunomiguelpinto
brunomiguelpinto deleted the ing-536-filter-code branch August 11, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants