Green Light - Custom CRM


Green Light is a group trip organization company. They sell and manage thousands of group trips per year.

Their process was done using Excel Spreadsheets and they needed a customized CRM to manage and scale their business successfully.


touropspro.com

ongoing +8 months

Design of ui and ux

The CRM had a basic style already defined. We worked closely with Green Light to define the interaction their team needed. We defined all views using Invision where we could collaborate and make comments. We continued their clean and functional look and feel that goes with their brand.


Development

This project started as a rescue project. This was somehow challenging, since the base of the project had not been written by our team and lacked proper testing.

For building this custom CRM we used Rails. This permits us write better code and faster. This app manages sensitive data for the core of Green Light business, so assuring a solid unit test suite was a must. We wrote these using RSPEC and Cucumber.

The development team is conformed by 4 engineers working full time.

class Supplier < ActiveRecord::Base
  include SearchableSorting
  include CSVImportable
  include Suppliers::Stringify
  include Suppliers::CSV
  include Suppliers::Contacts

  has_many :suppliers_activities_types, dependent: :destroy
  has_many :activity_types, through: :suppliers_activities_types
  has_many :default_services, dependent: :destroy
  has_many :locations, as: :addressable, dependent: :destroy
  has_many :phones, as: :phoneable, dependent: :destroy
  has_many :destinations, through: :locations
  has_many :supplier_activity_feeds, dependent: :destroy
  has_many :services, dependent: :destroy
  has_many :activities, dependent: :destroy

  alias_method :activity_feeds, :supplier_activity_feeds

  scope :by_proposal, -> (proposal_id) { where(destination_id: Proposal.find(proposal_id).destinations.pluck(:id)) }
  scope :search_by_name, -> (name) { where('lower(suppliers.name) LIKE ?', "%#{name.downcase}%") }
  scope :supplier_list, -> (trip_id) { Trip.find(trip_id).suppliers }
  scope :search_supplier, -> (trip_id, name) { supplier_list(trip_id).search_by_name(name) }

  validates :name, :activity_type_ids, presence: true

  accepts_nested_attributes_for :phones, allow_destroy: true, reject_if: proc { |phone| phone[:number].nil? || phone[:number].empty? }
  accepts_nested_attributes_for :locations, allow_destroy: true, reject_if: :all_blank
  accepts_nested_attributes_for :suppliers_activities_types, allow_destroy: true, reject_if: :all_blank
  accepts_nested_attributes_for :activity_types, allow_destroy: true, reject_if: :all_blank
  accepts_nested_attributes_for :default_services, allow_destroy: true, reject_if: :all_blank

  mapping do
    indexes :id, type: :integer
    indexes :name
    indexes :owner do
      indexes :name
    end
  end
end
require 'rails_helper'

RSpec.describe CSVImportable do
  let (:test_class) { double("TestClass") { include CSVImportable } }
  let (:file) { File.open(Rails.root.join("spec/fixtures/csv_example.csv")) }
  let(:hash_row){ {:id=>"1",:first_name => 'pepe',:last_name => 'trueno' } }

  before do
    class FakeModel
      include ActiveModel::Model
      include CSVImportable
      csv_allow_headers :id, :first_name, :last_name

      csv_allow_headers :id, :first_name, :last_name

      attr_accessor :id,:first_name,:last_name
      def self.create(params); end
      def self.find_by_id(id); end

      def update(params); end
    end
  end

  describe "create a new entity" do
    before { expect(FakeModel).to receive(:create).with(hash_row) }
    it { FakeModel.import_sync(file) }
  end

  describe "update an entity" do
    before do
      expect_any_instance_of(FakeModel).to receive(:update).with(hash_row)
      expect(FakeModel).to receive(:find_by_id).with("1").and_return(FakeModel.new(hash_row))
    end
    it { FakeModel.import_sync(file) }
  end

  describe "csv_rename_header" do
    before do
      class FakeModel
        csv_allow_headers :id, :first_name, :other_name
        csv_rename_headers last_name: :other_name
      end
    end
    let(:hash_row){ {:id=>"1",:first_name => 'pepe',:other_name => 'trueno' } }
    before { expect(FakeModel).to receive(:create).with(hash_row) }
    it { FakeModel.import_sync(file) }
  end

  describe "csv_allow_headers" do
    before do
      class FakeModel
        csv_rename_headers last_name: :other_name
        csv_allow_headers :id, :other_name
      end
    end
    let(:hash_row){ {:id=>"1", :other_name => 'trueno' } }
    before { expect(FakeModel).to receive(:create).with(hash_row) }
    it { FakeModel.import_sync(file) }
  end
end
class Multisearch
  def initialize(value)
    @value = value
    @client = Elasticsearch::Client.new(url: ENV['BONSAI_URL'], log: true)
  end

  def index
    @client.search(
      index: indexes(Proposal, Supplier, Trip, Person),
      body: body)
  end

  def tasks
    @client.search(
      index: indexes(Supplier, Trip, Person),
      body: body)
  end

  private

  def indexes(*models)
    models.map(&:index_name)
  end

  def body
    {
      min_score: 0.01,
      query: {
        bool:{
          should:[
            {match_phrase_prefix:{ name: @value }}]
        }
      },
      sort: [{name: 'asc'}, '_score'],
      size: 100
    }
  end
end

Don't take our word for it.

We run Green Light through Code Climate. This third party app rates our code and gives us recommendations of how to improve it.


Our Process

Meetings

We have weekly calls where we discuss progress. We use Skype, Google Hangouts or GotoMeeting.

We agreed on a day and time that worked for everyone.

Planning

In the beginning we send a timeline specifying the planning and tasks for that week.

These tasks are the same cards we have planned in Trello.

Trello

We translate all requirements into stories in Trello Cards.

Trello is the place to add any specification, ask questions or comments to the team.

Deliveries

Once the tasks are done they go through a testing process and we pass them for your approval.

Every week we will pass cards onto the client list for you to give us feedback.

Quality Assurance

Once the functionality is completed one of our functional testers will try to break the feature.

We either move it to Needs Improvement or pass it to you for feedback.

Feedback

We need the support from you to review the cards and Approve them or tell us how to improve it.

This is key to meet goals and being on time in the development of the application.