-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Computing shipment (packages) for an order (#100)
`Order.DefaultMachine` ---------------------- The `add_addresses` event performs: * persists the addresses * computes default packages from each location * finds an optimal shipment * splits any "too heavy" packages into smaller ship-able ones. * persists the packages to DB (in a transaction). - Using `Domain.Shipment.to_package/2` and `Model.Package.create/1`. > The computation is short-circuited wherever possible. The shipment is available in the `Context.state.shipment` field, and could be an empty list - indicating that there is no ship-able fulfillment for this order. Change to `EmbeddedShippingMethod` ---------------------------------- It is not possible to use the Money struct in an embed. Need to open an issue upstream. Misc changes ------------ 1. Add `:number` to `PackageItem` schema. 2. Add changeset for (partial) order update - this greatly simplifies the transition 3. Remove defunct `model/stock.ex` 4. Remove the unused `create/2` clause in `Model.Package` [delivers #157926007] [delivers #158236509] [finishes #155976354]
- Loading branch information
Showing
21 changed files
with
967 additions
and
432 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
apps/snitch_core/lib/core/domain/order/default_machine.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
defmodule Snitch.Domain.Order.DefaultMachine do | ||
@moduledoc """ | ||
The (default) Order state machine. | ||
The state machine is describe using DSL provided by `BeepBop`. | ||
Features: | ||
* handle both cash-on-delivery and credit/debit card payments | ||
## Customizing the state machine | ||
There is no DSL or API to change the `DefaultMachine`, the developer must make | ||
their own module, optionally making use of DSL from `BeepBop`. | ||
This allows the developer to change everything, from the names of the state to | ||
the names of the event-callbacks. | ||
## Writing a new State Machine | ||
The state machine module must define the following functions: | ||
_document this pls!_ | ||
### Tips | ||
`BeepBop` is specifically designed to used in defining state-machines for | ||
Snitch. You will find that the design and usage is inspired from | ||
`Ecto.Changeset` and `ExUnit` setups | ||
The functions that it injects conform to some simple rules: | ||
1. signature: | ||
``` | ||
@spec the_event(BeepBop.Context.t) :: BeepBop.Context.t | ||
``` | ||
2. The events consume and return contexts. BeepBop can manage simple DB | ||
operations for you like, | ||
- accumulating DB updates in an `Ecto.Multi`, and run it only if the | ||
whole event transition goes smoothly without any errors. | ||
Essentially run the event callback in a DB transaction. | ||
- auto updating the `order`'s `:state` as the last step of the callback. | ||
Make use of the helpers provided in `Snitch.Domain.Order.Transitions`! They | ||
are well documented and can be composed really well. | ||
### Additional information | ||
The "states" of an `Order` are known only at compile-time. Hence other | ||
modules/functions that perform some logic based on the state need to be | ||
generated or configured at compile-time as well. | ||
""" | ||
# TODO: How to attach the additional info like ability, etc with the states? | ||
# TODO: make the order state machine a behaviour to simplify things. | ||
|
||
use Snitch.Domain | ||
use BeepBop, ecto_repo: Repo | ||
|
||
alias Snitch.Domain.Order.Transitions | ||
alias Snitch.Data.Model.Order, as: OrderModel | ||
alias Snitch.Data.Schema.Order, as: OrderSchema | ||
|
||
state_machine OrderSchema, | ||
:state, | ||
~w(cart address payment processing rts shipping complete cancelled)a do | ||
event(:add_addresses, %{from: [:cart], to: :address}, fn context -> | ||
context | ||
|> Transitions.associate_address() | ||
|> Transitions.compute_shipments() | ||
|> Transitions.persist_shipment() | ||
end) | ||
|
||
event(:add_payment, %{from: [:address], to: :payment}, fn context -> | ||
context | ||
end) | ||
|
||
event(:confirm, %{from: [:payment], to: :processing}, fn context -> | ||
context | ||
end) | ||
|
||
event(:captured, %{from: [:processing], to: :rts}, fn context -> | ||
context | ||
end) | ||
|
||
event( | ||
:payment_pending, | ||
%{from: %{not: ~w(cart address payment cancelled)a}, to: :payment}, | ||
fn context -> | ||
context | ||
end | ||
) | ||
|
||
event(:ship, %{from: ~w[rts processing]a, to: :shipping}, fn context -> | ||
context | ||
end) | ||
|
||
event(:recieved, %{from: [:shipping], to: :complete}, fn context -> | ||
context | ||
end) | ||
|
||
event(:cancel, %{from: %{not: ~w(shipping complete cart)a}, to: :cancelled}, fn context -> | ||
context | ||
end) | ||
end | ||
|
||
def persist(%OrderSchema{} = order, to_state) do | ||
old_line_items = Enum.map(order.line_items, &Map.from_struct/1) | ||
OrderModel.update(%{state: to_state, line_items: old_line_items}, order) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
defmodule Snitch.Domain.Order.Transitions do | ||
@moduledoc """ | ||
Helpers for the `Order` state machine. | ||
The `Snitch.Domain.Order.DefaultMachine` makes direct use of these helpers. | ||
By documenting these handy functions, we encourage the developer of a custom | ||
state machine to use, extend or compose them to build large event transitions. | ||
""" | ||
|
||
use Snitch.Domain | ||
|
||
alias BeepBop.Context | ||
alias Snitch.Data.Model.Package | ||
alias Snitch.Data.Schema.Order | ||
alias Snitch.Domain.{Shipment, ShipmentEngine, Splitters.Weight} | ||
|
||
@doc """ | ||
Persists the address and associates them with the `order`. | ||
The following fields are required under the `:state` key: | ||
* `:billing_address` The billing `Address` params | ||
* `:shipping_address` The shipping `Address` params | ||
## Note | ||
This transition is "impure" as it does not use the multi, the addresses are | ||
associated "out-of-band". | ||
""" | ||
@spec associate_address(Context.t()) :: Context.t() | ||
def associate_address( | ||
%Context{ | ||
valid?: true, | ||
struct: %Order{} = order, | ||
state: %{ | ||
billing_address: billing, | ||
shipping_address: shipping | ||
} | ||
} = context | ||
) do | ||
changeset = | ||
order | ||
|> Repo.preload([:billing_address, :shipping_address]) | ||
|> Order.partial_update_changeset(%{ | ||
billing_address: billing, | ||
shipping_address: shipping | ||
}) | ||
|
||
case Repo.update(changeset) do | ||
{:ok, order} -> | ||
Context.new(order, state: context.state) | ||
|
||
{:error, errors} -> | ||
struct(context, valid?: false, multi: errors) | ||
end | ||
end | ||
|
||
def associate_address(%Context{} = context), do: struct(context, valid?: false) | ||
|
||
@doc """ | ||
Computes a shipment fulfilling the `order`. | ||
Returns a new `Context.t` struct with the `shipment` under the the [`:state`, | ||
`:shipment`] key-path. | ||
> The `:state` key of the `context` is not utilised here. | ||
## Note | ||
If `shipment` is `[]`, we mark the `context` "invalid" because we could not | ||
find any shipment. | ||
""" | ||
@spec compute_shipments(Context.t()) :: Context.t() | ||
# TODO: This function does not gracefully handle errors, they are raised! | ||
def compute_shipments( | ||
%Context{ | ||
valid?: true, | ||
struct: %Order{} = order | ||
} = context | ||
) do | ||
order | ||
|> Shipment.default_packages() | ||
|> ShipmentEngine.run(order) | ||
|> Weight.split() | ||
|> case do | ||
[] -> | ||
struct(context, valid?: false, state: %{shipment: []}) | ||
|
||
shipment -> | ||
struct(context, state: %{shipment: shipment}) | ||
end | ||
end | ||
|
||
def compute_shipments(%Context{valid?: false} = context), do: context | ||
|
||
@doc """ | ||
Persists the computed shipment to the DB. | ||
`Package`s and their `PackageItem`s are inserted together in a DB transaction. | ||
Returns a new `Context.t` struct with the `shipment` under the the [`:state`, | ||
`:shipment`] key-path. | ||
In case of any errors, an invalid Context struct is returned, with the error | ||
under the `:multi`. | ||
""" | ||
@spec persist_shipment(Context.t()) :: Context.t() | ||
def persist_shipment(%Context{valid?: true, struct: %Order{} = order} = context) do | ||
%{state: %{shipment: shipment}, multi: multi} = context | ||
|
||
function = fn _ -> | ||
shipment | ||
|> Stream.map(&Shipment.to_package(&1, order)) | ||
|> Stream.map(&Package.create/1) | ||
|> Enum.reduce_while({:ok, []}, fn | ||
{:ok, package}, {:ok, acc} -> | ||
{:cont, {:ok, [package | acc]}} | ||
|
||
{:error, _} = error, _ -> | ||
{:halt, error} | ||
end) | ||
end | ||
|
||
struct(context, multi: Multi.run(multi, :packages, function)) | ||
end | ||
|
||
def persist_shipment(%Context{valid?: false} = context), do: context | ||
end |
Oops, something went wrong.