Warrantora Manual, as shipped in the download
Warrantora — User Manual
A warranty and RMA register: what you sold, how long it is covered for, what has been claimed, what was decided and why, and where the parcel is. Version 1.0.4 [src: app/controllers/api.php:17].
About this manual
Every statement here was written by reading Warrantora's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder this docs directory sits in. If a sentence disagrees with the application, the application is right and this manual has a bug.
Nothing is described that the code does not do. Where something is deliberately limited, or currently wrong, that is said plainly rather than left out.
⭐ Expiry is computed, never stored
This is the decision that shapes the product [src: app/src/Warranty.php:5]. A stored expiry date goes stale the instant somebody corrects a sale date or edits a term, and the source is blunt about the consequence: a warranty register whose expiry column disagrees with its own inputs is worse than no register — it is a register that will be quoted at somebody in a dispute [src: app/src/Warranty.php:7].
So the expiry is worked out from the sale date and the term every time it is asked [src: app/src/Warranty.php:85]. Correct the sale date and every downstream figure moves with it.
Where it cannot be known, it says so. An item with no sale date has no expiry and never a guess [src: app/src/Warranty.php:82]:
sold 2026-08-31 -> expiry 2027-02-28
sold (none) -> expiry '' (status: unknown)
⭐ The month-end trap, and three days of free warranty
PHP's own +12 months on 31 January lands on 3 March, because it overflows a short month. A warranty sold on 31 August with a six-month term expires on 28 February, and the customer does not get three extra days because the calendar is awkward [src: app/src/Warranty.php:12].
Executed against PHP's own arithmetic:
2026-08-31 + 6m PHP: 2027-03-03 Warrantora: 2027-02-28
2026-01-31 + 1m PHP: 2026-03-03 Warrantora: 2026-02-28
2024-02-29 + 12m PHP: 2025-03-01 Warrantora: 2025-02-28
2026-11-30 + 3m PHP: 2027-03-02 Warrantora: 2027-02-28
Four dates, four wrong answers avoided. The clamp is tested on every short-month and leap-year edge [src: app/src/Warranty.php:15].
A mistyped sale date is refused rather than rolled forward. The source explains why the obvious check is not enough: createFromFormat does not reject an impossible date — it rolls 2026-02-31 to 3 March and carries on, so a typo would produce a real-looking expiry three days late. checkdate() is what actually refuses it [src: app/src/Warranty.php:39]. Executed:
2026-02-31 -> refused
2026-13-01 -> refused
not-a-date -> refused
2026-02-28 -> 2027-02-28
How the term is decided
The item's own override if it has one, otherwise the warranty's term, otherwise your fallback [src: app/src/Warranty.php:69]. Executed with a 24-month warranty and a 12-month fallback:
override 0 -> 0 months
override 36 -> 36 months
no override -> 24 months
no override, no warranty -> 12 months
An override of zero is a real answer — no cover — and is distinguished from "not set" by null, never by falsiness [src: app/src/Warranty.php:71]. That is the difference between an item you deliberately sold without warranty and one nobody has configured.
What an item's standing means
Five states [src: app/src/Warranty.php:93]. Executed with today at 2026-09-05 and a "soon" window of thirty days:
expiry 2026-09-04 -> expired "Expired" daysLeft −1
expiry 2026-09-30 -> expiring "Expiring soon" daysLeft 25
expiry 2027-06-01 -> active "In warranty" daysLeft 269
expiry 2026-09-30, voided -> void "Voided"
no sale date -> unknown "No sale date" daysLeft null
Voiding wins over everything else — a return, a cancelled sale or a mis-keyed serial takes the item out of the register's answers regardless of dates [src: app/src/Warranty.php:101].
One caution when reading the API rather than the screens: daysLeft is a pure count to the expiry date and does not consider voiding [src: app/src/Warranty.php:112], so a voided item still returns a number. Read the status, not the day count.
⭐ A claim is covered if it was covered when it was raised
Not "is it in warranty now". The source states the reason plainly: a claim raised inside the term is inside the term even if it takes three weeks to assess, and letting the clock run out during your own assessment would be a way of rejecting claims by being slow [src: app/src/Warranty.php:194].
Executed on an item sold 2025-09-10 with a twelve-month term, so cover to 2026-09-10:
raised 2026-09-05 -> covered
raised 2026-09-10 -> covered ← the expiry day itself
raised 2026-09-11 -> not covered
The claim machine, and the mandatory reason
Five statuses [src: app/src/Warranty.php:23] and six outcomes [src: app/src/Warranty.php:26]. A claim may only move where the transition table allows, and a reason is required on every step that changes the answer to the customer — assessed, approved and rejected [src: app/src/Warranty.php:153].
Executed, with the refusal each attempt actually produces:
raised -> approved (no reason) A claim cannot go from Raised to Approved.
raised -> assessed (no reason) Write what you found, and why. A decision with no reason
on it is the one that cannot be defended later.
raised -> assessed "cracked casing" ALLOWED
resolved -> raised A resolved claim stays resolved — raise a new claim if
the fault came back.
approved -> rejected "changed mind" A claim cannot go from Approved to Rejected.
raised -> raised That claim is already Raised.
Two things worth drawing out. A claim cannot jump from raised to approved — something has to be assessed first. And an approved claim cannot be flipped to rejected; the way back is a resolution, not a quiet reversal.
The RMA side is a separate machine with its own order — issued, on its way back, received, closed [src: app/src/Warranty.php:29].
Users and roles
Three roles [src: app/controllers/v3.php:20]. A viewer reads items; a member can also register items, work claims and attach evidence [src: app/controllers/v3.php:21].
Backups are administrator-only
Both backup downloads need the settings permission, which only an administrator holds — the JSON export [src: app/controllers/api.php:321] and the SQLite file [src: app/controllers/api.php:332]. Before 1.0.4 they were gated by sign-in alone, so a viewer could download the whole register.
Webhook events
Settings offers the events Warrantora sends: item.registered, claim.raised, claim.assessed, claim.approved, claim.rejected and claim.resolved [src: app/src/Webhook.php:23]. Each fires from the browser, the REST API and the agent tools alike:
| event | browser | REST | agent |
|---|---|---|---|
item.registered | [src: app/controllers/app.php:337] | [src: app/controllers/api.php:70] | [src: app/controllers/v3.php:216] |
claim.raised | [src: app/controllers/app.php:502] | [src: app/controllers/api.php:119] | [src: app/controllers/v3.php:245] |
claim.<status> | [src: app/controllers/app.php:556] | [src: app/controllers/api.php:152] | [src: app/controllers/v3.php:283] |
Before 1.0.4 Settings offered asset.created and asset.disposed — another product's events, never sent — so a receiver added from Settings heard nothing, and the agent tools fired nothing. A receiver saved that way is switched to every event on upgrade.
⚠️ The agent probe disagrees with the product about its own name and version
| surface | says |
|---|---|
| REST ping and OpenAPI | warrantora, version 1.0.4 [src: app/controllers/api.php:17] |
| the MCP specification | warrantora, version 1.0.4 [src: app/controllers/v3.php:93] |
| the unauthenticated MCP probe | Warrantora, version 3.1.1 [src: app/controllers/v3.php:310] |
The probe is the first thing an agent client sees, and it disagrees with the specification served on the very next request — on both the name's capitalisation and the version number. Match on the lower-case name and take the version from the specification. This has been raised against the product.
The API and agent access
Seven tools [src: app/controllers/v3.php:102], and they are annotated honestly: four read — the item list, an expiring report, the claim list and one claim in detail — carry readOnly true [src: app/controllers/v3.php:111], and the three that write carry it false [src: app/controllers/v3.php:193]. A client that trusts the annotation will not call a writing tool by accident.
The instructions lead with the same fact this manual does [src: app/controllers/v3.php:96]:
"An item is something you sold; its EXPIRY IS COMPUTED from the sale date plus the term and is never stored, so it is always right."
What Warrantora does not do
It does not store an expiry date [src: app/src/Warranty.php:5].
It does not guess an expiry when there is no sale date [src: app/src/Warranty.php:82].
It does not let a month-end overflow hand out extra cover [src: app/src/Warranty.php:12].
It does not accept an impossible date [src: app/src/Warranty.php:39].
It does not record a decision without a reason [src: app/src/Warranty.php:153].
It does not let your own assessment time expire a claim [src: app/src/Warranty.php:194].