Invoice Approval Workflow (FSM)
Submit persistent invoices and control approval or rejection with an entity-based finite state machine.
Exampleinvoiceapprovalrejectionfsmfinite-state-machineentity-basedentitystatetransitionrelationsdatabaseworkflowexpressionsapirestui
π Horizon
Invoice Approval at a Glance
Learning Scenario
A user submits an invoice for a customer. The invoice then waits for one decision: an approver may approve it, or a reviewer may reject it. The outcome, responsible user, remark, and decision time must remain part of the invoice record.
An entity-based FSM makes that flow explicit. Each invoice has its own persisted state, and only events declared for its current state can move it forward.
Lifecycle
What It Demonstrates
- An FSM controlling the status of each persisted invoice.
- Events carrying approval or rejection details.
- Looking up and recording related users during transitions.
- Related invoice, user, and customer entities.
- Submission logic and summary projections.
Expected Result
You will be able to create users and customers, submit invoices, approve or reject each invoice once, and inspect the persisted decision details afterwards.
π§ Voyage
1. Model the Approval Domain
Start with the three possible states. Every submitted invoice begins in
pendingApproval and may finish in one terminal state.
@datatype
enum InvoiceStatus
pendingApproval
approved
rejected
An invoice stores its customer and creator, plus separate fields for the user and time associated with either decision.
Invoice
id pattern INV-UUID
title : String
customer : *Customer
totalAmount : Float
submittedAt : DateTime
approvedAt : DateTime
rejectedAt : DateTime
status : InvoiceStatus
createdBy : *User
approvedBy : *User
rejectedBy : *User
remark : String
Decision requests carry data that is not part of the URL. Keeping approval and rejection separate makes each event contract clear.
ApproveInvoiceReq
approvedBy: String
remark: String
RejectInvoiceReq
rejectedBy: String
reason: String
2. Persist Related Entities
InvoiceDB stores invoices, users, and customers as separate
entities. Relations connect an invoice to the records it references.
Entity Invoice
key(id)
indexes: index(status), index(customer), index(createdBy), index(approvedBy)
customer -m2o-> Customer
createdBy -m2o-> User
approvedBy -m2o-> User
rejectedBy -m2o-> User
The many-to-one relations mean many invoices may share one customer or user. Queries can then find invoices by lifecycle state and relations.
query findById(id:String) : *Invoice
query findByStatus(status:InvoiceStatus) : List<*Invoice>
query findByCustomerId(customerId:String) : List<*Invoice>
query findByCreatedBy(createdById:String) : List<*Invoice>
query findByApprovedBy(approvedById:String) : List<*Invoice>
User and Customer entities have their own keys, indexes, queries, and commands. They must exist before their IDs can be used during submission or a decision.
3. Expose Management and Decisions
The API separates ordinary entity management from workflow commands. Submission creates a pending invoice; approval and rejection enter the FSM through explicit business endpoints.
post /invoice/submit submitInvoice(req:*SubmitInvoiceReq) : *Invoice
post /invoice/{id}/approve approveInvoice(req:ApproveInvoiceReq) : *Invoice
post /invoice/{id}/reject rejectInvoice(req:RejectInvoiceReq) : Invoice
The id in the decision path identifies the FSM entity. The
request body supplies the acting user and the remark or reason. The same
API also provides invoice searches and CRUD for users and customers.
4. Submit and Summarize Invoices
SubmitInvoice turns an external request into a controlled
entity. It copies business fields, resolves relation IDs through the
generated entity fields, sets the initial state, and timestamps it.
SubmitInvoice
input: req:*SubmitInvoiceReq
output: result:*Invoice
logic
inv.status = InvoiceStatus.pendingApproval
inv.submittedAt = nowUTC()
result = Invoice.createInvoice(inv)
A second expression maps full invoices into InvoiceSummary
values for the main table. MapSlice applies the projection to
every stored invoice, so the UI does not need the full entity graph.
GetInvoicesSummary
input: _
output: result:List<*InvoiceSummary>
logic
invoices = Invoice.listAllInvoices()
result = MapSlice(invoices, SummarizeInvoice)
In the current source, SummarizeInvoice assigns
pendingApproval instead of copying
invoice.status. Copying the entity status would let the
summary table reflect approval and rejection transitions accurately.
5. Govern Approval and Rejection
The declaration binds lifecycle state to a field on a particular invoice entity.
@fsm
InvoiceApprovalFSM controls Invoice.status
key: id
controls Invoice.statusmakes status the state field.key: ididentifies the invoice instance.- Each invoice therefore advances independently and persistently.
Give events their decision payloads
The entity ID is implicit from the FSM key. The remaining event input is a typed request containing who acted and what they said.
event approve in(appReq:ApproveInvoiceReq) out(appInvoice:*Invoice)
event reject in(rejReq:*RejectInvoiceReq) out(rejInvoice:Invoice)
See the complete state machine
Only PendingApproval handles decision events. Approved and Rejected are terminal, so an invoice cannot be decided twice in this model.
6. Update the Entity During Each Transition
Approve a pending invoice
The approval handler first copies the remark, records the current time, looks up the acting User, and stores that relation on the controlled entity. Only then does it move to Approved.
state PendingApproval
on event approve:
this.remark = appReq.remark
this.approvedAt = nowUTC()
approver = User.findById(appReq.approvedBy)
this.approvedBy = approver
next Approved
this is the invoice selected by the implicit ID.
appReq is the typed event input. The relation lookup turns a
user ID into the User entity stored in approvedBy.
Reject through a parallel path
on event reject:
this.remark = rejReq.reason
this.rejectedAt = nowUTC()
rejector = User.findById(rejReq.rejectedBy)
this.rejectedBy = rejector
next Rejected
The rejection branch has the same shape but writes rejection-specific fields. Keeping separate timestamps and user relations preserves a clear audit record of whichever decision occurred.
Declare terminal states
state Approved
# no outgoing events
state Rejected
# no outgoing events
Empty state bodies are meaningful: the statuses remain valid and persisted, but no further approval event is accepted there.
7. Wire the Workflow
CRUD and searches connect to entities or expressions; decisions connect to the FSM.
connect api.submitInvoice -> SubmitInvoice
connect api.approveInvoice -> fsm.approve
connect api.rejectInvoice -> fsm.reject
connect api.getAllInvoices -> sumInv
Decision endpoints are deliberately routed through the FSM, where state rules and audit updates are applied. Although the source also exposes a generic update operation, workflow clients should not use it to bypass approval or rejection transitions.
8. Configure the API and Database
The service needs an API port, database connection, and logging. The UI has a smaller configuration with its own port and logging settings.
@config
InvoiceConfig
apiConfig: ApiConfig
dbConfig: DatabaseConfig
logConfig: LogConfig
UiConfig
port: Int (default=8080)
logConfig: LogConfig
9. Organize the Workflow into Dashboards
Three working dashboards separate invoices, users, and customers. The invoice dashboard combines a summary table with decision, search, submission, and deletion forms.
Widget ApproveInvoiceByIdForm of type Form
fields:
- id String
- req ApproveInvoiceReq
buttons:
submit: βοΈ Approve Invoice
Widget RejectInvoiceByIdForm of type Form
fields:
- id String
- req RejectInvoiceReq
buttons:
submit: βοΈ Reject Invoice
The nested request fields mirror the FSM event payloads. User and Customer dashboards let readers create the related records needed before an invoice can be submitted and decided.
10. Connect UI Actions to the API
The UI routes each form to a typed API method. Approval and rejection remain ordinary UI actions even though the backend implements them as entity-based FSM events.
connect inv.InvTable.listRows -> InvoiceApi.getInvoices
connect inv.CreateInvoiceForm.submit -> InvoiceApi.submitInvoice
connect inv.ApproveInvoiceByIdForm.submit -> InvoiceApi.approveInvoice
connect inv.RejectInvoiceByIdForm.submit -> InvoiceApi.rejectInvoice
connect user.CreateUserForm.submit -> InvoiceApi.createUser
connect cus.CreateCustomerForm.submit -> InvoiceApi.createCustomer
Search and management connections follow the same pattern. The UI knows the API contract, while the service hides persistence and transition details behind it.
11. Deploy the Persistent Approval System
The invoice service depends on the database and exposes its API on port
9098. The UI is available on 8088 after the
backend starts.
InvoiceDeploy
service InvoiceService
replica 1
export 9098:InvoiceService.api
dependsOn InvoiceDatabase
InvoiceDatabase
service PostgresqlDB
InvoiceUiDeploy
service InvoiceUi
replica 1
export 8088:InvoiceUi.config
dependsOn InvoiceDeploy
Each invoiceβs status and decision details are entity data, so they remain available after the backend restarts.
12. Exercise the Complete Decision Flow
- Validate, generate, and start the database, backend, and UI.
- Open
localhost:8088. - Create a customer, a submitting user, and two decision users.
- Submit two invoices and retrieve them to verify their pending status.
- Approve one invoice with an approver ID and remark.
- Reject the other with a rejector ID and reason.
- Inspect the decision timestamps, users, remarks, and statuses.
- Try making a second decision on either terminal invoice.
- Restart the backend and confirm that both outcomes remain stored.
The important observation is that decision data and state change happen together inside the FSM handler for the identified invoice.
13. Conclusion
You have built a persistent approval workflow where every invoice owns its state and can receive one valid decision. The FSM records not only the destination state, but also who acted, when they acted, and why.
Afterwards, you should understand how to:
- bind an FSM to a persisted entity field;
- use an entity key as implicit event identity;
- give FSM events typed business payloads;
- access the controlled entity through
this; - look up and assign related entities during a transition;
- record timestamps and remarks before calling
next; - use empty states to make decisions terminal;
- combine workflow commands with CRUD, relations, and projections.
Executable model
<\> Implementation
Explore the runnable model by responsibility, then select a file to inspect its complete source.
# @ocean-meta-start
# tags:
# - documentation
# perspective:
# feature: invoice-approval
# @ocean-meta-end
@info
name: InvoiceService
title: Invoice Approval System
subtitle: Submit, approve, and manage invoices
version: 1.0.0
shortDescription: A complete workflow system for managing invoice approvals
description: InvoiceService is a reference example showcasing the use of
entity-based finite state machines (FSMs) in the Ocean-lab DSL platform.
It manages the lifecycle of invoices submitted for approval, using a
dedicated `Invoice` datatype and `InvoiceStatus` enum. The FSM governs
state transitions (e.g., Draft β PendingApproval β Approved/Rejected β Paid)
and encapsulates business logic and rule enforcement.
The service exposes an API for submitting, approving, rejecting, and
querying invoices. FSM transitions are driven by input events and
validated using expressions like `CanApprove`. Runtime context (e.g.,
user role, pending approvals) is kept internal to the FSM and isolated
from external access.
InvoiceService demonstrates how Ocean DSL can be used to declaratively
define API-driven workflows, enforce domain constraints, and model
business processes with clear, composable constructs.
# @ocean-meta-start
# tags:
# - invoice-approval
# - datatype
# perspective:
# feature: invoice-approval
# service: invoice-service
# @ocean-meta-end
@datatype
Invoice
id pattern INV-UUID # TODO: switch to INV-SEQ
title : String
customer : *Customer
totalAmount : Float
submittedAt : DateTime
approvedAt : DateTime
rejectedAt : DateTime
status : InvoiceStatus
createdBy : *User
approvedBy : *User
rejectedBy : *User
remark : String
enum InvoiceStatus
pendingApproval
approved
rejected
#paid
User
id pattern USR-UUID
name: String
role: String
Customer
id pattern CUS-UUID
name : String
department : String
SubmitInvoiceReq
title : String
customerId : String
totalAmount : Float
createdBy : String # userId
ApproveInvoiceReq
approvedBy : String # userId
remark : String
RejectInvoiceReq
rejectedBy : String # userId
reason : String
InvoiceSummary
id : String
title : String
customerName : String
totalAmount : Float
status : InvoiceStatus
# @ocean-meta-start
# tags:
# - invoice-approval
# - database
# perspective:
# feature: invoice-approval
# service: invoice-service
# @ocean-meta-end
@database
Database InvoiceDB
engine = postgres
configType = DatabaseConfig
tags = primary
# ---------------------------------------------------
# Invoice Entity
# ---------------------------------------------------
Entity Invoice
key(id)
indexes: index(status), index(customer), index(createdBy), index(approvedBy), index(submittedAt)
# Relations
customer -m2o-> Customer
createdBy -m2o-> User
approvedBy -m2o-> User
rejectedBy -m2o-> User
# Queries
query findById(id: String) : *Invoice
query findByStatus(status: InvoiceStatus) : List<*Invoice>
query findByCustomerId(customerId: String) : List<*Invoice>
query findByCreatedBy(createdById: String) : List<*Invoice>
query findByApprovedBy(approvedById: String) : List<*Invoice>
query findByStatusAndCustomer(status: InvoiceStatus, customerId: String) : List<*Invoice>
query listInvoices(offset: Int, limit: Int) : List<*Invoice>
query listAllInvoices() : List<Invoice>
#query countInvoice() : Int
# Commands
command T createInvoice(item: Invoice) : *Invoice
command T updateInvoice(item: *Invoice) : *Invoice
command deleteInvoice(id: String) : _
# ---------------------------------------------------
# User Entity
# ---------------------------------------------------
Entity User
key(id)
indexes: index(role), index(name), unique(name)
# Queries
query findById(id: String) : *User
query findByName(name: String) : *User
query findByRole(role: String) : List<*User>
query listUsers(offset: Int, limit: Int) : List<*User>
query listAllUsers() : List<User>
# Commands
command T createUser(item: User) : *User
command T updateUser(item: *User) : *User
command deleteUser(id: String) : _
# ---------------------------------------------------
# Customer Entity
# ---------------------------------------------------
Entity Customer
key(id)
indexes: index(name), index(department), unique(name)
# Queries
query findById(id: String) : *Customer
query findByName(name: String) : *Customer
query findByDepartment(department: String) : List<*Customer>
query listCustomers(offset: Int, limit: Int) : List<*Customer>
query listAllCustomers() : List<Customer>
# Commands
command T createCustomer(item: Customer) : *Customer
command T updateCustomer(item: *Customer) : *Customer
command deleteCustomer(id: String) : _
# @ocean-meta-start
# tags:
# - configuration
# perspective:
# feature: invoice-approval
# service: all
# @ocean-meta-end
@config
@import config O.database.postgres.config@1.0.0 as DatabaseConfig
@import config O.log.config@1.0.0 as LogConfig
InvoiceConfig
apiConfig : ApiConfig
dbConfig : DatabaseConfig
logConfig : LogConfig
ApiConfig
port: Int (default=8080)
UiConfig
port: Int (default=8080)
logConfig : LogConfig
# @ocean-meta-start
# tags:
# - invoice-api
# - rest-api
# perspective:
# feature: invoice-approval
# service: invoice-service
# @ocean-meta-end
@api
InvoiceApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
generateSwagger = true
# -------------------------
# Invoice
# -------------------------
get /invoice/{id} getInvoice(id:String) : *Invoice
get /invoices getAllInvoices() : List<*InvoiceSummary>
get /invoices/{offset}/{limit} getInvoices(offset:Int, limit:Int) : List<*Invoice>
#get /invoices/pending/{userId} getPendingInvoices(userId:String) : List<*Invoice>
post /invoice/submit submitInvoice(req:*SubmitInvoiceReq) : *Invoice
post /invoice/update updateInvoice(item: *Invoice) : *Invoice
post /invoice/{id}/approve approveInvoice(req:ApproveInvoiceReq) : *Invoice
post /invoice/{id}/reject rejectInvoice(req:RejectInvoiceReq) : Invoice
delete /invoice/{id}/delete deleteInvoice(id:String) : _
get /invoice/find-by-status/{status} findInvoiceByStatus(status: InvoiceStatus) : List<*Invoice>
get /invoice/find-by-customer/{customerId} findInvoiceByCustomer(customerId: String) : List<*Invoice>
get /invoice/find-by-created-by/{createdById} findInvoiceByCreatedBy(createdById: String) : List<*Invoice>
get /invoice/find-by-approved-by/{approvedById} findInvoiceByApprovedBy(approvedById: String) : List<*Invoice>
get /invoice/find-by-status-and-customer/{status}/{id} findInvoiceByStatusAndCustomer(status: InvoiceStatus, id : String) : List<*Invoice>
# -------------------------
# User
# -------------------------
get /user/{id} getUser(id:String) : *User
get /users getAllUsers() : List<User>
get /find-user-by-name/{name} findUserByName(name:String) : *User
get /find-user-by-role/{role} findUserByRole(role:String) : List<*User>
get /users/{offset}/{limit} getUsers(offset:Int, limit:Int) : List<*User>
post /user/create createUser(user:User) : *User
post /user/update updateUser(user:User) : *User
delete /user/{id} deleteUser(id:String) : _
# -------------------------
# Customer
# -------------------------
get /customer/{id} getCustomer(id:String) : *Customer
get /customers getAllCustomers() : List<Customer>
get /find-customer-by-name/{name} findCustomerByName(name:String) : *Customer
get /find-customer-by-department/{department} findCustomerByDepartment(department:String) : List<*Customer>
get /customers/{offset}/{limit} getCustomers(offset:Int, limit:Int) : List<*Customer>
post /customer/create createCustomer(customer:Customer) : *Customer
post /customer/update updateCustomer(customer:Customer) : *Customer
delete /customer/{id} deleteCustomer(id:String) : _
# @ocean-meta-start
# tags:
# - expression
# perspective:
# feature: invoice-approval
# service: invoice-service
# @ocean-meta-end
@expression
SummarizeInvoice
input : invoice:Invoice
output : summary:*InvoiceSummary
logic
summary.id = invoice.id
summary.title = invoice.title
summary.customerName = invoice.customer.name
summary.totalAmount = invoice.totalAmount
summary.status = InvoiceStatus.pendingApproval
GetInvoicesSummary
input: _
output: result:List<*InvoiceSummary>
logic
invoices = Invoice.listAllInvoices()
result = MapSlice(invoices, SummarizeInvoice)
SubmitInvoice
input: req:*SubmitInvoiceReq
output: result:*Invoice
logic
#cus = Customer.findByName(req.customerName)
#user = User.findById(req.createdBy)
var inv Invoice
inv.customerId = req.customerId
inv.createdById = req.createdBy
inv.totalAmount = req.totalAmount
inv.title = req.title
inv.status = InvoiceStatus.pendingApproval
inv.submittedAt = nowUTC()
result = Invoice.createInvoice(inv)
# @ocean-meta-start
# tags:
# - invoice-approval
# - approval-workflow
# - fsm
# perspective:
# feature: invoice-approval
# service: invoice-service
# @ocean-meta-end
@fsm
InvoiceApprovalFSM controls Invoice.status
key: id
# ---------------------------------------------------
# Events (id is implicit input)
# ---------------------------------------------------
event approve in(appReq:ApproveInvoiceReq) out(appInvoice:*Invoice)
event reject in(rejReq:*RejectInvoiceReq) out(rejInvoice:Invoice)
# ---------------------------------------------------
# States
# ---------------------------------------------------
state PendingApproval
on event approve:
this.remark = appReq.remark
this.approvedAt = nowUTC()
approver = User.findById(appReq.approvedBy)
this.approvedBy = approver
next Approved
on event reject:
this.remark = rejReq.reason
this.rejectedAt = nowUTC()
rejector = User.findById(rejReq.rejectedBy)
this.rejectedBy = rejector
next Rejected
state Approved
# nop
state Rejected
# nop
# @ocean-meta-start
# tags:
# - invoice-api
# - service
# perspective:
# feature: invoice-approval
# service: invoice-service
# @ocean-meta-end
@service
InvoiceService
@perspectives: version:1.0.0, lifestyle:stable
use config InvoiceConfig as myCfg
impl api InvoiceApi as api on myCfg.apiConfig.port
use database InvoiceDB as db
use expression GetInvoicesSummary as sumInv
use expression SubmitInvoice
use fsm InvoiceApprovalFSM as fsm
#use expression CanApprove
#use expression CanReject
# -------------------------
# Config Binding
# -------------------------
connect myCfg.dbConfig -> db
connect api.getInvoice -> Invoice.findById
connect api.getAllInvoices -> sumInv
connect api.getInvoices -> Invoice.listInvoices
#connect api.getPendingInvoices ->
connect api.submitInvoice -> SubmitInvoice
connect api.updateInvoice -> Invoice.updateInvoice
connect api.approveInvoice -> fsm.approve
connect api.rejectInvoice -> fsm.reject
connect api.deleteInvoice -> Invoice.deleteInvoice
connect api.findInvoiceByStatus -> Invoice.findByStatus
connect api.findInvoiceByCustomer -> Invoice.findByCustomerId
connect api.findInvoiceByCreatedBy -> Invoice.findByCreatedBy
connect api.findInvoiceByApprovedBy -> Invoice.findByApprovedBy
connect api.findInvoiceByStatusAndCustomer -> Invoice.findByStatusAndCustomer
connect api.getUser -> User.findById
connect api.getAllUsers -> User.listAllUsers
connect api.getUsers -> User.listUsers
connect api.createUser -> User.createUser
connect api.updateUser -> User.updateUser
connect api.deleteUser -> User.deleteUser
connect api.findUserByName -> User.findByName
connect api.findUserByRole -> User.findByRole
connect api.getCustomer -> Customer.findById
connect api.getAllCustomers -> Customer.listAllCustomers
connect api.getCustomers -> Customer.listCustomers
connect api.createCustomer -> Customer.createCustomer
connect api.updateCustomer -> Customer.updateCustomer
connect api.findCustomerByName -> Customer.findByName
connect api.findCustomerByDepartment -> Customer.findByDepartment
connect api.deleteCustomer -> Customer.deleteCustomer
# @ocean-meta-start
# tags:
# - ui
# - dashboard
# perspective:
# feature: invoice-approval
# service: ui-service
# @ocean-meta-end
@dashboard
InvoiceDashboard
Widget InvTable of type Table
title = Invoices π§Ύ
limit = 8
datatype = Invoice
Widget S51 of type Separator
size = lg
style = solid
label = Manage
Widget ManageInvoiceText of type Text
subtitle = To manage Invoices!
content = Here you can <b>approve</b> or <b>reject</b> invoices.
Widget S52 of type Separator
size = sm
style = solid
label = Approve!
Widget ApproveInvoiceByIdForm of type Form
title = Approve by ID
fields:
- id String
- req ApproveInvoiceReq
buttons:
submit : βοΈ Approve Invoice
Widget S53 of type Separator
size = sm
style = solid
label = Reject!
Widget RejectInvoiceByIdForm of type Form
title = Reject by ID
fields:
- id String
- req RejectInvoiceReq
buttons:
submit : βοΈ Reject Invoice
Widget S31 of type Separator
size = lg
style = solid
label = Search
Widget SearchInvoiceText of type Text
subtitle = To find Invoices!
content = Here you can find invoices by <b>ID</b>, <b>Status</b>, <b>Customer</b>, <b>CreatedBy</b>, <b>ApprovedBy</b>, or <b>Status and Customer</b>.
Widget S32 of type Separator
size = sm
style = solid
label = Find by ID
Widget FindInvoiceByIdForm of type Form
title = Find by ID
fields:
- id String
buttons:
submit : π Find Invoice by ID
Widget S33 of type Separator
size = sm
style = solid
label = Find by Status
Widget FindInvoiceByStatusForm of type Form
title = Find by Status
fields:
- status InvoiceStatus
buttons:
submit : π Find Invoice by Status
Widget S34 of type Separator
size = sm
style = solid
label = Find by Customer
Widget FindInvoiceByCustomerForm of type Form
title = Find by Customer
fields:
- customerId String
buttons:
submit : π Find Invoice by Customer
Widget S35 of type Separator
size = sm
style = solid
label = Find by CreatedBy
Widget FindInvoiceByCreatedByForm of type Form
title = Find by CreatedBy
fields:
- createdById String
buttons:
submit : π Find Invoice by CreatedBy
Widget S36 of type Separator
size = sm
style = solid
label = Find by ApprovedBy
Widget FindInvoiceByApprovedByForm of type Form
title = Find by ApprovedBy
fields:
- approvedById String
buttons:
submit : π Find Invoice by ApprovedBy
Widget S37 of type Separator
size = sm
style = solid
label = Find by Status and Customer
Widget FindInvoiceByStatusAndCustomerForm of type Form
title = Find by Status And Customer
fields:
- status InvoiceStatus
- id String
buttons:
submit : π Find Invoice by Status And Customer
Widget S38 of type Separator
size = lg
style = solid
label = CRUD
Widget CrudInvoiceText of type Text
subtitle = To Create/Update/Delete Invoices!
content = Here you can <b>Create</b> or <b>Delete</b> Invoices.
Widget S39 of type Separator
size = sm
style = solid
label = Create
Widget CreateInvoiceForm of type Form
title = Submit New Invoices
fields:
- title String
- customerId String
- totalAmount Float
- createdBy String
buttons:
submit : β Submit Invoice
# Widget S40 of type Separator
# size = sm
# style = solid
# label = Update
# Widget UpdateInvoiceForm of type Form
# title = Update Invoices
# datatype = Invoice
# buttons:
# submit : βοΈ Update Invoice
# fetch : π Fetch by ID
Widget S41 of type Separator
size = sm
style = solid
label = Delete
Widget DeleteInvoiceForm of type Form
title = Delete Invoices
fields:
- id String
buttons:
submit : β Delete Invoice
UserDashboard
Widget UserTable of type Table
title = Users π
limit = 8
datatype = User
Widget S9 of type Separator
size = md
style = solid
label = Search
Widget SearchUserText of type Text
subtitle = To find Users!
content = Here you can find users by <b>ID</b>, <b>Name</b>, or <b>Role</b>.
Widget S10 of type Separator
size = sm
style = solid
label = Find by ID
Widget FindUserByIdForm of type Form
title = Find by ID
fields:
- id String
buttons:
submit : π Find User by ID
Widget S11 of type Separator
size = sm
style = solid
label = Find by Name
Widget FindUserByNameForm of type Form
title = Find by Name
fields:
- name String
buttons:
submit : π Find User by Name
Widget S12 of type Separator
size = sm
style = solid
label = Find by Role
Widget FindUserByRoleForm of type Form
title = Find by Role
fields:
- role String
buttons:
submit : π Find Users by Role
Widget S4 of type Separator
size = md
style = solid
label = CRUD
Widget CrudUserText of type Text
subtitle = To Create/Update/Delete Users!
content = Here you can <b>Create</b>, <b>Update</b>, or <b>Delete</b> Users.
Widget S5 of type Separator
size = sm
style = solid
label = Create
Widget CreateUserForm of type Form
title = Create New Users
fields:
- name String
- role String
buttons:
submit : β Create User
Widget S6 of type Separator
size = sm
style = solid
label = Update
Widget UpdateUserForm of type Form
title = Update Users
fields:
- id String
- name String
- role String
buttons:
submit : βοΈ Update User
fetch : π Fetch by ID
Widget S7 of type Separator
size = sm
style = solid
label = Delete
Widget DeleteUserForm of type Form
title = Delete Users
fields:
- id String
buttons:
submit : β Delete User
CustomerDashboard
Widget CustomerTable of type Table
title = Customers ποΈ
limit = 8
datatype = Customer
Widget S20 of type Separator
size = md
style = solid
label = Search
Widget SearchCustomerText of type Text
subtitle = To find Customers!
content = Here you can find customers by <b>ID</b>, <b>Name</b>, or <b>Department</b>.
Widget S21 of type Separator
size = sm
style = solid
label = Find by ID
Widget FindCustomerByIdForm of type Form
title = Find by ID
fields:
- id String
buttons:
submit : π Find Customer by ID
Widget S22 of type Separator
size = sm
style = solid
label = Find by Name
Widget FindCustomerByNameForm of type Form
title = Find by Name
fields:
- name String
buttons:
submit : π Find Customer by Name
Widget S23 of type Separator
size = sm
style = solid
label = Find by Department
Widget FindCustomerByDepartmentForm of type Form
title = Find by Department
fields:
- department String
buttons:
submit : π Find Customers by Department
Widget S4 of type Separator
size = md
style = solid
label = CRUD
Widget CrudCustomerText of type Text
subtitle = To Create/Update/Delete Customers!
content = Here you can <b>Create</b>, <b>Update</b>, or <b>Delete</b> Customers.
Widget S5 of type Separator
size = sm
style = solid
label = Create
Widget CreateCustomerForm of type Form
title = Create New Customers
fields:
- name String
- department String
buttons:
submit : β Create Customer
Widget S6 of type Separator
size = sm
style = solid
label = Update
Widget UpdateCustomerForm of type Form
title = Update Customers
fields:
- id String
- name String
- department String
buttons:
submit : βοΈ Update Customer
fetch : π Fetch by ID
Widget S7 of type Separator
size = sm
style = solid
label = Delete
Widget DeleteCustomerForm of type Form
title = Delete Customers
fields:
- id String
buttons:
submit : β Delete Customer
AboutDashboard
title: π£ About
subtitle: Invoice Approval System!
layout: SingleColumnLayout
Widget AboutText of type Text
title = About Invoice Approval App π§Ύβ
subtitle = version 1.0.0
content = A complete workflow system for managing invoice approvals<br><b>Description:</b><br><p>InvoiceService is a reference example showcasing the use of entity-based finite state machines (FSMs) in the Ocean-lab DSL platform.</p><p>It manages the lifecycle of invoices submitted for approval, using a dedicated `Invoice` datatype and `InvoiceStatus` enum. The FSM governs state transitions (e.g., Draft β PendingApproval β Approved/Rejected β Paid) and encapsulates business logic and rule enforcement.</p><p>The service exposes an API for submitting, approving, rejecting, and querying invoices. FSM transitions are driven by input events and validated using expressions like `CanApprove`. Runtime context (e.g., user role, pending approvals) is kept internal to the FSM and isolated from external access.</p><p>InvoiceService demonstrates how Ocean DSL can be used to declaratively define API-driven workflows, enforce domain constraints, and model business processes with clear, composable constructs.</p>
# @ocean-meta-start
# tags:
# - ui
# perspective:
# feature: invoice-approval
# service: ui-service
# @ocean-meta-end
@ui
InvoiceUi
@perspectives: version:0.1.0, lifestyle:stable
framework: htmx
styling: bootstrap
template: go-html-template
backend: go-gin
static: static
configType: UiConfig
nav Invoices dashboard = InvoiceDashboard
nav Users dashboard = UserDashboard
nav Customers dashboard = CustomerDashboard
nav About dashboard = AboutDashboard
header title = π§Ύβ Invoice Approval App!
header subtitle = Managing invoices made simple π
header align = center
footer title = β‘ Built with Ocean-lab
footer subtitle = Version 1.0.0
footer align = center
use dashboard InvoiceDashboard as inv
use dashboard UserDashboard as user
use dashboard CustomerDashboard as cus
use dashboard AboutDashboard as about
use api InvoiceApi
connect inv.InvTable.listRows -> InvoiceApi.getInvoices
connect inv.ApproveInvoiceByIdForm.submit -> InvoiceApi.approveInvoice
connect inv.RejectInvoiceByIdForm.submit -> InvoiceApi.rejectInvoice
connect inv.FindInvoiceByIdForm.submit -> InvoiceApi.getInvoice
connect inv.FindInvoiceByStatusForm.submit -> InvoiceApi.findInvoiceByStatus
connect inv.FindInvoiceByCustomerForm.submit -> InvoiceApi.findInvoiceByCustomer
connect inv.FindInvoiceByCreatedByForm.submit -> InvoiceApi.findInvoiceByCreatedBy
connect inv.FindInvoiceByApprovedByForm.submit -> InvoiceApi.findInvoiceByApprovedBy
connect inv.FindInvoiceByStatusAndCustomerForm.submit -> InvoiceApi.findInvoiceByStatusAndCustomer
connect inv.CreateInvoiceForm.submit -> InvoiceApi.submitInvoice
# connect inv.UpdateInvoiceForm.submit -> InvoiceApi.updateInvoice
# connect inv.UpdateInvoiceForm.fetch -> InvoiceApi.getInvoice
connect inv.DeleteInvoiceForm.submit -> InvoiceApi.deleteInvoice
connect user.UserTable.listRows -> InvoiceApi.getUsers
connect user.CreateUserForm.submit -> InvoiceApi.createUser
connect user.UpdateUserForm.submit -> InvoiceApi.updateUser
connect user.UpdateUserForm.fetch -> InvoiceApi.getUser
connect user.FindUserByIdForm.submit -> InvoiceApi.getUser
connect user.FindUserByNameForm.submit -> InvoiceApi.findUserByName
connect user.FindUserByRoleForm.submit -> InvoiceApi.findUserByRole
connect cus.CustomerTable.listRows -> InvoiceApi.getCustomers
connect cus.CreateCustomerForm.submit -> InvoiceApi.createCustomer
connect cus.UpdateCustomerForm.submit -> InvoiceApi.updateCustomer
connect cus.UpdateCustomerForm.fetch -> InvoiceApi.getCustomer
connect cus.FindCustomerByIdForm.submit -> InvoiceApi.getCustomer
connect cus.FindCustomerByNameForm.submit -> InvoiceApi.findCustomerByName
connect cus.FindCustomerByDepartmentForm.submit -> InvoiceApi.findCustomerByDepartment
# @ocean-meta-start
# tags:
# - deployment
# perspective:
# feature: invoice-approval
# service: all
# @ocean-meta-end
@deploy
Name: InvoiceApproval-deploy
@import service P.db.postgres.docker@1.0.0 as PostgresqlDB
InvoiceDeploy
service InvoiceService
replica 1
export 9098:InvoiceService.api
dependsOn InvoiceDatabase
InvoiceDatabase
service PostgresqlDB
InvoiceUiDeploy
service InvoiceUi
replica 1
export 8088:InvoiceUi.config
dependsOn InvoiceDeploy
flowchart TD
u[User]
subgraph sys[InvoiceApprovalSystem]
subgraph s[InvoiceService]
a[API<br/>InvoiceAPI]
f[FSM<br/>InvoiceApprovalFSM]
e[Expressions<br/>ApprovalRules, Validators]
c[Context<br/>PendingInvoices, UserState]
end
d[(Database<br/>InvoiceDB)]
cfg{{Config<br/>InvoiceConfig}}
end
%% Interactions
u -.-> a
%% API routes
a -- "submit / approve / reject<br>getPending / getStatus" --> f
a -- "getInvoice / listInvoices" --> d
%% FSM dependencies
f -- "read/write" --> d
f -- "read/write" --> c
f -.-> e
%% Expression dependencies
e -.-> d
%% Config binding
s --> cfg
d -.-> cfg