Repository navigation
MCP slice 5: read tools — customers, orders, balances, with money rendered at the boundary #808
Description
Slice 5 of #789. Blocked on #788 and slice 3 (#806). Can run in parallel with slice 4 (#807).
Read tools for the sales side: customers, orders, payments / balances.
Money never crosses as minor units
Cluckwork stores long minor units plus a per-account DefaultCurrencyMinorUnit (2 for PHP, 0 for JPY, 3 for KWD). Handing a model {"minorUnits": 2825000, "minorUnit": 2} invites "₱2,825,000", a 100× error in a number a farmer acts on.
Render once, at the boundary, as Money(Amount, Currency) with a decimal string. The minor-unit representation is an internal decision. Re-exporting it leaks it into every model prompt that ever reads a Cluckwork amount. A guard walks tool reply types and fails any long …MinorUnits on the surface.
The money tier is decided live, not re-derived
list_orders fuses list and get, and decides SettlementScope by evaluating AuthPolicies.SalesAccess through the real IAuthorizationService. It never re-implements the tier. Keep the in-body money gate even though the coarse policy covers it today. A later widening of that policy must not silently widen the money question.
Bounded results
PaymentRepository.ListCustomerBalancesAsync takes no paging arguments and materializes every matching grouped row. The order cap lives in SaleEndpoints, not the repository. So calling the repository directly inherits no bound, and an unbounded read_receivables on a large farm serializes the whole book into a model's context.
This needs a paged balance query. The guard must assert rows consumed from the database, not items returned. The SqlCaptureInterceptor in ReportQueryBoundingTests is the right harness shape. It was written for #311, which had the identical "loaded the whole ledger, walked it in memory" defect. But it records (Sql, Parameters) only, so a page-fetch loop emits perfectly paged SQL and passes. Wrap the DbDataReader and count reads.
Design
docs/plans/770-mcp-server/, guards rows 28, 31, 31b.
Activity
mforce commentedon Oct 6, 2026
Scope added in the 2026-10-06 re-evaluation:
ICommerceModule.ListCustomerBalancesAsynchas no paging, andPaymentRepository.cs:43-63loads both grouped queries into memory before joining. Guards 31 and 31b need a bounded query, which means a contract method returning a DTO page and a repository change. The [B] #514 slice 5: seam-surface guard — no persistence type crosses a seam #847 seam guard allows that.SalesOrderDetailsalready exposesMoney.PaymentDetailsandCustomerBalancereturn rawlongminor units. Read currency and minor unit throughIFarmModule.GetSettingsAsync, asPaymentEndpoints.cs:130-138does.Customers and orders are covered by
ICommerceModule.SearchCustomersAsync,GetCustomerAsync,ListSalesOrdersAsyncandGetSalesOrderAsync.SettlementScopeis now inApplication/Modules/Commerce/Contracts/. Decide #792 before this ships, becauseCustomerDetailsincludes the free-textNote.mforce commentedon Oct 10, 2026
Re-check after milestone 10, 2026-10-10.
PaymentDetailsalready carriesCurrencyCodeandCurrencyMinorUnit(ICommerceModule.cs:91-94). OnlyCustomerBalancelacks currency, soIFarmModule.GetSettingsAsyncis needed for balances alone.farm:readas well as its role policy.mforce commentedon Oct 10, 2026
Decided on #792 (2026-10-10): phase 1 tools return no farm-entered free text. Leave
CustomerDetails.Noteand every other note or reference text field out of this slice's tool results, and list the omitted fields in the PR. #808 is no longer blocked.test(mcp): list #808's mutants in the MCP mutation check