Trailblazer tutorial: move business logic from controller - part 2.
Tags: ruby, tutorial, beginners, programming
Published: 2019-10-15 11:49:06

---
title: Trailblazer tutorial: move business logic from controller - part 2.
published: true
description: 
tags: ruby, tutorial, beginners, programming
series: Trailblazer tutorial step by step
cover_image: https://thepracticaldev.s3.amazonaws.com/i/nwjg58l1anucl20d58wf.png
canonical_url: https://www.2n.pl/blog/trailblazer-tutorial-move-business-logic-from-controller-part-2
---
Since we have basic cases and success flow tested and implemented, it is time to focus on our business logic which we want to move from controller to Operation.  

Our first identified logic is to care about if **event is closed or is about to be closed in 1 hour**. So let's add tests to check if we will not be able to add a proposal in any of those cases (we already have a test for a success flow in which an event is opened, as described by the context name).


```ruby
@event.closed? && @event.closes_at < 1.hour.ago
```

So lets add 2 tests and update “open case”
    
```ruby
context 'with open event' do
  let!(:event) {
    Event.create(
      state: 'open', slug: 'Lorem', name: 'Ipsum',
      url: 'http://www.c.url', closes_at: Time.current + 1.day
    ) }
  ...
end

context 'with closed event' do
  let(:session_format) { instance_double(SessionFormat, id: 53) }
  let!(:event) { Event.create(state: 'closed', slug: 'Lorem', name: 'Ipsum', url: 'http://www.c.url') }
  it 'returns result object with an error about closed event without saving the proposal' do
    expect(result[:errors]).to eq('Event is closed')
    expect(result).to be_failure
  end
end


context 'with event that is about to be closed' do
  let(:session_format) { instance_double(SessionFormat, id: 53) }
  let!(:event) {
    Event.create(
      state: 'open', slug: 'Lorem', name: 'Ipsum',
      url: 'http://www.c.url', closes_at: Time.current + 40.minutes
    ) }

  it 'returns result object with an error about closed event without saving the proposal' do
    expect(result[:errors]).to eq('Event is closed')
    expect(result).to be_failure
  end
end
```

And check if they pass:

```bash    
$ rspec spec/concepts/proposal/operation/create_spec.rb
..FF
Failures:

1) Proposal::Operation::Create with valid params with closed event returns result object with an error about closed event without saving the proposal
Failure/Error: expect(result[:errors]).to eq("Event is closed")
expected: 'Event is closed'
got: nil

2) Proposal::Operation::Create with valid params with event that is about to be closed returns result object with an error about closed event without saving the proposal
Failure/Error: expect(result[:errors]).to eq('Event is closed')
expected: 'Event is closed'
got: nil

Finished in 0.43801 seconds (files took 8.64 seconds to load)
4 examples, 2 failures
```

That wasn’t unexpected since we didn’t implement handling this case. Let’s do that. 
      

**_(Iteration 5)_** Add handling “event closed or about to be closed” case.
    

We need to add a step checking if the event is opened, and a method to handle failure flow. 

```ruby
module Proposal::Operation
  class Create < Trailblazer::Operation
    class Present < Trailblazer::Operation
      step Model(Proposal, :new)
      step Contract::Build(constant: Proposal::Contract::Create)
      step Contract::Validate(key: :proposal) 
    end
  
    step Nested(Present)
    step :event
    fail :event_not_found
    # step for checking if event is opened
    step :event_open?
    # step for handling failure
    fail :event_not_open_error
    step :assign_event
    step Contract::Persist()
  
    def event(ctx, params:, **)
      ctx[:event] = Event.find_by(slug: params[:event_slug])
    end

    def assign_event(ctx, **)
      ctx[:model].event = ctx[:event]
    end
  
    # we check if event.open? Method is true, and if event will be open for next hour
    def event_open?(ctx, **)
      ctx[:event].open? && ctx[:event].closes_at >= 1.hour.since
    end

    # -- bad stuff handled there --
    def event_not_found(ctx, **)
      ctx[:errors] = "Event not found"
    end

    def event_not_open_error(ctx, **)
      ctx[:errors] = "Event is closed"
    end
  end
end
```

Should all of our tests be green now? Let's check:
 
```bash
$ rspec spec/concepts/proposal/operation/create_spec.rb
FF..
Failures:

1) Proposal::Operation::Create with valid params with open event creates a proposal assigned to event identified by slug
Failure/Error: ctx[:event].open? && ctx[:event].closes_at >= 1.hour.since

NoMethodError:
undefined method `>=' for nil:NilClass
  
2) Proposal::Operation::Create with valid params without event returns result object with an error about closed event without saving the proposal
Failure/Error: expect(result[:errors]).to eq("Event not found")

expected: 'Event not found'
got: 'Event is closed'
  
Finished in 1.37 seconds (files took 12.98 seconds to load)
4 examples, 2 failures
```

So what can we see, is regression - newly implemented feature, broke our previous tests? Why is that? Let's try to debug “no event” case by one of the coolest TRB features which are **#wtf?** method: 

```ruby
context 'without event' do
  let(:session_format)  { instance_double(SessionFormat, id: 53) }
  it 'returns result object with an error about closed event without saving the proposal' do
    binding.pry
    expect(result[:errors]).to eq('Event not found')
    expect(result).to be_failure
  end
end

pry(...)> result.wtf?

`-- Proposal::Operation::Create
|-- Start.default
|-- Nested(Proposal::Operation::Create::Present)
| |-- Start.default
| |-- model.build
| |-- contract.build
| |-- contract.default.validate
| | |-- Start.default
| | |-- contract.default.params_extract
| | |-- contract.default.call
| | `-- End.success
| `-- End.success
|-- event
|-- event_not_found
|-- event_not_open_error
`-- End.failure
```

As you can see, **#wtf?** method called on our result, gave us a really explicit tree of what was called when and how it was nested. If we will track what happened, we can see that after the method **event** was called, _fail_ step **event_not_found** was called, and then every other (in our case it's just event_not_open_error) fail step was also called.

To understand it we need to fully understand what keyword **trail** in Trailblazer means, and how it works. And that will be the ***most important*** knowledge from this blogpost. [http://trailblazer.to/gems/operation/2.0/index.html](http://trailblazer.to/gems/operation/2.0/index.html) - Flow Control section should explain basics. 
But what else we have in our case? We have another fail step in the failure trail. So after calling **event_not_found** which calls:
 
```ruby
ctx[:errors] = 'Event not found'
```

Then we call **event_not_open_error** which calls

```ruby
ctx[:errors] = 'Event is closed'
```
and that overwrite our *[:errors]* value. 

Since sometimes we need to exit from failure trail and just stop operation, and sometimes we want to run all further fail steps, TRB comes with handy method:

```ruby
fail :event_not_found, fail_fast: true
```

After we change *fail_fast* value to true, whole operation logic will be stopped on the fail trail after calling failing method **event_not_found** in this case.  
So our Operation flow will look like:  
```ruby
step Nested(Present)
step :event
fail :event_not_found, fail_fast: true
step :event_open?
fail :event_not_open_error
step :assign_event
step Contract::Persist()
```

And running the tests will result in:

```bash
$ rspec spec/concepts/proposal/operation/create_spec.rb
....

Finished in 0.48573 seconds (files took 8.7 seconds to load)
4 examples, 0 failures
```