Classifications

TL;DR

The flags CCC assigns to each transaction and line, what they are for, and where their per-column meaning lives.

Classification is the second step of the pipeline. It reads an imported transaction and writes flags describing what kind of transaction it is, so that calculation rules can filter on business meaning rather than on raw upstream fields.

Two levels

Flags are written at two levels and they answer different questions.

Header flags describe the transaction as a whole: does it contain a bundle at all, which payment method was used, which source and medium brought it in, how many days after the promised date it shipped.

Line flags describe one line: is this line part of a bundle, is it an engraving, is it a private label item.

They live in <type>classifications and <type>lineclassifications tables, one pair per transaction type. A header flag counting something and a line flag marking membership often come in pairs, so a transaction can say "contains two bundles" while its lines say which ones.

What the flags cover

Broadly: promotional constructions such as bundles, combideals and aftikker; product properties such as engraving and private label; pricing such as from-price; commercial identity such as the commercial id that survives a localised SKU; timing such as days after the promised date and days after the sales order; acquisition such as payment method and source and medium; and order shape such as order intake and double orders.

Per-column meaning is not repeated here. It lives at /dictionary/classifications, which generates its matrix from the live column list and joins authored descriptions to it. That view cannot drift from the schema; a prose copy of it would.

The dictionary view also shows which transaction types carry which flag, which is often the question being asked: not what a flag means, but whether it exists on credit memos too.

The three commercial constructions

Three flags name something the shop sells rather than something the data looks like, so what they are is a commercial fact and how they are detected is a technical one. Both are below, because knowing only the detection rule leads to reading the flag wrong.

Aftikker

A single item sold off at a descending price because it is not sellable as new. An opened package, a failed engraving, or a product with traces of use because it was tested. The shop sells these as Countdown deals: the price drops every hour in set increments for up to fourteen days and then sits at its minimum. Each item is listed individually even where several of the same product exist, because each one is a different item in a different condition.

The Dutch name is what the code uses, and it is what the ledger uses too: 71110 Cost of Aftikkers is an account in the cogs group, so the markdown lands on cost of goods rather than on revenue.

Detected on a non-inventory line whose SKU is UNKNOWNID and whose memo contains XX. The header flag is set on the transaction as a whole, and it carries over onto a return authorization so a returned aftikker is still recognisable as one.

Read it as a condition, not as a promotion. Margin on an aftikker is low by design: the price fell on purpose because the item could not be sold at full price, so a low CM2 on one is the construction working.

Combideal

A second product offered at a discount when bought together with the one the customer is viewing. A product page carries a Recommended combi-deals block: each entry shows the second product with its normal price struck through, a percentage off and the amount saved.

Detected per sales order line, on a line whose memo contains **. The header flag says the transaction contains at least one, and combideals counts how many, so an order can say it carries two while its lines say which.

The discount is on the second product, not on the first. So an order with a combideal has one line at full margin and one at reduced margin, and averaging across the order hides that.

Order intake

Whether the order counts as one the business actually took. An order that was placed but never converts is not intake, and is what the business reads as lost.

The rule is the sales channel's intake method, which is on the channel and editable at /configuration/saleschannels:

Intake methodAn order counts
allOn arrival. orderintake is 1 for every order on that channel
paidOnce a payment has come in against it

A zero-value order on a paid channel is the exception: it never receives a payment, so waiting for one would drop it for good, and it counts on arrival like an all order.

Intake overrules doubleness. Where orderintake is 1 the doubleorder flag is forced to 0, so the two are never both set on one order and a count of either is unambiguous.

See sales channels for where the intake method is set and why it differs per channel.

Why a new column appears without being asked for

Two pieces of plumbing pull in opposite directions, and both are deliberate.

On the transaction screens, a new column does not appear. The list getters name their columns explicitly and the templates list explicit headers, so a column has to be added in both places to be visible. The exception is a classification table: those are selected with a star, so a new classification column reaches the JSON result automatically. The table template still needs a header naming that field before it is visible.

In the rule builder, a new column appears immediately. Filter fields are read from a live SELECT *, so every added column becomes a filter with a dropdown of its distinct values. Free text, reference numbers, dates and opaque identifiers should be excluded there; flags and low-cardinality dimensions should not.

Adding a classification column therefore makes it available as a rule filter without any further step.

Evidence

Two-level model and the table naming from AGENTS.md, The application. Flag families from app/models/classify.php and the keys in config/dictionary/transactionclassifications.json. The star-select exception and the rule-builder field discovery from AGENTS.md, What fails silently. Basis: code-checked against the current implementation. The commercial meaning of aftikker and combideal is from the data owner and from the public product pages on knivesandtools.com, the Countdown deals category page and the Recommended combi-deals block on any product page. The 71110 Cost of Aftikkers account and its cogs group are in the accounts and accountgroups tables. Detection rules from classify->classifysalesorder(), and the intake method and its zero-value exception from the same method and saleschannels->getintakemethods().