The Itinera bookRust edition

Part 4: Running it for real, Chapter 36

Orders refused before they start

What you want to do

The website now sends orders before they are finished: Quin's arrives without an order number, so nothing can name its journey, and order A-16 without the customer's name, so the logging service has nothing to file its lines under. You want such an order stopped before anything is done for it, and to know why.

The code

The shop is chapter 35's, with the order's number now optional in its initial data, since the website may leave it out:

Rust, from the program ch36_orders_refused_before_they_start
/// What the shop knows about an order when its journey starts. An order with no card leaves it
/// out.
#[derive(InitialData)]
struct NewOrder {
    order: Option<String>,
    customer: String,
    price: i64,
    discount: Option<i64>,
    parcel: Parcel,
    card: Option<String>,
    gift_card: bool,
}
Rust, the same in its asynchronous form, from the program ch36_orders_refused_before_they_start_async
/// What the shop knows about an order when its journey starts. An order with no card leaves it
/// out.
#[derive(InitialData)]
struct NewOrder {
    order: Option<String>,
    customer: String,
    price: i64,
    discount: Option<i64>,
    parcel: Parcel,
    card: Option<String>,
    gift_card: bool,
}

/// An instance of the shop's workflow for an order paid with this card.
fn order(
    order: Option<&str>,
    customer: &str,
    price: i64,
    discount: Option<i64>,
    parcel: Parcel,
    card: &str,
) -> Result<Instance<Orders, Asynchronous>, Box<dyn std::error::Error>> {
    Ok(Orders::builder()
        .channel("email")
        .reporters_global_metadata("customer", customer.to_owned())
        .initial_data(NewOrder {
            order: order.map(str::to_owned),
            customer: customer.to_owned(),
            price,
            discount,
            parcel,
            card: Some(card.to_owned()),
            gift_card: false,
        })
        .create()?)
}

In the asynchronous form, order takes the number as an Option too.

When the log receives the customer's name, it asks the logging service to file the journey's lines under it, and fails if the service refuses:

Rust, from the program ch36_orders_refused_before_they_start
fn set_global_metadata(&mut self, key: &str, value: &AnyValue) -> Result<(), Error> {
    if key == "customer" {
        let customer = value.downcast_ref::<String>().map_or("", String::as_str);
        self.service.file_under(customer)?;
    }
    self.metadata.insert(key, value.clone());
    Ok(())
}

The rest of the log is chapter 33's, with chapter 35's detail.

main creates each order's instance, then runs it, and says why an order went no further at either point:

Rust, from the program ch36_orders_refused_before_they_start
let created = Orders::builder()
    .channel("email")
    .reporters_global_metadata("customer", customer.to_owned())
    .initial_data(NewOrder {
        order: order.map(str::to_owned),
        customer: customer.to_owned(),
        price,
        discount,
        parcel: Parcel { weight, size },
        card: card.map(str::to_owned),
        gift_card,
    })
    .create();
let instance = match created {
    Ok(instance) => instance,
    Err(error) => {
        println!("no instance: {error}");
        continue;
    }
};
match executor.run(instance) {
    Ok(result) => follow_up(&result)?,
    Err(refusal) => println!("the journey was refused: {refusal}"),
}
Rust, the same in its asynchronous form, from the program ch36_orders_refused_before_they_start_async
let instance = match order(number, customer, price, None, parcel, card) {
    Ok(instance) => instance,
    Err(error) => {
        println!("no instance: {error}");
        continue;
    }
};
match journey(instance).await {
    Ok(result) => println!("order {} {}", result.journey_id, result.status.kind()),
    Err(refusal) => println!("the journey was refused: {refusal}"),
}

Orders is the workflow chapter 23 declares, with chapter 35's provider for cards, Parcel is chapter 21's and follow_up chapter 34's. In the asynchronous form, order creates the instance, and journey runs it, awaited, as in chapter 26. It prints:

What ch36_orders_refused_before_they_start prints, recorded in ch36_orders_refused_before_they_start.txt
Quin's order, without a number:
no instance: the journey ID could not be produced: the data bag holds no value under `order`
order A-16, without a customer's name:
the journey was refused: the global metadata could not be set: the logging service files no line without a customer's name

How it works

  1. An instance isn't created without its journey ID. The ID generator of chapter 11 names each journey after its order's number, read from the initial data with required. Quin's order has no number, so the generator returns an error, and create returns it in turn: the journey ID could not be produced. With no instance, there is nothing to run.
  2. An executor can refuse an instance. run returns a refusal for a journey that can't start, as chapter 5 said. Before the first event, the executor gives every reporter the journey ID, then the instance's entries of global metadata, as chapter 33 showed. The log failed on customer, so the executor refused the journey: the refusal says the global metadata could not be set, and carries the log's error.
  3. A refused journey never started. There is no journey_started, no step ran, and the bank was never called: unlike Pat's journey in chapter 31, aborted after the money was taken, A-16's has nothing to undo. There is no JourneyResult either, since there was no journey to end.
  4. The order is still the shop's to deal with. main learns why from the error and the refusal, as values. Once the website sends what was missing, a new instance runs the order, as chapter 24 said.

These orders were stopped by what they lacked. A mistake in how the shop's workflow is put together, such as two steps under one name, would stop every order instead, however complete.

Outcomes

We have shown how an order that can't be named or logged is stopped before its journey starts, and how the program learns why.

The usual way With Itinera
An incomplete request often gets halfway through, past the payment, before something notices the missing field. An order that can't be named, or whose journey a reporter can't take, is stopped before its first step.
Why a request never ran is often only a log line somewhere. create and run return why, as values the program reads.