# Equipment hire — module spec (generated from modules/hire/module.yaml)
# A starting vocabulary for phase 2: correct it with the customer, then write ERP_HIRE.md.
schema: dodil.module-spec/v1
id: hire
name: Equipment hire
short: Hire
version: 1
maturity: planned
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:
  - 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
  - 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
  - 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: []
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
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
lifecycle: https://blog.dodil.io/modules/lifecycle.md
module_pattern: https://blog.dodil.io/modules/pattern.md
design_template: https://blog.dodil.io/modules/template.md
