RealmPath
untyped stringRealmPath is this realm's own path, used to build Render links.
Value
"/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/receivables"
InvoicesPath
untyped stringInvoicesPath is the receivables source every figure here is read from. It is taken BY VALUE from that realm's own exported constant rather than typed out again, so this is the compile-time pin itself and not a second copy that could drift. The convention is r/statements' BooksPath.
Value
"/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoices"
RateScale
int64RateScale is the unit of a loss rate, re-exported by value from [p/aging](/p/aging): a rate of 2500 is 25%. Re-exported because every rate in this realm's API is in these units and a caller should not have to find the other package to learn the denominator.
Value
10000
MaxIDsPerCall
untyped bigintMaxIDsPerCall caps how many invoice ids one [Publish](#Publish) may name, and it is r/invoices' own per-issuer storage cap taken by value. That cap is the right bound and it is worth saying why, because the two candidate bounds differ by a factor of sixteen. [p/aging](/p/aging) accepts up to \`aging.MaxObligations\` (1024) obligations and offers a chunking protocol for more; r/invoices stores at most \`MaxRecordsPerIssuer\` (64) records of any status per issuer. An issuer therefore cannot have more than 64 ageable invoices in existence at one height, so \*\*this product never chunks\*\*, and p/aging's documented chunk-flooring error — where the sum of per-chunk provisions is not the provision of the whole — cannot arise here. The two caps were chosen independently and it is their inequality, not any code below, that makes the composition exact.
Value
(64 <untyped> bigint)
ReportsAreUncapped
untyped boolReportsAreUncapped records a decision by its absence, because every other realm in this portfolio caps its per-owner state and a reader will look for the cap here. There is none, for r/statements' reasons, which transfer unchanged: keys are per issuer so an issuer can fill only their own history; nothing iterates a history except Render, which reads the newest [RenderReportLimit](#RenderReportLimit) by direct key lookup; and there is no engine cap above this realm to mirror. Each publication costs its own gas and storage deposit. There is also no retraction, and that is the same decision from the other side. A \`Retract\` would split the sequence allocator from the live count — two numbers that can disagree — in exchange for letting an issuer delete a report they regret. A frozen record that can be deleted is not frozen, and the realm has no cap an issuer could be bricked against.
Value
true
RenderReportLimit
untyped bigintRenderReportLimit caps how many of an issuer's reports one Render call lists, which is what keeps an issuer's page cost flat while histories are unbounded.
Value
(20 <untyped> bigint)
report
typereport is one frozen aging report. It stores the [aging.Schedule](/gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/aging#Schedule) that priced it, WHOLE, and that field is the point of this struct. p/aging's own doc warns that a Report carries no reference to its Schedule, so two schedules with the same bucket count will accept each other's reports and silently price them on the wrong rates with no error. Replacing a schedule is legitimate here — rates change — so the hazard is live rather than theoretical, and snapshotting removes the opportunity structurally: no bare report ever persists, and a later [SetSchedule](#SetSchedule) cannot retro-price anything already published. The priced columns are stored rather than re-derived on read, because re-deriving would mean calling into p/aging from every accessor and a view that recomputes is a second place for the arithmetic to live.
Value
receivables.report
reports
*v0.Treereports maps "\<issuer>|\<padded seq>" to \*report. The pointer never leaves this file. Every accessor copies scalars out, so no caller receives a handle onto stored state — the discipline r/books and r/statements both apply.
- OID
- 0f8541…df1a:4
reports details
schedules
*v0.Treeschedules maps an issuer's address string to their current [aging.Schedule](/gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/aging#Schedule). One per issuer; [SetSchedule](#SetSchedule) replaces it.
- OID
- 0f8541…df1a:7
schedules details
counts
*v0.Treecounts maps an issuer's address string to how many reports they have published. It is the sequence allocator as well as the count, and the two can never disagree because nothing deletes a report.
- OID
- 0f8541…df1a:10
counts details
repCount
intrepCount is the global total, maintained alongside the tree so the index page never iterates.
Value
0
refuseCoins
func()refuseCoins panics if a user call arrives carrying coins. Adopted from r/books and r/statements, which face the identical slot: no withdrawal path, so accepted coins are stranded permanently. IsUserCall rather than IsUser, and the reason is not the canonical payment-guard one. That advice is for a realm that ACCEPTS payment, where IsUser is dangerously broad. This guard REFUSES, so a broader predicate would refuse more and "more conservative" is the wrong frame. The real reason is that OriginSend is a fact about the TRANSACTION and is a faithful receipt only on the direct MsgCall path; pairing it with IsUser adds a false refusal mode — a panic on a call that brought this realm nothing — and buys no protection. The residual is stated rather than guarded: coins can still reach this address through an intermediary's explicit banker send, which no predicate here can see. What makes that survivable is the absence of any consumption path — nothing in this realm can spend what arrives. The IsCurrent check is a no-op at today's call sites, where the forwarded cur is runtime-current by construction. It is here because this realm is IMMUTABLE after addpkg, so a future second caller cannot be made safe later, and a helper reading Previous() off an unchecked realm parameter is a designation-forgery shape whether or not it is reachable today.
- OID
- 0f8541…df1a:13
refuseCoins details
parseInt64List
func(s string, max int, what string) []int64parseInt64List parses a comma-separated list of decimal int64s, rejecting an empty list, a blank element, and more than max elements. The element cap is enforced BEFORE the slice grows, so a caller cannot make this realm allocate a long list on the way to a refusal. The \`what\` word is only for the panic message.
- OID
- 0f8541…df1a:15
parseInt64List details
parseIDs
func(ids string) ([]int64, int)parseIDs parses the invoice-id list, returning it sorted ascending with duplicates removed, and how many duplicates were dropped. Deduplication is THIS realm's responsibility and that is explicit in p/aging's contract: an Obligation "carries no identity", so the same obligation passed twice is counted twice and the provision is double. A repeated id in a hand-written list of sixty-four is an ordinary mistake, and the resulting over-provision is exactly the error class nothing downstream can detect — the report would still cross-foot. Duplicates are DROPPED and COUNTED rather than refused, because the count appears in the coverage line and so the caller learns about it either way, and a refusal would make an issuer re-type a sixty-four-element list to fix a repeat the realm already understood. Insertion sort over at most MaxIDsPerCall elements, which is 64: O(n²) at n=64 is cheaper here than importing a sort.
- OID
- 0f8541…df1a:16
parseIDs details
validCode
func(s string) boolvalidCode reports whether a string is usable as a ledger account code in a [ProposedEntry](#ProposedEntry) leg. It is a WHITELIST — ASCII letters, digits, \`.\`, \`\_\` and \`-\` — and that choice is the guard, not a side effect of it. The legs format r/books' PostMulti accepts is \`code:side:amount\` joined by commas, so a code containing \`:\` or \`,\` would inject extra legs into an entry this realm presents as its own arithmetic: a caller could make a one-line provision read as a balanced three-line entry crediting an account of their choosing. A blacklist of those two bytes would close today's format and not the class. What this does NOT check is whether the code exists, or is the right kind, in any ledger. This realm reads no ledger and has no way to; the entry is a PROPOSAL and the posting realm validates its own chart.
- OID
- 0f8541…df1a:17
validCode details
formatInt64List
func(v []int64) stringformatInt64List renders a slice as the comma-separated form the setters accept, so a round trip through [ScheduleOf](#ScheduleOf) and [SetSchedule](#SetSchedule) is exact.
- OID
- 0f8541…df1a:18
formatInt64List details
SetSchedule
func(edges string, rates string) intSetSchedule registers or replaces the caller's own aging schedule and returns its bucket count. \`edges\` is a comma-separated list of block ages, positive and strictly increasing; \`rates\` is one more number than there are edges, each in \[0, RateScale] and non-decreasing. So SetSchedule("216000,432000,648000", "0,1000,2500,5000") reads as: nothing provisioned under 216,000 blocks past due, 10% from there to 432,000, 25% to 648,000, 50% beyond. Every constraint is p/aging's and is validated by \`aging.New\` rather than re-checked here; a second copy of somebody else's validation is a second place for it to drift. The owner is \`cur.Previous().Address()\` and there is no owner parameter, so authorization is structural rather than a check: there is no address to forge and no way to set a schedule for somebody else. REPLACEMENT IS SAFE and that is a property of \[report], not of this function: every published report snapshots the schedule that priced it, so a new schedule applies only to reports published after it. Nothing already frozen is re-priced.
- OID
- 0f8541…df1a:19
SetSchedule details
Publish
func(ids string) int64Publish freezes an aging report over the named invoices at the current height and returns its sequence number, which starts at 1 for each issuer. \`ids\` is a comma-separated list of the caller's own invoice ids in [r/invoices](/r/invoices), at most [MaxIDsPerCall](#MaxIDsPerCall) of them. The caller must have registered a schedule first. ### What each id is checked for {#hdr-What_each_id_is_checked_for} An id with no record for this issuer PANICS and fails the whole call. That is deliberate: a silently-dropped id is an under-report, and under-reporting is the one error this product cannot detect after the fact. The existence probe is a separate \`HasInvoice\` call rather than letting the next read panic, because r/invoices' own message is \`"unknown invoice"\` and does not say WHICH — unhelpful in a list of sixty-four. An id that exists but is not ageable is filtered, counted, and reported: - not \`open\` — settled or voided, so nothing is owed. Counted \`notOpen\`. - open with nothing outstanding. Counted \`zeroOut\`. - open, owed, but \`dueHeight > now\`. Counted \`notDue\`. The third one is not an optimisation. \`aging.ReportAt\` requires \`DueAt \<= now\` for EVERY obligation and fails the whole call on the first that is not, so a report over a list containing one current invoice would produce nothing at all. Pre-filtering here is what makes "age everything I have" a usable call, and the count is what stops the filter being invisible. ### The outstanding cross-check {#hdr-The_outstanding_cross_check} \`amount - paid\` is computed here and compared against r/invoices' \`Outstanding\`. State its reach exactly, because the flattering reading is available: both sides are that realm's own two numbers, so this is a CONSTANT TRUE against any disagreement in ITS arithmetic and must never be quoted as cross-checking it. What it catches is a transcription error in THIS realm — reading \`paid\` where \`amount\` belongs in a six-value return, or subtracting the wrong way round — which is the one place in this path such an error could happen, and an inflated outstanding is an over-provision that still cross-foots. ### The id list is not stored {#hdr-The_id_list_is_not_stored} A report keeps the figures and the coverage counts, not the ids. Storing 64 ids per report would roughly double the record for the sake of a \`Verify\` that could not mean much anyway: the invoices keep moving, so re-deriving would answer "do the figures still hold at today's height" and not "was this report right when made". r/statements documents the same distinction about its own Verify. The honest artifact is the frozen figure plus the coverage line.
- OID
- 0f8541…df1a:20
Publish details
HasSchedule
func(issuer .uverse.address) boolHasSchedule reports whether an issuer has registered a schedule, so a reader can distinguish that from the panic [ScheduleOf](#ScheduleOf) raises.
- OID
- 0f8541…df1a:21
HasSchedule details
ScheduleOf
func(issuer .uverse.address) (edges string, rates string)ScheduleOf returns an issuer's CURRENT schedule in the comma-separated form [SetSchedule](#SetSchedule) accepts, so the round trip is exact. This is the schedule that would price the issuer's NEXT report, not necessarily the one that priced their last. [ScheduleUsed](#ScheduleUsed) answers that.
- OID
- 0f8541…df1a:22
ScheduleOf details
Buckets
func(issuer .uverse.address) intBuckets is how many age bands an issuer's current schedule has.
- OID
- 0f8541…df1a:23
Buckets details
Count
func(issuer .uverse.address) intCount is how many reports an issuer has published. Zero for an address that never has, which is not an error.
- OID
- 0f8541…df1a:24
Count details
Latest
func(issuer .uverse.address) (int64, bool)Latest is an issuer's highest sequence number, and whether they have published at all. The second result matters: sequence numbers start at 1, so a zero first result means "never published" and never "report 0".
- OID
- 0f8541…df1a:25
Latest details
Total
func() intTotal is how many reports exist across every issuer. It is a count and nothing more; see the package doc on why there is no cross-issuer provision.
- OID
- 0f8541…df1a:26
Total details
Has
func(issuer .uverse.address, seq int64) boolHas reports whether a report exists, so a reader can distinguish it from the panics the accessors below raise for an unknown one.
- OID
- 0f8541…df1a:27
Has details
PublishedAt
func(issuer .uverse.address, seq int64) int64PublishedAt is the chain height a report was frozen at. It is also the \`now\` every age in it was measured against.
- OID
- 0f8541…df1a:28
PublishedAt details
Provision
func(issuer .uverse.address, seq int64) int64Provision is the allowance for credit losses a report concluded: the sum of the per-bucket column, each bucket floored.
- OID
- 0f8541…df1a:29
Provision details
Unpaid
func(issuer .uverse.address, seq int64) int64Unpaid is the total outstanding the report aged. It is the denominator the provision is a fraction of, and it covers ONLY the aged invoices — not the issuer's whole receivable book. See [Coverage](#Coverage).
- OID
- 0f8541…df1a:30
Unpaid details
Totals
func(issuer .uverse.address, seq int64) (unpaid int64, provision int64, aged int)Totals returns a report's headline figures together, which is the shape a reader wants and the shape that cannot be assembled from two reads at different heights.
- OID
- 0f8541…df1a:31
Totals details
BucketCount
func(issuer .uverse.address, seq int64) intBucketCount is how many age bands a frozen report has, which is the bucket count of the schedule that priced it and may differ from the issuer's current schedule.
- OID
- 0f8541…df1a:32
BucketCount details
BucketAt
func(issuer .uverse.address, seq int64, i int) (lo int64, hi int64, unbounded bool, rate int64, unpaid int64, provision int64)BucketAt returns one line item of a frozen report, youngest band first. \`lo\` and \`hi\` are the band's age range in BLOCKS, half-open \[lo, hi); when \`unbounded\` is true the band has no upper edge and \`hi\` is meaningless. An age equal to an edge belongs to the OLDER band. \`rate\` is in [RateScale](#RateScale) units, \`unpaid\` is the outstanding total that landed in the band, and \`provision\` is \`floor(unpaid × rate / RateScale)\`.
- OID
- 0f8541…df1a:33
BucketAt details
ScheduleUsed
func(issuer .uverse.address, seq int64) (edges string, rates string)ScheduleUsed returns the schedule that PRICED a frozen report, in the comma-separated form [SetSchedule](#SetSchedule) accepts. This exists because p/aging warns that a report carries no reference to its schedule and two schedules of the same bucket count will silently accept each other's reports. Here the reference is stored, so a reader can see the rates that produced the figure rather than inferring them from whatever the issuer has registered today.
- OID
- 0f8541…df1a:34
ScheduleUsed details
Coverage
func(issuer .uverse.address, seq int64) (listed int, aged int, notOpen int, zeroOut int, notDue int, dupes int, openAt int, recordsAt int)Coverage is the part of a report a reader must not skip. This realm cannot enumerate an issuer's invoices (see the package doc), so a report covers exactly the ids the issuer named. These eight numbers are what let a reader judge whether that was everything: - \`listed\` — distinct ids in the call, after deduplication. - \`aged\` — ids that became obligations in the report. The provision covers these and nothing else. - \`notOpen\` — settled or voided, so nothing owed. - \`zeroOut\` — open with nothing outstanding. - \`notDue\` — open and owed, but not yet due at \`PublishedAt\`. - \`dupes\` — repeated ids dropped before aging. - \`openAt\` — \`invoices.OpenOf(issuer)\` at publication: how many open invoices the issuer actually held. - \`recordsAt\` — \`invoices.RecordsOf(issuer)\` at publication: records of any status, which is the figure r/invoices' per-issuer cap applies to. The arithmetic to do: \`aged + zeroOut + notDue\` is how many OPEN invoices the report accounted for, so \`openAt\` minus that is \*\*how many open invoices were left out\*\*. A non-zero remainder means the provision is understated by an unknown amount, and no part of this realm can say by how much. \`listed = aged + notOpen + zeroOut + notDue\` always holds; \`dupes\` is outside it, having been dropped before the filter. ### ACCOUNTED FOR IS NOT THE SAME AS AT RISK {#hdr-ACCOUNTED_FOR_IS_NOT_THE_SAME_AS_AT_RISK} \`notDue\` counts toward "accounted for" and carries NO provision. That is right on its own terms — the invoice was examined, and p/aging deliberately refuses to bucket something that is not yet due — but it makes one reading wrong in a way the arithmetic above will not catch. A reader who takes \`aged\` (or the provision) as "the receivables at risk" while \`notDue\` is non-zero has measured a STRICT SUBSET of the open book: the omitted part is not omitted by mistake, it is merely not due yet, and it becomes due later with no further call to this realm. So \`OmittedOpen == 0\` means "every open invoice was examined", never "every open invoice is provisioned". The second question is answered by reading \`notDue\` beside the provision, and the render pages print both for exactly this reason.
- OID
- 0f8541…df1a:35
Coverage details
OmittedOpen
func(issuer .uverse.address, seq int64) intOmittedOpen is the coverage arithmetic as one number: how many of the issuer's open invoices the report did not account for, at the height it was published. Zero is the answer a complete report gives. A positive answer means the provision is understated. A NEGATIVE answer is possible and is not a bug here: \`openAt\` is read after the loop, so an invoice settled or voided by a transaction between — impossible within one transaction, but the figure is a snapshot either way — would make the accounted set larger than the open count. It is returned signed rather than clamped, because a clamp would turn an inconsistency into a clean zero.
- OID
- 0f8541…df1a:36
OmittedOpen details
ProposedEntry
func(issuer .uverse.address, seq int64, expenseCode string, allowanceCode string) stringProposedEntry returns the double entry that records a report's provision, in the \`code:side:amount\` form r/books' PostMulti accepts — and this realm does NOT post it. It cannot. r/books derives a book's owner structurally from its caller, so a posting made from here would land in this realm's own book rather than the issuer's, and there is no owner parameter and no delegation path. The issuer posts this string themselves, from their own account, into their own book. That keeps this realm keyless with respect to every ledger: it cannot write a wrong entry because it cannot write. The entry debits \`expenseCode\` and credits \`allowanceCode\` for the whole provision: ProposedEntry(issuer, seq, "E-BADDEBT", "A-ALLOW") → "E-BADDEBT:D:1250,A-ALLOW:C:1250" Both codes go through \[validCode], an ASCII whitelist, and the reason is the format itself: a code containing \`:\` or \`,\` would inject extra legs into a string this realm presents as its own arithmetic. Whether a code EXISTS, or is the right kind, in any ledger is not checked and cannot be — this realm reads no ledger. A zero provision is refused rather than returned as a balanced pair of zeros, which would be a storage deposit spent to post nothing. It is a PROPOSAL in the full sense: nothing here records that it was posted, posting it twice double-counts, and the allowance it credits is a contra asset whose previous balance this realm cannot see. An issuer provisioning monthly posts the DELTA, not this figure, and working that out is theirs.
- OID
- 0f8541…df1a:37
ProposedEntry details
Source
func() stringSource is the realm every figure here is read from, by value from its own exported constant.
- OID
- 0f8541…df1a:38
Source details
Height
func() int64Height is the current chain height, which is the \`now\` a report published in this transaction would be aged against.
- OID
- 0f8541…df1a:39
Height details
repKey
func(issuer string, seq int64) stringrepKey is issuer|paddedSeq. The sequence is left-padded to 19 digits — the width of MaxInt64 — so lexicographic order over an issuer's prefix is numeric order over their reports. An unpadded decimal would sort "10" before "9". The convention is r/invoices' invKey, adopted rather than re-derived.
- OID
- 0f8541…df1a:40
repKey details
issuerCount
func(issuer string) intissuerCount reads an issuer's report count, treating an absent key as zero. avl.Tree.Get returns ONE value and cannot distinguish an absent key from a nil-valued one, so the two-value type assertion carries the existence answer in one traversal. Only this file ever Sets, and it only ever Sets an int.
- OID
- 0f8541…df1a:41
issuerCount details
mustSchedule
func(issuer .uverse.address) aging.Schedule- OID
- 0f8541…df1a:42
mustSchedule details
mustReport
func(issuer .uverse.address, seq int64) *receivables.report- OID
- 0f8541…df1a:43
mustReport details
Render
func(path string) stringRender serves the index at "", an issuer's page at "\<address>", and one report at "\<address>/\<seq>". Every caller-supplied string reaching the output goes through the sanitizer. The surface is narrower here than in the sibling realms — this realm stores no free text, so the only untrusted bytes are the path itself — and it is still the one injection-shaped surface a realm with no custody has.
- OID
- 0f8541…df1a:44
Render details
renderIndex
func() string- OID
- 0f8541…df1a:45
renderIndex details
renderIssuer
func(issuer .uverse.address) string- OID
- 0f8541…df1a:46
renderIssuer details
renderReport
func(issuer .uverse.address, seq int64) string- OID
- 0f8541…df1a:47