{
  "format": "mxscout-descriptions",
  "version": 1,
  "project": "Dockyard",
  "language": "en",
  "by": "Claude, mendix-describe skill",
  "written": "2026-10-16",

  "app": {
    "summary": "Dockyard keeps a marine service company's customers at sea. It turns an alarm from an engine room, or a call from a skipper, into a work order, plans a technician to go on board, follows the visit on the technician's phone, takes the spare parts used out of stock and invoices the work.",
    "story": "Dockyard is the working day of a company that services ships: tugs, ferries, workboats and the yachts of a few demanding owners. Its customers are shipping companies and owners; what they buy is time at sea. Every hour a vessel lies in port with a broken generator costs them money, so the whole application is built around one question — how fast does a problem on board become a technician on board, with the right part in their bag.\n\nWork comes in two ways. The engine-room monitoring systems on the vessels send alarms on their own, through a published REST service; a serious alarm opens a work order without anyone touching it. Dispatchers open the rest by hand, from the machine a skipper called about. Either way the work order is triaged: its priority sets the time it is due by, and a critical one — a vessel out of service — calls the duty manager straight away.\n\nThe dispatcher then plans a visit: a technician and a day, refused when that technician's day is already full. The visit gets a checklist made for the kind of machine. On board the technician works from a phone app that runs offline: they start the visit (the phone records where it is), tick the checks off, take photos, record the parts they used, and press Finish. Finishing closes the work order, takes the parts out of the warehouse — asking the storekeeper to order more when a part runs low — and drafts the invoice.\n\nAccountants check the draft and send it to the external accounting system. Behind all of it a scheduled job watches the service level: every fifteen minutes it marks the work orders that went past their due date and tells the duty manager.\n\nWhat the model does not show: where the application runs, which database it uses, how the phone app is distributed to the technicians, and who looks after the accounting system on the other side. A few things in it deserve attention before the next release — the alarm service takes requests from anyone, a live key for the accounting system is written into the model, and the parts used are never put on the invoice. They are on the Risks page.",
    "audience": [
      { "who": "Dispatchers", "what": "Triage incoming work orders and plan the technicians' visits." },
      { "who": "Technicians", "what": "Go on board with the phone app: checklists, photos, parts used." },
      { "who": "Service managers", "what": "Watch the service level, are called for critical work, may cancel a job." },
      { "who": "Storekeepers", "what": "Keep the spare parts in stock and order what runs low." },
      { "who": "Accountants", "what": "Check the draft invoices and send them to accounting." },
      { "who": "Monitoring systems", "what": "Engine-room systems on the vessels that report alarms by themselves." }
    ],
    "processes": [
      {
        "name": "From an engine alarm to a work order",
        "summary": "A monitoring system on board reports an alarm; Dockyard finds the machine, and a serious alarm becomes a triaged work order without anyone touching it.",
        "story": "It starts in an engine room at sea. The vessel's monitoring system notices something — a generator running hot, oil pressure dropping — and sends an alarm to Dockyard's alarm service, with the serial number of the machine, a code and a message.\n\nDockyard looks the serial number up. An alarm for a machine it does not know is written to the log as a warning and goes no further: someone has to register the machine first. For a known machine it looks at the severity. Information and warnings are only recorded; an alarm or a shutdown opens a work order on the spot, for that machine and the vessel it is on, with the alarm's message as its title — critical when the machine shut down, high otherwise.\n\nThe new work order is triaged at once, exactly as a dispatcher would triage it: it gets the time it is due by, and when it is critical the duty manager is e-mailed. The alarm is then marked handled and linked to its work order, so a dispatcher opening the Alarms page sees what became of each one.",
        "steps": [
          "The vessel's monitoring system sends an alarm to the alarm service",
          "Dockyard finds the machine by its serial number",
          "An unknown machine is written to the log, and the alarm stops there",
          "A serious alarm opens a work order for the machine and its vessel",
          "The work order is triaged and, when critical, the duty manager is told",
          "The alarm is marked handled and linked to its work order"
        ],
        "flows": ["Integration.REST_Alarm_Post", "Maintenance.ACT_WorkOrder_Triage", "Maintenance.SUB_NotifyDutyManager"],
        "diagram": [
          { "step": "The monitoring system on board sends an alarm", "who": "Monitoring system" },
          { "step": "Finds the machine by its serial number", "by": "app" },
          { "decision": "Is the machine known?", "ways": [
            { "label": "no", "then": [{ "step": "Writes a warning to the log", "by": "app" }], "end": "Nothing more happens" },
            { "label": "yes" }
          ] },
          { "decision": "How serious is it?", "ways": [
            { "label": "info or warning", "end": "Recorded only" },
            { "label": "alarm or shutdown" }
          ] },
          { "step": "Opens a work order for the machine and its vessel", "by": "app" },
          { "step": "Triages it — due date, and the duty manager for a critical one", "by": "app" },
          { "end": "Alarm handled, work order waiting for a dispatcher" }
        ]
      },
      {
        "name": "Triage and planning a visit",
        "summary": "A dispatcher triages a work order — its priority sets when it is due — and plans a technician on a day, which Dockyard refuses when that day is full.",
        "story": "Not every problem comes from an alarm. A skipper calls; the dispatcher opens the vessel, finds the machine on board and presses Report a problem. Dockyard gives the work order the next number of the year and opens it to be filled in.\n\nTriage is one button. Dockyard refuses it while the work order says nothing about what is wrong. Otherwise the priority decides the rest: a critical job is due in four hours and the duty manager is e-mailed, a high one is due tomorrow, anything else in five days. The work order is then triaged and waits to be planned.\n\nPlanning is a small window: a technician — only active ones are offered — and a day. Dockyard counts the visits that technician already has that day and refuses when the day is full, saying how many there are. Otherwise it plans the visit, makes its checklist — engine checks for an engine or a generator, electrical checks for the navigation electronics, and always a last check to photograph the work and write down what was done — and marks the work order scheduled. The visit appears on the technician's phone.",
        "steps": [
          "A dispatcher reports a problem from the machine's page, or an alarm opened the work order",
          "The dispatcher describes the problem and presses Triage",
          "Dockyard sets the due date from the priority, and calls the duty manager for a critical job",
          "The dispatcher chooses a technician and a day",
          "Dockyard refuses a day that is already full for that technician",
          "Dockyard plans the visit, makes its checklist and marks the work order scheduled"
        ],
        "flows": ["Maintenance.ACT_WorkOrder_New", "Maintenance.SUB_NextWorkOrderNumber", "Maintenance.ACT_WorkOrder_Triage", "Maintenance.ACT_Technician_Assign", "Maintenance.SUB_Checklist_Create"],
        "diagram": [
          { "step": "Reports the problem from the machine's page", "who": "Dispatcher" },
          { "step": "Describes it and presses Triage", "who": "Dispatcher" },
          { "decision": "What priority?", "ways": [
            { "label": "critical", "then": [{ "step": "Due in 4 hours; e-mails the duty manager", "by": "app" }] },
            { "label": "high", "then": [{ "step": "Due tomorrow", "by": "app" }] },
            { "label": "normal or low", "then": [{ "step": "Due in 5 days", "by": "app" }] }
          ] },
          { "step": "Chooses a technician and a day", "who": "Dispatcher" },
          { "decision": "Is that day full?", "ways": [
            { "label": "yes", "end": "Refused — choose another day or technician" },
            { "label": "no" }
          ] },
          { "step": "Plans the visit and makes its checklist", "by": "app" },
          { "end": "Scheduled — on the technician's phone" }
        ]
      },
      {
        "name": "The visit on board",
        "summary": "The technician works the visit from a phone that runs offline: starts it, ticks the checklist, takes photos, records the parts and finishes — which closes the work order, books the parts and drafts the invoice.",
        "story": "The technician's phone shows their open visits, soonest first, even with no signal in the harbour. On board they press Go on board: the phone records the start time and, when it can, where it is — a technician without a position can still start.\n\nThe visit screen holds the checklist made when the visit was planned. Each check is one tick. Photos are taken with the phone's camera and kept with the visit; a photo the technician cancels is not kept. Parts used are added from the parts in stock, with a quantity. What was done goes into the notes.\n\nFinish is only offered once the visit has started. Dockyard refuses to close a visit with checks still open and says how many are left. Otherwise it records the finish time and the hours worked — rounded to the quarter hour — takes every part used out of the warehouse, marks the work order done and drafts its invoice. When a part falls below its minimum stock, the storekeeper is asked to order more. The phone goes back to the list of visits.",
        "steps": [
          "The technician opens their visits on the phone and goes on board",
          "The phone records the start time and the position",
          "The technician ticks off the checklist, takes photos and records the parts used",
          "The technician presses Finish",
          "Dockyard refuses while checks are open",
          "Dockyard records the hours, books the parts, closes the work order and drafts the invoice",
          "A part that runs low is put on the storekeeper's list to order"
        ],
        "flows": ["Maintenance.DS_Visits_Mine_Offline", "Maintenance.NF_Visit_Start", "Maintenance.NF_Checklist_Toggle", "Maintenance.NF_Photo_Take", "Maintenance.NF_Visit_Finish", "Maintenance.ACT_Visit_Complete", "Inventory.SUB_PartUsage_Book", "Inventory.SUB_PurchaseRequest_Create"],
        "diagram": [
          { "step": "Goes on board; the phone records time and position", "who": "Technician" },
          { "step": "Ticks the checklist, takes photos, records parts", "who": "Technician" },
          { "step": "Presses Finish", "who": "Technician" },
          { "decision": "Is the checklist done?", "ways": [
            { "label": "no", "end": "Refused — says how many checks are open" },
            { "label": "yes" }
          ] },
          { "step": "Records the hours worked", "by": "app" },
          { "step": "Takes the parts out of stock; asks for more of what runs low", "by": "app" },
          { "step": "Closes the work order and drafts the invoice", "by": "app" },
          { "end": "Visit done" }
        ]
      },
      {
        "name": "Invoicing the work",
        "summary": "A finished work order becomes a draft invoice for the vessel's owner; an accountant checks it and sends it to the accounting system.",
        "story": "The invoice is drafted the moment a visit is finished. Dockyard finds the vessel's owner and stops when that customer is blocked for credit: no invoice is made, and a warning is written to the log for someone to pick up. For every other customer it makes a draft with one labour line per visit — the hours worked at the hourly rate — adds the lines up, numbers the invoice after its work order and marks the work order invoiced.\n\nAn accountant opens the invoice, checks it and presses Send to accounting, which is only offered for a draft. Dockyard renders the invoice as a PDF and posts it to the external accounting system. When that succeeds the invoice is sent and the accountant is told so.\n\nTwo things about this process are on the Risks page: the parts used on a visit are looked up but never put on the invoice, and when the accounting system does not answer, the accountant is not told — the invoice simply stays a draft.",
        "steps": [
          "A finished visit drafts the invoice for the vessel's owner",
          "A customer blocked for credit gets no invoice; a warning goes to the log",
          "Dockyard adds a labour line per visit and the total",
          "The accountant checks the draft and sends it",
          "Dockyard renders a PDF and posts it to the accounting system"
        ],
        "flows": ["Billing.SUB_Invoice_CreateFromWorkOrder", "Billing.ACT_Invoice_Send"],
        "diagram": [
          { "step": "A visit is finished", "by": "app" },
          { "decision": "Is the customer blocked for credit?", "ways": [
            { "label": "yes", "then": [{ "step": "Writes a warning to the log", "by": "app" }], "end": "No invoice" },
            { "label": "no" }
          ] },
          { "step": "Drafts the invoice with a labour line per visit", "by": "app" },
          { "step": "Checks the draft and presses Send", "who": "Accountant" },
          { "step": "Renders the PDF and posts it to accounting", "by": "app" },
          { "end": "Sent" }
        ]
      },
      {
        "name": "Keeping the service level",
        "summary": "Every fifteen minutes Dockyard marks the work orders that went past their due date and e-mails the duty manager about each one.",
        "story": "A work order's due date is set at triage, from its priority. Nobody has to watch the clock: every fifteen minutes a scheduled job looks for open work orders — not done, not invoiced, not cancelled — that went past their due date and are not yet marked late.\n\nEach one is marked late, which every screen that shows the work order makes visible, and the duty manager receives an e-mail about it. When the mail relay cannot be reached the failure is written to the log and the job carries on. At the end the job writes how many work orders went late in that run.",
        "steps": [
          "Every fifteen minutes the job looks for open work orders past their due date",
          "Each one is marked late",
          "The duty manager is e-mailed about each one",
          "The job writes how many went late"
        ],
        "flows": ["Maintenance.SCH_SLA_Escalate", "Maintenance.SUB_NotifyDutyManager"]
      },
      {
        "name": "Keeping spare parts in stock",
        "summary": "A part that drops below its minimum is put on the storekeeper's list to order — once, however many visits use it.",
        "story": "Every part has a minimum stock. When a finished visit takes a part below it, Dockyard asks the storekeeper to order twice the minimum — unless a request for that part is already open, so the same part is not ordered twice. The storekeeper sees what to order on the Spare parts page, under To order.\n\nThere is meant to be a second way in: a nightly job that goes through every part below its minimum, also the ones no visit took, for example after a stock count. That job is switched off in the model, so today only the parts a visit used are ever reordered.",
        "steps": [
          "A finished visit takes a part below its minimum",
          "Dockyard asks to order twice the minimum, unless a request is already open",
          "The storekeeper orders what is on the list"
        ],
        "flows": ["Inventory.SUB_PartUsage_Book", "Inventory.SUB_PurchaseRequest_Create", "Inventory.SCH_Stock_Reorder"]
      }
    ]
  },

  "modules": {
    "Fleet": {
      "summary": "The customers, their vessels and every machine on board, with its running hours and when it is due for service.",
      "story": "Fleet is the register everything else hangs on. A customer owns vessels, known by their IMO number; a vessel carries equipment — main engines, generators, thrusters, pumps, hydraulics and navigation electronics — each with a serial number, its running hours and the date it is next due for service.\n\nTechnicians update the running hours on board, and Dockyard opens a service job by itself when a machine is due within two weeks. Editors keep the register; everyone else reads it. A technician sees only a customer's name and phone number.",
      "notes": [
        { "title": "Key entities", "table": { "columns": ["Entity", "Role in the business"], "rows": [
          ["Fleet.Customer", "A shipping company or owner; may be blocked for credit"],
          ["Fleet.Vessel", "A ship, by its IMO number, owned by a customer"],
          ["Fleet.Equipment", "A machine on board, by its serial number, with its running hours"]
        ] } },
        { "title": "Rules", "items": ["Running hours only go up: Fleet.ACT_Equipment_LogHours refuses a lower number.", "An IMO number and a serial number are unique."] }
      ]
    },
    "Maintenance": {
      "summary": "The heart of Dockyard: work orders from report to done, their triage, the technicians' visits and the phone app they work from.",
      "story": "A work order is one job on one machine. It is triaged — its priority sets when it is due — planned as one or more visits of a technician, and worked on board from the phone app. Its status follows it: new, triaged, scheduled, in progress, waiting for parts, done, invoiced, or cancelled.\n\nThe phone app is the module's nanoflows: they run on the device, offline, and call the server only to close a visit. A scheduled job watches the due dates and tells the duty manager about every job that goes late.",
      "notes": [
        { "title": "Key entities", "table": { "columns": ["Entity", "Role in the business"], "rows": [
          ["Maintenance.WorkOrder", "One job, from report to invoice"],
          ["Maintenance.Visit", "A technician on board on a planned day"],
          ["Maintenance.ChecklistItem", "One check of a visit"],
          ["Maintenance.Technician", "A service engineer, linked to their account"],
          ["Maintenance.AssignTechnician", "The planning window's choice — not stored"]
        ] } },
        { "title": "Naming and layout", "items": ["ACT_ flows are started by a button; SUB_ flows are called by other flows; SCH_ by a scheduled event; DS_ are data sources; NF_ run on the phone.", "Folders follow the work: Work orders, Planning, Visits, Mobile, Scheduled, Notifications."] },
        { "title": "Integrations", "items": ["E-mail to the duty manager through the Java action Maintenance.SendEmail.", "The phone's position and camera through JavaScript actions."] }
      ]
    },
    "Inventory": {
      "summary": "The spare parts in the warehouse, the parts each visit used, and what to order when stock runs low.",
      "story": "Every part has a code, a shelf, a price, the number in stock and a minimum. A visit records the parts used on board; finishing the visit takes them out of stock. A part that falls below its minimum is put on the storekeeper's list to order, once.\n\nA nightly job that would reorder every part below its minimum exists but is switched off.",
      "notes": [
        { "title": "Key entities", "table": { "columns": ["Entity", "Role in the business"], "rows": [
          ["Inventory.Part", "A spare part in the warehouse"],
          ["Inventory.PartUsage", "Parts used on one visit"],
          ["Inventory.PurchaseRequest", "A part the storekeeper should order"]
        ] } }
      ]
    },
    "Billing": {
      "summary": "Invoices drafted from finished work orders, checked by an accountant and sent to the accounting system.",
      "story": "An invoice is drafted for the vessel's owner the moment the work is done: a labour line per visit and the total. Accountants check it and send it to the external accounting system, as a PDF and through a REST call.\n\nA customer blocked for credit gets no invoice; the draft is simply not made.",
      "notes": [
        { "title": "Key entities", "table": { "columns": ["Entity", "Role in the business"], "rows": [
          ["Billing.Invoice", "What one finished work order costs the customer"],
          ["Billing.InvoiceLine", "Labour or a part, with its amount"]
        ] } },
        { "title": "Integrations", "items": ["POST to the accounting system at Billing.AccountingUrl, from Billing.ACT_Invoice_Send.", "The PDF is made by the Java action Billing.RenderInvoicePdf."] }
      ]
    },
    "Integration": {
      "summary": "The alarm service the vessels' monitoring systems call, and what each alarm became.",
      "story": "One published REST service, api/alarms, takes an alarm at a time from the monitoring systems on board. Dockyard finds the machine by its serial number and opens a work order for a serious alarm. Every alarm is kept, marked handled and linked to the work order it opened, for the dispatchers' Alarms page.",
      "notes": [
        { "title": "Security", "items": ["The service fleet_alarms names no role and asks for no authentication: see the Risks page."] }
      ]
    }
  },

  "microflows": {
    "Integration.REST_Alarm_Post": "Takes one alarm from a vessel's monitoring system: finds the machine by its serial number and, for an alarm or a shutdown, opens and triages a work order.",
    "Maintenance.ACT_WorkOrder_New": "Opens a new work order for a machine and its vessel, with the next number, and shows it to the dispatcher to fill in.",
    "Maintenance.SUB_NextWorkOrderNumber": "Works out the next work order number of the year, WO-yyyy-nnnnn.",
    "Maintenance.ACT_WorkOrder_Triage": "Triages a work order: refuses one without a title, sets the due date from the priority and e-mails the duty manager for a critical one.",
    "Maintenance.SUB_NotifyDutyManager": "E-mails the duty manager about a work order; a failure is logged and the caller carries on.",
    "Maintenance.ACT_Technician_Assign": "Plans a visit for a technician on a day, unless that day is already full, and makes its checklist.",
    "Maintenance.SUB_Checklist_Create": "Makes a visit's checklist from the kind of machine: engine or electrical checks, and always the closing report.",
    "Maintenance.ACT_Visit_Complete": "Closes a visit whose checklist is done: hours worked, parts booked, work order done, invoice drafted.",
    "Maintenance.ACT_WorkOrder_Cancel": "Cancels a work order and deletes all its visits. Only a service manager may.",
    "Maintenance.SCH_SLA_Escalate": "Every fifteen minutes: marks open work orders past their due date as late and tells the duty manager about each.",
    "Maintenance.DS_Visits_Mine": "The signed-in technician's open visits from today on. Nothing uses it: the phone reads its own list.",
    "Maintenance.ACT_WorkOrder_Duplicate_OLD": "Copies a work order. Replaced by ACT_WorkOrder_New and no longer called from anywhere.",
    "Inventory.SUB_PartUsage_Book": "Takes the parts used on a visit out of stock, and asks to order a part that falls below its minimum.",
    "Inventory.SUB_PurchaseRequest_Create": "Asks the storekeeper to order twice a part's minimum, unless a request for it is already open.",
    "Inventory.SCH_Stock_Reorder": "Asks for every part below its minimum. Its scheduled event is switched off.",
    "Billing.SUB_Invoice_CreateFromWorkOrder": "Drafts the invoice for a finished work order — a labour line per visit and the total — unless the customer is blocked for credit.",
    "Billing.ACT_Invoice_Send": "Renders a draft invoice as a PDF and sends it to the accounting system.",
    "Fleet.ACT_Equipment_LogHours": "Records a machine's running hours and opens a service job when it is due within two weeks."
  },
  "nanoflows": {
    "Maintenance.NF_Visit_Start": "On the phone: starts a visit, with the time and the phone's position when it has one, and opens the visit screen.",
    "Maintenance.NF_Checklist_Toggle": "On the phone: ticks a check off, or back on.",
    "Maintenance.NF_Photo_Take": "On the phone: takes a photo of the work and keeps it with the visit; a cancelled photo is dropped.",
    "Maintenance.NF_Visit_Finish": "On the phone: asks the server to close the visit and goes back to the list when it did.",
    "Maintenance.DS_Visits_Mine_Offline": "On the phone: the visits not finished yet, soonest first, from what is synced to the device."
  },
  "pages": {
    "Maintenance.WorkOrder_Overview": "The dispatchers' list of open work orders, with priority, due date and whether it went late.",
    "Maintenance.WorkOrder_Edit": "One work order: what is wrong, its priority and machine, its visits and — for a manager — its invoice.",
    "Maintenance.Technician_Assign": "The small window where a dispatcher chooses a technician and a day.",
    "Maintenance.Visit_MyList": "The technician's phone: their open visits.",
    "Maintenance.Visit_Mobile": "The technician's phone on board: checklist, parts used, notes, photo and Finish.",
    "Inventory.PartUsage_NewMobile": "The technician's phone: adds a part used, from the parts in stock.",
    "Fleet.Vessel_Overview": "Every vessel we service, with its owner and home port.",
    "Fleet.Vessel_Detail": "One vessel and the machines on board; a dispatcher reports a problem from here.",
    "Inventory.Part_Overview": "The warehouse: all parts, those running low, and what to order.",
    "Billing.Invoice_Overview": "Every invoice, with its customer, total and status.",
    "Billing.Invoice_Detail": "One invoice with its lines; an accountant sends a draft from here.",
    "Integration.Alarm_Overview": "Every alarm the vessels sent, and whether it was handled."
  },
  "entities": {
    "Fleet.Customer": "A shipping company or owner whose vessels we service.",
    "Fleet.Vessel": "A ship we service, known by its IMO number.",
    "Fleet.Equipment": "A machine on board, with its running hours and next service date.",
    "Maintenance.WorkOrder": "One job on one machine, from report to invoice.",
    "Maintenance.Visit": "A technician on board on a planned day.",
    "Maintenance.Technician": "A service engineer, linked to their account.",
    "Maintenance.ChecklistItem": "One check or task of a visit.",
    "Maintenance.VisitPhoto": "A photo taken on board, kept with its visit.",
    "Maintenance.AssignTechnician": "The planning window's choice of technician and day, not stored.",
    "Inventory.Part": "A spare part in the warehouse.",
    "Inventory.PartUsage": "Parts used on a visit.",
    "Inventory.PurchaseRequest": "A part the storekeeper should order.",
    "Billing.Invoice": "What a finished work order costs the customer.",
    "Billing.InvoiceLine": "One line of an invoice.",
    "Integration.Alarm": "An alarm a vessel's monitoring system sent."
  },

  "risks": [
    {
      "id": "p1-open-alarm-service",
      "level": "P1", "theme": "access",
      "title": "Anyone who can reach the app can open work orders through the alarm service",
      "detail": "The published REST service fleet_alarms names no role and asks for no authentication, and Integration.REST_Alarm_Post runs without entity access. Whoever knows a machine's serial number can open critical work orders — each one e-mailing the duty manager — or flood the dispatchers' list.",
      "where": ["Integration.REST_Alarm_Post", "Integration.Alarm"],
      "fix": "Give the service Basic or custom authentication, allow only the MonitoringSystem role, and give each monitoring system its own account."
    },
    {
      "id": "p1-accounting-key-in-model",
      "level": "P1", "theme": "secrets",
      "title": "A live accounting API key is written into the model, and sent in the address",
      "detail": "The constant Billing.AccountingApiKey holds a key that looks like a live one, so everyone with the model has it. Billing.ACT_Invoice_Send then puts it in the query string of the call, where web servers and proxies write it to their logs.",
      "where": ["Billing.ACT_Invoice_Send"],
      "fix": "Leave the constant empty in the model and set it per environment; send the key in an Authorization header. Rotate the key that is in the model now."
    },
    {
      "id": "p1-technicians-read-all-invoices",
      "level": "P1", "theme": "access",
      "title": "Technicians can read every invoice of every customer",
      "detail": "The access rule on Billing.Invoice for Maintenance.Technician has no XPath, while no page a technician reaches shows an invoice. The phone app's client can still fetch them all, with their totals.",
      "where": ["Billing.Invoice"],
      "fix": "Remove the Technician rule on Billing.Invoice; nothing a technician does needs it."
    },
    {
      "id": "p2-parts-never-invoiced",
      "level": "P2", "theme": "logic",
      "title": "The parts used on a visit are never put on the invoice",
      "detail": "Billing.SUB_Invoice_CreateFromWorkOrder retrieves the parts used on each visit inside its loop but only makes a labour line; nothing turns the parts into invoice lines. Every invoice is short by the parts' price.",
      "where": ["Billing.SUB_Invoice_CreateFromWorkOrder", "Inventory.PartUsage"],
      "fix": "Loop over the parts used and add a line per part at its unit price, before the total is added up."
    },
    {
      "id": "p2-invoice-send-fails-silently",
      "level": "P2", "theme": "silent",
      "title": "When the accounting system does not answer, nobody is told",
      "detail": "The REST call in Billing.ACT_Invoice_Send has a custom error handler that goes straight to an end event: no message for the accountant, nothing in the log, and the invoice stays a draft as if Send was never pressed.",
      "where": ["Billing.ACT_Invoice_Send"],
      "fix": "Log the error, set the invoice to Sending failed and show the accountant a message."
    },
    {
      "id": "p2-stock-reorder-switched-off",
      "level": "P2", "theme": "schedule",
      "title": "Parts no visit used are never reordered: the nightly reorder is switched off",
      "detail": "The scheduled event Stock_Reorder, which runs Inventory.SCH_Stock_Reorder, is switched off in the model. A part that drops below its minimum any other way — a stock count, a return — stays below it.",
      "where": ["Inventory.SCH_Stock_Reorder"],
      "fix": "Switch the event on, or remove it and say in the storekeepers' instructions that low parts have to be checked by hand."
    },
    {
      "id": "p2-commits-in-loops",
      "level": "P2", "theme": "loops",
      "title": "Late work orders and booked parts are committed one at a time",
      "detail": "Maintenance.SCH_SLA_Escalate commits each late work order inside its loop and e-mails the duty manager once per order; Inventory.SUB_PartUsage_Book commits each part and each usage inside its loop. After a quiet weekend that is a long run of single commits and a full mailbox.",
      "where": ["Maintenance.SCH_SLA_Escalate", "Inventory.SUB_PartUsage_Book"],
      "fix": "Change inside the loop without committing, commit the list once after it, and send the duty manager one e-mail listing the late orders."
    },
    {
      "id": "p2-cancel-deletes-finished-visits",
      "level": "P2", "theme": "delete",
      "title": "Cancelling a work order deletes its finished visits too",
      "detail": "Maintenance.ACT_WorkOrder_Cancel deletes every visit of the work order, also those already worked — with their hours, notes and photos — while the Visits tab only offers Remove for a visit that has not started.",
      "where": ["Maintenance.ACT_WorkOrder_Cancel"],
      "fix": "Delete only the visits that have not started, and refuse to cancel a work order with finished visits."
    },
    {
      "id": "p3-hourly-rate-hardcoded",
      "level": "P3", "theme": "hardcoded",
      "title": "The hourly rate is written into the invoice flow",
      "detail": "Billing.SUB_Invoice_CreateFromWorkOrder prices labour at 95 written in the flow, while the constant Billing.HourlyRate exists and nothing reads it. A new rate set in the constant changes nothing.",
      "where": ["Billing.SUB_Invoice_CreateFromWorkOrder"],
      "fix": "Use @Billing.HourlyRate in both the unit price and the amount."
    },
    {
      "id": "p3-unused-flows",
      "level": "P3", "theme": "dead",
      "title": "Two flows nothing calls",
      "detail": "Maintenance.ACT_WorkOrder_Duplicate_OLD was replaced by ACT_WorkOrder_New, and Maintenance.DS_Visits_Mine by the phone's own list; nothing in the model reaches either.",
      "where": ["Maintenance.ACT_WorkOrder_Duplicate_OLD", "Maintenance.DS_Visits_Mine"],
      "fix": "Delete both, after checking that no other app calls them."
    }
  ],

  "c4": {
    "context": {
      "nodes": [
        { "id": "dispatchers", "name": "Dispatchers", "note": "triage and plan", "kind": "person", "group": "Office" },
        { "id": "managers", "name": "Service managers", "note": "service level", "kind": "person", "group": "Office" },
        { "id": "accountants", "name": "Accountants", "note": "invoices", "kind": "person", "group": "Office" },
        { "id": "storekeepers", "name": "Storekeepers", "note": "spare parts", "kind": "person", "group": "Office" },
        { "id": "technicians", "name": "Technicians", "note": "phone app, offline", "kind": "person", "group": "On board" },
        { "id": "monitoring", "name": "Engine-room monitoring", "note": "REST, api/alarms", "kind": "system", "group": "On board" },
        { "id": "app", "name": "Dockyard", "note": "Mendix application", "kind": "app" },
        { "id": "accounting", "name": "Accounting system", "note": "REST", "kind": "system", "group": "Systems it calls" },
        { "id": "mail", "name": "Mail relay", "note": "SMTP, from Java", "kind": "system", "group": "Systems it calls" }
      ],
      "links": [
        { "from": "dispatchers", "to": "app", "label": "browser" },
        { "from": "managers", "to": "app", "label": "browser" },
        { "from": "accountants", "to": "app", "label": "browser" },
        { "from": "storekeepers", "to": "app", "label": "browser" },
        { "from": "technicians", "to": "app", "label": "phone" },
        { "from": "monitoring", "to": "app", "label": "alarms" },
        { "from": "app", "to": "accounting", "label": "invoices" },
        { "from": "app", "to": "mail", "label": "e-mail" }
      ]
    },
    "containers": {
      "nodes": [
        { "id": "office", "name": "Office staff", "kind": "person" },
        { "id": "tech", "name": "Technicians", "kind": "person" },
        { "id": "mon", "name": "Monitoring systems", "kind": "system" },
        { "id": "web", "name": "Web client", "note": "responsive pages", "kind": "part" },
        { "id": "phone", "name": "Phone app", "note": "nanoflows, offline", "kind": "part" },
        { "id": "rest", "name": "Alarm service", "note": "published REST", "kind": "part" },
        { "id": "jobs", "name": "Scheduled events", "note": "SLA every 15 min", "kind": "part" },
        { "id": "db", "name": "Database", "kind": "store" },
        { "id": "acc", "name": "Accounting system", "kind": "system" },
        { "id": "smtp", "name": "Mail relay", "kind": "system" }
      ],
      "links": [
        { "from": "office", "to": "web", "label": "browser" },
        { "from": "tech", "to": "phone", "label": "taps" },
        { "from": "mon", "to": "rest", "label": "POST" },
        { "from": "web", "to": "db" },
        { "from": "phone", "to": "db", "label": "sync" },
        { "from": "rest", "to": "db" },
        { "from": "jobs", "to": "db" },
        { "from": "web", "to": "acc", "label": "invoices" },
        { "from": "jobs", "to": "smtp", "label": "e-mail" }
      ]
    },
    "open": [
      "Where the application is hosted, and which database it uses, cannot be told from the model.",
      "How the phone app reaches the technicians — an app store, a private distribution — is not in the model."
    ]
  },

  "context": {
    "where": [
      { "topic": "Where a work order's due date is set", "place": "Maintenance.ACT_WorkOrder_Triage, from the priority — nowhere else." },
      { "topic": "Where stock goes down", "place": "Inventory.SUB_PartUsage_Book, called when a visit is finished; not when a part is added on the phone." },
      { "topic": "Where the invoice is made", "place": "Billing.SUB_Invoice_CreateFromWorkOrder, called by Maintenance.ACT_Visit_Complete — not by the accountant." }
    ],
    "conventions": [
      { "pattern": "ACT_", "meaning": "Started by a button on a page." },
      { "pattern": "SUB_", "meaning": "Called by other flows only." },
      { "pattern": "SCH_", "meaning": "Run by a scheduled event." },
      { "pattern": "NF_", "meaning": "Runs on the technician's phone." },
      { "pattern": "DS_", "meaning": "A data source of a page." }
    ],
    "pitfalls": [
      "Setting Maintenance.WorkOrder.Status to Done anywhere but Maintenance.ACT_Visit_Complete skips the parts booking and the invoice.",
      "The phone app works offline: a change to an entity it syncs needs a new version of the app on every technician's phone.",
      "A second visit on the same work order drafts a second invoice: Billing.SUB_Invoice_CreateFromWorkOrder does not look for one that exists."
    ]
  }
}
