# Equipment hire — the module source.
#
# PROVENANCE: this spec did not come from a product plan. It was derived from a real engagement on
# 2026-09-18 — a construction-equipment hire company running off a whiteboard and a phone — where
# an agent with no hire module in the catalog did the understanding phase from scratch, borrowed
# vocabulary from ar and business-partner, and left its design behind (see the lifecycle: "if the
# module is one the catalog expects to exist, write its spec too, so the next engagement starts
# from something"). Every story below is a story that customer signed off.
#
# The two things that make hire different from selling, and that a generic spec would miss:
# - availability: a serialised machine can be out once; bulk stock is counted against what you own
# - the charge is a CALCULATION over time, not a price — and it is recomputed on read, never stored
id: hire
name: Equipment hire
short: Hire
version: 1
maturity: planned # derived from one engagement; no package yet — pattern §14
summary: >-
Know what is out, what is due back, what it has earned so far, and who has not paid — for a
business that hires equipment out by the day or the week.
facets:
domain: operations
industries: []
customer_words:
- "what's out and what's coming back"
- "who hasn't paid"
- "we run it off a whiteboard"
- "hire out our kit"
- "the yard"
replaces: [a whiteboard and a phone, spreadsheets, MCS, inspHire]
pillars: [sql]
first_module_fit: high
depends_on:
- module: business-partner
why: the same builder must be one record for hires, bills and payments — and for invoicing later
required: true
- module: ar
why: formal numbered invoices, statements and tax; hire's own billing is deliberately simpler
required: optional
feeds: [ar]
owns_master_data: []
references_master_data: [business_partner]
actors:
- id: owner
name: The owner
does: everything — books, returns, bills, records payments, writes off, sets rates and terms
sees: everything
permissions: [hire:hire:read, hire:hire:write, hire:return:write, hire:bill:write, hire:payment:write, hire:writeoff:write, hire:customer:write, hire:equipment:write, hire:settings:write]
- id: yard
name: Yard staff
does: books equipment out, extends, takes returns, adds a builder who turns up
sees: the board, hires, fleet and customers — never money
permissions: [hire:hire:read, hire:hire:write, hire:return:write, hire:customer:write]
- id: system
name: The system
does: works out days out, charge so far, what is unbilled, what is overdue, who owes what
sees: everything, through its own service account
permissions: []
stories:
# person
- id: HIRE-01
kind: person
actor: yard
story: As the yard, I want to book equipment out to a builder, so that the whiteboard has a replacement.
given: a builder, a start date, a due-back date, and items — a digger by fleet number, 40 scaffold boards by quantity
when: I save the hire
then: the items show as out; a serialised item already out is refused, naming the hire it is on; bulk stock is refused beyond what we own minus what is out; each line copies the day and week rate from the rate card at booking
screen: New hire
reference: not built
- id: HIRE-02
kind: person
actor: yard
story: As the yard, I want to see what is out right now, so that I know where the fleet is.
given: open hires
when: I open the board
then: every open hire shows the builder, the site, the items, the date out, the date due and days out so far, ordered by due date
screen: Board
reference: not built
- id: HIRE-03
kind: person
actor: owner
story: As the owner, I want to see what is coming back this week and what is late, so that I can chase and plan the next booking.
given: one hire due Tuesday and another that was due last Friday and is still out
when: I open the board
then: Tuesday's is under "due this week" and Friday's under "overdue" with days late — worked out from today's date, never stored
screen: Board
reference: not built
- id: HIRE-04
kind: person
actor: yard
story: As the yard, I want to record a return, so that the item can go out again and the hire can be billed.
given: an open hire with a digger and 40 boards
when: I record the return, with the quantity back and any missing or damaged count
then: those items are available again from that date, a partial return leaves the rest out, and the hire closes when every line is back
screen: Hire
reference: not built
- id: HIRE-05
kind: person
actor: yard
story: As the yard, I want to extend a hire when the builder rings to keep it longer, so that the board shows the real date.
given: a hire due Friday
when: I set the due date to next Wednesday
then: the board updates and the change is recorded with who made it and when
screen: Hire
reference: not built
- id: HIRE-06
kind: person
actor: owner
story: As the owner, I want to raise a bill for a hire, so that short jobs are billed on return and long ones are billed monthly.
given: a hire with days not yet billed
when: I raise a bill
then: it covers each line from the day after its last bill (or its start) to today or its return date, both ends counted; the amount is fixed on the bill; the due date follows the terms; a hire with nothing unbilled is refused
screen: Hire
reference: not built
- id: HIRE-07
kind: person
actor: owner
story: As the owner, I want a message I can send the builder with the bill, so that billing stays a text and a bank transfer.
given: a raised bill
when: I copy the text
then: I get the builder's name, what was on hire and for how long, the amount, the bank details and the due date, ready to paste — and the send is recorded
screen: Hire
reference: not built
customer_varies: some businesses want a real invoice here instead — that is the invoicing module, not this one
- id: HIRE-08
kind: person
actor: owner
story: As the owner, I want to see who owes me money and for how long, so that I know who to ring this morning.
given: bills with unpaid balances
when: I open the unpaid list
then: each builder shows their total outstanding, their oldest unpaid bill and days past due; a bill that is settled drops off
screen: Unpaid
reference: not built
- id: HIRE-09
kind: person
actor: owner
story: As the owner, I want to record a payment, so that a builder who settles three bills with one transfer is handled properly.
given: a transfer of 1,200 against three unpaid bills
when: I record it and spread it, oldest first
then: each bill's balance falls by its share, any leftover stays as credit on the builder, and paying more than a bill's balance is refused
screen: Unpaid
reference: not built
- id: HIRE-10
kind: person
actor: owner
story: As the owner, I want a list of my equipment, so that every hire is booked against something real.
given: the fleet
when: I add a machine by fleet number, or a bulk line with the quantity owned
then: it becomes available to book, and taking it out of service hides it from booking without losing its history
screen: Fleet
reference: not built
- id: HIRE-11
kind: person
actor: yard
story: As the yard, I want a list of the builders we deal with, so that every hire and payment hangs off the same one.
given: a new builder on the phone
when: I add them with a name and a mobile
then: they exist once — adding the same mobile again reuses the record, through the shared customer list
screen: Customers
reference: not built
- id: HIRE-12
kind: person
actor: owner
story: As the owner, I want rates and terms where I can edit them, so that a price change never needs a developer.
given: the rate card
when: I change the day rate for a machine type
then: new hires take the new rate and hires already out keep the rate they were booked at
screen: Settings
reference: not built
- id: HIRE-13
kind: person
actor: owner
story: As the owner, I want to write off a balance that will never be paid, so that it stops showing on the unpaid list.
given: a bill with a small leftover balance
when: I write it off with a reason
then: the balance clears, the write-off is recorded with who and when, and the bill drops off the list
screen: Unpaid
reference: not built
- id: HIRE-14
kind: person
actor: owner
story: As the owner, I want to see which long hires are due a bill, so that scaffolding out for months never goes unbilled.
given: a hire out 35 days with no bill, and one billed 10 days ago
when: I open the to-bill list
then: the first is listed with its unbilled days and charge so far, the second is not, and anything returned but not fully billed is always listed
screen: Unpaid
reference: not built
# system
- id: HIRE-20
kind: system
actor: system
trigger: on read (derived)
story: The system works out days out, the charge so far, what is unbilled, what is overdue and who owes what whenever anyone looks, so that nothing is stale and no overnight job can be missed.
given: a hire out since the 1st at 80 a day and 400 a week, not yet returned
when: it is opened on the 10th
then: it shows 10 days out (both ends counted) and 480 — one week plus three days, because three days at 80 is less than another week — and opening it twice gives the same answer
reference: not built
# integration
- id: HIRE-30
kind: integration
actor: yard
trigger: when a builder is added
story: The builder is created or reused in the shared customer list, so that invoicing later means the same builder.
given: business-partner is installed
when: the yard adds a builder
then: a partner exists, created or reused on the mobile number, and hire never writes the partner table itself
owner: business-partner
reference: not built
workflows:
- entity: hire
states: [out, returned]
transitions:
- { from: none, to: out, by: [owner, yard], gate: "a builder, at least one line, a start and due date, and the items available", effect: "items unavailable from the start date; rates copied onto the lines" }
- { from: out, to: out, by: [owner, yard], gate: "the new due date is not before the start", effect: "extension recorded with who and when" }
- { from: out, to: returned, by: [owner, yard], gate: "the return date is not before the start; bulk quantity back is not more than the quantity out", effect: "items free again; the hire closes when every line is back" }
note: "returned is derived — it is true when every line has a return date, not a flag somebody sets"
- entity: bill
states: [raised, paid]
transitions:
- { from: none, to: raised, by: owner, gate: "at least one unbilled day on the hire", effect: "period, amount and due date fixed; the text can be copied" }
- { from: raised, to: paid, by: system, gate: "balance reaches zero", effect: "drops off the unpaid list" }
note: >-
paid is derived: balance = amount − allocations (payments and write-offs). The charge rule is
also a calculation, not a stored price — days counted at both ends, whole weeks at the week
rate, the remainder at the day rate but never more than one more week.
entities:
- id: equipment
meaning: one machine, or a stock line of identical items
natural_key: equipment_code (the fleet number, "D-03", or the stock code, "BOARD-2.4")
key_attributes: [kind, type, description, quantity_owned, in_service]
relationships: ["-> rate by type", "0..n hire_line"]
master_data: module-owned
reference_table: hire_equipment
note: "kind is serialised (out once) or bulk (counted against quantity_owned) — the distinction the whole availability rule rests on"
- id: rate
meaning: what a type of equipment costs per day and per week
natural_key: equipment_type
key_attributes: [day_rate, week_rate]
master_data: module-owned (configuration)
reference_table: hire_rates
- id: hire
meaning: one booking, to one builder, from one date
natural_key: hire_no (H-<year>-<n>, from a high-water-mark row — there are no sequences here)
key_attributes: [bp_id, site, start_date, due_back, notes, created_by]
relationships: ["-> business_partner", "1..n hire_line", "0..n bill"]
master_data: module-owned
reference_table: hire_hires
- id: hire_line
meaning: one item, or a quantity of stock, on a hire
natural_key: [hire_no, equipment_code]
key_attributes: [quantity, day_rate, week_rate, returned_at, returned_qty, damage_note, damage_amount]
master_data: module-owned
reference_table: hire_lines
note: "the rates are copied here at booking, so a later price change never alters a hire already out"
- id: bill
meaning: a charge for a period of a hire
natural_key: [hire_no, bill_no]
key_attributes: [period_from, period_to, amount, raised_at, due_at, sent_at]
relationships: ["-> hire", "0..n allocation"]
master_data: module-owned
reference_table: hire_bills
- id: payment
meaning: money received from a builder
natural_key: payment_id (derived from the date, the builder and the bank reference, so a re-entry is not a second payment)
key_attributes: [bp_id, amount, method, received_at, reference]
master_data: module-owned
reference_table: hire_payments
- id: allocation
meaning: how much of a payment or write-off settles which bill
natural_key: [source_kind, source_id, hire_no, bill_no]
key_attributes: [amount, reason, allocated_at]
master_data: module-owned
reference_table: hire_allocations
note: "source_kind is payment | writeoff — one mechanism, as in invoicing, so the balance is one SUM"
- id: settings
meaning: terms, bank details, the monthly-billing threshold and the message wording
natural_key: key
key_attributes: [value]
master_data: module-owned (configuration)
reference_table: hire_settings
master_data:
owns: []
references:
- entity: business_partner
owner: business-partner
how: hire_hires.bp_id = business_partner.bp_id
if_absent: "the hire system keeps its own builder list, and the day formal invoicing arrives the two disagree"
warning: >-
A hire business that later wants proper invoices, statements or tax needs the same builder in
both places. Reference the shared partner from the first booking.
jobs: [] # deliberately none: every figure is derived on read (HIRE-20)
seams:
- story: HIRE-30
with: business-partner
owner: business-partner
rule: hire calls promote; it never writes the partner table
screens:
- { id: board, name: Board, stories: [HIRE-02, HIRE-03], actors: [owner, yard] }
- { id: new_hire, name: New hire, stories: [HIRE-01], actors: [owner, yard] }
- { id: hire, name: Hire, stories: [HIRE-04, HIRE-05, HIRE-06, HIRE-07], actors: [owner, yard] }
- { id: unpaid, name: Unpaid, stories: [HIRE-08, HIRE-09, HIRE-13, HIRE-14], actors: [owner] }
- { id: fleet, name: Fleet, stories: [HIRE-10], actors: [owner] }
- { id: customers, name: Customers, stories: [HIRE-11], actors: [owner, yard] }
- { id: settings, name: Settings, stories: [HIRE-12], actors: [owner] }
permissions:
namespace: hire
catalog:
- hire:hire:read
- hire:hire:write
- hire:return:write
- hire:bill:write
- hire:payment:write
- hire:writeoff:write
- hire:customer:write
- hire:equipment:write
- hire:settings:write
row_visibility:
rule: "everyone sees every hire; money is hidden from the yard by permission, not by row"
enforced: "the money screens and their routes require hire:bill:write / hire:payment:write"
not_found_not_forbidden: true
config_rows:
- { table: hire_rates, changes: "day and week rates per type of equipment" }
- { table: hire_equipment, changes: "the fleet, and what is in or out of service" }
- { table: hire_settings, changes: "payment terms, bank details for the message, how many unbilled days make a long hire due a bill, and the wording of the message" }
in_code_not_rows:
- "the charge rule: both ends counted, whole weeks at the week rate, the remainder never more than another week"
- "that a serialised item can be out once, and bulk stock never beyond what is owned"
- "that rates are copied onto a hire at booking"
- "that days out, charge so far, unbilled, overdue and balances are computed on read"
tests:
acceptance: "one per story id above, named for it"
access:
- "the yard cannot open a bill, record a payment or write anything off"
- "a request with no gateway identity is rejected"
invariants:
- "a serialised item is never on two open hires at once"
- "bulk stock out never exceeds the quantity owned"
- "10 days at 80/day with a 400 week rate bills 480, not 640"
- "a hire billed twice for the same period is refused — periods never overlap"
- "balance = amount − allocations; no stored paid flag"
- "re-entering the same bank transfer does not create a second payment"
scale:
reads: "the tables reader serves 16 concurrent reads; the board and the unpaid list are the wide reads — compute the charge in the query and page the history"
hot_paths: ["the board with days out and charge so far", "availability when booking", "the unpaid list"]
workers: none
volumes: "a fleet in the hundreds with thousands of hires a year is comfortable; the charge calculation is per line, so a very long hire is still one row"
platform_constraints:
- constraint: "Hire numbers come from a high-water-mark row, and concurrent inserts of the same key are currently all acknowledged with one row surviving."
today: "one writer issues hire and bill numbers, and the number is re-read after saving"
# What differs between customers — with something for them to react to, and where one exists,
# something they can look up.
#
# A business user can rarely recall a policy on demand and can always react to one. "Most firms
# like yours chase at 7, 14 and 30 days — does that match?" gets a truer answer, faster, than
# "when do you chase?". Read it out, let them correct it, write down what they said.
#
# Two kinds of answer, and they must never be confused:
#
# standard: a NAMED external authority the customer can look up and hold us to — a
# directive, an accounting standard, a statute. Adopt it, or decide deliberately
# to differ and know why. If a line here cannot be cited, it does not belong.
# typical: arrangements we have SEEN. No authority, no survey behind them, no implication
# that a customer doing otherwise is doing it wrong. Conversation, not evidence.
#
# Calling convention a standard is how an implementation ends up defending a choice nobody
# ever made.
customer_varies:
- question: Do you hire by the day, the week, or both — and do both ends count as days?
changes: the charge rule and the rate card
typical:
tool_and_plant_hire: day rate with a cheaper weekly rate, and both collection and return days charged
event_hire: a flat rate per booking rather than per day
warning: whether the return day is charged is the single most argued line on a hire bill
- question: Is anything hired in bulk, where you count what comes back?
changes: the availability rule, partial returns, and missing-item handling
- question: Do you bill when it comes back, or along the way for long hires?
changes: HIRE-06 and HIRE-14, and the threshold in settings
- question: Do you need proper invoices with tax, or is a message with the amount enough?
changes: whether the invoicing module is in scope at all
- question: Does anyone but you handle money?
changes: the yard role and what the money screens require
- question: Do you take deposits or charge for damage?
changes: damage lines on the bill, and whether deposits need the invoicing module
typical:
consumer_hire: a card deposit taken up front and released on return
trade_hire: no deposit for account customers; damage billed after inspection
reference:
packages: []
contracts: []
build_guide: null
known_gaps:
- "no package yet — this spec is the vocabulary a build starts from"
- "no tax, statements or numbered invoices: that is the invoicing module"
- "no deposits or damage waivers"
- "no transport or delivery scheduling, which a larger hire business will ask for"
- "no maintenance or inspection records against a machine (LOLER, PUWER) — a legal requirement in some markets"