Commissa · Test run · run 2026-09-25
Commissa Test run, as shipped in the download
299 checks passed, 0 failed, when the suite that ships in the zip was run on 2026-09-25 with php tests/run.php. Run it yourself after you download.
[DB: SQLite]
== 1. flatCents: pure integer round-half-up commission ==
PASS $1000 @ 5% = $50 (got 5000, want 5000)
PASS $1234.56 @ 7% (got 8642, want 8642)
PASS flatCents(123456,700) == 8642 (got 8642, want 8642)
PASS 1c @ 50% rounds up to 1c (got 1, want 1)
PASS 1c @ 49.99% rounds to 0c (got 0, want 0)
PASS 0 volume -> 0 (got 0, want 0)
PASS 0% rate -> 0 (got 0, want 0)
PASS 100% rate -> whole (got 4200, want 4200)
PASS rate clamps above 100% (got 1000, want 1000)
== 2. Tiered payout: progressive bands, deal straddles a boundary ==
PASS cumulative $6,000 = $300 (got 30000, want 30000)
PASS cumulative $10,000 = $500 (got 50000, want 50000)
PASS cumulative $13,000 = $740 (got 74000, want 74000)
PASS cumulative $25,000 = $1,700 (got 170000, want 170000)
PASS cumulative $27,000 = $1,900 (got 190000, want 190000)
PASS straddle $6k->$13k = $440 (got 44000, want 44000)
PASS straddle $22k->$27k = $440 (got 44000, want 44000)
PASS $13k->$22k all @8% = $720 (got 72000, want 72000)
PASS $0->$6k all @5% = $300 (got 30000, want 30000)
PASS zero deal earns 0 (got 0, want 0)
== 3. Reconciliation: statement lines sum EXACTLY to the period total ==
PASS 4 lines produced (got 4, want 4)
PASS line commissions [30000,44000,72000,44000] (got array (
0 => 30000,
1 => 44000,
2 => 72000,
3 => 44000,
), want array (
0 => 30000,
1 => 44000,
2 => 72000,
3 => 44000,
))
PASS lines sum = $1,900 (got 190000, want 190000)
PASS sum == cumulative(total volume) EXACTLY (got 190000, want 190000)
PASS running commission ends at gross (got 190000, want 190000)
PASS running volume ends at total (got 2700000, want 2700000)
PASS reconciliation invariant holds across 25 tier×sequence combos (no penny lost, none negative)
-- flat reconciliation --
PASS flat lines sum == flatCents(total,733) (got 74516, want 74516)
== 4. Category scheme: per-category rate + default ==
PASS Software -> 10% (got 1000, want 1000)
PASS case-insensitive match (got 1000, want 1000)
PASS unknown -> default 4% (got 400, want 400)
PASS blank -> default (got 400, want 400)
PASS category commissions [120000,30000,6000,4000] (got array (
0 => 120000,
1 => 30000,
2 => 6000,
3 => 4000,
), want array (
0 => 120000,
1 => 30000,
2 => 6000,
3 => 4000,
))
PASS category gross = $1,600 (got 160000, want 160000)
== 5. Only WON deals earn commission ==
PASS pending deal excluded (2 lines) (got 2, want 2)
PASS only won volume counted ($2000 @10% = $200) (got 20000, want 20000)
== 6. Rate labels + tier normalisation ==
PASS single-band label (got '5%', want '5%')
PASS straddle label 5%->8% (got '5% → 8%', want '5% → 8%')
PASS band-2 only label (got '8%', want '8%')
PASS straddle 8%->10% (got '8% → 10%', want '8% → 10%')
PASS normalizeTiers sorts ascending, open band last (got array (
0 =>
array (
0 => 1000000,
1 => 500,
),
1 =>
array (
0 => 2500000,
1 => 800,
),
2 =>
array (
0 => NULL,
1 => 1000,
),
), want array (
0 =>
array (
0 => 1000000,
1 => 500,
),
1 =>
array (
0 => 2500000,
1 => 800,
),
2 =>
array (
0 => NULL,
1 => 1000,
),
))
== 7. Basis-point + cents parsing / formatting (never floats) ==
PASS parse "5" -> 500bp (got 500, want 500)
PASS parse "8.5" -> 850bp (got 850, want 850)
PASS parse "6.25" -> 625bp (got 625, want 625)
PASS parse "12.5%" -> 1250bp (got 1250, want 1250)
PASS parse "abc" -> null (got NULL, want NULL)
PASS parse "1.234" -> null (got NULL, want NULL)
PASS fmt_bp 500 -> "5%" (got '5%', want '5%')
PASS fmt_bp 625 -> "6.25%" (got '6.25%', want '6.25%')
PASS fmt_bp 850 -> "8.5%" (got '8.5%', want '8.5%')
PASS fmt_bp 0 -> "0%" (got '0%', want '0%')
PASS bp round-trip 733 (got 733, want 733)
PASS parse "$6,000.00" cents (got 600000, want 600000)
PASS fmt_cents 600000 (got '6,000.00', want '6,000.00')
PASS fmt_cents -25000 (got '-250.00', want '-250.00')
PASS clamp_bp caps at 10000 (got 10000, want 10000)
PASS clamp_bp floors at 0 (got 0, want 0)
PASS clamp_signed_cents keeps negative (got -25000, want -25000)
PASS clamp_cents floors at 0 (got 0, want 0)
PASS clamp_int caps 15-digit (got 2000000000, want 2000000000)
== 8. Period date helpers (inclusive, no off-by-one) ==
PASS monthBounds 2026-02 (leap? no) end 28 (got array (
0 => '2026-02-01',
1 => '2026-02-28',
), want array (
0 => '2026-02-01',
1 => '2026-02-28',
))
PASS monthBounds 2024-02 leap end 29 (got array (
0 => '2024-02-01',
1 => '2024-02-29',
), want array (
0 => '2024-02-01',
1 => '2024-02-29',
))
PASS monthBounds 2026-07 end 31 (got array (
0 => '2026-07-01',
1 => '2026-07-31',
), want array (
0 => '2026-07-01',
1 => '2026-07-31',
))
PASS biweekly 14-day inclusive (start..+13) (got array (
0 => '2026-05-01',
1 => '2026-05-14',
), want array (
0 => '2026-05-01',
1 => '2026-05-14',
))
PASS validRange ok (got true, want true)
PASS validRange reversed rejected (got false, want false)
PASS validRange bad date rejected (got false, want false)
== 9. CSV: numeric-aware formula-injection guard ==
PASS =SUM neutralised (got '\'=SUM(A1)', want '\'=SUM(A1)')
PASS +1 neutralised (got '\'+1', want '\'+1')
PASS -SUM neutralised (got '\'-SUM(A1)', want '\'-SUM(A1)')
PASS @cmd neutralised (got '\'@cmd', want '\'@cmd')
PASS leading TAB neutralised (got '\' x', want '\' x')
PASS -45.00 kept numeric (got '-45.00', want '-45.00')
PASS -250 kept numeric (got '-250', want '-250')
PASS 1250 kept numeric (got '1250', want '1250')
PASS plain text untouched (got 'Maya Rivera', want 'Maya Rivera')
PASS comma/quote RFC-4180 (got '"Rivera, ""the closer"""', want '"Rivera, ""the closer"""')
PASS header row (got 'Rep,Net', want 'Rep,Net')
PASS CRLF endings
PASS formula defused in body
PASS negative money stays numeric
== 10. CSRF: empty stored token must never validate ==
PASS empty stored + empty given -> REJECT
PASS empty stored + any given -> REJECT
PASS matching token -> accept
PASS mismatched -> reject
== 11. Pdf: valid, non-trivial document ==
PASS %PDF header
PASS %%EOF
PASS xref table
PASS page object
PASS non-trivial length
== 12. Payout end-to-end (SQLite throwaway DB) — hand-derived cents ==
PASS Maya rule is tiered (got 'tiered', want 'tiered')
PASS Maya won deals = 4 (pending excluded) (got 4, want 4)
PASS Maya volume = $27,000 (got 2700000, want 2700000)
PASS Maya gross = $1,900 (got 190000, want 190000)
PASS Maya lines sum to gross (got 190000, want 190000)
PASS Maya adjustments = +$500 (got 50000, want 50000)
PASS Maya NET = $2,400 (got 240000, want 240000)
PASS Lena rule tiered (global) (got 'tiered', want 'tiered')
PASS Lena gross = $350 (got 35000, want 35000)
PASS Lena net = $350 (no adj) (got 35000, want 35000)
PASS Diego rep rule flat (got 'flat', want 'flat')
PASS Diego gross = $910 (got 91000, want 91000)
PASS Diego adjustments = -$250 (got -25000, want -25000)
PASS Diego NET = $660 (got 66000, want 66000)
PASS Priya rule category (got 'category', want 'category')
PASS Priya gross = $1,560 (got 156000, want 156000)
PASS summary covers 4 reps (got 4, want 4)
PASS period gross total = $4,720 (got 472000, want 472000)
PASS period volume total = $66,000 (got 6600000, want 6600000)
PASS period adjustments net = +$250 (got 25000, want 25000)
PASS period NET total = $4,970 (got 497000, want 497000)
PASS gross + adjustments == net (reconciles) (got 497000, want 497000)
== 13. Rule resolution: rep beats global, latest effective date wins ==
PASS Maya (no rep rule) -> global tiered (got 'Standard tiered', want 'Standard tiered')
PASS Diego -> rep flat rule (got 'Diego 7%', want 'Diego 7%')
PASS Val on 2026-05-31 -> latest rep rule (9%) (got 'Val 9% (May 20)', want 'Val 9% (May 20)')
PASS Val on 2026-05-17 -> earlier rep rule (6%) (got 'Val 6% (May 15)', want 'Val 6% (May 15)')
PASS Val on 2026-05-10 -> falls back to global tiered (got 'Standard tiered', want 'Standard tiered')
PASS Val before any global rule -> null (got NULL, want NULL)
== 14. Period range inclusive; no off-by-one ==
PASS boundary days included, outside excluded (2 deals) (got 2, want 2)
PASS single-day range includes that day (got 1, want 1)
PASS empty range excludes all (got 0, want 0)
== 15. A rep with no applicable rule earns 0 but volume is shown ==
PASS no rule -> null (got NULL, want NULL)
PASS no rule -> gross 0 (got 0, want 0)
PASS no rule -> volume still surfaced ($4,000) (got 400000, want 400000)
== 16. Auth (password hashing) ==
PASS password verifies
PASS wrong password fails
PASS hash is not plaintext
== 17. Oversize/adversarial writes land clamped ==
PASS 9000-char rep name clipped to 190 (got 190, want 190)
PASS 15-digit amount clamped to 2e9 (got 2000000000, want 2000000000)
PASS product clipped to 190 (got 190, want 190)
PASS category clipped to 120 (got 120, want 120)
PASS huge negative adjustment clamped to -2e9 (got -2000000000, want -2000000000)
PASS licence: every font ships the SIL OFL text for its own family beside it
PASS licence: LICENSE.txt is present and carries the IF WE STOP (CONTINUITY) clause
--------------------------------------------------------
== 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_block_whole
PASS v2: no_rival_login_table
PASS v2: mint
PASS v2: revoke
PASS v2: totp
PASS v2: recovery
PASS v2: delivery
PASS v2: events_are_commissas
PASS v2: rate
PASS v2: csv_dry
PASS v2: backup_tables
PASS v2: upgrade_from_1x
PASS v2: payout_penny_exact
PASS v2: lines_telescope
PASS v2: adjustment_moves_net_only
PASS v2: serializer_matches_core
PASS v2: pending_excluded
PASS v2: index.php calls Database::migrate() at boot — the ONLY upgrade path for an existing install
PASS v2: routes registered (api, 2fa, security, backup, import, healthz)
PASS v2: 2FA intercepts login + login is rate-limited
PASS v2: API reuses the tested core (deal guard, deal insert, Commission::payout)
PASS v2: the deal guard is defined ONCE, in the controller both callers share
PASS v2: the API never re-adds money — no arithmetic on *_cents in the API controller
PASS v2: statementOut reads the keys Commission::statementLines actually returns
PASS v2: no ternary key re-reads (PLAN item 10)
PASS v2: a malformed date is REFUSED, not silently stamped with today
PASS v2: the deal import never invents a rep
PASS v2: dark palette + zero hardcoded white backgrounds
PASS v2/kit: dark ink flip spares ghost/danger buttons (layout brand override) and native controls follow the theme (kit)
PASS v2: theme.js + qr.js ship and layout loads theme.js + has a toggle
PASS v2: API.md ships + sw cache is v3.1.5
PASS v2: cli-server base guard present (PLAN item 9)
PASS v2: /backup.sqlite is carved out of the blanket .sqlite block in BOTH dev router and .htaccess
PASS v2: the API serializer is Commissa's, not a copied one from another product
== Own It 3.0 ==
PASS v3: every copied core is byte-identical to the shared core
PASS v3: the statement renders on the core Pdf and the legacy writer is gone
PASS v3: index.php calls Database::migrate() after the schemaExists guard
PASS v3: every column the API-keys view prints is actually selected
PASS v3: every MCP tool carries a role guard
PASS v3: admin still administers, manager records deals, viewer writes nothing
PASS v3: Database::migrate() ensures the settings singleton
PASS v3: the installer writes its settings even though the row already exists
PASS v3: v3_mcp_spec() provides every key Mcp::handle() reads
PASS v3: GET /mcp reaches the transport hint WITHOUT the key check in front of it
PASS v3: MCP reads money through Commission::, never a second computation
PASS v3: every adjustment goes through commissa_post_adjustment()
PASS v3: a dispute is decided ONCE, and the refusal says why
PASS v3: an upheld dispute records WHICH adjustment settled it
PASS v3: every admin-surface handler is permission-gated, not merely login-gated
PASS v3: commission rules are not reachable through the manager's grants
PASS v3: the role ladder means what the Users screen says it means
PASS v3: every POST route that mutates is audited
PASS v3: the audit CSV reads the same rows the screen does, with the core's writer
PASS v3: the shareable backup redacts credentials, not just the SMTP password
PASS v3: password_hash and token_hash are in the redaction list
PASS v3: restore reports what it actually wrote
PASS v3: the restore page states the JSON-vs-.sqlite trade-off
PASS v3: anonymise refuses while a rep is active or still owed money
PASS v3/v3.1: rep email exists but only through the operator's own SMTP, off by default, and never automatically
PASS v3/kit: commissa_adjust_inline() routes through the ONE adjustment writer (so the audit exemption above is earned)
PASS v3: the accent AND the AA-safe ink are published to BOTH theme selectors
PASS v3: no rule hardcodes the light value of a token the dark theme redefines
PASS v3/kit: dark mode reads its label and button ink from tokens, not literals
PASS v3: the uploaded logo is RENDERED, with a URL /media can serve
PASS v3: the statement PDF is built from Commission::payout, not re-derived
PASS v3: P6 is a clean opt-out — no Ical core shipped, no dead calendar route
PASS v3: every view gets the variables it reads from its controller
PASS v3: deploy kit ships
PASS v3: the deploy healthcheck is the frozen template's, not a hand-written substitute
PASS v3: no placeholder survived the fill
PASS v3: README documents the 3.0 release
PASS v3: API.md documents the MCP surface
PASS v3: no 2.0.0 version string survives outside the tests
PASS v3: no REST handler writes on the bare key guard
== UI kit: commission run + inline adjust + dashboard ==
PASS kit: layout loads the kit css+js, groups the nav (>=3 groups), keeps the theme toggle and a who-line
PASS kit: no theme.js; style.css is an app layer with a dark accent; pre-kit scratch file removed
PASS kit: App::asset() is versioned by mtime and the service worker revalidates code
PASS kit: no font or script is fetched from another host (fonts bundled)
PASS routes: POST /periods/{id}/reps/{rep}/adjust is registered, role-gated on adjustment.edit and CSRF-checked
PASS hero/js: the inline POST sets Content-Type — without it the browser sends text/plain and PHP never fills $_POST
PASS adjust/permission: a viewer may not adjust; a manager and an admin may
PASS hero: the run is a rep x period matrix showing the rule applied, with inline adjust wired
PASS hero: a rep with no won deals is still listed at zero rather than silently dropped
PASS dashboard: the first-run checklist and needs-your-action are computed from the DATABASE
PASS adjust/happy: a bonus lands, and the row comes back with the engine-computed net
PASS adjust/sign rule: a clawback stores NEGATIVE however the amount was typed
PASS adjust/refused: a zero amount is refused with a human sentence and writes nothing
PASS adjust/refused: an adjustment with no reason is refused — the audit trail needs the why
PASS adjust/totals: the period totals returned match a fresh engine run over the whole period
PASS adjust/totals: the returned period net equals the engine total, so the matrix cannot drift
[audit] write failed: SQLSTATE[HY000]: General error: 1 no such table: audit_log
[audit] write failed: SQLSTATE[HY000]: General error: 1 no such table: audit_log
PASS close/audit: the close writes its audit row (the period route delegates to this writer)
PASS close/happy: closing freezes the period, stamped with when
PASS close/frozen: editing a deal after the close does not change the closed statement
PASS close/frozen: the period run, the rep statement and the API read the same frozen figures
PASS close/guard: nothing is added to a closed period — the run, the form and a dispute settlement all refuse
PASS close/guard: a closed period cannot be closed twice, and a period that has not ended cannot be closed
PASS close/permission: a manager may close; only an administrator may reopen; a viewer may do neither
PASS close/routes: close and reopen are registered, and deleting or editing a closed period is refused
PASS close/reopen: a reopened period computes from today's deals again
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)
== v3.1: rep statement email + CRM import presets ==
PASS P6/happy: a raw HubSpot deal export maps without editing the header row
PASS P6/happy: a raw Pipedrive deal export maps without editing the header row
PASS P6/happy: a raw Salesforce deal export maps without editing the header row
PASS P6/happy: a raw Spreadsheet deal export maps without editing the header row
PASS P6/refusal: a file that is not a deal export is refused and NAMES the columns it needs
PASS P6/edge: a rep is still matched by name and never created from an import
PASS P1/permission: master switch off → nothing sent, nothing recorded
PASS P1/permission: the per-event flag gates too
PASS P1/refusal: a rep with no address is refused with a reason — reps have no logins, so there is nowhere else to send
PASS P1/refusal: unconfigured SMTP refuses honestly and writes no outbox row
PASS P1/happy: an enabled send is built, recorded and reported
PASS P1/content: the statement mail carries THIS rep's figures, says Commissa pays nobody, and names no other rep
PASS P1/idempotency: sending the same statement twice records two attempts and changes no figure
PASS P1/never automatic: the ONLY trigger is an explicit POST — no cron route, no send inside a status transition
PASS P1/permission: the endpoint is role-gated and CSRF-checked like every other mutation here
PASS v3.1 audit-fix: statement_email calls v3_audit(action, entity, id, detail, $me) — the $me-first call fataled live (found in review, 2026-08-20)
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 docs: README and QUICKSTART say PHP 8.1+, and the README names the Single or Extended licence
PASS demo seed: last month is closed with its statements frozen, this month is open
PASS demo guard: the hourly reseed runs after the controllers load, so the seed can close a period
PASS webhooks: a deal recorded by the AI tool sends deal.recorded
PASS webhooks: the Settings page answers and lists every event it offers
PASS webhooks: the Add, Pause, Resume and Delete buttons each reach a route and do what they say
PASS webhooks: a paused webhook is sent nothing
PASS webhooks: deal.recorded is sent by the browser action where it happens [302]
PASS webhooks: every event the Settings page offers is sent somewhere
PASS backup gate: the app answered under PHP's built-in server with no PHP error in its log
PASS backup gate: a signed-in viewer gets 403 on /backup.json and /backup.sqlite, and nothing is served
PASS backup gate: a signed-in manager gets 403 on /backup.json and /backup.sqlite, and nothing is served
PASS backup gate: the administrator gets both backups (JSON and the SQLite file)
PASS backup gate: the JSON backup leaves out the 2FA seed, SMTP password, webhook secret and SSO secret
PASS backup gate: the scheduled-backup route refuses a missing and a wrong token
PASS backup gate: a signed-in viewer gets 403 on the restore page and on POST /restore/upload, /restore/commit, /restore/token [{"page":403,"posts":{"\/restore\/upload":403,"\/restore\/commit":403,"\/restore\/token":403},"csrf":true}]
PASS backup gate: a signed-in viewer without the token is refused by the scheduled-backup route
PASS backup gate: a signed-in manager gets 403 on the restore page and on POST /restore/upload, /restore/commit, /restore/token [{"page":403,"posts":{"\/restore\/upload":403,"\/restore\/commit":403,"\/restore\/token":403},"csrf":true}]
PASS backup gate: a signed-in manager without the token is refused by the scheduled-backup route
PASS backup gate: the administrator opens the restore page and is not refused on any restore or backup-token route [{"page":200,"posts":{"\/restore\/upload":302,"\/restore\/commit":302,"\/restore\/token":302}}]
PASS backup gate: the JSON backup carries no reusable credential from any table (9 columns: api_keys.token_hash, invites.token_hash, settings.oidc_client_secret, settings.backup_token_hash, settings.smtp_pass, users.password_hash, users.totp_secret, users.totp_recovery, webhooks.secret)
PASS backup gate: each planted credential row is in the backup with the cell redacted, not dropped
PASS links: https behind a trusted TLS proxy (X-Forwarded-Proto or CF-Visitor); a direct client cannot claim it; plain http stays http
PASS links: nothing builds a scheme from $_SERVER[HTTPS] alone any more (src, controllers, views)
PASS demo ribbon: every page carries the admin wording (the product has no public page)
PASS demo ribbon: below 600px it starts as the small "Own it" pill; a tap opens the card, and that choice holds for the session
PASS demo ribbon: injected just before </body>; output with no </body> (JSON, CSV, PDF) passes through untouched
299/299 PASS, 0 FAIL