payslips.gno
48.77 Kb · 1229 lines
1// Package payslips publishes graduated withholding statements over the wage
2// facts of a live r/payroll.
3//
4// An employer sets a withholding schedule — a [p/tiers] bracket table — and
5// publishes a payslip naming the employees it covers. For each one this realm
6// reads payroll's own views, reconstructs the settled gross, prices it through
7// the schedule, and records the result. Nothing is supplied by the caller
8// except the schedule and the selection: every amount comes from payroll.
9//
10// # WHAT THIS REALM IS, AND WHAT IT CANNOT BE
11//
12// It is a REPORTER. It holds no coins, has no withdrawal path, and moves
13// nothing. It does not deduct the withholding it computes — payroll settles
14// wages and this realm states what a graduated schedule would have withheld
15// from them. The separation is deliberate and it is also forced: r/payroll is
16// live and immutable, so a graduated rate cannot be charged inside it, and a
17// realm that charged one here would be a second custody surface over coins
18// payroll already holds.
19//
20// The consequence is the realm's only real safety argument and it is a strong
21// one: THIS REALM CANNOT WRITE A WRONG PAYMENT, BECAUSE IT CANNOT WRITE A
22// PAYMENT. The failure mode it does have is a wrong or incomplete REPORT, and
23// every mitigation below is aimed there.
24//
25// # SETTLED IS NOT EARNED
26//
27// The gross on a payslip is the gross payroll has SETTLED for the position
28// that is open right now:
29//
30// gross = PaidPeriodsOf(employer, employee) * PerPeriodOf(employer, employee)
31//
32// That identity is exact rather than approximate, and the reason is structural:
33// payroll exports no setter for perPeriod, so a position's rate is fixed for
34// its whole life by the Hire that created it, and paidPeriods only ever
35// increases. The product is therefore the position's lifetime settled gross and
36// not an estimate of it.
37//
38// It is still not the employee's lifetime earnings under this employer, and the
39// gap is not small. payroll's Release removes a settled position and frees its
40// storage, and Hire refuses only while a position is OPEN — so
41// Terminate → Release → Hire is a legal sequence that resets paidPeriods to
42// zero and makes every period before it unreadable from any view. The Released
43// event carries paidPeriods, but an event is not realm-readable. So:
44//
45// A PAYSLIP IS SCOPED TO THE POSITION OPEN AT ITS OWN HEIGHT, AND MUST NEVER
46// BE READ AS LIFETIME GROSS.
47//
48// The failure mode is silent under-reporting, which is the one error a reader
49// cannot detect from the number itself. The mitigations are that every payslip
50// records its height, its own scope, and a coverage line; that [Coverage] and
51// [OmittedRoster] say how much of the employer's roster the payslip left out;
52// and that an employee named with no open position PANICS rather than being
53// dropped — a silently-dropped line is exactly the under-report this realm
54// cannot detect after the fact.
55//
56// # PERIODS THAT ARE DUE BUT NOT ACCRUED ARE EXCLUDED, AND DISCLOSED
57//
58// payroll accrues on demand: anyone may call Accrue, and until someone does,
59// completed periods are due and unsettled. A payslip published before an
60// Accrue therefore reports a smaller gross than the employee is owed. Rather
61// than guess, each line records dueGross — DuePeriods times perPeriod at the
62// payslip's height — and [UnsettledGrossOf] totals it. A payslip whose
63// UnsettledGrossOf is non-zero is complete about what was settled and explicit
64// about what was not.
65//
66// # THE PLATFORM FEE IS AN INTERVAL, AND THAT IS A MEASUREMENT
67//
68// payroll takes an accrual fee of AccrueFeeBps from each settlement, and the
69// obvious reconstruction — a flat rate on the cumulative gross — is wrong. Its
70// Accrue settles every due period in ONE deposit and floors the fee on that
71// aggregate, so the fee actually taken depends on how the accruals were
72// BATCHED, and payroll exports no view of the batching. What it does export is
73// paidPeriods and perPeriod, and those bound the fee exactly:
74//
75// feeLo = paidPeriods * floor(perPeriod * bps / RateScale) one period per call
76// feeHi = floor(gross * bps / RateScale) one call for all
77//
78// Both endpoints are achievable, so the interval is tight and not a hedge:
79// floor(x) + floor(y) <= floor(x+y) gives the upper bound, and
80// floor(n*x) >= n*floor(x) gives the lower. They coincide whenever
81// perPeriod * bps divides RateScale evenly, which is the common case.
82//
83// So [PlatformFeeRange] and [NetRange] return pairs. A single platform-fee
84// number here would have been plausible, derivable, and wrong for any employee
85// whose accruals were batched — and nothing on the chain would have
86// contradicted it.
87//
88// # THE SCHEDULE PRICES GROSS, AND THE TWO DEDUCTIONS ARE NEVER COMBINED
89//
90// The withholding schedule is applied to gross. payroll's platform fee is a
91// separate deduction that payroll has already taken. They are reported side by
92// side and never folded into one effective rate, because they have different
93// payees, different arithmetic (graduated versus flat) and different
94// certainty (exact versus an interval).
95//
96// A schedule in [p/tiers] carries no identity and no effective date, and a
97// falling rate array is legal there — it is a volume discount — so a
98// transposed table is a silent authoring error that tiers.New cannot detect.
99// Two consequences this realm acts on: every payslip SNAPSHOTS the schedule
100// that priced it, so a later SetSchedule cannot retroactively re-price a
101// published payslip; and [OverWithheld] names the one arithmetic outcome that
102// is almost certainly an authoring error — a schedule withholding more than
103// the employee actually received — rather than hiding it or refusing to
104// publish it.
105//
106// # AUTHORITY
107//
108// An employer publishes about their OWN roster: the employer of every payslip
109// is cur.Previous().Address(), never an argument. This realm therefore cannot
110// publish a payslip about a roster the caller does not hold, and a payslip's
111// key is the employer that published it.
112package payslips
113
114import (
115 "chain"
116 "chain/runtime"
117 "chain/runtime/unsafe"
118 "strconv"
119 "strings"
120
121 "gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/tiers"
122 "gno.land/p/nt/avl/v0"
123 "gno.land/p/nt/markdown/sanitize/v0"
124 "gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/payroll"
125)
126
127// RealmPath is this realm's own path, used to build Render links.
128const RealmPath = "/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/payslips"
129
130// PayrollPath is the wage source every figure here is read from.
131//
132// It is taken BY VALUE from that realm's own exported constant rather than
133// typed out again, so this is the compile-time pin itself and not a second
134// copy that could drift. The convention is r/receivables' InvoicesPath.
135const PayrollPath = payroll.RealmPath
136
137// RateScale is the unit of a withholding rate, re-exported by value from
138// [p/tiers]: a rate of 2500 is 25%. Re-exported because every rate in this
139// realm's API is in these units and a caller should not have to find the other
140// package to learn the denominator.
141const RateScale = tiers.RateScale
142
143// MaxTiers is the widest schedule [p/tiers] will build, by value. A schedule
144// has one rate per tier and one edge between each adjacent pair, so an
145// employer may name at most MaxTiers rates and MaxTiers-1 edges.
146const MaxTiers = tiers.MaxTiers
147
148// PlatformFeeBps is the accrual fee r/payroll charges, taken by value from
149// that realm's own constant.
150//
151// Taking it by value is sound here for a reason worth stating rather than
152// assuming: payroll is LIVE and a realm's bytes are immutable after addpkg, so
153// that constant cannot change for the payroll this realm imports. A future
154// payroll version would be a different path and a different import.
155const PlatformFeeBps = payroll.AccrueFeeBps
156
157// MaxEmployeesPerCall caps how many employees one [Publish] may name, and it
158// is r/payroll's own per-employer roster cap taken by value.
159//
160// That is the right bound and not an arbitrary one: payroll refuses a Hire
161// past MaxRoster open positions, so an employer cannot have more than
162// MaxRoster employees to report on, and a cap above it would admit a call that
163// must contain a duplicate or an absent position. A cap below it would make
164// some rosters unreportable in one call, which is the under-report this realm
165// exists to avoid.
166const MaxEmployeesPerCall = payroll.MaxRoster
167
168// PayslipsAreUncapped records a decision by its absence: there is no limit on
169// how many payslips one employer may publish. Each one is a fresh record and
170// the storage deposit for it is paid by the caller at publish time, so the
171// chain already prices the only resource at stake. A cap here would cause the
172// one failure this realm cannot tolerate — an employer unable to publish the
173// statement that would have disclosed an omission.
174const PayslipsAreUncapped = true
175
176// RenderPayslipLimit is how many payslips an employer's Render page lists.
177// Not a storage cap — see [PayslipsAreUncapped].
178const RenderPayslipLimit = 20
179
180// line is one employee's statement within a payslip. Every field is derived
181// from r/payroll's views at the payslip's own height; none is supplied by the
182// caller.
183type line struct {
184 employee address
185
186 // paidPeriods and perPeriod are the two payroll facts gross is built
187 // from. They are kept so a reader can re-derive the gross rather than
188 // take it on trust, and so the fee interval can be re-checked.
189 paidPeriods int64
190 perPeriod int64
191
192 // gross is paidPeriods * perPeriod: the settled gross of the position
193 // that was open at the payslip's height. NOT lifetime gross — see the
194 // package doc.
195 gross int64
196
197 // withheld is the schedule's charge on gross, floored per slice by
198 // [p/tiers].
199 withheld int64
200
201 // feeLo and feeHi bound the accrual fee r/payroll already took. The
202 // interval is tight and both endpoints are reachable — see the package
203 // doc.
204 feeLo int64
205 feeHi int64
206
207 // dueGross is gross that has completed and NOT been accrued at the
208 // payslip's height. It is excluded from gross and disclosed here.
209 dueGross int64
210
211 // ended records whether the position's clock had stopped at the
212 // payslip's height. An ended position can still have dueGross: payroll
213 // deliberately keeps an unsettled obligation alive past Terminate.
214 ended bool
215}
216
217// payslip is one published statement. Its totals are stored rather than
218// recomputed so a reader gets the numbers as published, not as re-derived
219// under a later schedule.
220type payslip struct {
221 at int64
222
223 // sched is the schedule that priced THIS payslip, snapshotted. A
224 // schedule has no identity in [p/tiers], so a later SetSchedule would
225 // otherwise silently re-price every published payslip.
226 sched tiers.Schedule
227
228 // feeBps is PlatformFeeBps as this realm understood it at publish
229 // time. It cannot vary while payroll is immutable; it is recorded so a
230 // reader checking the fee interval does not have to resolve a constant
231 // in another package to do it.
232 feeBps int64
233
234 lines []*line
235
236 gross int64
237 withheld int64
238 feeLo int64
239 feeHi int64
240 dueGross int64
241
242 // the coverage line. listed is what the caller named; priced is what
243 // was reported; the rest describe the shape of what was priced and how
244 // much of the roster was left out.
245 listed int
246 priced int
247 zeroGross int
248 withDue int
249 ended int
250 dupes int
251 rosterAt int
252 openAt int
253}
254
255var (
256 // payslips holds one record per published statement, keyed
257 // employer|padded-seq so an employer's payslips are a contiguous prefix
258 // range in sequence order.
259 payslips = avl.NewTree() // "<employer>|<padded seq>" -> *payslip
260
261 // schedules holds each employer's CURRENT withholding schedule. A
262 // payslip never reads this after publication; it carries its own
263 // snapshot.
264 schedules = avl.NewTree() // employer string -> tiers.Schedule
265
266 // counts is both the per-employer sequence allocator and the
267 // per-employer payslip count. One tree, because they are the same
268 // number.
269 counts = avl.NewTree() // employer string -> int
270
271 // slipCount is the global count, for the Render index.
272 slipCount int
273
274 // platformFee is PlatformFeeBps expressed as a one-tier [p/tiers]
275 // schedule, which is how this realm computes the fee endpoints.
276 //
277 // Reusing the audited primitive instead of writing the arithmetic again
278 // is the point. A one-tier schedule is a flat rate, tiers.Charge floors
279 // it, and tiers does the multiply through bits.Mul64/Div64 so a product
280 // near the int64 ceiling cannot wrap. MEASURED EQUIVALENCE: p/feeledger's
281 // feeFor computes (x/10000)*bps + (x%10000)*bps/10000, which is exactly
282 // floor(x*bps/10000) and so exactly this schedule's charge.
283 //
284 // That equivalence is the hinge the whole fee interval hangs from, so it
285 // is BOTH argued and asserted, and the assertion is NAMED rather than
286 // gestured at: TestPlatformFeeScheduleAgreesWithFeeledger compares the
287 // two engines two-sidedly over an amount sample and the whole legal bps
288 // range. An earlier draft of this comment said the equivalence "is
289 // asserted in the test suite rather than argued here" while the suite did
290 // not import p/feeledger at all -- it pointed a reader at evidence that
291 // was not there, on the one fact it names load-bearing. A citation with no
292 // referent is worse than no citation, and this text is frozen by addpkg
293 // while a test is not, which is why the name is written in.
294 //
295 // CEILING: that test is a sample, not a proof over int64. The exhaustive
296 // claim is the identity above; the test is what fires if either primitive
297 // is ever redeployed with different rounding.
298 platformFee = mustFlat(PlatformFeeBps)
299)
300
301func mustFlat(bps int64) tiers.Schedule {
302 s, err := tiers.New(nil, []int64{bps})
303 if err != nil {
304 panic("payslips: the platform-fee schedule is invalid: " + err.Error())
305 }
306 return s
307}
308
309// refuseCoins aborts when a direct EOA MsgCall attaches coins to an
310// entrypoint here. This realm has no custody and no withdrawal path, so a
311// send would be unrecoverable by anyone — including the sender.
312//
313// The IsCurrent() check is not decoration. Every call site forwards a crossing
314// entrypoint's own live cur, so it cannot fire today; it exists so a future
315// refactor that hands this helper a stale realm value fails closed instead of
316// trusting it. That shape is security.md's Class 2 and it has been found in
317// this portfolio's own realms more than once.
318func refuseCoins(cur realm) {
319 if !cur.IsCurrent() {
320 panic("payslips: realm capability is not current")
321 }
322 if cur.Previous().IsUserCall() && len(unsafe.OriginSend()) > 0 {
323 panic("payslips: this realm holds no coins and has no withdrawal path; send none")
324 }
325}
326
327// --- the schedule ---
328
329// SetSchedule installs the caller's own withholding schedule and returns its
330// tier count.
331//
332// edges and rates are comma-separated int64 lists in [p/tiers]'s units: rates
333// are of [RateScale], edges are gross amounts in ugnot. There is one more rate
334// than there are edges, because the last tier is unbounded. An EMPTY edges
335// string is legal and means a one-tier schedule — a flat rate — which is the
336// shape to use for a flat withholding and the shape this realm uses internally
337// for the platform fee.
338//
339// SetSchedule(cross(cur), "1000000,5000000", "500,1500,2500")
340//
341// is 5% on the first 1,000,000 ugnot of gross, 15% on the next 4,000,000, and
342// 25% above 5,000,000.
343//
344// REPLACING A SCHEDULE DOES NOT RE-PRICE ANYTHING. Every published payslip
345// carries the schedule that priced it, so this only affects payslips published
346// after it. That is a design requirement rather than a convenience: a
347// published statement whose numbers change under it is not a record.
348//
349// A FALLING RATE ARRAY IS ACCEPTED, because [p/tiers] accepts one — it is a
350// volume discount, which is a real schedule. The consequence is that a
351// transposed array is a silent authoring error here, and neither this realm
352// nor tiers can detect it. Read back [ScheduleOf] after setting one.
353func SetSchedule(cur realm, edges, rates string) int {
354 refuseCoins(cur)
355 employer := cur.Previous().Address()
356
357 var e []int64
358 if strings.TrimSpace(edges) != "" {
359 e = parseInt64List(edges, MaxTiers-1, "edge")
360 }
361 r := parseInt64List(rates, MaxTiers, "rate")
362
363 s, err := tiers.New(e, r)
364 if err != nil {
365 panic("payslips: " + err.Error())
366 }
367 schedules.Set(employer.String(), s)
368 chain.Emit("ScheduleSet",
369 "employer", employer.String(),
370 "tiers", strconv.Itoa(s.NumTiers()),
371 "edges", formatInt64List(s.Edges()),
372 "rates", formatInt64List(s.Rates()))
373 return s.NumTiers()
374}
375
376// --- publishing ---
377
378// Publish prices the named employees under the caller's own roster and records
379// a payslip. It returns the payslip's sequence number, which is per-employer
380// and starts at 1.
381//
382// employees is a comma-separated list of addresses, at most
383// [MaxEmployeesPerCall] of them. Order is the caller's and is preserved in the
384// record, so a payslip reads in the order it was requested.
385//
386// THREE REFUSAL RULES, AND THE ASYMMETRY BETWEEN THEM IS THE POINT:
387//
388// - An employee with NO OPEN POSITION under the caller PANICS and fails the
389// whole call. A silently-dropped line is an under-report, and under-
390// reporting is the one error this product cannot detect after the fact. A
391// caller that wants to skip an absent position must leave it out, which is
392// a decision in the transaction rather than a silence in the record.
393// - A DUPLICATE is dropped, counted in the coverage line, and does not fail
394// the call. Dropping it cannot under-report: the employee is still priced
395// once, and double-counting them would be worse.
396// - An employee with ZERO SETTLED GROSS is PRICED, not filtered. A payslip
397// for a position that has settled nothing yet is a correct payslip with a
398// gross of zero, and filtering it would turn a true statement into a
399// missing line.
400//
401// An empty list is refused. Nothing here filters a list down to nothing, so
402// after deduplication a call always prices at least one employee.
403func Publish(cur realm, employees string) int64 {
404 refuseCoins(cur)
405 employer := cur.Previous().Address()
406 s := mustSchedule(employer)
407 now := runtime.ChainHeight()
408
409 list, dupes := parseEmployees(employees)
410
411 lines := make([]*line, 0, len(list))
412 var gross, withheld, feeLo, feeHi, dueGross int64
413 var zeroGross, withDue, ended int
414
415 for _, who := range list {
416 // Existence first, and with the one payroll view that does not
417 // panic on absence: every other position accessor there panics,
418 // which makes them unusable as a probe. The panic below is this
419 // realm's refusal, with this realm's message.
420 if !payroll.HasPosition(employer, who) {
421 panic("payslips: " + who.String() +
422 " has no open position under " + employer.String() +
423 "; a dropped line would be a silent under-report")
424 }
425
426 l := priceOne(employer, who, s, now)
427 lines = append(lines, l)
428
429 var ok bool
430 if gross, ok = checkedAdd(gross, l.gross); !ok {
431 panic("payslips: total gross would overflow")
432 }
433 if withheld, ok = checkedAdd(withheld, l.withheld); !ok {
434 panic("payslips: total withholding would overflow")
435 }
436 if feeLo, ok = checkedAdd(feeLo, l.feeLo); !ok {
437 panic("payslips: total platform-fee floor would overflow")
438 }
439 if feeHi, ok = checkedAdd(feeHi, l.feeHi); !ok {
440 panic("payslips: total platform-fee ceiling would overflow")
441 }
442 if dueGross, ok = checkedAdd(dueGross, l.dueGross); !ok {
443 panic("payslips: total unsettled gross would overflow")
444 }
445
446 if l.gross == 0 {
447 zeroGross++
448 }
449 if l.dueGross > 0 {
450 withDue++
451 }
452 if l.ended {
453 ended++
454 }
455 }
456
457 // Cross-foot the per-line columns against the totals. The loop above
458 // already built them, so this asserts nothing about the arithmetic — it
459 // asserts that the record about to be stored agrees with the lines
460 // stored beside it, which is the one inconsistency a reader of a view
461 // could not see.
462 crossFoot(lines, gross, withheld, feeLo, feeHi, dueGross)
463
464 // feeLo <= feeHi is an identity of the two bounds, not an assumption.
465 // If it ever fails, the fee interval is not an interval and no number
466 // derived from it is quotable, so this refuses rather than publishes.
467 if feeLo > feeHi {
468 panic("payslips: the platform-fee interval is inverted")
469 }
470
471 key := employer.String()
472 seq := int64(employerCount(key)) + 1
473 payslips.Set(slipKey(key, seq), &payslip{
474 at: now,
475 sched: s,
476 feeBps: PlatformFeeBps,
477 lines: lines,
478 gross: gross,
479 withheld: withheld,
480 feeLo: feeLo,
481 feeHi: feeHi,
482 dueGross: dueGross,
483 listed: len(list) + dupes,
484 priced: len(lines),
485 zeroGross: zeroGross,
486 withDue: withDue,
487 ended: ended,
488 dupes: dupes,
489
490 // The incompleteness disclosure. rosterAt is how many open
491 // positions the employer held at this height, so
492 // rosterAt - priced is exactly how many this payslip did not
493 // cover. openAt is the chain-wide figure, which is what makes
494 // an employer's share of the realm legible.
495 rosterAt: payroll.RosterOf(employer),
496 openAt: payroll.OpenPositions(),
497 })
498 counts.Set(key, int(seq))
499 slipCount++
500
501 // Every attribute here reads the record that was just stored, and
502 // `omitted` goes through [OmittedRoster] rather than recomputing
503 // rosterAt - priced. Two reasons, and the second is the load-bearing one:
504 //
505 // - a second payroll.RosterOf call is a second chain read, and nothing
506 // guarantees it agrees with the rosterAt now frozen in the record;
507 // - AN EMITTED ATTRIBUTE IS UNASSERTABLE FROM A GNO UNIT TEST. chain.Emit
508 // has no reader and the testing stdlib exposes none, so an event copy
509 // of a derived figure has no control over it at all. Routing it through
510 // the exported accessor the suite does pin makes the event unable to
511 // disagree with Coverage by construction, which is the only mitigation
512 // available at this layer. Measured: a sabotage replacing this figure
513 // with len(lines) was UNCAUGHT by a 33-test suite.
514 chain.Emit("PayslipPublished",
515 "employer", key,
516 "seq", strconv.FormatInt(seq, 10),
517 "at", strconv.FormatInt(now, 10),
518 "lines", strconv.Itoa(len(lines)),
519 "gross", strconv.FormatInt(gross, 10),
520 "withheld", strconv.FormatInt(withheld, 10),
521 "feeLo", strconv.FormatInt(feeLo, 10),
522 "feeHi", strconv.FormatInt(feeHi, 10),
523 "dueGross", strconv.FormatInt(dueGross, 10),
524 "omitted", strconv.Itoa(OmittedRoster(employer, seq)))
525 return seq
526}
527
528// priceOne builds one line from r/payroll's views. Every arithmetic refusal
529// here is a panic rather than a zero, because a view that refuses is
530// recoverable and a view that lies is not — payroll's own DueAmount states the
531// same rule for the same reason.
532func priceOne(employer, who address, s tiers.Schedule, now int64) *line {
533 pp := payroll.PaidPeriodsOf(employer, who)
534 per := payroll.PerPeriodOf(employer, who)
535
536 gross, ok := checkedMul(pp, per)
537 if !ok {
538 panic("payslips: settled gross for " + who.String() +
539 " would overflow; read PaidPeriodsOf and PerPeriodOf directly")
540 }
541
542 withheld, err := s.Charge(gross)
543 if err != nil {
544 panic("payslips: " + err.Error())
545 }
546 // p/tiers guarantees Charge(n) <= n. Asserting it costs one comparison
547 // and turns a broken dependency into a refusal instead of a payslip
548 // claiming more was withheld than was earned.
549 if withheld > gross {
550 panic("payslips: withholding exceeds gross, which p/tiers forbids")
551 }
552
553 // The fee interval. Both endpoints go through the same audited
554 // primitive; see the platformFee var for why.
555 feeHi, err := platformFee.Charge(gross)
556 if err != nil {
557 panic("payslips: " + err.Error())
558 }
559 unit, err := platformFee.Charge(per)
560 if err != nil {
561 panic("payslips: " + err.Error())
562 }
563 feeLo, ok := checkedMul(pp, unit)
564 if !ok {
565 panic("payslips: platform-fee floor for " + who.String() + " would overflow")
566 }
567
568 dp := payroll.DuePeriods(employer, who)
569 dueGross, ok := checkedMul(dp, per)
570 if !ok {
571 panic("payslips: unsettled gross for " + who.String() +
572 " would overflow; read DuePeriods and PerPeriodOf directly")
573 }
574
575 end := payroll.EndHeightOf(employer, who)
576
577 return &line{
578 employee: who,
579 paidPeriods: pp,
580 perPeriod: per,
581 gross: gross,
582 withheld: withheld,
583 feeLo: feeLo,
584 feeHi: feeHi,
585 dueGross: dueGross,
586 ended: end != payroll.OpenEnded && end <= now,
587 }
588}
589
590func crossFoot(lines []*line, gross, withheld, feeLo, feeHi, dueGross int64) {
591 var g, w, lo, hi, d int64
592 for _, l := range lines {
593 g += l.gross
594 w += l.withheld
595 lo += l.feeLo
596 hi += l.feeHi
597 d += l.dueGross
598 }
599 if g != gross || w != withheld || lo != feeLo || hi != feeHi || d != dueGross {
600 panic("payslips: the stored totals do not agree with the stored lines")
601 }
602}
603
604// --- reading a payslip ---
605
606// PublishedAt is the height a payslip was published at, which is the height
607// every figure in it was read at.
608func PublishedAt(employer address, seq int64) int64 {
609 return mustPayslip(employer, seq).at
610}
611
612// GrossOf is a payslip's total SETTLED gross. Read it beside
613// [UnsettledGrossOf] and [OmittedRoster]: neither is an error, and both change
614// what this number means.
615func GrossOf(employer address, seq int64) int64 {
616 return mustPayslip(employer, seq).gross
617}
618
619// WithheldOf is the schedule's total charge on the gross.
620func WithheldOf(employer address, seq int64) int64 {
621 return mustPayslip(employer, seq).withheld
622}
623
624// PlatformFeeRange bounds the accrual fee r/payroll already took on this
625// gross. It is an interval because the fee depends on accrual batching, which
626// payroll does not expose — see the package doc. lo == hi is common and means
627// the batching could not have mattered.
628func PlatformFeeRange(employer address, seq int64) (lo, hi int64) {
629 p := mustPayslip(employer, seq)
630 return p.feeLo, p.feeHi
631}
632
633// NetRange bounds what the employee kept: gross, less the schedule's
634// withholding, less payroll's fee. The interval is inherited whole from
635// [PlatformFeeRange] — the withholding itself is exact.
636//
637// IT CAN BE NEGATIVE, and that is a finding rather than a bug. A schedule may
638// legally charge up to 100%, and payroll's fee is taken on top of whatever
639// this realm withholds, so a schedule at or near the ceiling withholds more
640// than the employee actually received. [OverWithheld] names that case.
641func NetRange(employer address, seq int64) (lo, hi int64) {
642 p := mustPayslip(employer, seq)
643 return p.gross - p.withheld - p.feeHi, p.gross - p.withheld - p.feeLo
644}
645
646// UnsettledGrossOf is gross that had completed and not been accrued at the
647// payslip's height, and is therefore EXCLUDED from [GrossOf]. Non-zero is
648// normal: payroll accrues on demand.
649func UnsettledGrossOf(employer address, seq int64) int64 {
650 return mustPayslip(employer, seq).dueGross
651}
652
653// OverWithheld reports whether this payslip's withholding plus payroll's
654// largest possible fee exceeds the gross. True is almost always a transposed
655// or mis-scaled rate array, which [p/tiers] cannot detect, so this realm
656// surfaces it instead of refusing to publish it — a refusal would lose the
657// evidence.
658func OverWithheld(employer address, seq int64) bool {
659 lo, _ := NetRange(employer, seq)
660 return lo < 0
661}
662
663// FeeBpsAt is the platform fee rate this payslip was priced against.
664func FeeBpsAt(employer address, seq int64) int64 {
665 return mustPayslip(employer, seq).feeBps
666}
667
668// ScheduleAt is the schedule that priced this payslip, as comma-separated
669// lists that round-trip exactly through [SetSchedule]. The snapshot, not the
670// employer's current schedule — see [ScheduleOf] for that.
671func ScheduleAt(employer address, seq int64) (edges, rates string) {
672 s := mustPayslip(employer, seq).sched
673 return formatInt64List(s.Edges()), formatInt64List(s.Rates())
674}
675
676// Coverage is the payslip's coverage line, and it is the number to read before
677// any total.
678//
679// listed employees the caller named, duplicates included
680// priced lines in the record
681// zeroGross priced lines whose settled gross is zero
682// withDue priced lines with gross due and not accrued
683// ended priced lines whose position's clock had stopped
684// dupes duplicate names dropped
685// rosterAt open positions the employer held at this height
686// openAt open positions across every employer at this height
687//
688// listed == priced + dupes always, by construction: nothing else is dropped.
689// rosterAt - priced is [OmittedRoster].
690func Coverage(employer address, seq int64) (listed, priced, zeroGross, withDue, ended, dupes, rosterAt, openAt int) {
691 p := mustPayslip(employer, seq)
692 return p.listed, p.priced, p.zeroGross, p.withDue, p.ended, p.dupes, p.rosterAt, p.openAt
693}
694
695// OmittedRoster is how many of the employer's open positions this payslip did
696// not price. It is the one number that says whether a payslip is complete, and
697// a total read without it is being read wrongly.
698//
699// It cannot be negative: every priced line has an open position under this
700// employer at the payslip's height, duplicates are dropped before pricing, so
701// priced is a count of distinct open positions and rosterAt is their total.
702func OmittedRoster(employer address, seq int64) int {
703 p := mustPayslip(employer, seq)
704 return p.rosterAt - p.priced
705}
706
707// Lines is how many employees a payslip prices.
708func Lines(employer address, seq int64) int {
709 return len(mustPayslip(employer, seq).lines)
710}
711
712// LineAt is one employee's statement, by index in the order the caller named
713// them. gross is paidPeriods * perPeriod and can be re-derived from the two
714// facts returned beside it.
715func LineAt(employer address, seq int64, i int) (employee address, paidPeriods, perPeriod, gross, withheld, feeLo, feeHi, dueGross int64) {
716 l := mustLine(employer, seq, i)
717 return l.employee, l.paidPeriods, l.perPeriod, l.gross, l.withheld, l.feeLo, l.feeHi, l.dueGross
718}
719
720// LineEnded reports whether this line's position had been terminated or had
721// passed its end height when the payslip was published. An ended position can
722// still carry unsettled gross: payroll keeps the obligation alive past
723// Terminate on purpose.
724func LineEnded(employer address, seq int64, i int) bool {
725 return mustLine(employer, seq, i).ended
726}
727
728// LineNetRange bounds what this employee kept. Same interval as [NetRange],
729// one line.
730func LineNetRange(employer address, seq int64, i int) (lo, hi int64) {
731 l := mustLine(employer, seq, i)
732 return l.gross - l.withheld - l.feeHi, l.gross - l.withheld - l.feeLo
733}
734
735// TierBreakdown is the withholding on one line split by bracket, as a
736// comma-separated list in tier order — the column a payslip reader actually
737// wants, because it shows which brackets the gross reached.
738//
739// It is recomputed from the payslip's own schedule snapshot rather than
740// stored, so it is exact for the schedule that priced the line and costs no
741// storage. It cross-foots the column against the stored total before returning
742// it: [p/tiers] DOCUMENTS that Charge is defined as the sum of ChargeByTier,
743// and a documented identity across a package boundary is worth one loop to
744// assert.
745func TierBreakdown(employer address, seq int64, i int) string {
746 p := mustPayslip(employer, seq)
747 l := mustLine(employer, seq, i)
748 col, err := p.sched.ChargeByTier(l.gross)
749 if err != nil {
750 panic("payslips: " + err.Error())
751 }
752 var sum int64
753 for _, c := range col {
754 sum += c
755 }
756 if sum != l.withheld {
757 panic("payslips: the per-tier column does not sum to the withholding")
758 }
759 return formatInt64List(col)
760}
761
762// --- reading an employer ---
763
764// ScheduleOf is an employer's CURRENT schedule, as lists that round-trip
765// through [SetSchedule]. It is not what priced any published payslip — see
766// [ScheduleAt].
767func ScheduleOf(employer address) (edges, rates string) {
768 s := mustSchedule(employer)
769 return formatInt64List(s.Edges()), formatInt64List(s.Rates())
770}
771
772// NumTiersOf is how many brackets an employer's current schedule has.
773func NumTiersOf(employer address) int { return mustSchedule(employer).NumTiers() }
774
775// HasSchedule reports whether an employer has set a schedule, without
776// panicking on absence — every other schedule accessor panics, which makes
777// them unusable as an existence probe.
778func HasSchedule(employer address) bool {
779 _, ok := schedules.Get(employer.String()).(tiers.Schedule)
780 return ok
781}
782
783// Has reports whether a payslip exists, so the record accessors can be
784// pre-checked rather than recovered from. Every one of them panics on an
785// absent record, which is deliberate — returning a zero figure for a payslip
786// that was never published is the exact silent under-report this realm exists
787// to refuse — but a panic is only a usable contract when the caller has a way
788// not to hit it.
789//
790// It is here before the first deploy rather than in a successor because a
791// realm's bytes are frozen by addpkg: an accessor a consumer needs cannot be
792// added afterwards.
793func Has(employer address, seq int64) bool {
794 _, ok := payslips.Get(slipKey(employer.String(), seq)).(*payslip)
795 return ok
796}
797
798// ChargeOn quotes an employer's current schedule against an arbitrary gross
799// without publishing anything. The quote a caller needs before choosing a
800// schedule, and the one view here that is not about a record.
801func ChargeOn(employer address, gross int64) int64 {
802 c, err := mustSchedule(employer).Charge(gross)
803 if err != nil {
804 panic("payslips: " + err.Error())
805 }
806 return c
807}
808
809// MarginalRateOf is the rate an employer's current schedule would charge on
810// the next ugnot of gross past the given amount, of [RateScale].
811func MarginalRateOf(employer address, gross int64) int64 {
812 r, err := mustSchedule(employer).MarginalRate(gross)
813 if err != nil {
814 panic("payslips: " + err.Error())
815 }
816 return r
817}
818
819// Count is how many payslips an employer has published. Sequence numbers run
820// 1..Count with no gaps: a payslip is never removed.
821func Count(employer address) int { return employerCount(employer.String()) }
822
823// Payslips is how many payslips exist across every employer.
824func Payslips() int { return slipCount }
825
826// Employers is how many addresses have set a schedule here. Note the tense: a
827// schedule survives, so this is not a count of employers with payslips.
828func Employers() int { return schedules.Size() }
829
830// Height is the current chain height, exposed so a caller can see the clock
831// this realm reads payroll against.
832func Height() int64 { return runtime.ChainHeight() }
833
834// --- internals ---
835
836// parseEmployees splits, validates and deduplicates the selection, preserving
837// the caller's order. It returns the distinct list and how many duplicates
838// were dropped.
839//
840// The cap is checked BEFORE the slice grows, so a caller cannot make this
841// realm allocate past the bound on the way to being refused.
842func parseEmployees(s string) ([]address, int) {
843 parts := strings.Split(s, ",")
844 if len(parts) > MaxEmployeesPerCall {
845 panic("payslips: at most " + strconv.Itoa(MaxEmployeesPerCall) +
846 " employees, got " + strconv.Itoa(len(parts)))
847 }
848 out := make([]address, 0, len(parts))
849 dupes := 0
850 for _, raw := range parts {
851 t := strings.TrimSpace(raw)
852 if t == "" {
853 panic("payslips: empty employee in " + strconv.Quote(s))
854 }
855 who := address(t)
856
857 // The lowercase clause is carried from r/payroll's Hire, where it
858 // is measured: address.IsValid() defers to a decoder that
859 // lowercases before verifying the checksum, so it returns true for
860 // the all-uppercase spelling of a valid address.
861 //
862 // HERE THE STAKE IS DIFFERENT FROM PAYROLL'S AND SMALLER, which is
863 // why this is a guard and not a loss-prevention control. payroll
864 // keys its ledger on this string, so an uppercase spelling there
865 // stranded real coins. This realm would merely fail to find the
866 // position and panic at HasPosition — already fail-closed. The
867 // guard is kept because that panic would name the wrong cause: a
868 // caller would read "has no open position" about an employee who
869 // plainly does. A refusal that misdiagnoses itself is how a real
870 // defect gets attributed to the chain.
871 if !who.IsValid() || t != strings.ToLower(t) {
872 panic("payslips: " + strconv.Quote(t) +
873 " is not a valid address in canonical (lowercase) form")
874 }
875
876 seen := false
877 for _, have := range out {
878 if have == who {
879 seen = true
880 break
881 }
882 }
883 if seen {
884 dupes++
885 continue
886 }
887 out = append(out, who)
888 }
889 if len(out) == 0 {
890 panic("payslips: no employees named")
891 }
892 return out, dupes
893}
894
895func parseInt64List(s string, max int, what string) []int64 {
896 parts := strings.Split(s, ",")
897 if len(parts) > max {
898 panic("payslips: at most " + strconv.Itoa(max) + " " + what +
899 "s, got " + strconv.Itoa(len(parts)))
900 }
901 out := make([]int64, 0, len(parts))
902 for _, p := range parts {
903 t := strings.TrimSpace(p)
904 if t == "" {
905 panic("payslips: empty " + what + " in " + strconv.Quote(s))
906 }
907 v, err := strconv.ParseInt(t, 10, 64)
908 if err != nil {
909 panic("payslips: " + strconv.Quote(t) + " is not a valid " + what)
910 }
911 out = append(out, v)
912 }
913 return out
914}
915
916func formatInt64List(v []int64) string {
917 var sb strings.Builder
918 for i, n := range v {
919 if i > 0 {
920 sb.WriteString(",")
921 }
922 sb.WriteString(strconv.FormatInt(n, 10))
923 }
924 return sb.String()
925}
926
927// slipKey left-pads seq to the width of MaxInt64 so that an employer's
928// payslips sort in numeric order under the tree's lexical key order.
929//
930// NOTHING IN THIS REALM CONSUMES THAT ORDER TODAY, and the padding is in the
931// source anyway. The tree is only ever Set and Get here — renderEmployer walks
932// the sequence numbers it already knows from [Count] rather than iterating —
933// so unpadded keys would produce byte-identical output from every view, and no
934// test can distinguish the two. It is kept because the bytes freeze at addpkg:
935// an enumerator added in a successor realm, or an off-chain indexer doing a
936// prefix scan over this realm's storage, would read "10" before "9" forever,
937// and the fix would be unavailable. Twelve bytes of padding now against a
938// defect that cannot be repaired later.
939//
940// Stated this way deliberately. The comment here used to claim the padding is
941// "what makes a prefix iteration in Render read in sequence", which is a
942// reachability argument for a prefix iteration that does not exist.
943func slipKey(employer string, seq int64) string {
944 s := strconv.FormatInt(seq, 10)
945 for len(s) < 19 {
946 s = "0" + s
947 }
948 return employer + "|" + s
949}
950
951func employerCount(employer string) int {
952 n, ok := counts.Get(employer).(int)
953 if !ok {
954 return 0
955 }
956 return n
957}
958
959func mustSchedule(employer address) tiers.Schedule {
960 s, ok := schedules.Get(employer.String()).(tiers.Schedule)
961 if !ok {
962 panic("payslips: " + employer.String() +
963 " has no withholding schedule; call SetSchedule first")
964 }
965 return s
966}
967
968func mustPayslip(employer address, seq int64) *payslip {
969 p, ok := payslips.Get(slipKey(employer.String(), seq)).(*payslip)
970 if !ok {
971 panic("payslips: no payslip " + strconv.FormatInt(seq, 10) +
972 " for " + employer.String())
973 }
974 return p
975}
976
977func mustLine(employer address, seq int64, i int) *line {
978 p := mustPayslip(employer, seq)
979 if i < 0 || i >= len(p.lines) {
980 panic("payslips: line " + strconv.Itoa(i) + " is out of range for payslip " +
981 strconv.FormatInt(seq, 10) + ", which has " +
982 strconv.Itoa(len(p.lines)) + " lines")
983 }
984 return p.lines[i]
985}
986
987func checkedAdd(a, b int64) (int64, bool) {
988 s := a + b
989 if (b > 0 && s < a) || (b < 0 && s > a) {
990 return 0, false
991 }
992 return s, true
993}
994
995// checkedMul is written for the non-negative operands this realm has — every
996// multiplicand is a period count or a per-period amount, both of which
997// r/payroll refuses to make negative. The division test is exact for those.
998func checkedMul(a, b int64) (int64, bool) {
999 if a < 0 || b < 0 {
1000 return 0, false
1001 }
1002 if a == 0 || b == 0 {
1003 return 0, true
1004 }
1005 p := a * b
1006 if p/b != a {
1007 return 0, false
1008 }
1009 return p, true
1010}
1011
1012// ---------------------------------------------------------------------------
1013
1014// Render serves the index at "", an employer's page at "<address>", and one
1015// payslip at "<address>/<seq>".
1016//
1017// Every caller-supplied string reaching the output goes through the sanitizer.
1018// This realm stores no free text, so the untrusted bytes are the path itself
1019// and the addresses in a record — and that is still the one injection-shaped
1020// surface a realm with no custody has.
1021func Render(path string) string {
1022 p := strings.TrimSpace(strings.Trim(path, "/"))
1023 if p == "" {
1024 return renderIndex()
1025 }
1026 if i := strings.Index(p, "/"); i >= 0 {
1027 employer := address(strings.TrimSpace(p[:i]))
1028 rest := strings.TrimSpace(strings.Trim(p[i+1:], "/"))
1029 seq, err := strconv.ParseInt(rest, 10, 64)
1030 if err != nil {
1031 return "# payslips\n\n`" + sanitize.InlineText(rest) +
1032 "` is not a payslip number.\n"
1033 }
1034 return renderPayslip(employer, seq)
1035 }
1036 return renderEmployer(address(p))
1037}
1038
1039func renderIndex() string {
1040 var sb strings.Builder
1041 sb.WriteString("# payslips — graduated withholding over a live payroll\n\n")
1042 sb.WriteString("Withholding statements priced from wage facts in [`" +
1043 PayrollPath + "`](" + PayrollPath + ") — never from amounts supplied ")
1044 sb.WriteString("here. This realm holds no coins, moves nothing, and ")
1045 sb.WriteString("deducts nothing: it states what a bracket schedule would ")
1046 sb.WriteString("have withheld from gross that payroll has already ")
1047 sb.WriteString("settled.\n\n")
1048
1049 sb.WriteString("> **A payslip covers the position that was open at its ")
1050 sb.WriteString("own height, and nothing else.** `" + PayrollPath + "`'s ")
1051 sb.WriteString("Release removes a settled position, so a re-hire resets ")
1052 sb.WriteString("the period count and earlier periods become unreadable ")
1053 sb.WriteString("from any view. A gross here is therefore never lifetime ")
1054 sb.WriteString("gross. Every payslip publishes a coverage line, and ")
1055 sb.WriteString("`OmittedRoster` is the one number that says whether it ")
1056 sb.WriteString("is complete.\n\n")
1057
1058 sb.WriteString("> **The platform fee is an interval.** payroll floors its ")
1059 sb.WriteString("accrual fee on each settlement and settles every due ")
1060 sb.WriteString("period in one deposit, so the fee it took depends on how ")
1061 sb.WriteString("the accruals were batched — which no view exposes. ")
1062 sb.WriteString("`PlatformFeeRange` bounds it tightly from the two facts ")
1063 sb.WriteString("that are exposed. A single number would have been ")
1064 sb.WriteString("plausible and wrong.\n\n")
1065
1066 sb.WriteString("| | |\n|---|---|\n")
1067 sb.WriteString("| payslips published | " + strconv.Itoa(slipCount) + " |\n")
1068 sb.WriteString("| employers with a schedule | " + strconv.Itoa(schedules.Size()) + " |\n")
1069 sb.WriteString("| platform fee | " + strconv.FormatInt(PlatformFeeBps, 10) +
1070 " of " + strconv.FormatInt(RateScale, 10) + " |\n")
1071 sb.WriteString("| widest schedule | " + strconv.Itoa(MaxTiers) + " tiers |\n")
1072 sb.WriteString("| employees per payslip | at most " +
1073 strconv.Itoa(MaxEmployeesPerCall) + " |\n\n")
1074
1075 sb.WriteString("Browse an employer at `" + RealmPath + ":<address>`.\n")
1076 return sb.String()
1077}
1078
1079func renderEmployer(employer address) string {
1080 var sb strings.Builder
1081 name := sanitize.InlineText(employer.String())
1082 sb.WriteString("# payslips — " + name + "\n\n")
1083
1084 if HasSchedule(employer) {
1085 e, r := ScheduleOf(employer)
1086 sb.WriteString("Current schedule: edges `" + sanitize.InlineText(e) +
1087 "` (gross, ugnot), rates `" + sanitize.InlineText(r) + "` (of " +
1088 strconv.FormatInt(RateScale, 10) + ").\n\n")
1089 sb.WriteString("A replacement does not re-price a published payslip — ")
1090 sb.WriteString("each one carries the schedule that priced it.\n\n")
1091 } else {
1092 sb.WriteString("No withholding schedule set. `SetSchedule` first; ")
1093 sb.WriteString("`Publish` refuses without one.\n\n")
1094 }
1095
1096 n := employerCount(employer.String())
1097 sb.WriteString("Open positions now: " +
1098 strconv.Itoa(payroll.RosterOf(employer)) + ". Payslips published: " +
1099 strconv.Itoa(n) + ".\n\n")
1100 if n == 0 {
1101 return sb.String()
1102 }
1103
1104 sb.WriteString("| # | height | lines | gross | withheld | fee (lo–hi) | omitted |\n")
1105 sb.WriteString("|---|---|---|---|---|---|---|\n")
1106 first := int64(1)
1107 if int64(n) > int64(RenderPayslipLimit) {
1108 first = int64(n) - int64(RenderPayslipLimit) + 1
1109 }
1110 for seq := int64(n); seq >= first; seq-- {
1111 p := mustPayslip(employer, seq)
1112 s := strconv.FormatInt(seq, 10)
1113 sb.WriteString("| [" + s + "](" + RealmPath + ":" + name + "/" + s + ") | " +
1114 strconv.FormatInt(p.at, 10) + " | " +
1115 strconv.Itoa(len(p.lines)) + " | " +
1116 strconv.FormatInt(p.gross, 10) + " | " +
1117 strconv.FormatInt(p.withheld, 10) + " | " +
1118 strconv.FormatInt(p.feeLo, 10) + "–" +
1119 strconv.FormatInt(p.feeHi, 10) + " | " +
1120 strconv.Itoa(p.rosterAt-p.priced) + " |\n")
1121 }
1122 if int64(n) > int64(RenderPayslipLimit) {
1123 sb.WriteString("\nShowing the most recent " +
1124 strconv.Itoa(RenderPayslipLimit) + " of " + strconv.Itoa(n) +
1125 ". Older ones are still readable by number — nothing is removed.\n")
1126 }
1127 return sb.String()
1128}
1129
1130func renderPayslip(employer address, seq int64) string {
1131 name := sanitize.InlineText(employer.String())
1132
1133 // A URL is caller-supplied, so an absent record is a wrong guess and not a
1134 // broken invariant. The accessors panic on purpose; a page does not, and
1135 // gnoweb serves a panic as an error rather than as markdown. This is the
1136 // receivables precedent (receivables.gno renderReport), followed here
1137 // deliberately rather than diverged from.
1138 if !Has(employer, seq) {
1139 return "# payslips\n\nNo payslip " + strconv.FormatInt(seq, 10) +
1140 " for `" + name + "`.\n"
1141 }
1142 p := mustPayslip(employer, seq)
1143
1144 var sb strings.Builder
1145 sb.WriteString("# payslip " + strconv.FormatInt(seq, 10) + " — " + name + "\n\n")
1146 sb.WriteString("Published at height " + strconv.FormatInt(p.at, 10) +
1147 ", priced from [`" + PayrollPath + "`](" + PayrollPath + ").\n\n")
1148
1149 netLo := p.gross - p.withheld - p.feeHi
1150 netHi := p.gross - p.withheld - p.feeLo
1151
1152 sb.WriteString("| | |\n|---|---|\n")
1153 sb.WriteString("| settled gross | " + strconv.FormatInt(p.gross, 10) + " |\n")
1154 sb.WriteString("| withheld by schedule | " + strconv.FormatInt(p.withheld, 10) + " |\n")
1155 sb.WriteString("| payroll accrual fee | " + strconv.FormatInt(p.feeLo, 10) +
1156 " – " + strconv.FormatInt(p.feeHi, 10) + " |\n")
1157 sb.WriteString("| net to employees | " + strconv.FormatInt(netLo, 10) +
1158 " – " + strconv.FormatInt(netHi, 10) + " |\n")
1159 sb.WriteString("| gross due, not accrued | " +
1160 strconv.FormatInt(p.dueGross, 10) + " |\n\n")
1161
1162 if netLo < 0 {
1163 sb.WriteString("> **Over-withheld.** The schedule charges more than ")
1164 sb.WriteString("the employee received after payroll's fee. That is a ")
1165 sb.WriteString("legal schedule and almost certainly a transposed or ")
1166 sb.WriteString("mis-scaled rate array — `p/tiers` cannot detect one. ")
1167 sb.WriteString("The payslip is published rather than refused so the ")
1168 sb.WriteString("evidence survives.\n\n")
1169 }
1170 if p.dueGross > 0 {
1171 sb.WriteString("> **" + strconv.Itoa(p.withDue) + " of " +
1172 strconv.Itoa(p.priced) + " lines carry gross that had completed ")
1173 sb.WriteString("and not been accrued.** It is excluded from the gross ")
1174 sb.WriteString("above, by design: this realm reports what payroll ")
1175 sb.WriteString("settled, and payroll accrues on demand.\n\n")
1176 }
1177
1178 sb.WriteString("## Coverage\n\n")
1179 sb.WriteString("| | |\n|---|---|\n")
1180 sb.WriteString("| named | " + strconv.Itoa(p.listed) + " |\n")
1181 sb.WriteString("| priced | " + strconv.Itoa(p.priced) + " |\n")
1182 sb.WriteString("| duplicates dropped | " + strconv.Itoa(p.dupes) + " |\n")
1183 sb.WriteString("| zero settled gross | " + strconv.Itoa(p.zeroGross) + " |\n")
1184 sb.WriteString("| positions ended | " + strconv.Itoa(p.ended) + " |\n")
1185 sb.WriteString("| employer's open positions then | " +
1186 strconv.Itoa(p.rosterAt) + " |\n")
1187 sb.WriteString("| **omitted from this payslip** | **" +
1188 strconv.Itoa(p.rosterAt-p.priced) + "** |\n")
1189 sb.WriteString("| open positions chain-wide then | " +
1190 strconv.Itoa(p.openAt) + " |\n\n")
1191 if p.rosterAt-p.priced > 0 {
1192 sb.WriteString("> The employer held " + strconv.Itoa(p.rosterAt) +
1193 " open positions and this payslip prices " + strconv.Itoa(p.priced) +
1194 ". `" + PayrollPath + "` exports no per-employer enumerator, so ")
1195 sb.WriteString("this realm cannot find the omitted ones — it can only ")
1196 sb.WriteString("count them. A total read without this line is being ")
1197 sb.WriteString("read wrongly.\n\n")
1198 }
1199
1200 sb.WriteString("## Schedule that priced this payslip\n\n")
1201 sb.WriteString("Edges `" + sanitize.InlineText(formatInt64List(p.sched.Edges())) +
1202 "`, rates `" + sanitize.InlineText(formatInt64List(p.sched.Rates())) +
1203 "` (of " + strconv.FormatInt(RateScale, 10) + "), " +
1204 strconv.Itoa(p.sched.NumTiers()) + " tiers. Platform fee " +
1205 strconv.FormatInt(p.feeBps, 10) + " of " +
1206 strconv.FormatInt(RateScale, 10) + ".\n\n")
1207
1208 sb.WriteString("## Lines\n\n")
1209 sb.WriteString("| employee | periods | per period | gross | withheld | fee (lo–hi) | due, unaccrued |\n")
1210 sb.WriteString("|---|---|---|---|---|---|---|\n")
1211 for _, l := range p.lines {
1212 mark := ""
1213 if l.ended {
1214 mark = " (ended)"
1215 }
1216 sb.WriteString("| `" + sanitize.InlineText(l.employee.String()) + "`" +
1217 mark + " | " + strconv.FormatInt(l.paidPeriods, 10) + " | " +
1218 strconv.FormatInt(l.perPeriod, 10) + " | " +
1219 strconv.FormatInt(l.gross, 10) + " | " +
1220 strconv.FormatInt(l.withheld, 10) + " | " +
1221 strconv.FormatInt(l.feeLo, 10) + "–" +
1222 strconv.FormatInt(l.feeHi, 10) + " | " +
1223 strconv.FormatInt(l.dueGross, 10) + " |\n")
1224 }
1225 sb.WriteString("\nGross is `periods × per period`, so every line can be ")
1226 sb.WriteString("re-derived from the two facts beside it. `TierBreakdown` ")
1227 sb.WriteString("splits a line's withholding by bracket.\n")
1228 return sb.String()
1229}