Skip to content

Reports: discount totals per salesperson and per customer, in report and CSV export #725

Description

@mforce

Important

Amended 2026-09-08 after reading the source. Part of the scope below is not buildable as written. The body is left as filed for history; see this comment for what actually holds.

Part of #719. Blocked by #720 (needs the list-price snapshot).

What

Discount totals in the sales report and the CSV export, broken down per
salesperson
and per customer over a date range.

Why this is the slice that matters most

The badges in #723 and #724 are lagging indicators — they only tell you something
if a person opens the screen. The discounts that opened #719 went unnoticed for
months precisely because nobody was opening those screens one order at a time.

A number in the weekly report is what would have surfaced it. This slice is the
detection mechanism; the badges are the explanation once you go looking.

Scope

  • Extend SalesSummary (src/Cluckwork.Application/Features/Reports/IReportQueries.cs:43)
    with discount totals for the period.
  • Discount columns on the sales export dataset (ExportEndpoints.cs /
    CsvExport.cs).
  • Per-salesperson breakdown: total discount given, order count, average discount
    percent.
  • Per-customer breakdown: same shape.
  • Discount reason (Sales: require a discount reason when confirming a below-list order #721) as a column, so the export answers why and not only
    how much.

Rules

  • The period's figures cover orders whose order date falls in range, matching
    the existing SalesSummary contract — do not invent a second date convention
    in the same record.
  • Orders with no list-price snapshot (pre-Sales: snapshot the list price on the order line (discount foundation) #720) are excluded from discount totals
    and counted separately as unknown, not folded in as zero. A silent zero
    would make the first months after launch look clean.
  • Money stays in minor units through the query and formats at the edge, like the
    rest of SalesSummary.

Acceptance

Repo rules

  • Export dataset registry — if the export datasets are walked by a test,
    find its guards by grepping the registry's readers, not by recall
    (grep -rn "exports.Datasets\|GetDataset" tests/).
  • i18n — report labels and export column headers in all three locales.

Activity

  1. added this to the Phase 1.5 — Hardening milestone on Sep 8, 2026
  2. added
    sliceThin vertical work item
    epic-719Discount visibility and control on sales (epic #719)
    area:apiAPI/endpoint layer
    on Sep 8, 2026
  3. mforce commented on Sep 8, 2026

    @mforce
    OwnerAuthor

    Mockup

    #725 Report

    Amendment: the per-salesperson breakdown in this issue's scope cannot be built

    SalesOrders has 11 columns and none of them records a creating user. Verified at both the C# and SQL layers: Id, ReferenceNumber, CustomerId, Status, OrderDate, TotalMinorUnits, TotalCurrencyCode, TotalCurrencyMinorUnit, VoidReason, Version, AccountId. grep -rn "CreatedBy" src/ returns only response DTOs and EntityProvenance, every one audit-derived, none a stored column.

    Three options, none free:

    1. Add a CreatedByUserId column — its own migration and schema-docs cycle, and null for every existing order with no backfill except the audit trail.
    2. Join AuditEvents on Action = 'SalesOrder.Create' — AuditEventConfiguration declares two indexes, (AccountId, OccurredAtUtc) and (AccountId, EntityId), neither supporting a filter on Action. Worse, orders written before Show "created by / when" and "last changed by / when" inline on record pages #494 have no creation event and never get one, so the column is permanently blank for historical rows with no way to distinguish unknown from nobody.
    3. Cut the per-salesperson half from this slice.

    My recommendation is 3 now and 1 as a separate slice, because option 2 produces a report that is silently wrong about exactly the history the owner wants to audit.

    The per-customer half is genuinely easy

    SalesOrder.CustomerId exists, ReportQueries.cs is plain LINQ-to-EF (265 lines, no raw SQL), and ExpenseCategoryTotal/GetExpensesAsync is a working template for group-by-an-id-and-look-up-names.

    Second correction: "the CSV export" is two different things

    The sales report has no CSV download. ReportEndpoints.cs maps four MapGets that all return Results.Ok(<record>). The repo's CSV machinery is a separate feature, the 20-dataset account backup, whose sales-orders/sales-order-items datasets are hand-written column lists in ExportQueries.cs. So this issue's "CSV export" is either two lines in that dataset, or new report-download machinery with no precedent to copy. Roughly a 10× difference, and the issue does not say which.

    Also worth pricing: MaxRangeDays = 366 plus the per-account report concurrency cap (#311/#545) makes a new grouped query a real cost, not a free GroupBy.

  4. added
    size:LSeveral days; wide blast radius or unresolved scope
    needs-scopeScope contradicts the code; decide before scheduling
    on Sep 8, 2026
  5. mforce commented on Sep 13, 2026

    @mforce
    OwnerAuthor

    Closing as won't-do — owner decision, 2026-09-13

    Closed during the issue cleanup as part of winding up epic #719. Not deferred — decided against.

    Why this is defensible now, when it would not have been on 2026-09-08

    This issue's body argues it is "the slice that matters most", because the badges in #723/#724 are
    lagging indicators that only speak when somebody opens a screen, and the discounts that opened #719
    went unnoticed for months precisely because nobody did.

    That argument was correct when it was written and has since been overtaken by two slices that
    moved the control from detection to prevention:

    So the failure mode this slice was the answer to — discounts happening quietly for months — is now
    blocked at the point of entry rather than discovered in a weekly report. A report is still the better
    instrument for patterns, but it is no longer the only thing standing between the farm and an
    unnoticed discount.

    And half of it was unbuildable as filed

    Per the amendment above, verified at both the C# and SQL layers: SalesOrders has 11 columns and
    none of them records a creating user.
    The per-salesperson breakdown — the half that answers "who
    is giving these away" — cannot be built without first adding an actor column to the order, writing
    its migration, and backfilling or declaring the pre-existing rows unattributable. That is a separate
    slice with its own decisions, not a reporting change.

    What is consequently NOT available, stated plainly

    What would reopen this

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

    area:apiAPI/endpoint layerarea:frontendReact/Vite web clientepic-719Discount visibility and control on sales (epic #719)needs-scopeScope contradicts the code; decide before schedulingsize:LSeveral days; wide blast radius or unresolved scopesliceThin vertical work item

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions