Show full sourceHide full sourceEquipmentRequestsFinance.can · 128 lines
# Revised application: a company-selected EUR 1,000 Finance threshold.
# Before applying to an existing app, migrate earlier requests to policy=original and leave an unknown historical amount empty.
# Preserve their original decisions; new requests receive the finance policy.
app EquipmentRequests
Given
role reviewer
role coordinator
role finance
Equipment {
name:text trim max=120 label="Equipment",
category:enum(computer,furniture,accessory) label="Category",
active:bool=true label="Available to request"
}
Request {
equipment:Equipment label="Equipment",
reason:text trim max=500 label="Business reason",
amount:money? label="Total in EUR",
policy:enum(original,finance_threshold) server=finance_threshold label="Approval policy",
finance_by:user? label="Finance approver",
finance_at:datetime? label="Finance approved at",
requested_by:user server=actor label="Requested by",
status:enum(requested,awaiting_finance,approved,delivered)=requested label="Status",
approved_by:user? label="Approved by",
approved_at:datetime? label="Approved at",
delivered_at:datetime? label="Delivered at",
asset_tag:text? label="Asset tag"
}
policy Equipment read=members
policy Request read=members where=row.requested_by==actor
policy Request read=reviewer
policy Request read=coordinator
policy Request read=finance where=row.policy==finance_threshold and row.amount!=null and row.amount.minor>100000
invariant Request: row.policy==original or (row.amount!=null and row.amount.minor>0 and row.amount.currency=="EUR")
lock Request fields=policy
lock Request fields=finance_by,finance_at when=row.status!=awaiting_finance
invariant Request: trim(row.reason)!=""
lock Request fields=equipment,reason,amount,requested_by when=row.status!=requested
lock Request fields=approved_by,approved_at when=row.status!=requested
lock Request fields=status,delivered_at,asset_tag when=row.status==delivered
fixture claimant=user {roles=[finance]}
fixture first_reviewer=user {roles=[reviewer,finance]}
fixture finance_reviewer=user {roles=[finance]}
fixture desk=Equipment {name="Standing desk",category=furniture}
fixture pending=Request {equipment=desk,reason="Adjustable working height",amount=money(450,"EUR"),requested_by=claimant}
When
crud Equipment by=coordinator fields=name,category,active delete=none
crud Request by=members fields=equipment,reason,amount when=row.requested_by==actor and row.status==requested delete=none
# Review another teammate's request against the equipment catalogue.
scenario approve(request:Request) by=reviewer
require request.status==requested and request.requested_by!=actor
require request.equipment.active
do
if request.policy==finance_threshold and request.amount!=null and request.amount.minor>100000
set request {status=awaiting_finance,approved_by=actor,approved_at=now}
else
set request {status=approved,approved_by=actor,approved_at=now}
examples request=pending
as,request.amount,request.policy -> request.status
first_reviewer,money(450,"EUR"),finance_threshold -> approved
first_reviewer,money(1000,"EUR"),finance_threshold -> approved
first_reviewer,money(1400,"EUR"),finance_threshold -> awaiting_finance
first_reviewer,money(1400,"EUR"),original -> approved
first_reviewer,null,original -> approved
members,money(450,"EUR"),finance_threshold -> error(forbidden)
examples request=pending
as,request.requested_by,request.status -> request.status
first_reviewer,first_reviewer,requested -> error(rule_failed)
first_reviewer,claimant,approved -> error(rule_failed)
# Finance must be a third person, even when one person has multiple roles.
scenario approve_finance(request:Request) by=finance
require request.status==awaiting_finance and request.policy==finance_threshold
require request.requested_by!=actor and request.approved_by!=actor
do set request {status=approved,finance_by=actor,finance_at=now}
examples request=pending
as,request.status,request.amount,request.approved_by -> request.status,request.finance_by
finance_reviewer,awaiting_finance,money(1400,"EUR"),first_reviewer -> approved,finance_reviewer
first_reviewer,awaiting_finance,money(1400,"EUR"),first_reviewer -> error(rule_failed)
claimant,awaiting_finance,money(1400,"EUR"),first_reviewer -> error(rule_failed)
finance_reviewer,approved,money(1400,"EUR"),first_reviewer -> error(rule_failed)
# Record the physical asset when approved equipment reaches the requester.
scenario deliver(request:Request,asset_tag:text) by=coordinator
require request.status==approved and trim(asset_tag)!=""
do set request {status=delivered,asset_tag=trim(asset_tag),delivered_at=now}
examples request=pending
as,request.status,asset_tag -> request.status,request.asset_tag
coordinator,approved,"DESK-104" -> delivered,"DESK-104"
coordinator,awaiting_finance,"DESK-104" -> error(rule_failed)
coordinator,approved," " -> error(rule_failed)
members,approved,"DESK-104" -> error(forbidden)
coordinator,delivered,"DESK-104" -> error(rule_failed)
Then
preferences { status:Request.status=requested }
page / title="Equipment requests"
breadcrumbs
card "Request equipment"
form Request.create
input equipment
textarea reason
input amount
card "Requests and delivery"
tabs preferences.status
list Request as request where request.status==preferences.status order=-created empty="No equipment requests"
badge row.status
text row.equipment.name,row.reason,row.amount,row.requested_by
badge row.policy
edit
action approve
action approve_finance
action deliver
collapse "Approval and delivery details"
text row.approved_by,row.approved_at,row.finance_by,row.finance_at
text row.asset_tag,row.delivered_at
history
pagination
page /catalogue title="Equipment catalogue"
breadcrumbs
card "Maintain requestable equipment"
form Equipment.create
input name
input category
input active
card "Equipment catalogue"
list Equipment search=name order=name empty="No equipment listed"
text row.name
badge row.category
badge row.active
edit
pagination