Skip to content

crm_opportunity_line_item.line_number has no writer, yet the billing hand-off payload ships it on every won deal #1828

Description

@os-steve

Found while working #1826 (the field-no-consumers display gaps). ⛔ Out of that card's scope — filed rather than fixed there.

The finding

crm_opportunity_line_item.line_number is Field.number({ readonly: true }). Grep the tree for every site that names it, and nothing writes it:

  • no hook — opportunity_line_item.hook.ts and the shared _line-item-price-fill.ts never touch it;
  • no flow node, no action;
  • readonly: true means a user cannot type it either, and since @objectstack/objectql@17.4.0 a static readonly field is stripped from a non-system caller's INSERT as well, so an ordinary rep creating a line cannot seed it even by accident.

The only values that exist are the 19 rows in src/data/, which the seed loader writes as a system caller. test/seed-consistency.test.ts pins those to be 1..n within each parent — a pin on the seed fixture, not evidence of a runtime writer.

Why it matters beyond a blank column

src/flows/billing-handoff.flow.ts puts line_number in LINE_ITEM_FIELDS, the projection that composes the billing payload for both closed_won and contract-activated hand-offs. That payload is described in content/docs/revenue/billing-handoff.mdx (three locales) with "line_number": 1 in the worked example.

So the app hands the billing system a field that is null on every line item created in the product, and non-null only on demo seed rows. The docblock on that flow is explicit that the payload "IS the contract with the billing system" and is authored field-by-field so it cannot silently drift — this is a drift it did not catch, because the field is declared and merely never filled.

The decision this needs

Two coherent answers, and it is a product call, not a mechanical one:

  1. Give the ordinal a writer. A per-parent assigner on insert, in the shape _line-item-price-fill.ts already establishes for this pair of objects. Because the field is readonly, the writer has to be a system-context path — which puts it under AGENTS.md house rule 9 (elevate as little as possible), the same question Migrate this repo onto the @objectstack/* 17.4.0 line — the whole-line pin bump, the spec rename via the platform's own codemod, and the two contract changes it surfaces (epic #1579) #1807 settled for crm_campaign_member.added_date. A line then carries a stable ordinal and the billing payload becomes truthful.
  2. Retire the ordinal. If the ordering a reader wants is the related list's own sort, the column is a promise nothing keeps. Removal has to clean the declaration plus its carrier sites and drop the key from LINE_ITEM_FIELDS and the three billing-handoff doc pages.

⚠️ Note the twin: crm_quote_line_item.line_number has exactly the same gap, and is the reason that field is reported standing on #1826 rather than given a view column — a column there would render blank on every row a user creates. Whichever answer is chosen should cover both objects, since the two line-item objects deliberately mirror each other.

Refs: #1826 · #1807 (the readonly + elevation precedent) · #1667.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    metadataDeclarative metadata — schema, security posture, UI surfaces

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions