Type to search pages and rules.

Browse pages

Modules and Records

The spending module is the Claimwick example's one business area: it turns product intent into operations by organizing the business state the company tracks, the rules that bind it, who may act on it, and the messages, work, and tunables around it. This page shows each piece as the project writes it: the module record declared in Intent, 4 aggregates for expenses, receipts, the policy store, and the export-request store with their states, transitions, data classes, and data, the rules that bind them, the access chain of actors, roles, permissions, and the entry point with its screen and the outside system for payouts, the Submit expense and List expenses use cases with their results and scenarios, and the reaction, jobs, notifications, setting, integration event, and retention row around them.

Markdown

The Spending Module#

A module is one business area that organizes language, records, use cases, and ownership. Claimwick keeps one module, spending, which tracks expenses, receipts, and the policy. Its record file lives at .litespec/intent/modules/spending/module.yaml, and its empty dependsOn list states that it depends on no other module, which applies litespec/core/domain-model#state-every-module-dependency on Behavior Records.

YAML
type: module
id: module/spending
title: Spending
summary: Spending submits, decides, reimburses and exports work expenses.
dependsOn: []

Each aggregate below belongs to this module through its identifier: aggregate/spending.expense names spending before the dot, so containment needs no link field. Intent features live under the same name at .litespec/intent/modules/spending/features/, as The Claimwick Product shows.

Aggregates and Their States#

An aggregate is one unit of business state that stays consistent and changes in one step. Spending keeps 4, each in its own record file under the module folder. Each aggregate declares its kind and its lifecycle as states entries with a name and a meaning, which applies litespec/core/domain-model#declare-the-kind-of-every-aggregate and litespec/core/domain-model#model-each-aggregate-lifecycle-with-states.

RecordHomeKindStates
aggregate/spending.expense.litespec/behavior/modules/spending/expenses/aggregate.yamlentitySubmitted, Approved, Refused, Reimbursed
aggregate/spending.receipt.litespec/behavior/modules/spending/receipts/aggregate.yamlentityAttached, Removed
aggregate/spending.policy.litespec/behavior/modules/spending/policies/aggregate.yamlstoreNone
aggregate/spending.export-request.litespec/behavior/modules/spending/export-requests/aggregate.yamlstoreNone

An expense is one work cost an employee submitted. A receipt is proof of payment attached to an expense. The policy is a store: it holds the current receipt threshold value, names the spending settings store as its owner, and carries no states, because no invariant guards the stored value. The setting record below declares the threshold default and bounds. The store names its owner, which applies litespec/core/domain-model#name-the-owner-of-every-store. The export-request store holds each export request with the file the export job built for it, and names the spending module as its owner. Each expense state carries its meaning: Submitted waits for a decision, Approved and Refused record a manager decision, and Reimbursed records the finance payout.

Expense Transitions and Data#

The expense aggregate connects its states through a transitions field and declares its data classes with their erasure positions. Each transition names the state it leaves, the state it enters, and the use case behind the move, which applies litespec/core/domain-model#connect-every-state-through-transitions.

FromToBy
SubmittedApproveduse-case/spending.approve-expense
SubmittedRefuseduse-case/spending.refuse-expense
ApprovedReimburseduse-case/spending.reimburse-expense

The data classes group fields by their shared fate on erasure, which applies litespec/core/privacy#declare-every-data-class-with-its-erasure-position:

Data classClassificationFieldsPurposeOn erasureEnforced by
expense-moneyinternalamount, state, approver, receiptDecide each expense and keep the tax file defensible.retain for 7 years by statuterecord-expiry
submitterpersonalemployeeLink each expense to its submitter.erase on approved requestjob/spending.erase-expired-personal-data

The data field names each field the aggregate holds with its meaning, data class, and classification, which applies litespec/core/privacy#classify-every-aggregate-data-field.

NameMeaningData ClassClassification
employeeThe employee who submitted the expense.submitterpersonal
amountThe exact amount with its currency.expense-moneyinternal
receiptThe proof of payment, when one is attached.expense-moneyinternal
stateThe lifecycle state of the expense.expense-moneyinternal

The employee field is personal data in the submitter class, so the erase job and the erasure use case cover it, and the retention schedule below schedules it. The receipt aggregate carries its own data class for the filed receipt; the policy store holds one internal field, the current threshold value.

The Full Expense Record#

The expense record file below shows the full shape, with its states, transitions, data, data classes, and its rule and event entries in records:

YAML
type: aggregate
id: aggregate/spending.expense
title: Expense
summary: An expense is one work cost an employee submitted.
kind: entity
states:
  - name: Submitted
    meaning: The expense waits for a decision.
  - name: Approved
    meaning: A manager approved the expense.
  - name: Refused
    meaning: A manager refused the expense.
  - name: Reimbursed
    meaning: Finance reimbursed the expense.
transitions:
  - from: Submitted
    to: Approved
    by: use-case/spending.approve-expense
  - from: Submitted
    to: Refused
    by: use-case/spending.refuse-expense
  - from: Approved
    to: Reimbursed
    by: use-case/spending.reimburse-expense
data:
  - name: employee
    meaning: The employee who submitted the expense.
    classification: personal
    dataClass: submitter
  - name: amount
    meaning: The exact amount with its currency.
    classification: internal
    dataClass: expense-money
  - name: receipt
    meaning: The proof of payment, when one is attached.
    classification: internal
    dataClass: expense-money
  - name: state
    meaning: The lifecycle state of the expense.
    classification: internal
    dataClass: expense-money
dataClasses:
  - name: expense-money
    classification: internal
    fields:
      - amount
      - state
      - approver
      - receipt
    onErasure: retain
    basis: statute
    period: P7Y
    enforcedBy: record-expiry
    purpose: Decide each expense and keep the tax file defensible.
    reach: own-tenant
  - name: submitter
    classification: personal
    fields:
      - employee
    onErasure: erase
    basis: business
    period:
      mode: none
      reason: erasure happens on approved request
    enforcedBy: job/spending.erase-expired-personal-data
    purpose: Link each expense to its submitter.
    reach: own-tenant
records:
  - type: rule
    id: rule/expense.receipt-required-above-threshold
    title: Receipt required above threshold
    kind: precondition
    statement: An expense above the policy threshold needs a receipt.
    governs:
      - use-case/spending.submit-expense
    refusal: result/submit-expense.receipt-missing
    fulfills:
      - requirement/approval-reminders.receipt-over-threshold
  - type: rule
    id: rule/expense.approver-is-not-submitter
    title: Approver is not submitter
    kind: precondition
    statement: An approver is not the submitter.
    governs:
      - use-case/spending.approve-expense
    refusal: result/approve-expense.own-expense
    fulfills:
      - requirement/approval-reminders.no-self-approval
  - type: rule
    id: rule/expense.reimbursed-once
    title: Reimbursed once
    kind: invariant
    statement: An expense is reimbursed at most once.
    governs:
      - use-case/spending.reimburse-expense
    refusal: result/reimburse-expense.already-reimbursed
    fulfills:
      - requirement/approval-reminders.single-reimbursement
  - type: rule
    id: rule/expense.approved-before-reimbursement
    title: Approved before reimbursement
    kind: precondition
    statement: An expense is reimbursed only after approval.
    governs:
      - use-case/spending.reimburse-expense
    refusal: result/reimburse-expense.not-approved
    fulfills:
      - requirement/approval-reminders.single-reimbursement
  - type: event
    id: event/expense.submitted
    title: Expense submitted
    meaning: An expense was submitted.
    carries:
      - expenseId
      - amount
      - currency
      - employeeId
  - type: event
    id: event/expense.approved
    title: Expense approved
    meaning: An expense was approved.
    carries:
      - expenseId
      - approvedBy
      - decidedAt
  - type: event
    id: event/expense.refused
    title: Expense refused
    meaning: An expense was refused.
    carries:
      - expenseId
      - refusedBy
      - decidedAt
  - type: event
    id: event/expense.reimbursed
    title: Expense reimbursed
    meaning: An expense was reimbursed.
    carries:
      - expenseId
      - amount
      - currency
      - reimbursedAt

The receipt record file carries one rule, rule/receipt.attaches-before-decision, and one event, event/receipt.attached. Every entry states what happened or what stays true, and each names the data it carries or the requirement it fulfills.

The Rules#

A rule is one testable commitment about what stays true. Each rule states its kind and names the refusal its use case returns when the rule breaks, which applies litespec/core/domain-model#state-each-rule-with-its-kind and litespec/core/domain-model#name-the-refusal-a-broken-rule-produces. The 6 spending rules are:

RuleStatementKind
rule/expense.receipt-required-above-thresholdAn expense above the policy threshold needs a receipt.precondition
rule/expense.approver-is-not-submitterAn approver is not the submitter.precondition
rule/expense.reimbursed-onceAn expense is reimbursed at most once.invariant
rule/expense.approved-before-reimbursementAn expense is reimbursed only after approval.precondition
rule/receipt.attaches-before-decisionA receipt attaches before the expense is decided.precondition
rule/spending.receipt-filed-onceA receipt file supports one expense across spending.cross-aggregate

Each rule governs the use cases or aggregates it guards and fulfills one approval-reminders requirement:

RuleGovernsFulfills
rule/expense.receipt-required-above-thresholduse-case/spending.submit-expenserequirement/approval-reminders.receipt-over-threshold
rule/expense.approver-is-not-submitteruse-case/spending.approve-expenserequirement/approval-reminders.no-self-approval
rule/expense.reimbursed-onceuse-case/spending.reimburse-expenserequirement/approval-reminders.single-reimbursement
rule/expense.approved-before-reimbursementuse-case/spending.reimburse-expenserequirement/approval-reminders.single-reimbursement
rule/receipt.attaches-before-decisionuse-case/spending.attach-receiptrequirement/approval-reminders.receipt-over-threshold
rule/spending.receipt-filed-onceaggregate/spending.receiptrequirement/approval-reminders.receipt-over-threshold

Each rule names the refusal its use case returns when the rule fails: rule/expense.receipt-required-above-threshold names result/submit-expense.receipt-missing, rule/expense.approver-is-not-submitter names result/approve-expense.own-expense, rule/expense.reimbursed-once names result/reimburse-expense.already-reimbursed, rule/expense.approved-before-reimbursement names result/reimburse-expense.not-approved, and rule/receipt.attaches-before-decision names result/attach-receipt.expense-decided. Each rule but the last lives as an entry on its aggregate record file, so the rule and the state it guards change in one edit. The cross-aggregate rule lives in its own record file at .litespec/behavior/modules/spending/rules/receipt-filed-once.yaml, which applies litespec/core/domain-model#keep-cross-aggregate-rules-on-a-rule-page.

Actors, Roles, and Permissions#

Access runs one chain: an actor holds a role at a scope, the role allows permissions, and each use case declares its access against one grant. Actors live at product level under .litespec/intent/actors/, roles live at product level under .litespec/intent/roles/ with allows as a down-link to permissions, and permissions live in the module whose use cases they guard. Claimwick keeps 3 actors, each with one role:

ActorKindRole
actor/employeepersonrole/employee at self
actor/managerpersonrole/approver at team
actor/financepersonrole/finance at platform

An employee submits expenses and attaches receipts. A manager approves or rejects expenses. Finance reimburses approved expenses. The 3 roles allow these permissions, which applies litespec/core/access#grant-permissions-through-roles on Access:

RoleScopeAllows
role/employeeselfpermission/submit-expense, permission/list-expenses
role/approverteampermission/approve-expense, permission/list-expenses, permission/manage-policy
role/financeplatformpermission/reimburse-expense, permission/list-expenses, permission/export-expenses, permission/erase-personal-data

Permissions live as entries on one list record file at .litespec/behavior/modules/spending/permissions.yaml. Each of the 7 entries names one allowed action with its meaning:

PermissionMeaning
permission/submit-expenseSubmit an expense and attach receipts.
permission/approve-expenseApprove or reject a submitted expense.
permission/reimburse-expenseReimburse an approved expense.
permission/list-expensesRead own expenses.
permission/manage-policySet the receipt threshold.
permission/export-expensesExport approved expenses for finance.
permission/erase-personal-dataErase the personal data of a decided expense.

The reimburse permission carries the money risk and the erase permission carries the personal-data risk. Submit expense declares permission/submit-expense as its access grant with its own tenant scope, which applies litespec/core/access#declare-access-on-every-use-case: a use case declares who may run it, so nobody guesses.

Entry Points#

An entry point is one way people or systems reach the product. Claimwick keeps 1 at product level, with its record file at .litespec/behavior/entry-points/claimwick-app/entry-point.yaml, with its kind and auth, which applies litespec/core/access#name-every-entry-point-with-its-kind:

Entry pointKindAuth
entry-point/claimwick-appbrowsersession

Employees reach spending through the expenses app, and every spending use case but the reminder command names it in reachedThrough. The reminder command runs from the nightly reminder job instead, so it names no entry point. Its screen lives under it at .litespec/behavior/entry-points/claimwick-app/screens/expense-list.yaml with id screen/claimwick-app.expense-list, as The Claimwick Screen follows; the folder states the entry point, so the screen carries no entry-point link.

The External System#

An external system is one outside system the product exchanges data with. Claimwick keeps one: the payout bank moves reimbursed money to employees. Its record file lives at .litespec/behavior/modules/spending/external-systems/payout-bank.yaml, states its summary, declares its operations with their timeouts and responses, and routes erasure through an operation, which applies litespec/core/access#declare-every-operation-on-every-external-system and litespec/core/access#declare-erasure-for-every-external-system.

YAML
type: external-system
id: external-system/payout-bank
title: Payout Bank
summary: The payout bank moves reimbursed money to employees.
operations:
  - name: submit-batch
    direction: out
    timeout: PT30S
    onTimeout: Retry once, then fail the batch for finance to re-send.
    response: A batch receipt with the accepted payout count.
  - name: purge-batch
    direction: out
    timeout: PT30S
    onTimeout: Retry once, then alert finance.
    response: A purge receipt per batch.
erasure:
  mode: call
  operation: external-system/payout-bank#purge-batch
sends:
  - Approved payouts as one bank file per batch.
receives:
  - A settlement report per payout batch.
failure: The bank rejects a payout file, and finance fixes the entry and re-sends the batch.

The reimburse use case raises the reimbursement event and emits the payout integration event, and the payout batch carries it to this bank. The settlement report closes the loop: finance reads back one report per batch.

The Use Cases#

Spending keeps 10 use cases: 9 commands and 1 query. Each one is a command or a query, serves journey steps, fulfills requirements, declares its access, and states what it changes and how the writes hold together, which applies litespec/core/use-cases#give-each-use-case-a-command-or-query-kind, litespec/core/use-cases#state-the-consistency-of-every-use-case, and litespec/core/use-cases#declare-every-effect-with-one-verb-and-its-timing.

Use caseKindServesGrantChangesConsistencyEffects
use-case/spending.submit-expensecommandjourney/submit-and-reimburse#s1permission/submit-expenseexpenseatomicNone
use-case/spending.attach-receiptcommandjourney/submit-and-reimburse#s1permission/submit-expensereceiptatomicNone
use-case/spending.approve-expensecommandjourney/submit-and-reimburse#s2permission/approve-expenseexpenseatomicNone
use-case/spending.refuse-expensecommandjourney/submit-and-reimburse#s2permission/approve-expenseexpenseatomicNone
use-case/spending.reimburse-expensecommandjourney/submit-and-reimburse#s3permission/reimburse-expenseexpenseatomicemits integration-event/payout.requested after commit
use-case/spending.change-receipt-thresholdcommandNonepermission/manage-policypolicy storeatomicNone
use-case/spending.export-expensescommandjourney/submit-and-reimburse#s3permission/export-expensesexport-request storeatomicNone
use-case/spending.erase-personal-datacommandNonepermission/erase-personal-dataexpenseatomicNone
use-case/spending.send-approver-remindercommandjourney/submit-and-reimburse#s2systemNoneNonesends notification/spending.approver-reminder async
use-case/spending.list-expensesqueryjourney/submit-and-reimburse#s4self(permission/list-expenses)NoneNoneNone

Set-receipt-threshold fulfills the receipt requirement, and erase-personal-data fulfills the erasure requirement, so every use case answers Intent through serves or fulfills. The reminder command runs under the system grant from the reminder job: no person runs it. The export command records its request in the export-request store, and the build job below builds the file, which applies litespec/backend/application#run-heavy-work-as-a-request-then-a-build.

One Use Case Tying Them Together#

Submit expense is a command: an employee submits a work expense with a receipt through the expenses app. Its record file serves the journey's first step, fulfills the receipt requirement, declares its access, states what it is in summary, names its input with types, origins, and classifications, changes the expense while reading the policy store, commits atomically, and raises the submitted event. Its steps live under What Happens in its explanation file, and its result and scenario entries live in records, which is the shape the record schema defines.

YAML
type: use-case
id: use-case/spending.submit-expense
title: Submit Expense
kind: command
summary: An employee submits a work expense.
serves:
  - journey/submit-and-reimburse#s1
fulfills:
  - requirement/approval-reminders.receipt-over-threshold
access:
  grant: permission/submit-expense
  tenant: own
reachedThrough:
  - entry-point/claimwick-app
input:
  - name: amount
    meaning: The exact amount claimed.
    type: decimal
    required: true
    rules:
      - A positive decimal with at most two fraction digits.
    origin: caller
    classification: internal
  - name: currency
    meaning: The ISO 4217 code of the amount.
    type: text
    required: true
    rules:
      - A three-letter code.
    origin: caller
    classification: internal
  - name: receiptId
    meaning: The attached receipt, required above the threshold.
    type: id
    of: aggregate/spending.receipt
    required: false
    origin: caller
    classification: internal
  - name: employeeId
    meaning: The submitting employee.
    type: id
    of: actor/employee
    required: true
    origin: claim
    classification: personal
output:
  - name: expenseId
    meaning: The new expense identity.
changes:
  - aggregate/spending.expense
reads:
  - aggregate/spending.policy
consistency:
  mode: atomic
raises:
  - event/expense.submitted
idempotency:
  mode: key
  key: "{employeeId}:{amount}:{currency}"
  scope: login
audit:
  level: none
  target: expense
records:
  - type: result
    id: result/submit-expense.submitted
    title: Submitted
    outcome: success
    meaning: The expense is submitted.
  - type: result
    id: result/submit-expense.receipt-missing
    title: Receipt missing
    outcome: refusal
    code: spending.receipt-missing
    meaning: The expense is above the receipt threshold and carries no receipt.
  - type: scenario
    id: scenario/submit-expense.submitting-with-a-receipt-succeeds
    title: Submitting with a receipt succeeds
    given: An employee paid for a train ticket and kept the receipt.
    when: The employee submits the expense with the receipt.
    result: result/submit-expense.submitted
    then: The expense is submitted.
  - type: scenario
    id: scenario/submit-expense.submitting-above-threshold-without-a-receipt-is-refused
    title: Submitting above threshold without a receipt is refused
    given: An employee paid 48.90 EUR for a train ticket and kept no receipt.
    when: The employee submits the expense without the receipt.
    result: result/submit-expense.receipt-missing
    then: The expense is not submitted.

Its explanation file at .litespec/behavior/modules/spending/expenses/submit-expense.md walks the steps under What Happens, then holds the worked example:

Markdown
# Submit Expense

## What Happens

The employee sends the amount with its currency and the receipt, when one is attached.
Spending checks the receipt rule against the policy threshold.
An expense above the threshold with no receipt is refused.
Otherwise the expense is stored as Submitted and the submitted event is raised.

## Example

Ava takes the train to a client visit and keeps the ticket receipt.
Back at the office she submits the 48.90 EUR fare with the receipt attached.
The expense waits for Ben to decide.

The record ends in named results, which applies litespec/core/model#give-every-use-case-its-results: one success and one refusal, and the refusal carries the stable code spending.receipt-missing. The generic forbidden and invalid-input results are derived from the access grant and the input rules and never written. The scenarios prove the success and the refusal in given, when, then, which applies litespec/core/use-cases#write-every-scenario-in-given-when-then. The receipt rule governs this use case, the employee role allows its permission, and the journey's first step is served by it. Its summary field states what it is in one paragraph, its What Happens section walks the behavior in order, and its Example follows Ava submitting her train fare.

Reactions and Jobs#

A reaction answers an event and a job keeps a schedule or runs by hand, each through the edges its effects declare. When an expense is reimbursed, Claimwick notifies the employee. The notification reaction record file lives at .litespec/behavior/modules/spending/reactions/notify-employee-on-reimbursement.yaml, filed with the spending module whose notification it sends, which applies litespec/core/model#write-every-record-in-its-home, and its name follows the reaction name form, which applies litespec/core/language#name-each-record-by-its-types-name-form. Its triggeredBy lists the domain event, and it declares its sends edge in effects and writes no aggregate.

YAML
type: reaction
id: reaction/spending.notify-employee-on-reimbursement
title: Notify Employee on Reimbursement
triggeredBy:
  - event/expense.reimbursed
effects:
  - sends: notification/spending.reimbursement-received
reads:
  - aggregate/spending.expense
fulfills:
  - requirement/approval-reminders.decided-within-five-days
serves:
  - journey/submit-and-reimburse#s4
idempotency:
  mode: state
onFailure:
  mode: retry
  retry:
    maxAttempts: 5
    initialDelay: PT1M
    maxDelay: PT1M
    backoff: fixed
  then: dead-letter

The entry omits timing, so it reads as async. The reaction serves the journey's last step: the employee sees the reimbursement land because the message says so. Its failure handling retries five times one minute apart before dead-lettering, and repeated firings collapse by state, so the employee is told once.

Claimwick keeps 3 scheduled jobs beside it. The reminder job runs nightly and runs the reminder command once per waiting expense. Its record file lives at .litespec/behavior/modules/spending/jobs/send-approver-reminders.yaml, which applies litespec/core/model#write-every-record-in-its-home, and its name carries the verb of the command it runs, which applies litespec/core/language#name-each-record-by-its-types-name-form. Its kind is scheduled with a nightly schedule, and it states its selection, lease, and runbook.

YAML
type: job
id: job/spending.send-approver-reminders
title: Send Approver Reminders
kind: scheduled
schedule:
  cron: 0 6 * * *
  timezone: Europe/Amsterdam
selection: Submitted expenses waiting past three business days without a decision.
batchSize: 200
lease:
  name: send-approver-reminders
  duration: PT15M
effects:
  - runs: use-case/spending.send-approver-reminder
    timing: async
reads:
  - aggregate/spending.expense
runbook: runbook/reminder-failures
runsWhere: api worker
fulfills:
  - requirement/approval-reminders.decided-within-five-days
serves:
  - journey/submit-and-reimburse#s2
onFailure:
  mode: retry
  retry:
    maxAttempts: 3
    initialDelay: PT5M
    maxDelay: PT5M
    backoff: fixed
  then: dead-letter
idempotency:
  mode: state

This job carries the approval reminders feature down the tree: the feature promises reminders past three days, the five-day requirement bounds them, and the nightly schedule fires them. The runbook it names lives in Implementation, where runbooks answer jobs. The second job, job/spending.erase-expired-personal-data, runs weekly and runs use-case/spending.erase-personal-data once per decided expense with an approved erasure request, enforcing the submitter data class. The third job, job/spending.build-expense-exports, runs every five minutes and builds one file per requested export, storing each file beside its request, which applies litespec/backend/application#run-heavy-work-as-a-request-then-a-build.

The Notifications#

A notification is one person-facing message with its addresses, channel, consent, and claims. The approver reminder record file lives at .litespec/behavior/modules/spending/notifications/approver-reminder.yaml, names its addresses and channel with the consent rule, and claims each promise it makes from a requirement, which applies litespec/core/edges#describe-every-notification-with-audience-channel-and-consent and litespec/core/edges#claim-every-notification-promise-from-a-requirement.

YAML
type: notification
id: notification/spending.approver-reminder
title: Approver Reminder
template: approver-reminder
addresses:
  - actor/manager
channel:
  - email
consent: Managers receive reminders as part of the approver role.
frequency: per-attempt
dedupKey: reminder:{expenseId}:{date}
params:
  - expenseId
  - waitingDays
locale: the manager profile language
claims:
  - claim: The message names the expense and its waiting time.
    requirement: requirement/approval-reminders.decided-within-five-days
fulfills:
  - requirement/approval-reminders.decided-within-five-days

The reminder command's sends edge carries its own suppression: when the expense was decided overnight, the command sends nothing, and a suppression scenario proves the quiet path. The second notification, notification/spending.reimbursement-received, tells the employee once by email that the money is on its way, sent by the reaction above.

The Settings#

A setting is one tunable with its value type, default, range, unit, setter, and bound behavior. The spending settings live on one setting-set record file at .litespec/behavior/modules/spending/settings.yaml. Each commercial or varying default fulfills a requirement, which applies litespec/core/use-cases#hold-every-setting-default-to-a-requirement.

YAML
type: setting-set
title: Spending Settings
records:
  - type: setting
    id: setting/spending.receipt-threshold
    title: Receipt Threshold
    valueType: money
    default: EUR 25
    range: EUR 0..1000
    unit: EUR
    setBy: operator
    atBound: refuse
    secret: false
    meaning: An expense above this value needs a receipt.
    fulfills:
      - requirement/approval-reminders.receipt-over-threshold
  - type: setting
    id: setting/spending.reminder-delay
    title: Reminder Delay
    valueType: duration
    default: P3D
    range: P1D..P5D
    unit: days
    setBy: product
    atBound: refuse
    secret: false
    meaning: Submitted expenses wait this long before the first reminder.
    fulfills:
      - requirement/approval-reminders.decided-within-five-days

The threshold default is the 25 EUR the decision set, and a manager may move it inside the range through the set-threshold use case; a value outside the range is refused. The reminder delay is product-set, so the nightly job reads a value no operator changes.

The Query#

List expenses is a query: an employee reads their own expenses with the latest state of each. Its record file states what it is in summary, and declares its output in include groups with freshness and its paging, which applies litespec/core/use-cases#declare-every-query-output and litespec/core/use-cases#declare-paging-on-every-collection-query.

YAML
type: use-case
id: use-case/spending.list-expenses
title: List Expenses
kind: query
summary: An employee reads their own expenses with the latest state of each.
serves:
  - journey/submit-and-reimburse#s4
access:
  grant: self(permission/list-expenses)
  tenant: own
reachedThrough:
  - entry-point/claimwick-app
reads:
  - aggregate/spending.expense
output:
  include:
    rows:
      fields:
        - name: expenseId
          meaning: The expense identity.
        - name: amount
          meaning: The amount claimed, with its currency.
        - name: state
          meaning: The lifecycle state of the expense.
        - name: submittedAt
          meaning: When the expense was submitted.
      classification: internal
      reads:
        - aggregate/spending.expense
      freshness:
        mode: immediate
paging:
  mode: pages
  order: submittedAt desc
  cursor: expenseId
  defaultLimit: 20
  maxLimit: 100
idempotency:
  mode: none
  reason: Reads change nothing.
records:
  - type: result
    id: result/list-expenses.listed
    title: Listed
    outcome: success
    meaning: The expenses are listed.
  - type: scenario
    id: scenario/list-expenses.listing-own-expenses-succeeds
    title: Listing own expenses succeeds
    given: An employee submitted two expenses.
    when: The employee lists their expenses.
    result: result/list-expenses.listed
    then: Both expenses are listed.

The output names the one row group the screen reads: identity, amount, state, and submission time, newest first, read fresh from the expense aggregate. The self(...) grant limits the permission to the caller's own records. The Claimwick Screen reads this output through the generated client.

The Integration Event#

A fact that leaves the product is an integration event. When finance reimburses an expense, the payout batch carries a payout request to the bank. The event record file lives at .litespec/behavior/modules/spending/integration-events/payout.requested.yaml, states its direction with its CloudEvents type, source, subject, and payload, and translates the domain event, which applies litespec/core/edges#describe-every-integration-event-for-its-direction.

YAML
type: integration-event
id: integration-event/payout.requested
title: Payout Requested
direction: out
eventType: claimwick.payout.requested
source: /spending/reimbursements
subject: The reimbursed expense.
payload:
  - name: expenseId
    meaning: The reimbursed expense.
    classification: internal
    type: id
    of: aggregate/spending.expense
    required: true
  - name: amount
    meaning: The payout amount, with its currency.
    classification: internal
    type: money
    required: true
trust: payload
translates: event/expense.reimbursed

The reimburse use case declares the event in its effects with emits after commit, so the edge from the command to the outside world is declared beside the command.

The Retention Row#

The retention schedule at .litespec/behavior/retention-schedule.yaml schedules every data class with its purpose, basis, period, enforcement, and Intent acceptance, which applies litespec/core/privacy#schedule-every-data-class-for-retention. The submitter row reads:

YAML
dataClass: submitter
covers:
  - aggregate/spending.expense
basis: business
purpose: Link each expense to its submitter.
period:
  mode: none
  reason: erasure happens on approved request
enforcedBy: job/spending.erase-expired-personal-data
cites: requirement/approval-reminders.erasure-on-request
processors:
  - system: external-system/payout-bank
    forwarding:
      mode: impossible
      reason: settlement records belong to the bank
    basedOn:
      - requirement/approval-reminders.erasure-on-request
conflicts: Erasure wins for the submitter link; the money facts stay for the statutory seven years, and the erasure receipt records the split.

The row names what the class covers, what enforces its erasure, the requirement that accepts it, how erasure forwards to the bank, and which side wins when keeping and erasing collide.

Next, A Use Case in Code follows the Submit expense record into .NET code and tests.