Customer merges and splits
What happens to history when two customers become one or one becomes two, and which identifier survives.
Because a customer is a computed group rather than a record, it can change shape. Two customers can become one when a new order links them, and one can become two when a link is removed. Both move history.
A merge
A new order that shares a key with two existing groups joins them. From that point the combined group has one identity and one order sequence covering all of it.
What moves:
- Order numbering is recomputed across the whole group. An order that was someone's first can become their third, without that order changing at all.
- New-customer counts move in the past. A customer counted as new in March can stop being new in March once a merge reveals an earlier order.
- Lives recombine. Two customers each with one life can become one customer with one life, or with two, depending on the gap between their orders.
Historic figures move when a merge happens, and that is accepted. A metric computed from CCC is a reconstruction of what is understood today, not a snapshot of what was believed at the time. A report run in March and the same report run in June can legitimately differ, using the current reconstruction. Consumers needing a reproducible published figure must keep their own dated snapshot and distinguish it from a later reconstruction.
Worked example
Two groups, before the merge. Every figure here is synthetic.
| Customer | Orders | Order dates | Sequence | New customer in |
|---|---|---|---|---|
| 4001 | 1 | 2025-02-10 | 1 | February 2025 |
| 4002 | 2 | 2025-06-03, 2025-09-18 | 1, 2 | June 2025 |
In November a fourth order arrives carrying the email of 4001 and the delivery address of 4002. All four orders become one customer.
| Customer | Orders | Order dates | Sequence |
|---|---|---|---|
| 4001 | 4 | 2025-02-10, 2025-06-03, 2025-09-18, 2025-11-04 | 1, 2, 3, 4 |
What moved, and what a report notices:
- The June order is no longer a first order. It is sequence 2. Anything that counted it as a new customer in June now counts it as a returning one.
- New customers in June falls by one, months after June closed. Nothing about the June order changed; what changed is that CCC now knows an earlier order existed.
- New customers in February is unchanged. The earliest order in the group keeps sequence 1, so the group's "new" moment moves backwards to February, where it was already counted.
- Lives recombine. With a one year definition, the four orders are within a year of each other, so what was two customers with one life each becomes one customer with one life. A customer-life count over 2025 falls by one for the same reason.
- Customer 4002 still resolves. Its row stays, with
mergedintopointing at 4001, so a report holding 4002 lands on the right group. - Margin does not move. CM1 and CM2 are per order and per transaction; nothing in the margin chain reads the customer grouping. Only the customer dimension moves.
A split renumbers each resulting group independently. Which order becomes first depends on the remaining links; removing one link does not automatically restore the earlier groups.
A split
Removing a key splits a group only when no other path connects the resulting parts. Excluding an office address does not break an account or entity match.
The group containing an existing customer's anchor order keeps that id. The anchor is the lowest currently assigned sales order id, not the earliest transaction date. If a group contains several existing anchors, it keeps the lowest customer id and absorbs the others. A group with no anchor receives a new id; splitfrom records the lowest previous customer id among its orders.
Continue the synthetic example: suppose the February order has the lowest order id, and removing the only address link separates the June and September orders from February and November.
| Resulting customer | Orders | Sequence | Identity record |
|---|---|---|---|
| 4001 | February, November | 1, 2 | Keeps the anchor |
| New id, for example 5000 | June, September | 1, 2 | splitfrom = 4001 |
| 4002 | No current assignment | None | Remains absorbed into 4001 |
The June order becomes a first order again. 4002 is not resurrected. Resolve a particular order through customersalesorders.salesorder_id when you need its current group; following an old customer id alone cannot tell you which side of a later split that order moved to.
An identifier always resolves, and the pointer can be more than one hop
No customer is ever deleted. A merge leaves the absorbed row in place with mergedinto pointing at the survivor, so an id stored years ago still resolves. A split records splitfrom.
Follow the pointer in a loop, not once. A customer absorbed last year can point at one that is absorbed this year, because the job computes anchors only for customers that are still live and so never rewrites an already-absorbed row's pointer. A row with mergedinto = 0 is the live customer and ends the walk.
app/models/customers.php::getcustomer() is that walk in PHP, with a cycle guard, for anything inside this repository. A consumer reading over SQL has to implement the same loop, and a consumer that follows one hop will land on a row that was itself absorbed.
So a stored customer id is durable as a starting point, not as an answer.
What to do before acting on a grouping
- Look at what actually connects the orders, not the connection you assume.
- If a split is wanted, establish every path between the orders first.
- Expect historic figures to move, and say so to whoever consumes them.
- Do not treat a customer id captured elsewhere as authoritative.
Related
Evidence
Grouping and anchor selection from app/models/customers.php::setgroups() and the anchor ordering at customers.php:778. Renumbering from customers.php:1190-1192. The resolution contract, the multi-hop chain and its cycle guard from customers.php::getcustomer(), whose own comment states why a chain forms across runs; mergedinto and splitfrom from the customers schema in install.php. Margin not depending on the grouping from AGENTS.md, What fails silently. Basis: code-checked against the current implementation.
Historic metric movement after a merge is understood and accepted by the data owner. The worked example is synthetic, as the authoring contract requires.
- Type
concept - Status
active - Updated
2026-09-14 - Created
2026-09-13