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.
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
longminor units plus a per-accountDefaultCurrencyMinorUnit(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 anylong …MinorUnitson the surface.The money tier is decided live, not re-derived
list_ordersfuses list and get, and decidesSettlementScopeby evaluatingAuthPolicies.SalesAccessthrough the realIAuthorizationService. 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.ListCustomerBalancesAsynctakes no paging arguments and materializes every matching grouped row. The order cap lives inSaleEndpoints, not the repository. So calling the repository directly inherits no bound, and an unboundedread_receivableson 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
SqlCaptureInterceptorinReportQueryBoundingTestsis 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 theDbDataReaderand count reads.Design
docs/plans/770-mcp-server/, guards rows 28, 31, 31b.