Case Hierarchies That Double-Count Revenue When a Child Case Is Reparented
A technical guide for Dynamics 365 Customer Service and finance teams: why reports on the live parent case lookup rewrite history, and how an append-only allocation ledger, backfilled from audit data, makes every period report the same numbers twice.
Reports that read the current parent case lookup double count as soon as a child case is reparented, because the lookup only holds today's parent and the history is overwritten. Last month's figures move to the new parent, while stored rollups, exports and snapshots taken earlier still count the child under the old one. The fix is an append-only allocation ledger: a Dataverse table that records which parent each child was attributed to and from when, written on every parent change and never updated or deleted. Write it synchronously with a plugin, lock it with security roles, rebuild past allocations from audit history, and have finance query the ledger instead of the live hierarchy.
Why does reparenting a child case double count revenue in reports?
Because the parent case lookup on the child case, parentcaseid, is a current value, not a history. When an agent moves a child case from one parent to another, Dataverse overwrites the lookup. Any report that groups child revenue, billable time or cost by parent now attributes the whole life of that child to the new parent, including the months before it moved. On its own that is a restatement. It becomes double counting when two numbers taken at different times are added together or compared, which is exactly what month-end reporting does.
| Where it happens | What goes wrong |
|---|---|
| Stored rollup columns on the parent | Rollup values are recalculated by a system job, so until the old parent is recalculated its stored total still includes the child while the new parent already does |
| Monthly exports and Power BI snapshots | Last month was exported with the child under the old parent; this month the live query puts the same history under the new parent, and cumulative totals include it twice |
| Parent level invoices or contract reports | Revenue already reported under the old parent is reported again under the new one when the period is rerun |
| Reports that combine live and archived data | Archived rows carry the old parent, live rows carry the new one, and the child appears in both |
What is an append-only allocation ledger and why does it fix this?
It is a separate Dataverse table that records attribution as a series of events instead of a single current value. Every time a child case gets a parent, loses one or moves between parents, the ledger gains rows saying so, with the moment it took effect. Nothing in the ledger is ever changed. The live parentcaseid lookup keeps doing its job for agents, routing and the case form, and finance stops reading it.
Because history is never overwritten, a report for March run in March and the same report run in September return the same attribution for March. Revenue earned by the child before the move stays with the old parent, revenue after the move goes to the new parent, and a total across both parents counts it once. This is the same principle an accounting ledger uses: corrections are new entries, never edits. The general modelling choices for cases, such as how to link several products to one case, are covered in our multiple products per case guide; this page deals only with the hierarchy between cases.
What columns should the allocation ledger table have?
Keep it narrow. The ledger records attribution, not amounts: revenue, time and cost stay on the records that already hold them, and are joined to the ledger by date. Make the table organization owned unless there is a real need to restrict rows by owner.
| Column | Type | Purpose |
|---|---|---|
| Child case | Lookup to case, required, with a text copy of the case number | Which case is being attributed; the text copy keeps the row readable even if the case is later hidden from a user |
| Parent case | Lookup to case, optional | Which parent the entry applies to; empty means the child had no parent |
| Allocation share | Decimal | Positive to attribute the child to a parent, negative to reverse a previous attribution; use fractions only if splits are a real business rule |
| Effective from | Date and time, time zone independent | When the attribution starts or stops, normally the moment the parent changed |
| Entry type | Choice: opening, allocation, reversal, correction, backfill | Why the row exists, so finance can filter corrections and backfilled history |
| Source | Text | The plugin step, backfill run or correction request that wrote it |
| Reason | Text | Required for corrections and backfill, so every exception has an explanation |
| Created by and created on | System columns | Who or what wrote the row and when, which is separate from when it took effect |
How should the ledger be written when the parent case changes?
Write it in the same transaction as the change, which in Dataverse means a plugin. Register it on Update of the case table with parentcaseid as the filtering attribute, in the synchronous post-operation stage, with a pre-image containing parentcaseid. The plugin compares the old and new parent and appends a reversal row for the old parent and an allocation row for the new one, both with the same effective from. Register a second step on Create for cases created with a parent, which writes the opening allocation. If the ledger write fails, the parent change fails with it, so the ledger can never silently fall behind the case.
Be precise about the alternatives. A Power Automate cloud flow using the Dataverse trigger for a modified row runs asynchronously after the change is committed, can run later than the change and out of order with other changes, and does not receive the previous value of the column, so it has to read the last ledger rows for the child to know the old parent. It can work for low change volumes if it is idempotent and reconciled, but it is not a real-time write. A classic real-time workflow runs synchronously, but it is a legacy feature that Microsoft steers new work away from, and it cannot easily see the previous parent. For a ledger finance relies on, a small, tested plugin is the right tool; how to test one is in our plugin unit testing guide.
How do you make the ledger genuinely append-only in Dataverse?
Use two layers, because security roles alone do not stop everyone.
- Security roles: grant Create and Read on the ledger table to the roles and application users that need them, and no Write, Delete, Assign or Share privileges to anyone. Most users never create rows directly anyway, because the plugin runs in the context of the change.
- A blocking plugin: register synchronous pre-operation steps on Update and Delete of the ledger table that throw an error for every caller. This is what stops a System Administrator, whose role cannot be restricted, and it stops bulk delete jobs and well-meaning data fixes.
- Relationship behaviour: set the child case relationship so that deleting a case is restricted while ledger rows exist. A remove link or cascade delete behaviour would try to change or delete ledger rows, which the blocking plugin will then refuse. Cancel or resolve cases rather than deleting them.
- Auditing on the ledger table, so an attempt to change the plugin registration or the table itself leaves a trace; enabling and scoping auditing is covered in our audit trail compliance guide.
- Corrections as new rows: a wrong allocation is fixed with a reversal and a new allocation of entry type correction, with a reason, never by editing the original.
How do effective-from and effective-to periods work without updating rows?
Store only effective from in the ledger and derive effective to in the reporting layer. For each child and parent, order the rows by effective from; an allocation opens a period and the matching reversal closes it, so effective to is simply the effective from of the reversal. A view in Power BI, a Fabric or Azure SQL copy of the data, or the query finance already uses can turn the event rows into one row per period: child, parent, effective from, effective to.
The signed shares give a second, simpler check. Summing the allocation share per child and parent for all rows effective on or before a date returns one for the parent the child belonged to at that moment and zero for every former parent. If any child sums to more than one across parents at any date, the ledger has a defect, and a scheduled check should say so before finance finds it. Decide one rule for backdated changes: normally a reparent takes effect when it is made, and a request to move history into an earlier period is a correction, approved and recorded with a reason, especially if that period is already closed.
How do you rebuild historical allocations from audit data?
If auditing was enabled on the case table and on the parentcaseid column, the audit history holds every past parent change with its old and new value and the time it happened. A one-off backfill reads that history for each child case and writes the ledger rows the plugin would have written, with entry type backfill.
- Start each child with an opening row: the old value of its earliest audited parent change, or its current parent if it never changed, effective from the case creation date.
- For each audited change in time order, write a reversal for the old parent and an allocation for the new parent at the time of the change.
- Check what your audit retention has already removed and whether auditing was ever off for the case table or the column. Where history is missing, record the assumption explicitly on backfill rows and agree it with finance rather than guessing silently.
- Fill gaps from other evidence where it exists: past monthly exports, data warehouse snapshots or issued invoices, each noted as the source.
- Run the backfill in a sandbox copy first and compare its results for a closed month with the figures finance actually reported.
- Deploy the plugin before the backfill runs in production, so no change falls between the two.
How should finance query the ledger instead of the live hierarchy?
Attribute each revenue, time or cost record by its own date. For every billable entry on a child case, find the ledger period for that child that contains the entry date, and report it under that period's parent. Entries on the parent case itself need no ledger and are reported directly. The live parentcaseid lookup does not appear in any finance query.
Three habits keep the numbers stable. Report from a copy of the data, such as Power BI with a scheduled refresh or an analytical store, rather than from live views and stored rollups on the case form. Reconcile each period on close: total revenue across all parents equals total revenue across all cases, so any difference points straight at a ledger defect. And keep rollups on the parent case for agents only, labelled as current, never for invoicing. When the wider model is producing numbers finance does not trust, the causes beyond reparenting are covered in our Dataverse data model remediation guide.
Which case settings and processes should change alongside the ledger?
The ledger records reparenting accurately; it is still worth making reparenting deliberate. Review the parent and child case settings in the Customer Service admin center, which control which columns a child inherits from its parent when it is created and how closing a parent treats its children, and check current Microsoft documentation for exactly when inheritance applies in your version.
- Limit who can change the parent of an existing child case, for example with a validation plugin that checks the caller's role, or column security if your environment allows it on that column, and ask for a reason when it happens.
- Include case merge in testing. Merging changes the status and relationships of the merged case, so confirm what happens to its child cases in your environment and make sure the ledger reflects it.
- Treat a child moving to a parent for a different customer or contract as an exception that someone reviews, because that is where billing disputes start.
- Keep agent-facing rollups and views on the live hierarchy, and label finance reports as ledger based, so nobody compares the two expecting them to match for past periods.
Is a Dataverse ledger the right platform choice for attribution history?
For organisations already running Dynamics 365 Customer Service, yes: the ledger sits next to the cases it describes, shares their security and audit, and is written in the same transaction as the change, which an external database cannot guarantee. Keeping the analysis in Power BI or an analytical copy keeps heavy period queries off the operational environment. Solzet does not implement finance systems; the ledger is the attribution source the finance team's own reporting reads.
We recommend the right solution - whether that's Microsoft Dynamics 365, Power Platform, or a custom-built CRM. Some businesses need the Microsoft ecosystem. Others need full control without licensing. We deliver both. Where the service operation needs this kind of time-aware attribution everywhere, and Microsoft licensing does not fit, a custom-built CRM on React, Node.js, PostgreSQL or .NET can make append-only history part of the data model from the start.
Should attribution history live in Dynamics 365 or a custom-built CRM?
Can afford licensing and want the Microsoft ecosystem
Dynamics 365
Microsoft 365, Teams and Outlook integration, a mature partner ecosystem, Copilot, and apps for sales, service and field operations that are configured rather than built.
Need full control and zero licensing
Custom CRM
A CRM built on React, Node.js, PostgreSQL or .NET that you own outright: your data model, your hosting, no per-user subscription, and features shaped exactly to your process.
Not sure which fits
We help you decide
A short discovery weighs licensing budget, process complexity, integrations and long-term ownership, then recommends one path. We deliver both, so the recommendation has no reason to lean.
How does Solzet help fix double counting in case hierarchies?
We start by reproducing the double count with finance: one reparented child case, the reports that disagree and the snapshots involved. Then we design the ledger table, build and test the plugin steps and the blocking rules, backfill history from audit data in a sandbox with finance checking a closed month, and move finance reporting onto ledger periods with a reconciliation check on close. It is a contained piece of work with a clear finish: the same period reported twice returns the same numbers.
The work is done by senior consultants and full-stack developers delivering remotely from Yerevan, Armenia, with 8+ years of Dynamics 365 Customer Engagement and Power Platform work, directly for your team or white-label for Microsoft partners. Wider case management, routing and entitlement design sits with our Dynamics 365 Customer Service implementation service.
What do people ask us?
Why do our revenue reports double count when a child case is moved to another parent?
Because the parent case lookup holds only the current parent. Reports that group by it move the child's whole history to the new parent, while stored rollups, earlier exports and snapshots still count it under the old one, so totals combined across time include the child twice. An append-only allocation ledger that records attribution with effective dates removes the problem.
Can a Power Automate flow write the allocation ledger in real time?
Not strictly. A cloud flow on the Dataverse trigger runs asynchronously after the change is committed, may run late or out of order, and does not receive the previous parent value. A synchronous plugin on Update of the case with a pre-image writes the ledger in the same transaction. A flow can be acceptable at low volume if it is idempotent and reconciled.
How do I stop anyone updating or deleting ledger rows in Dataverse?
Give roles Create and Read on the ledger table but no Write or Delete, and register synchronous pre-operation plugin steps on Update and Delete of the table that refuse every caller, which also stops System Administrators and bulk delete jobs. Restrict deleting cases that have ledger rows, and make corrections as new reversal and allocation rows.
Can we rebuild past allocations from Dynamics 365 audit history?
Yes, where auditing was enabled on the case table and the parent case column for the period and retention has not removed the records. Each audited change gives the old and new parent and the time, which becomes a reversal and an allocation row. Where history is missing, record the assumption on backfill rows and agree it with finance.
Should the ledger store effective-to dates?
Store only effective from and derive effective to in the reporting layer from the next reversal for the same child and parent. Writing effective to back onto an existing row would be an update, which breaks the append-only rule. Signed allocation shares also let you check that each child belongs to exactly one parent at any date.
How should finance report revenue by parent case after this change?
Join each revenue, time or cost entry on a child case to the ledger period that contains the entry date and report it under that period's parent. Do not use the live parent lookup or stored rollups for finance reporting, and reconcile each period so total revenue across parents equals total revenue across cases.
Do stored rollup columns on the parent case cause the double count?
They contribute. Rollup columns are recalculated by a system job, so for a time the old parent's stored total still includes a child that has already moved and the new parent's total may include it too. Keep rollups for agents as current indicators and never invoice or report closed periods from them.
Where should you go next?
Link multiple products to one case
The case modelling guide: native N:N, Connections or a junction table, with reporting and audit trade offs.
Dynamics 365 Customer Service implementation
Case management, routing, SLAs and entitlements designed by senior consultants.
Dynamics 365 audit trails
Enabling and scoping auditing, retention, retrieving change history and documenting gaps.
Dataverse data model remediation
When hierarchies, duplicates and rollups produce numbers finance does not trust.
Unit testing Dynamics 365 plugins
Testing plugin logic like the ledger writer without a heavy framework.
Custom CRM Development
Applications on React, Node.js, PostgreSQL and .NET for organizations that need full control without Microsoft licensing.
Which solution is right for your business?
Tell us what you need. A senior consultant replies within one business day with a recommendation - Dynamics 365, Power Platform, or a custom-built CRM - not a sales script.