Rust's type system allows implementing state machine in a straightforward way. In our application we might use it to define a sale's state in a specific point in time. We can define 4 different states for a sale:
Draft: An user can edit a sale, this can be used as some form of budget or presale, it does not affect inventory or accounting, you can only approve a draft sale.
Approved: The sale can't be edited, now it's an invoice that should be delivered to the client. You can cancel, pay or partially pay an approved sale.
Pay: An user payed the sale, now you can generate a collection receipt for the sale. You can only cancel a payed sale.
Partially Pay: The user received a part of the total payment, this could be used if you want to sell a product by parts using some form of credit, then you generate a collection receipt. You can cancel and pay a partially payed sale.
Cancel: This is an annulled invoice, if you commit a mistake with a sale, you need to generate a credit/debit note afterwards. This is the final state, once you cancel a sale, you can't change its state.
Let's see it in code. Take a look at src/models/sale_state.rs
:
use crate::models::sale::Sale;
#[derive(DbEnum, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(juniper::GraphQLEnum)]
pub enum SaleState {
Draft,
Approved,
PartiallyPayed,
Payed,
Cancelled
}
#[derive(Debug)]
pub enum Event {
Approve,
Cancel,
PartiallyPay,
Pay,
}
impl SaleState {
pub fn next(self, event: Event) -> Result<SaleState, String> {
match (self, event) {
(SaleState::Draft, Event::Approve) => Ok(SaleState::Approved),
(SaleState::Approved, Event::Pay) => Ok(SaleState::Payed),
(SaleState::Approved, Event::PartiallyPay) => Ok(SaleState::PartiallyPayed),
(SaleState::Approved, Event::Cancel) => Ok(SaleState::Cancelled),
(SaleState::Payed, Event::Cancel) => Ok(SaleState::Cancelled),
(SaleState::PartiallyPayed, Event::Cancel) => Ok(SaleState::Cancelled),
(SaleState::PartiallyPayed, Event::Pay) => Ok(SaleState::Payed),
(sale_state, sale_event) => Err(format!("You can't {:#?} from {:#?} state", sale_event, sale_state))
}
}
}
The enum SaleState
is used as a database enum, let's take a look at the migration migrations/2019-09-25-114234_add_state_to_sales/up.sql
:
CREATE TYPE sale_state AS ENUM ('draft', 'approved', 'partially_payed', 'payed', 'cancelled');
ALTER TABLE sales ADD COLUMN state sale_state;
UPDATE sales SET state = 'approved';
ALTER TABLE sales ALTER COLUMN state SET NOT NULL;
Thanks to diesel-derive-enum
crate we can map a Rust enum to db enum.
In order to make sure we are respecting the rules we already defined, we might add a function in src/models/sale.rs
:
fn set_state(context: &Context, sale_id: i32, event: Event) -> FieldResult<bool> {
use crate::schema::sales::dsl;
use diesel::ExpressionMethods;
use diesel::QueryDsl;
use diesel::RunQueryDsl;
let conn: &PgConnection = &context.conn;
let sale_query_builder = dsl::sales
.filter(dsl::user_id.eq(context.user_id))
.find(sale_id);
let sale = sale_query_builder.first::<Sale>(conn)?;
let sale_state = sale.state.next(event)?;
diesel::update(sale_query_builder)
.set(dsl::state.eq(sale_state))
.get_result::<Sale>(conn)?;
Ok(true)
}
We can add a filter to updateSale
function, to make sure we only edit draft sales:
let sale = diesel::update(
dsl::sales
.filter(
dsl::user_id
.eq(context.user_id)
.and(dsl::state.eq(SaleState::Draft)),
)
.find(sale_id),
)
.set(¶m_sale)
.get_result::<Sale>(conn)?;
Then we add the approve, pay, partially pay and cancel functions:
fn approveSale(context: &Context, sale_id: i32) -> FieldResult<bool> {
Sale::set_state(context, sale_id, Event::Approve)
}
fn cancelSale(context: &Context, sale_id: i32) -> FieldResult<bool> {
//TODO: perform credit note or debit note
Sale::set_state(context, sale_id, Event::Cancel)
}
fn paySale(context: &Context, sale_id: i32) -> FieldResult<bool> {
//TODO: perform collection
Sale::set_state(context, sale_id, Event::Pay)
}
fn partiallyPaySale(context: &Context, sale_id: i32) -> FieldResult<bool> {
//TODO: perform collection
Sale::set_state(context, sale_id, Event::PartiallyPay)
}
Full source code here
Top comments (1)
Thank you