Home›Rentara›Test run
Rentara · Test run · run 2026-11-07

Rentara Test run, as shipped in the download

663 checks passed, 0 failed, when the suite that ships in the zip was run on 2026-11-07 with php tests/run.php. Run it yourself after you download.

== 1. occupancy: occupied / total, notice excluded ==
  PASS  total units (got 5, want 5)
  PASS  occupied count (got 3, want 3)
  PASS  vacant count (got 1, want 1)
  PASS  notice count (got 1, want 1)
  PASS  occupancy pct = occupied/total (3/5=60) (got 60, want 60)
  PASS  empty portfolio pct is 0 (no div-by-zero) (got 0, want 0)
  PASS  empty portfolio total 0 (got 0, want 0)
  PASS  fully occupied = 100 (got 100, want 100)
  PASS  rounding 1/3 -> 33 (got 33, want 33)

== 2. arrears: unpaid AND due_date strictly before today ==
  PASS  arrears count (only #1 and #5) (got 2, want 2)
  PASS  arrears total is sum of overdue unpaid (got 235000, want 235000)
  PASS  due-today charge is NOT arrears
  PASS  paid charge is NOT arrears
  PASS  future charge is NOT arrears
  PASS  yesterday charge IS arrears
  PASS  no charges -> zero arrears (got 0, want 0)

== 3. monthTotals: collected vs due for a calendar month ==
  PASS  due this month = sum of July charges (got 455000, want 455000)
  PASS  collected this month = paid July charges (got 335000, want 335000)
  PASS  outstanding = due - collected (got 120000, want 120000)
  PASS  due count (got 3, want 3)
  PASS  paid count (got 2, want 2)
  PASS  month with no charges -> 0 due (got 0, want 0)

== 4. planCharges: idempotent monthly generation for active leases ==
  PASS  generates one charge per active lease (2) (got 2, want 2)
  PASS  ended lease produces no charge
  PASS  upcoming lease produces no charge
  PASS  due date is month-01 (got '2026-07-01', want '2026-07-01')
  PASS  amount comes from lease rent (got 145000, want 145000)
  PASS  re-running with existing charges creates nothing (idempotent) (got 0, want 0)
  PASS  only the un-charged active lease is planned (got 1, want 1)
  PASS  and it is lease 11 (got 11, want 11)
  PASS  a different month generates fresh charges (got 2, want 2)
  PASS  duplicate lease id in input -> single charge (got 1, want 1)

== 5. expiringLeases: active leases ending within N days (inclusive) ==
  PASS  three leases in the 30-day window (got 3, want 3)
  PASS  lease ending exactly today is included
  PASS  lease ending on the last day (today+30) is included
  PASS  lease ending one day past the window is excluded
  PASS  already-expired lease excluded
  PASS  non-active lease excluded
  PASS  open-ended lease excluded
  PASS  sorted soonest-first (first is today) (got 1, want 1)
  PASS  next is July 20 (id 7) before Aug 4 (id 2) (got 7, want 7)

== 6. ticket state machine: open -> in_progress -> resolved ==
  PASS  open -> in_progress allowed
  PASS  open -> resolved allowed
  PASS  in_progress -> resolved allowed
  PASS  in_progress -> open allowed
  PASS  resolved -> open allowed (reopen)
  PASS  resolved -> in_progress NOT allowed
  PASS  open -> open NOT allowed (no-op)
  PASS  unknown state NOT allowed
  PASS  moving to resolved stamps resolved_at (got '2026-07-05 12:00:00', want '2026-07-05 12:00:00')
  PASS  resolving keeps an existing stamp (got '2026-06-01 09:00:00', want '2026-06-01 09:00:00')
  PASS  reopening clears resolved_at (got NULL, want NULL)
  PASS  moving to in_progress clears resolved_at (got NULL, want NULL)

== 7. unit status flips on lease assign / end ==
  PASS  assigning a lease occupies the unit (got 'occupied', want 'occupied')
  PASS  ending a lease frees the unit (got 'vacant', want 'vacant')
  PASS  occupyUnit does not mutate the input array
  PASS  lease status derives to upcoming for a future start (got 'upcoming', want 'upcoming')
  PASS  lease status derives to active for a past/today start (got 'active', want 'active')

== 8. toCsv: RFC-4180 escaping + CRLF ==
  PASS  header row (got 'Property,Tenant,Amount,Note', want 'Property,Tenant,Amount,Note')
  PASS  plain row unquoted (got 'Maple Court,Sarah Johnson,1450.00,ok', want 'Maple Court,Sarah Johnson,1450.00,ok')
  PASS  CRLF line endings used
  PASS  field with a comma is quoted
  PASS  embedded double-quotes are doubled
  PASS  newline-containing field survives a round-trip (got 'line1
line2', want 'line1
line2')
  PASS  quoted comma field parses back correctly (got 'O\'Brien, James', want 'O\'Brien, James')
  PASS  formula = neutralized
  PASS  formula + neutralized
  PASS  formula - neutralized
  PASS  formula @ neutralized
  PASS  leading TAB neutralized
  PASS  formula with comma is neutralized AND rfc-quoted
  PASS  dangerous cell in a later column neutralized
  PASS  neutralized value carries the guard quote (got '\'=1+1', want '\'=1+1')
  PASS  a plain non-formula value is NOT altered
  PASS  negative amount exports verbatim (numeric-aware guard) (got '-1234.56', want '-1234.56')
  PASS  positive amount exports verbatim (got '1450.00', want '1450.00')
  PASS  zero exports verbatim (got '0', want '0')
  PASS  negative amount round-trips to a parseable number (got '-1234.56', want '-1234.56')

== 9. money / cents helpers ==
  PASS  money formats cents with symbol (got '$1,450.00', want '$1,450.00')
  PASS  money handles negatives (got '-$45.00', want '-$45.00')
  PASS  money zero (got '$0.00', want '$0.00')
  PASS  to_cents parses plain decimal (got 145000, want 145000)
  PASS  to_cents strips grouping + symbol (got 145050, want 145050)
  PASS  to_cents empty -> 0 (got 0, want 0)
  PASS  to_cents clamps oversize positive to INT max (got 2147483647, want 2147483647)
  PASS  to_cents clamps oversize negative to INT min (got -2147483648, want -2147483648)
  PASS  to_cents leaves an in-range amount intact (got 2100000000, want 2100000000)
  PASS  str_cap truncates to the column length (got 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', want 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
  PASS  str_cap leaves a short string intact (got 'Maple Court', want 'Maple Court')
  PASS  centsToDecimal round-trips (got '1450.00', want '1450.00')
  PASS  centsToDecimal null -> empty (got '', want '')
  PASS  currency_symbol known code (got '€', want '€')
  PASS  currency_symbol unknown code falls back (got 'SEK ', want 'SEK ')

== 10. date helpers + validators ==
  PASS  date_add_days forward (got '2026-08-04', want '2026-08-04')
  PASS  date_add_days backward (got '2026-06-30', want '2026-06-30')
  PASS  date_add_months (got '2027-07-05', want '2027-07-05')
  PASS  Rent::addDays matches (got '2026-08-19', want '2026-08-19')
  PASS  normalizeUnitStatus keeps valid (got 'notice', want 'notice')
  PASS  normalizeUnitStatus defaults invalid (got 'vacant', want 'vacant')
  PASS  normalizePriority keeps valid (got 'high', want 'high')
  PASS  normalizePriority defaults invalid (got 'med', want 'med')
  PASS  normalizeLeaseStatus defaults invalid (got 'active', want 'active')

== 11. billingPeriods: anchor day, clamping, frequency, end date ==
  PASS  mid-month start: multi-month catch-up plans 5 periods (Mar–Jul) (got 5, want 5)
  PASS  first due date IS the lease start date (got '2026-03-10', want '2026-03-10')
  PASS  first period key (got '2026-03', want '2026-03')
  PASS  later periods keep the anchor day (got '2026-06-10', want '2026-06-10')
  PASS  Jan-31 anchor clamps to Feb 28 (got '2026-02-28', want '2026-02-28')
  PASS  …back to the 31st in March (got '2026-03-31', want '2026-03-31')
  PASS  …and the 30th in April (got '2026-04-30', want '2026-04-30')
  PASS  quarterly: 3 periods Jan/Apr/Jul (got 3, want 3)
  PASS  quarterly periods land every 3rd month (got '2026-04', want '2026-04')
  PASS  quarterly keeps the anchor day (got '2026-07-15', want '2026-07-15')
  PASS  periods due after the lease end date are excluded (got 3, want 3)
  PASS  last period is the one still due before end (got '2026-05-10', want '2026-05-10')
  PASS  future start yields no periods (got 0, want 0)
  PASS  garbage start date yields no periods (got 0, want 0)

== 12. planRecurring: idempotent catch-up keyed on lease+period ==
  PASS  catch-up plans 5 + 2 periods for the two active leases (got 7, want 7)
  PASS  ended lease posts nothing
  PASS  rows carry the clear ledger label (got 'Rent 2026-03', want 'Rent 2026-03')
  PASS  amount comes from the lease (got 100000, want 100000)
  PASS  2nd catch-up run plans nothing (idempotent) (got 0, want 0)
  PASS  3rd catch-up run still plans nothing (got 0, want 0)
  PASS  legacy month-01 charge suppresses that period (no double-billing) (got 4, want 4)
  PASS  and the suppressed period is March
  PASS  planForMonth returns only the asked month (got 2, want 2)
  PASS  …with the anchored due date (got '2026-07-10', want '2026-07-10')
  PASS  planForMonth on a pre-start month is empty (got 0, want 0)
  PASS  quarterly lease skips an off-cycle month (got 0, want 0)

== 13. planLateFees: grace boundary, flat vs bp vs both, one per period ==
  PASS  due+grace day exactly -> NO fee (grace boundary) (got 0, want 0)
  PASS  due+grace+1 -> fee posts (got 1, want 1)
  PASS  flat fee amount (got 2500, want 2500)
  PASS  fee due date is deterministic (due+grace+1) (got '2026-06-07', want '2026-06-07')
  PASS  fee label names the overdue period (got 'Late fee — Rent 2026-06', want 'Late fee — Rent 2026-06')
  PASS  fee keeps the rent period key (got '2026-06', want '2026-06')
  PASS  bp fee: 5% of 1450.00 = 72.50 (got 7250, want 7250)
  PASS  flat + bp combine: 25.00 + 72.50 = 97.50 (got 9750, want 9750)
  PASS  an existing fee for the period suppresses a second one, forever (got 0, want 0)
  PASS  a PAID rent charge never gets a fee (got 0, want 0)
  PASS  late fees never compound (late_fee input ignored) (got 0, want 0)
  PASS  rule disabled (0 flat, 0 bp) -> automation off (got 0, want 0)
  PASS  zero grace: overdue by 1 day fees immediately (got 1, want 1)
  PASS  lateFeeCents clamps to the INT range (got 2147483647, want 2147483647)

== 14. statementTotals: hand-computed cash-basis fixture ==
  PASS  rent collected = 1450.00 + 1650.00 (got 310000, want 310000)
  PASS  late fees collected = 97.50 (got 9750, want 9750)
  PASS  maintenance = only the in-range resolved cost (got 40000, want 40000)
  PASS  NET = 3100.00 + 97.50 − 400.00 = 2797.50 (got 279750, want 279750)
  PASS  billed in range (all types due Jun) (got 609750, want 609750)
  PASS  still-open billed (got 190000, want 190000)
  PASS  empty statement nets zero (got 0, want 0)

== 15. Billing on a real database: 3× catch-up -> identical ledger; token flow ==
[DB: SQLite]
  PASS  3.0: migrate() created the settings singleton, so the fixture UPDATE lands
  PASS  run 1 posts 5 rent periods (Mar 15 … Jul 15) (got 5, want 5)
  PASS  run 1 posts 4 late fees (Jul rent not yet past grace) (got 4, want 4)
  PASS  late fee amount = 25.00 flat + 1% of 1000.00 (got 3500, want 3500)
  PASS  run 2 posts nothing (got 0, want 0)
  PASS  run 3 posts nothing (got 0, want 0)
  PASS  ledger after 3 runs is byte-identical to after 1 run
  PASS  ledger row count stays 9 (got 9, want 9)
  PASS  rent rows carry clear labels (got 'Rent 2026-03', want 'Rent 2026-03')
  PASS  fee rows carry clear labels (got 'Late fee — Rent 2026-03', want 'Late fee — Rent 2026-03')
  PASS  later run: only the missing Aug rent posts; paid-July gets no fee (got 1, want 1)
  PASS  …and no new fees for old periods (one per period, ever) (got 0, want 0)
  PASS  DB unique index rejects a duplicate (lease,type,period) row

-- portal token flow --
  PASS  unknown token -> null (404 path) (got NULL, want NULL)
  PASS  empty token -> null (got NULL, want NULL)
  PASS  oversize token -> null (got NULL, want NULL)
  PASS  issued token is 40 hex chars (got 1, want 1)
  PASS  valid token resolves to the lease with display context
  PASS  regenerating rotates the token
  PASS  the OLD link is dead immediately after regenerate (got NULL, want NULL)
  PASS  the new link works
  PASS  revoked token -> null (404 path) (got NULL, want NULL)

-- portal per-IP rate limit --
  PASS  5 submissions/hour from one IP -> limited
  PASS  a different IP is not limited
  PASS  empty IP never limited (proxied installs)

== 16. Database::migrate(): old-shape install upgrades in place ==
  PASS  settings gains the version marker (got 2, want 2)
  PASS  settings data intact (got 'Buyer LLC', want 'Buyer LLC')
  PASS  leases gain portal_token / frequency / auto_bill
  PASS  migrated lease keeps automation OFF (no surprise backfill) (got 0, want 0)
  PASS  migrated lease defaults to monthly (got 'monthly', want 'monthly')
  PASS  charge rows intact (got 2, want 2)
  PASS  period keys backfilled from due dates (got '2026-05', want '2026-05')
  PASS  legacy charges typed as rent (got 'rent', want 'rent')
  PASS  paid data intact (got '2026-05-02', want '2026-05-02')
  PASS  lease_documents table exists and accepts rows
  PASS  properties gain owner fields (got '', want '')
  PASS  tickets gain source (default staff) (got 'staff', want 'staff')
  PASS  late fee can share a due date with a rent charge (old index dropped) (got 2, want 2)
  PASS  second migrate() is a no-op (version short-circuits) (got 3, want 3)
  PASS  migrated lease does not auto-post (automation off by default) (got 0, want 0)

== 17. upload-name safety + honeypot + clip ==
  PASS  pdf upload gets a random name under data/uploads (got 1, want 1)
  PASS  jpeg upload name pattern (got 1, want 1)
  PASS  names are random (two calls differ)
  PASS  text/html upload rejected (got NULL, want NULL)
  PASS  image/svg+xml rejected (script-capable) (got NULL, want NULL)
  PASS  application/x-php rejected (got NULL, want NULL)
  PASS  pdf is NOT allowed for tenant photos (got NULL, want NULL)
  PASS  png IS allowed for tenant photos (got 1, want 1)
  PASS  honeypot trips on a filled website field
  PASS  honeypot silent for real users (empty/absent)
  PASS  9000-char tenant title clips to the column width (got 190, want 190)
  PASS  tenant details clip to 16000 chars (utf8mb4-safe under 64 KB) (got 16000, want 16000)

== 18. CSRF empty-token rejection + new POST handlers guarded ==
  PASS  empty stored + empty submitted CSRF token is REJECTED (no hash_equals("","") bypass)
  PASS  new POST handler portal_submit enforces CSRF
  PASS  new POST handler lease_portal_issue enforces CSRF
  PASS  new POST handler lease_portal_revoke enforces CSRF
  PASS  new POST handler lease_autobill enforces CSRF
  PASS  new POST handler document_upload enforces CSRF
  PASS  new POST handler document_delete enforces CSRF
  PASS  new POST handler ticket_cost enforces CSRF
  PASS  portal submit checks the honeypot
  PASS  portal submit enforces the per-IP rate limit
  PASS  document serve is auth-gated
  PASS  document serve sends nosniff
  PASS  ticket photo serve is auth-gated + nosniff
  PASS  statement endpoints are auth-gated
  PASS  document upload verifies MIME with finfo, not the client

== 19. statement CSV: formula injection neutralized, money stays numeric ==
  PASS  malicious property name neutralized in statement CSV
  PASS  malicious tenant name neutralized
  PASS  malicious label neutralized
  PASS  negative money survives verbatim (numeric-aware guard)
  PASS  positive money survives verbatim

== PWA ==
  PASS  manifest.json is valid JSON
  PASS  manifest has required fields
  PASS  manifest URLs are RELATIVE (subpath-install safe)
  PASS  manifest icons exist on disk (192 + 512 + apple-touch)
  PASS  sw.js never caches non-GET
  PASS  sw.js never caches page navigations (auth HTML can not enter cache storage)
  PASS  sw.js cache name carries the version (old caches purged on activate)
  PASS  offline fallback page ships
  PASS  every app layout links the manifest + registers the SW

-- Own It 2.0 --
  PASS  v2: tables
  PASS  v2: totp_cols
  PASS  v2: smtp_cols
  PASS  v2: v20_survives_version_guard
  PASS  v2: mint
  PASS  v2: revoke
  PASS  v2: totp
  PASS  v2: recovery
  PASS  v2: delivery
  PASS  v2: rate
  PASS  v2: csv_dry
  PASS  v2: csv_commit
  PASS  v2: csv_dup_skips
  PASS  v2: payment_rule
  PASS  v2: payment_fallbacks
  PASS  v2: ticket_rules
  PASS  v2: arrears_computed
  PASS  v2: backup_tables
  PASS  v2: routes registered (api, 2fa, security, backup, import, healthz)
  PASS  v2: 2FA intercepts login + login is rate-limited
  PASS  v2: notifications + webhooks fire on the BROWSER path (incl. the tenant portal), never fatally
  PASS  v2: API reuses the tested core (no re-derived guards)
  PASS  v2: the UI posts payments through the SAME rule as the API
  PASS  v2: dark palette + no hardcoded white backgrounds outside @media print
  PASS  v2: theme.js + qr.js ship and layout loads theme.js + a toggle
  PASS  v2: shared modules are copy-identical to the shared core (never forked per app)
  PASS  v2: API.md ships (the sw cache version is asserted in the 3.0 block)

== Own It 3.0: MCP - roles - GDPR - iCal - PDF - the migrate trap ==
  PASS  migrate() creates audit_log + invites + saved_views
  PASS  users gain role; settings gain branding, SSO and the calendar token; tenants gain anonymized_at
  PASS  install() ALONE produces the current schema
  PASS  migrate() creates the settings singleton row
  PASS  so "UPDATE settings ... WHERE id = 1" is no longer a silent no-op
  PASS  **THE TRAP: migrate() returns early on a current install — the v3 block still runs**
  PASS  a member records payments, works tickets, edits leases, issues portal links
  PASS  but cannot delete a tenant or property, or reach settings
  PASS  a viewer reads the rent roll and statements and cannot record or edit
  PASS  a viewer canNOT read the audit trail or the team roster
  PASS  an admin holds deletions and the late-fee rule
  PASS  AN UPGRADE NEVER LOOSENS: the existing user stays an admin
  PASS  the last admin cannot be demoted into a lockout
  PASS  tickets move open -> in_progress -> resolved
  PASS  **resolved -> open is ALLOWED (the intentional reopen the row names)**
  PASS  but resolved -> in_progress is not a legal move
  PASS  resolving stamps resolved_at
  PASS  and reopening clears it
  PASS  MCP exposes exactly the row's seven tools, in order
  PASS  every tool description declares READ-ONLY or WRITES
  PASS  GET /mcp is refused with 405, not 404
  PASS  a nested argument that is not allowlisted is refused
  PASS  an unknown argument is refused BY NAME
  PASS  lease_detail returns the ledger and the outstanding balance
  PASS  **arrears_report EQUALS Rent::arrears() — never re-derived**
  PASS  and it finds the overdue charges with the right total
  PASS  rent_roll EQUALS Rent::monthTotals()
  PASS  create_ticket raises a ticket with a normalized priority
  PASS  advance_ticket moves a ticket forward
  PASS  **an agent may REOPEN a resolved ticket, exactly as the UI can**
  PASS  and an illegal move is refused with the allowed ones named
  PASS  MCP role gate matches the UI: a viewer key cannot raise a ticket
  PASS  and a viewer key can still read arrears
  PASS  MCP writes land in the audit trail
  PASS  the fixture has a live tenancy AND money owed (the blocked case)
  PASS  subject access returns the person
  PASS  **anonymize really blanks name, email and phone (verified on the ROW)**
  PASS  **and the tenancy stays — it is the record of a tenancy, not of a person**
  PASS  and it reports what it touched
  PASS  the statement renders as a real PDF
  PASS  and its totals come from Rent::statementTotals()
  PASS  the calendar token verifies and a wrong one does not
  PASS  the feed is a real VCALENDAR with stable UIDs
  PASS  **the calendar feed carries no tenant name or email**
  PASS  the backup drops bcrypt password hashes
  PASS  and API-key hashes
  PASS  and the SMTP password
  PASS  **and the tenant portal token — a live magic link into a ledger**
  PASS  while still carrying the estate itself
  PASS  and a restore keeps the live token rather than blanking it
  PASS  inkOn() clears 4.5:1 on a very light and a very dark accent
  PASS  a PHP script named .png is refused
  PASS  another product's backup is refused by name stamp
  PASS  OIDC refuses alg=none
  PASS  mutations write audit entries
  PASS  the trail exports as CSV
  PASS  **no audit row contains a tenant portal token**
  PASS  exactly ONE place fires payment.recorded
  PASS  exactly ONE place fires maintenance.status_changed
  PASS  the ticket transition guard is called from one place
  PASS  all four payment doors go through _rn_payment_recorded()
  PASS  all four ticket doors go through _rn_ticket_status_apply()
  PASS  no door invents an event name nobody subscribes to
  PASS  bulk actions email and fire webhooks AFTER the commit
  PASS  the two tenant-facing toggles exist
  PASS  a fresh install writes to no tenant
  PASS  a dead mail server returns false, never an exception
  PASS  no address (or a malformed one) is skipped, not an error
  PASS  the receipt reaches the tenant: amount, date, what is left
  PASS  **the receipt never asks a tenant to pay online**
  PASS  a status change reaches the tenant who raised it
  PASS  both messages carry ONE well-formed portal link
  PASS  a refused transition writes nothing and says why
  PASS  saving the org card no longer erases the SMTP config
  PASS  absolute URLs are built once — no doubled scheme, views included
  PASS  Settings shows and saves the tenant-facing switches

== Own It 3.0: wiring (static) ==
  PASS  Database::migrate() runs at boot
  PASS  POST and GET /mcp are both routed
  PASS  src includes are idempotent
  PASS  migrateV3() is called before migrate()'s fast-path return
  PASS  install() also reaches the v3 schema
  PASS  the maintenance bulk literal is registered BEFORE /maintenance/{id}
  PASS  the MCP layer computes no arrears, rent totals or transitions of its own
  PASS  the statement PDF reuses the screen's own scope/range/rows helpers
  PASS  bulk pay loops Billing::recordPayment() inside one transaction
  PASS  the roll's pay endpoint delegates to Billing::recordPayment and never writes the ledger itself
  PASS  the roll's pay endpoint is role-gated (403) and CSRF-checked before any write
  PASS  the roll's pay endpoint maps refusals to honest codes and audits the payment
  PASS  the rent roll is unit-centric: every unit is a row, vacant ones included
  PASS  the roll keeps its bulk-pay and saved views (no feature lost to the redesign)
  PASS  no bulk action mass-updates the ledger behind the product's back
  PASS  bulk ticket moves are validated per ticket, not per batch
  PASS  the GDPR map uses the key the core actually reads
  PASS  anonymizing is refused while a tenancy is active or money is owed
  PASS  the audit trail never receives a raw portal token
  PASS  destructive + money actions are role-gated
  PASS  the REST surface is role-gated too
  PASS  money mutations write audit entries
  PASS  the backup redaction list covers the portal token
  PASS  BOTH backup paths use that one list
  PASS  filled controls take their ink from the branding contrast helper
  PASS  the uploaded logo is actually RENDERED in the layout
  PASS  the branding accent overrides BOTH themes
  PASS  P5 is reachable and does not nest forms on the rent roll
  PASS  P5 is reachable on the maintenance list too
  PASS  the tenant page exposes subject access and anonymization
  PASS  the PDFs are reachable from the screens that own them
  PASS  src/Mcp.php is byte-identical to the shared core copy of Mcp.php
  PASS  src/AuditLog.php is byte-identical to the shared core copy of AuditLog.php
  PASS  src/Pdf.php is byte-identical to the shared core copy of Pdf.php
  PASS  src/Ical.php is byte-identical to the shared core copy of Ical.php
  PASS  src/BackupRestore.php is byte-identical to the shared core copy of BackupRestore.php
  PASS  src/Branding.php is byte-identical to the shared core copy of Branding.php
  PASS  src/RolesKit.php is byte-identical to the shared core copy of RolesKit.php
  PASS  src/Oidc.php is byte-identical to the shared core copy of Oidc.php
  PASS  src/Gdpr.php is byte-identical to the shared core copy of Gdpr.php
  PASS  a switched-off person's API key stops working (u.active in requireKey)
  PASS  sw.js cache is static-v4.1.3
  PASS  healthz + api_ping + openapi report 4.1.3
  PASS  API.md documents the MCP endpoint and the reopen rule
  PASS  README documents "New in 3.0"
  PASS  the deploy kit is filled in
  PASS  nav: every sidebar link is highlighted by the page it opens
  PASS  nav: no page highlights a sidebar item that does not exist
  PASS  ip: a direct visitor's forged headers are ignored
  PASS  ip: behind a trusted proxy the forwarded visitor is used
  PASS  ip: CF-Connecting-IP wins over the X-Forwarded-For chain
  PASS  ip: the chain is read right-to-left, past trusted hops
  PASS  ip: a Cloudflare edge is not a trusted proxy by default
  PASS  ip: CIDR maths (v4 boundaries + v6, no cross-family match)
  PASS  scope: api_keys gains a scope column defaulting to full — an upgrade never loosens
  PASS  scope: requireKey reads the key scope and refuses a write on a read-only key
  PASS  scope: mintKey persists the chosen scope, and anything but read is full access
  PASS  scope: /mcp hands the key scope to the core, which refuses write tools per tool
  PASS  scope: the key form lets an administrator mint a read-only key
  PASS  license: every font ships the SIL OFL text for its own family beside it
  PASS  license: LICENSE.txt is present and carries the IF WE STOP (CONTINUITY) clause

== fix wave: the landlord hears when a lease is ending ==
  PASS  lease end setting and ledger exist
  PASS  lease end is off on a fresh install
  PASS  lease end needs smtp and an address
  PASS  lease end dry run lists only active leases inside 60 days
  PASS  lease end a refused send releases its claims
  PASS  lease end one message to the landlord listing both
  PASS  lease end the same stage is never sent twice
  PASS  lease end the 14 day stage comes once more
  PASS  lease end a renewed lease starts over
  PASS  lease end is audited
  PASS  lease ends: the viewer is refused the check-now button (403)
  PASS  lease ends: the member is refused the check-now button (403)
  PASS  lease ends: an admin presses the button and is told what happened
  PASS  lease ends: the switch and the schedule line are on the Email settings
  PASS  lease ends: cron/lease-ends.php runs the one function and refuses anything but the command line
  PASS  lease ends: .htaccess refuses the cron/ script, src/ and data/
  PASS  lease ends: .htaccess lets the /cron/reminders route reach the app (the manual's daily cron line)
  PASS  lease ends: the dev router refuses cron/ files only, not the /cron/reminders route
  PASS  ui: the sidebar brand cannot be shrunk to nothing (flex-shrink:0)

== fix wave: the webhook buttons work ==
  PASS  webhooks: Add creates the endpoint (no 404)
  PASS  webhooks: Pause switches it off (no 404)
  PASS  webhooks: pressing it again switches it back on
  PASS  webhooks: the page offers Pause and Delete for the endpoint, at those same addresses
  PASS  webhooks: Delete removes it (no 404)
  PASS  readme: no feature is still labeled "new in 1.1"

== fix wave: every backup and restore route is administrator-only ==
  PASS  backup: a signed-in viewer gets 403 on GET /backup.json
  PASS  backup: a signed-in viewer gets 403 on GET /backup.sqlite
  PASS  backup: a signed-in viewer gets 403 on GET /restore
  PASS  backup: a signed-in viewer gets 403 on POST /restore/preview
  PASS  backup: a signed-in viewer gets 403 on POST /restore/commit
  PASS  backup: a signed-in viewer gets 403 on POST /backup/token
  PASS  backup: a signed-in viewer without the token gets nothing from GET /backup/scheduled
  PASS  backup: a signed-in member gets 403 on GET /backup.json
  PASS  backup: a signed-in member gets 403 on GET /backup.sqlite
  PASS  backup: a signed-in member gets 403 on GET /restore
  PASS  backup: a signed-in member gets 403 on POST /restore/preview
  PASS  backup: a signed-in member gets 403 on POST /restore/commit
  PASS  backup: a signed-in member gets 403 on POST /backup/token
  PASS  backup: a signed-in member without the token gets nothing from GET /backup/scheduled
  PASS  backup: an admin still opens the restore page

-- F2b: the demo ribbon's wording and size; the JSON backup's secrets --
  PASS  demo ribbon: on the owner link (/o/…) it says what the page is, not "signed in as the admin"
  PASS  demo ribbon: on the tenant link (/t/…) it says what the page is, not "signed in as the admin"
  PASS  demo ribbon: an administrator's page keeps the admin wording
  PASS  demo ribbon: below 600px it starts as the small "Own it" pill; a tap opens it and the choice holds for the session
  PASS  demo ribbon: registered only behind the .demo-mode marker, and non-HTML output passes through untouched
  PASS  backup: the JSON download carries none of the 16 credentials this schema holds (settings.oidc_client_secret, settings.backup_token_hash, settings.ical_token_hash, settings.smtp_pass, settings.stripe_secret_key, settings.stripe_webhook_secret, users.password_hash, users.totp_secret, users.totp_recovery, tenants.sms_stop_token, leases.portal_token, invites.token_hash, backup_state.s3_secret, api_keys.token_hash, webhooks.secret, owner_links.token_hash)

== 3.2 · part payments: a payment splits a charge; the rest stays owed ==
  PASS  3.2 schema: the charge key is (lease, type, period, part), and the old three-column key is gone
  PASS  part payment: 1,500.00 of 2,100.00 — the charge keeps the paid 1,500.00, a balance row carries 600.00, same period and due date
  PASS  part payment: collected + outstanding still add up to what was charged (4,200.00)
  PASS  part payment: the message says what is still owed on that charge
  PASS  part payment: a part of the balance splits again (part 2), without breaking the unique key
  PASS  part payment: more than the charge is refused (spread a larger payment on the lease instead)
  PASS  lease payment: 1,000.00 pays the oldest first — the 500.00 January balance in full, then 500.00 of February
  PASS  lease payment: a receipt row is kept for it
  PASS  lease payment: more than is owed is refused and changes nothing
  PASS  lease payment: nothing, or a negative amount, is refused
  PASS  the catch-up guarantee holds: a second part-0 rent row for the same period is still refused by the database
  PASS  late fees: a period with a paid part and an unpaid balance gets one fee, not one per row

== 3.2 · the tenant's pay button (the landlord's own Stripe) ==
  PASS  card payment is OFF on a fresh install
  PASS  with a key, a signing secret and a card currency, it is ready (charged in the landlord's currency, usd)
  PASS  checkout: the lease's whole balance (2,400.00), the lease as reference, back to the tenant's own page
  PASS  checkout: Stripe is asked with the landlord's key, and the tenant goes to Stripe's page
  PASS  webhook: Stripe's signature verifies; a wrong secret or a stale event does not
  PASS  webhook: a paid checkout is recorded oldest charge first (March in full, 300.00 of April)
  PASS  webhook: Stripe retrying the same event records nothing more
  PASS  webhook: another currency is refused
  PASS  webhook: a lease that does not match the metadata is ignored
  PASS  webhook: paying more than is owed by then applies what is owed and writes the excess on the payment for the landlord
  PASS  backup + audit: both Stripe secrets are on the redaction lists

== 3.2 · rent reminders and the landlord's digest ==
  PASS  reminders: every kind is OFF on a fresh install, and a run writes nothing
  PASS  reminders: the preview lists exactly who gets one today — rent coming up (Cara) and 3 days late (Dev) — and writes nothing
  PASS  reminders: a tenancy first seen long in arrears gets nothing today (past every step's window), never a burst
  PASS  reminders: a paused lease and a tenant with no address are held, with the reason
  PASS  reminders: the run sends those two (demo: recorded, no socket), each claimed with its reference
  PASS  reminders: a second run the same day sends nothing
  PASS  reminders: a tenant with two charges coming up gets ONE note today; the other waits, with the reason
  PASS  reminders: the overdue note states what is owed and since when, links the ledger, and calls itself a reminder, not a notice
  PASS  digest: the landlord's list goes once a day (demo: recorded), and again today says done
  PASS  digest: it lists every tenancy in arrears (Dev, Eli, Fay, Gil among them), paused or not — it goes to the landlord, not a tenant
  PASS  reminders: through a real SMTP conversation, Hana's "rent coming up" leaves, to her, with the amount and the date
  PASS  reminders: a refused send is recorded as failed, gives its claim back (tomorrow retries) and ends the run

== 3.2 pass B · late fees: per day, and a cap ==
  PASS  late: the cap is the lower of a fixed amount and a percent of the rent, and absent when neither is set
  PASS  late: the per-day fee counts each day after the grace days, until the rent is paid in full
  PASS  late: the per-day fee counts only from the day it was switched on, and never reaches rent settled before then
  PASS  late: the one-off fee is held under the cap too
  PASS  late: the one-off fee, then the daily fee growing one row, and all of it held at the cap
  PASS  late: rent paid three days late earns three days, and the fee stops there
  PASS  late: once a payment has reached the daily fee, the next days are a new part and the paid one is untouched
  PASS  late: the per-day fee counts as a fee on the owner statement

== 3.2 pass B · the owner portal ==
  PASS  owner: a link only for an owner named on a property
  PASS  owner: the link opens its owner, a new one stops the old, and a withdrawn one opens nothing
  PASS  owner: months and their ranges
  PASS  owner: only their properties, with the rent owed today
  PASS  owner: the monthly email names the month and carries the link
  PASS  owner: the monthly note goes once a month to an owner with an email and a live link, and not at all while it is off

== 3.2 · the doors, through the real front controller ==
  PASS  door: a viewer cannot record a payment (403)
  PASS  door: a member records 1,250.00 against the lease — January in full, 250.00 of February, 750.00 still owed
  PASS  api: POST /api/leases/{id}/payments with amount_cents and no charge_id pays the lease oldest-first through Billing::applyPayment
  PASS  door: the lease page lists the payment and offers "Record a payment" while something is owed
  PASS  door: POST /hooks/stripe with no signature is refused 400
  PASS  door: the tenant's page shows no card button while card payment is off
  PASS  door: "Pay by card" while it is off asks Stripe for nothing and says so
  PASS  door: the reminders preview opens for an admin and sends nothing; a member is refused
  PASS  door: /cron/reminders without the token is refused
  PASS  door: Settings carries both 3.2 sections

== 3.2 pass B · the owner portal and the late-fee rule, through the front controller ==
  PASS  door: the owner link opens their statement with no login, out of search engines, and nothing of another owner
  PASS  door: the owner downloads the month as CSV
  PASS  door: a wrong owner link is a 404 that names nobody
  PASS  door: a viewer cannot make an owner link; a member can
  PASS  door: only an administrator switches the monthly owner email on
  PASS  door: Settings saves the per-day fee and the cap, dates the day it was switched on, keeps that day when the amount changes, and clears it when switched off

————————————————————————————————————————————————————

== 3.3 · a bank statement matched to rent, and the units import's preview ==
  PASS  bank statement: dates read day-first or month-first as you say, ISO dates always, an impossible date refused [sqlite]
  PASS  bank statement: money in is read (with thousands and a currency sign); money out and a number it cannot read are left out [sqlite]
  PASS  bank statement: money in only, one fixed reference per line (identical lines apart), an unreadable line named [sqlite]
  PASS  bank statement: proposed by the tenant's name; refused when two tenancies fit, when it is more than owed, or when nothing fits [sqlite]
  PASS  bank statement: with no name, an amount only one tenancy owes is proposed and flagged to check [sqlite]
  PASS  bank statement: a confirmed line is recorded like a payment typed in (oldest first), as a transfer from the bank, once [sqlite]
  PASS  bank statement: the same statement again shows what is already recorded [sqlite]
  PASS  bank statement: more than is owed is refused, as for a payment typed in [sqlite]
  PASS  installing again over a database that holds a part payment works, and keeps the split (the demo reset runs it) [sqlite]
  PASS  units import preview: it counts exactly what the import brings in, names the same skipped rows, and writes nothing [sqlite]
  PASS  units import preview: a unit that exists, one twice in the file and a row with no property are each named [sqlite]
  PASS  units import preview: the upload step uses the import's own handler in normal mode, never the dry-run flag [sqlite]
  PASS  bank statement door: a viewer cannot open it or record from it (403)
  PASS  bank statement door: an administrator gets the upload form, and recording with no statement uploaded records nothing
  PASS  bank statement door: the rent roll links to it for someone who records rent

== rent texts: the landlord's own SMS gateway, only to tenants who agreed ==
  PASS  texts: off on a new install; when switched on the defaults are 3 days before and the due day, the late-fee text, 09:00 to 20:00 [sqlite]
  PASS  texts: while they are off nothing is sent and nothing is claimed, even for a tenant who agreed [sqlite]
  PASS  texts: a new tenant has not agreed to texts [sqlite]
  PASS  texts: a gateway address that is not https never switches texts on [sqlite]
  PASS  consent: agreeing needs a phone number a text can go to [sqlite]
  PASS  consent: the agreement records when and who, and makes the tenant's stop link [sqlite]
  PASS  consent: of two tenants with rent coming up, only the one who agreed gets a text [sqlite]
  PASS  consent: the agreement is a condition inside both queries that pick who gets a text [sqlite]
  PASS  texts: each one names the business, says what is due and when, and ends with the stop link [sqlite]
  PASS  texts: the tenant's own link rides along, with no card offer while card payment is off [sqlite]
  PASS  texts: the log keeps the words, never the tenant's link or stop link [sqlite]
  PASS  exactly once: a second run the same day sends nothing [sqlite]
  PASS  exactly once: the claim is one row under a UNIQUE key (lease, rent period, step); the database refuses a second [sqlite]
  PASS  exactly once: a text another run already claimed is not sent [sqlite]
  PASS  exactly once: each step goes once per rent period, and again for next month's rent [sqlite]
  PASS  texts: rent first seen inside a later step gets that step's text only, never a run of them [sqlite]
  PASS  texts: paid rent gets no text [sqlite]
  PASS  texts: a paused lease and the daily cap are held, each with its reason [sqlite]
  PASS  send window: outside it nothing goes or is claimed, and the page says why [sqlite]
  PASS  send window: when it opens, the waiting text goes [sqlite]
  PASS  send window: within one day only; a window from 22:00 to 06:00 is never open [sqlite]
  PASS  send window: hours saved the wrong way round fall back to 09:00 to 20:00; nothing goes at 23:30 or 02:00 [sqlite]
  PASS  send window: the default 09:00 to 20:00 is a valid window, not a fallback [sqlite]
  PASS  late fee: a fee posted by Rentara's own late-fee rule gets one factual text with what is owed now [sqlite]
  PASS  late fee: the text goes once per rent period [sqlite]
  PASS  late fee: a fee posted before the last three days is never texted [sqlite]
  PASS  late fee: several old months posted at once get one text, the rest covered by it [sqlite]
  PASS  late fee: the per-day fee is texted as an amount a day [sqlite]
  PASS  late fee: the text has its own switch [sqlite]
  PASS  texts: at most one a day per tenancy; the due-day text first, the late-fee text the next day [sqlite]
  PASS  pay link: with card payment on, the text carries the tenant's pay-by-card link [sqlite]
  PASS  pay link: a lease with no tenant link gets no account line, only the stop link [sqlite]
  PASS  field mapping: JSON with to, from, text and a Bearer key by default [sqlite]
  PASS  field mapping: Twilio's To, From, Body, form-encoded, with Basic auth [sqlite]
  PASS  field mapping: a key in the address sends no header; a blank field name falls back [sqlite]
  PASS  phone numbers: cleaned to digits with a leading +, nonsense refused, masked to the last four [sqlite]
  PASS  settings: days before the due date and send times are read strictly [sqlite]
  PASS  failure: a refusal is logged with what the gateway answered, and audited [sqlite]
  PASS  failure: a refusal stops texts for the rest of the day, and the page says so [sqlite]
  PASS  failure: a failed text is not tried again the same day, even after a test text starts texts again [sqlite]
  PASS  failure: on a later day, while it is still due, the failed text is tried again by taking back its own row (UPDATE … WHERE failed and earlier) [sqlite]
  PASS  exactly once: once a text went, it is never sent again [sqlite]
  PASS  consent: the agreement and the charge are read again before each text, so a stop or a payment during a run stops it [sqlite]
  PASS  audit: a sent text is audited with the last four digits only [sqlite]
  PASS  failure: an unreachable gateway is never fatal [sqlite]
  PASS  test text: one that goes through is logged and starts texts again the same day [sqlite]
  PASS  test text: works before texts are switched on; tenants still get none [sqlite]
  PASS  demo: a text is recorded and no connection is opened, a test text included [sqlite]
  PASS  texts ride the reminders run (dashboard and cron), with email reminders off [sqlite]
  PASS  erasure: no more texts, no stop link, and the number leaves the text log [sqlite]
  PASS  stop: after a stop no further text goes, and the stop is recorded [sqlite]
  PASS  door: the tenant link offers no texts while texts are off
  PASS  door: a viewer cannot change the text settings or send a test (403)
  PASS  door: an http:// gateway address is refused and nothing is saved
  PASS  door: a send window with the same time twice, or running past midnight (22:00 to 06:00), is refused and nothing is saved
  PASS  door: an administrator switches texts on with the gateway, the steps and the window
  PASS  door: the Settings card shows texts on, keeps the key write-only, and offers a test text
  PASS  door: saving again with the key left blank keeps it
  PASS  door: with texts on, the tenant's own link offers rent texts at the masked number, off until they ask
  PASS  door: the tenant switches texts on from their link; it is recorded as theirs, with the time and a stop link
  PASS  door: opening the stop link shows a Stop button and changes nothing (a link preview cannot stop texts)
  PASS  door: an unknown stop link is a 404
  PASS  door: pressing Stop ends the texts and records when
  PASS  door: the tenant form shows the stop and asks for a new agreement before texts go again
  PASS  door: the office ticks "they agreed to texts" on the tenant form; it is recorded with who ticked it
  PASS  door: unticking it on the form stops the texts
  PASS  door: ticking it with no phone number switches nothing on, and says why
  PASS  door: the Reminders page lists today's texts beside the email reminders
  PASS  backup: the SMS gateway key is left out of backups and audit details
  PASS  front controller: src/Sms.php and controllers/texts.php are loaded, and the five routes are there
  PASS  subject access: the tenant's data download lists the rent reminder emails and the rent texts sent to them
  PASS  erasure: anonymizing a tenant blanks the address on the reminder emails sent to them (dates and subjects stay), stops their texts and blanks the number in the text log; the landlord's own digest row keeps its address

== 4.0: the Update button is wired ==
  PASS  updates: the release public key is pinned
  PASS  updates: Database::migrate() creates the updater table (in code, not in a comment)
  PASS  updates: the updater table holds exactly one state row
  PASS  updates: index.php clears stale compiled code before anything else loads
  PASS  updates: index.php finishes or rolls back an update before any controller loads, with this app's canary
  PASS  updates: route get /updates is updates_page()
  PASS  updates: route post /updates/key is updates_key_save()
  PASS  updates: route post /updates/check is updates_check()
  PASS  updates: route post /updates/apply is updates_apply()
  PASS  updates: updates_page() opens with the restore page's admin gate
  PASS  updates: updates_key_save() opens with the restore page's admin gate
  PASS  updates: updates_check() opens with the restore page's admin gate
  PASS  updates: updates_apply() opens with the restore page's admin gate
  PASS  updates: every POST checks the CSRF token
  PASS  updates: the page is told why updates are off here (Updater::whyOff, F5 A1 pass)
  PASS  updates: the menu links to the page
  PASS  updates: bin/update.php refuses the web and finishes in a fresh process
  PASS  updates: where updates are off (Cloud, the demo, a missing extension) the page says why and offers no button
  PASS  updates: with a key and a newer signed release the page offers "Back up and update"

== 4.0 A2: sample data, backups, health ==
  PASS  A2: the owner's-install checks ran to the end
  PASS  A2: an owner's own install (no Cloud config, no demo marker)
  PASS  A2: a fresh install's sample tables are all empty, so the installer's sample data can go in
  PASS  A2: switched off: an install without "Start with sample data" gets none
  PASS  A2: first request: the marker is used once and the sample business is there
  PASS  A2: every one of the owner's settings is theirs, not the sample's
  PASS  A2: the sample logins cannot sign in: no password matches, and switched off where the product can
  PASS  A2: the owner's own login is untouched
  PASS  A2: the load is in the audit log
  PASS  A2: a second marker on a Rentara with records seeds nothing and changes nothing
  PASS  A2: real use: one of the owner's own rows among the sample
  PASS  A2: remove: the owner's own row stays, and a sample row it points at is kept
  PASS  A2: remove: the report says what went, and what was kept because the owner's records use it
  PASS  A2: remove: the rest of the sample is gone
  PASS  A2: remove: every kept sample row keeps all its own parts
  PASS  A2: remove: no row is left pointing at a row that was removed
  PASS  A2: remove: the sample logins are gone but the ones kept rows use, the owner stays, and nothing counts as sample any more
  PASS  A2: remove: the sample's files are gone from disk (2 written; one a row that stays names stays with it)
  PASS  A2: remove: the owner's own uploaded file is untouched
  PASS  A2: "Load demo data" on a Rentara with records refuses and deletes nothing
  PASS  A2: backup: written, sent to the bucket (a signed PUT), and read back
  PASS  A2: backup: the bucket secret is not in the backup it protects
  PASS  A2: backup: under the standard keep count (14) every backup is kept
  PASS  A2: switched off: with automatic backups off, a backup is never due however old the last one
  PASS  A2: the backup lines for Backup & restore: last backup fine, offsite fine, read-back fine
  PASS  A2: an offsite endpoint must be https for an owner (plain http only in tests)
  PASS  A2: all 25 owner's-install checks reported
  PASS  A2: Database::migrate() creates the sample-data and backup tables (in code, not in a comment)
  PASS  A2: index.php loads the three cores and the wiring
  PASS  A2: the first request seeds the installer's sample, and an owner's install backs itself up when due
  PASS  A2: route post /sample-data/remove is sample_data_remove()
  PASS  A2: route post /restore/backups is backups_save()
  PASS  A2: route post /restore/backup-now is backup_now()
  PASS  A2: route get /health is health_page()
  PASS  A2: sample_data_remove() opens with the restore page's admin gate
  PASS  A2: backups_save() opens with the restore page's admin gate
  PASS  A2: backup_now() opens with the restore page's admin gate
  PASS  A2: health_page() opens with the restore page's admin gate
  PASS  A2: every POST checks the CSRF token
  PASS  A2: off an owner's install (Cloud, the demo) the backup settings and "Back up now" refuse in words
  PASS  A2: the installer offers sample data, ticked, and only leaves the first-run marker (with every table's row count)
  PASS  A2: the first run is strictly once (a marker that cannot be removed seeds nothing), and treats the installer's rows as its defaults only while nothing has changed
  PASS  A2: the scheduled-backup URL runs the same backup (offsite copy and read-back included)
  PASS  A2: "Load demo data" outside the public demo goes through SampleData (only into an empty Rentara, removable)
  PASS  A2: the menu links to Health
  PASS  A2: the sample-data banner shows only to an admin, only while sample data is there, with its Remove button, and stays put (nested: the UI kit toasts a bare .main > .alert away)
  PASS  A2: Backup & restore shows the automatic backups, "Back up now" and the bucket settings (none on Cloud)
  SKIP  A2: the Health page render (this suite loads no view helpers in its own process; the owner's-install check and update_e2e.sh run the real app)
  PASS  erase planting · sqlite: the subject is created and a marker is planted in all 8 personal columns
  PASS  erase planting · sqlite: the app's own erase runs through its route and reports no failure
  PASS  erase planting · sqlite: no planted marker survives anywhere (28 tables dumped), outside the 1 column(s) kept by ruling (possible legal records)
  PASS  erase planting · sqlite: the erase page (/tenants/1) says what an erase keeps: "Kept: the documents filed on their lease, with their titles …"
  PASS  erase planting · sqlite: MANUAL.md says the same: "Kept: the documents filed on their lease, with their titles …"
ALL 663 TESTS PASSED (0 failed)
663 assertions

← Back to Rentara · Manual · Quickstart · API