Customer sequence and life

TL;DR

How CCC numbers a customer's orders, which orders do not count, and how a new life is detected.

Once orders are grouped into a customer, CCC numbers them. The number answers "was this their first order, their second, their tenth", which is what every new-versus-returning metric rests on.

How the numbering works

Orders within a customer are numbered by transaction date, then sales order id as a tiebreak.

That basis matters: it is trandate, not createddate. Transaction listing screens default to a creation-date window, so a screen and a sequence can legitimately disagree about which order came first. See dates, currency and signs.

Which orders do not count

Cancelled and closed orders are excluded from the numbering. Both statuses mean the order did not happen: C for cancelled, H for closed, where closed means written off rather than shipped.

A surface that means "cancelled" uses both codes. Treating C alone as cancelled silently counts every written-off order as real.

Those orders keep their membership in the customer group. After a completed reconcile they carry zero for every sequence, life and new flag. A newly inserted row also starts at zero before the numbering pass finishes, so check reconcile completion before interpreting a zero as cancelled.

A metric that counts rows in customersalesorders rather than numbered orders therefore counts cancelled and closed orders as real. Filter on customerordersequence > 0.

Zero and NULL are different here. After numbering finishes, a zero sequence means an order CCC deliberately did not count. A NULL in the comparison table means the old pipeline never numbered that order at all. Reading either as "no value" produces a wrong count.

Lives

A customer can lapse and come back, and CCC detects that as a new life. Sequence is numbered separately within each life, while the overall customer order sequence continues increasing.

An uncounted order does not bridge a gap. A cancelled order placed in the middle of a dormant stretch does not keep the life alive, because it did not happen.

Two lives run in parallel, on a one year and a two year gap. A new life begins when an order falls more than that long after the previous one. The schema names them explicitly: customerlife_1y, customerlifesequence_1y and newlife_1y, with a _2y set beside them.

So a customer can be new under one definition and returning under the other. Three columns answer "is this new", and they answer differently:

ColumnMeans
newcustomerThe customer's first counted order, over their whole history
newlife_1yThe first counted order of a life, where a gap over one year starts a new one
newlife_2yThe same, on a two year gap

A figure quoted as "new customers" without naming the column is ambiguous, and the three do not agree.

A life is a run of orders with no gap strictly greater than the threshold since the previous counted order, and the life sequence restarts at 1 whenever a gap breaks the run. Both readings are stored because both are wanted and neither is derivable from the other.

Worked example

Order dateGap since previousseqlife_1ylifeseq_1ylife_2ylifeseq_2y
2021-05-05first11111
2021-10-055 months21212
2023-05-0519 months32113
2026-01-0532 months43121

On row three a 19 month gap breaks the one year run and not the two year one, so the two pairs report different lives for the same order.

Because the test is strictly greater, an order exactly one year later continues the run. Leap years follow the calendar, so a year after 2020-02-29 is 2021-02-28. On every row customerlife_2y is less than or equal to customerlife_1y.

Asking for the three states

StateTest
Newnewcustomer = 1
Reactivatednewlife_1y = 1 AND customerlife_1y > 1
Retainednewlife_1y = 0 AND customerordersequence > 0

Substitute _2y throughout for the two year reading. The customerordersequence > 0 in the retained test is what excludes the uncounted orders.

Reading a new-customer figure

Before quoting one, settle three things: which life definition, which date basis, and whether uncounted orders are excluded from the denominator as well as the numerator. Two people can compute "new customers last month" from the same tables and disagree entirely.

Boundary examples

These examples are synthetic and concern counted orders belonging to one customer.

OrdersResult
2024-05-05 then 2025-05-05Same one-year life: the gap must be strictly greater than a calendar year
2024-05-05 then 2025-05-06New one-year life, same two-year life
Two orders on one date, ids 100 and 200Id 100 is numbered first
Counted order, then C/H order, then a counted order after a long gapThe C/H order remains a member but neither consumes a number nor keeps the life open
Only C/H ordersA customer group exists but has no counted first order or life

newlife_1y = 1 includes the first-ever order as well as reactivations. To count reactivations, require customerlife_1y > 1 too; use the analogous pair for two-year lives. Overall customerordersequence does not restart when a life starts. Only customerlifesequence_1y or _2y restarts for its respective life.

Evidence

Numbering basis from app/models/customers.php:1190-1192, ROW_NUMBER() OVER (PARTITION BY n.customer_id, n.life1 ORDER BY n.trandate ASC, n.salesorder_id ASC), with a second life partition alongside it. Uncounted statuses from customers.php::$uncountedstatuses and the exclusion at customers.php:1221. Status meanings from config/reference/ns_transactionstatusses.json. Life accumulation from the windowed sum at customers.php:1200. Basis: code-checked against the current implementation.

The two windows are INTERVAL 1 YEAR and INTERVAL 2 YEAR in app/models/customers.php, which is what customerlife_1y and customerlife_2y are named after. They are deliberately not configurable: changing a window would make the column name lie.

About this document
  • Type
    concept
  • Status
    active
  • Updated
    2026-09-14
  • Created
    2026-09-13