Skip to main content

The expense lifecycle

All four expense types in Bezala — receipts, daily allowances, trips, and rewards — share the same lifecycle. The state machine looks like this:

J
Written by Julia Winberg

States exist for a reason: each one represents who is responsible for the record at that moment.

The states, in detail

draft

The submitter is still working on it. Bezala does not validate required fields while a record is in draft, so you can save a partial entry and come back to it. Drafts are visible only to the submitter.

A draft becomes a real expense when it's submitted — moving it to reviewing. In the API, this happens by POSTing or PUTting the record without draft: 1.

reviewing

The record is complete and someone needs to approve it. The submitter can no longer edit the substantive fields, but the approver can — when they click approve, they can pass updated parameters in the same call, and Bezala will apply the edits before flipping the state.

Multiple approvers may be required (see approval workflows). While the chain is partially complete, the record stays in reviewing and the next approver is notified.

unapproved

An approver rejected the record with a comment. The record bounces back to the submitter, who can read the comment, edit, and re-submit. Re-submission moves it back to reviewing.

queue

Fully approved. Waiting for the next accounting batch. The record is locked from edits; the only change that can still happen is being included in a batch.

You can pull all queued expenses with GET /api/expenses (look for the *_in_queue arrays in the response).

accounted

Included in a batch that was sent to bookkeeping. Read-only. The record carries a reference to the batch document; the batch document carries the bookkeeping voucher reference and the SEPA payment file (where applicable).

You can pull all accounted expenses with GET /api/archive, paginated by batch date.

The transitions, by endpoint

The transition endpoints are the same shape across the four expense types — only the noun changes:

Transition

Receipt

Daily allowance

Trip

Reward

Create as draft

POST /api/transactions with draft=1

POST /api/daily_allowances with draft=1

POST /api/trips with draft=1

POST /api/rewards with draft=1

Create and submit

Same POST, no draft

Same

Same

Same

Submit a draft

PUT /api/transactions/:id without draft=1

PUT /api/daily_allowances/:id without draft=1

PUT /api/trips/:id without draft=1

PUT /api/rewards/:id without draft=1

Approve

PUT /api/transactions/:id/approve

PUT /api/daily_allowances/:id/approve

PUT /api/trips/:id/approve

PUT /api/rewards/:id/approve

Disapprove (with comment)

POST /api/transactions/:id/disapprove

POST /api/daily_allowances/:id/disapprove

POST /api/trips/:id/disapprove

POST /api/rewards/:id/disapprove

Send to accounting

PUT /api/send_batch (company-wide)

same

same

same

The approve endpoints accept an optional payload with the same fields as the corresponding update endpoint, so you can edit and approve in a single call.

What you can and can't do at each state

Action

draft

reviewing

unapproved

queue

accounted

Submitter edits the record

Approver edits while approving

n/a

n/a

n/a

Submitter deletes the record

Approver approves

Approver disapproves

Included in next batch

(already was)

A note on partial approval chains

When a record is in reviewing and there are multiple approvers in sequence (A then B then C), an approve call by A doesn't move the record to queue — it just advances the chain. The record stays in reviewing until the last approver in the chain approves. The state machine is "is anyone still expected to act?", not "has anyone approved yet?".

Disapproval at any point in the chain bumps the record all the way back to unapproved. The submitter has to fix it and re-submit; the chain restarts from the beginning.

Listing by state

The state query parameter on list endpoints filters to a single state:

The endpoints GET /api/expenses (non-accounted, grouped by state) and GET /api/archive (accounted, grouped by batch) are the two views the Bezala web UI uses. They're a good shape for any integration that wants the same dashboard data.

Did this answer your question?