dodil/erp-method

node_modules/.cache/erp-method/v0.1.21/modules/hire/module.yaml

# 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"