Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}