Skip to content

Commit

Permalink
moving the north_harbor demo under this for documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
bokmann committed Jul 30, 2014
1 parent 77c146b commit 77e84dc
Show file tree
Hide file tree
Showing 105 changed files with 9,850 additions and 0 deletions.
Binary file added north_harbor/.DS_Store
Binary file not shown.
4 changes: 4 additions & 0 deletions north_harbor/.gitignore
@@ -0,0 +1,4 @@
db/schema.rb
db/*.sqlite3
config/database.yml
log/*.log
1 change: 1 addition & 0 deletions north_harbor/.ruby-version
@@ -0,0 +1 @@
1.8.7
129 changes: 129 additions & 0 deletions north_harbor/README
@@ -0,0 +1,129 @@
I have to admit, this app, as far as real solutions go, has some problems:
- There is too much logic in the views for my taste. Of course, that is what the StoneWall gem would address, and this isn't about stonewall (yet).
- I don't explain some of the tradeoffs in the modeling made. I'll explain it in a class based on this, but why, for example, don't I have tasks for data entry and verification? The short story - when the workflow is linear and the work to be done is done by the owner, it is acceptable to reduce the workflow down to 'linear, done by owner', and not worry about tasks.
- It needs to have tests. In the context that I change this app constantly to prove different points, the tests would slow that down. A future version based on real coursework would have tests though.





This is an example app using StonePath.

It isn't perfect yet, and the scope has diverged slightly since I wrote these requirements, but it is good enough for now, and it shows how to do some useful stuff with Stonepath. I'll be adding more to this very shortly.

This app shows:

- simple stonepath workflow
- restful conventions
- highjacking the ActiveRecord errors hash.

----
North Harbor Coffee company is a small chain of coffee shops in the Pacific Northwest of the United States. They have approximately 20 stores, and they are opening a few new ones every quarter.

Todd Saboe, the Director of Marketing for NHCC has decided they need a consolidated company-wide system for handling customer complaints. These complaints might be left in a "complaints or suggestions" box in each store, they might be mailed (or emailed) to people at the company, they might come in via a phone call or fax, or might even be the result of a customer survey. Where they come from isn't important, as long as the company has a consolidated way to handle (and report on) all complaints.

Complaints can be categorized as follows:
1) Complaint about service
"I waited forever for my drink, the barista was rude, and my coffee was wrong"
2) Complaint about quality
"Your beans are burnt and the coffee is bitter"
3) Complaint about details of a specific location
"The parking here is horrible and the drive-through blocks the main traffic"
4) Complaint about the company overall
"You guys exploit the peasant farmers of Guatemala"

And they are typed as follows:
1) Out of Scope - We might be exploiting peasant farmers, but that isn't something this system is meant to address. We might send those up the chain to management, but this system is about managing store quality and customer experience.
2) Minor complaint - the store manager or other appropriate parties should be notified about the complaint, but no followup is necessary.
3) Intermediate complaint - We will assign some action that the store manager should follow up on based on this complaint. The store manager should take action and record it in the system.
4) Major complaint - This category is referred to the corporate attorney and the store manager. Notes about this complaint are tracked in this system.

When a complaint is about a particular store, that store's Identification number will be entered along with the complaint. This number is displayed in the store and printed out on every receipt.

The complaint will have a description, generally the text of the complaint as provided by the complainer.

Users of the system can attach comments to the complaint.

If complaints result in any follow-up actions necessary by the store, those will be tracked along with the complaint. Actions have a description, comments, and a due date. If a store has an issue with any of these actions, there is an 'appeals' process where they can make an alternate suggestion for how to resolve the issue and kick it back to the "Customer Satisfaction Manager".


The workflow associated with a complaint is straightforward, but varies by the type.

1) The complaint is entered into the system.
Various different people enter the complaints. For instance, all of the complaints stuffed into the 'suggestions' boxes are sent to a third party company that does data entry. We may have the help desk answering phones enter complaints, and in the future, we may have a 'complaints and suggestions' box on our corporate website.

2) There are several employees with the title of "Customer Satisfaction Manager". They will receive the complaints, categorize and rate them as above. Depending on the rating, several different workflow paths will occur:


Out of Scope Path:
1) Out of Scope complaints stop here, and are pulled into one great big report every quarter.

Minor Complaint Path:
1) Minor complaints are sent to the store/store manager. The store manager can then mark them as 'addressed' or 'ignored', as they see fit. As always, they can attach any comments they want to this complaint.

Intermediate Complaint Path:
1) Intermediate complaints are sent to a "Customer Satisfaction Specialist" to handle as per their job. The store manager is also notified the complaint exists and can add comments to it. The Customer Satisfaction Specialist is free to do whatever is necessary to investigate the complaint, including talk to the store manager, employees, or even the customer if contact info is provided. The Customer resolution specialist can create one or more "Follow-up actions" for the store manager.

2) The store manager looks at each follow-up action and is responsible for implementing the action by the due date provided. (These may come up during the store's annual inspection, but that is outside the scope of this process). The store manager can also 'kick back' the action if they disagree. The kickback can go to either the Customer Resolution Specialist that created the follow-up action, or it can go to their manager, at the discretion of the store manager.

3) If kicked back, the action is either cancelled, or sent back to the manager as is, or potentially modified.


Major Complaint Path:
1) Major complaints are assigned to the corporate attorney and the store manager is notified. At this point it is marked "Under Investigation", and everyone is free to add comments to the complaint.

2) At some point the lawyer marks this complaint as "addressed", and is free to add whatever comments are necessary. The system doesn't track any other details of this workflow.


Assignment: What are the roles identified above?

Roles of the system:
Data Entry
Store/Store Manager
Customer Satisfaction Manager
Customer Satisfaction Specialist
Corporate Attorney

What are the classes identified, and what StonePath Entities are they?

Classes in the domain:
User (workbench)
Role (workbench)
Complaint (workitem, commentable))
Comment (just a polymorphic activerecord class)
Follow-up Actions (workitem, commentable, maybe just a custom task)
Tasks (task)
Store (workbench?)


What is the workflow of the Complaint workitem?

complaint workflow:

In Creation -> CSM Review -> Out of Scope
-> Store Manager Review -> Addressed
-> Ignored
-> CSS Review -> Pending Action -> Addressed
-> Attorney Review -> Addressed

What is the workflow of the Action Workitem? Could this simply be modeled as a custom task?
Action Workflow:
Defining -> Pending -> Completed
-> Kicked Back -> &Pending
-> Cancelled


What about 'owner'? If most actions in the system are taken care of by tasking, then is the owner even necessary?

Creation - owner is the data entry clerk.
CSM review - can stay data entry
Out of Scope - no owner
Store Manager Review - Store Owner
CSS Review - CSS
Attorney Purvue - Attorney
Addressed/Ignored - no owner


or, maybe it always stays the data entry clerk

no right answer - depends o how you want to use it in the domain
16 changes: 16 additions & 0 deletions north_harbor/Rakefile
@@ -0,0 +1,16 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require(File.join(File.dirname(__FILE__), 'config', 'boot'))

require 'rake'
require 'rake/testtask'
require 'rdoc/task'

require 'tasks/rails'


#require(File.join(File.dirname(__FILE__), 'vendor', 'gems', "**", 'lib', 'tasks', '*.rake'))

Dir["#{RAILS_ROOT}/vendor/gems/*/lib/tasks/*.rake"].sort.each { |rakefile| load rakefile }

12 changes: 12 additions & 0 deletions north_harbor/app/controllers/application_controller.rb
@@ -0,0 +1,12 @@
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.

class ApplicationController < ActionController::Base
include Authentication
include SentientController
helper :all # include all helpers, all the time
protect_from_forgery # See ActionController::RequestForgeryProtection for details

# Scrub sensitive parameters from your log
# filter_parameter_logging :password
end
88 changes: 88 additions & 0 deletions north_harbor/app/controllers/complaints_controller.rb
@@ -0,0 +1,88 @@
class ComplaintsController < ApplicationController
before_filter :login_required

# GET /complaints
# GET /complaints.xml
def index
@complaints = Complaint.all

respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @complaints }
end
end

# GET /complaints/1
# GET /complaints/1.xml
def show
@complaint = Complaint.find(params[:id])

respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @complaint }
end
end

# GET /complaints/new
# GET /complaints/new.xml
def new
@complaint = Complaint.new
@managers = User.find_all_by_role("customer_satisfaction_manager")
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @complaint }
end
end

# GET /complaints/1/edit
def edit
@complaint = Complaint.find(params[:id])
@managers = User.find_all_by_role("customer_satisfaction_manager")
end

# POST /complaints
# POST /complaints.xml
def create
@complaint = Complaint.new(params[:complaint])

respond_to do |format|
if @complaint.save
flash[:notice] = 'Complaint was successfully created.'
format.html { redirect_to(@complaint) }
format.xml { render :xml => @complaint, :status => :created, :location => @complaint }
else
format.html { render :action => "new" }
format.xml { render :xml => @complaint.errors, :status => :unprocessable_entity }
end
end
end

# PUT /complaints/1
# PUT /complaints/1.xml
def update
@complaint = Complaint.find(params[:id])

respond_to do |format|
if @complaint.update_attributes(params[:complaint])
flash[:notice] = 'Complaint was successfully updated.'
format.html { redirect_to(@complaint) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @complaint.errors, :status => :unprocessable_entity }
end
end
end

# DELETE /complaints/1
# DELETE /complaints/1.xml
def destroy
@complaint = Complaint.find(params[:id])
@complaint.destroy

respond_to do |format|
format.html { redirect_to(complaints_url) }
format.xml { head :ok }
end
end
end
7 changes: 7 additions & 0 deletions north_harbor/app/controllers/dashboard_controller.rb
@@ -0,0 +1,7 @@
class DashboardController < ApplicationController
before_filter :login_required

def index
end

end
44 changes: 44 additions & 0 deletions north_harbor/app/controllers/events_controller.rb
@@ -0,0 +1,44 @@
# Any workitem or task implements a state machine, and will have events that
# can be triggered from the current state. This controller does that.
# as long as the routes file has events as a nested resource of the workitem's controller,
# you can say:
# [post] /workitem/:id/event
class EventsController < ApplicationController

before_filter :setup_parentage

def index
@events = @parentage.last.aasm_events_for_current_state
end

def new
@event = params[:name]
@workitem = @parentage.last

if @workitem.class.aasm_events[@event.to_sym].respond_to?(:required_data)
@required_data = @workitem.class.aasm_events[@event.to_sym].required_data
end
end

def create
if @parentage.last.send(params[:name] + "!", nil, params)
# set flash, redirect to workitem. Other logic here when we get smart.
flash[:notice] = "Entered #{@parentage.last.aasm_state} state."
else
flash[:error] = @parentage.last.errors.full_messages
end
redirect_to polymorphic_path(@parentage)
end

private

# need to figure out how to simulate a url in testing. mocking maybe?
def setup_parentage
parent_path_parts = request.path.split("/")
parent_path_parts.shift
@parentage = Array.new
parent_path_parts.each_slice(2) do |k,v|
@parentage << k.classify.constantize.find(v.to_i) unless (k == "events" || v.nil?)
end
end
end
81 changes: 81 additions & 0 deletions north_harbor/app/controllers/follow_up_actions_controller.rb
@@ -0,0 +1,81 @@
class FollowUpActionsController < ApplicationController
before_filter :login_required, :setup_complaint

def index
@follow_up_actions = Complaint.find(params[:complaint_id]).follow_up_actions

respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @follow_up_actionss }
end
end

def show
@follow_up_action = Complaint.find(params[:complaint_id]).follow_up_actions.find(params[:id])

respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @follow_up_actions }
end
end

def new
@follow_up_action = FollowUpAction.new
workbench = @complaint.store.manager
@follow_up_action.workbench = workbench
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @follow_up_actions }
end
end

def edit
@follow_up_action = Complaint.find(params[:complaint_id]).follow_up_actions.find(params[:id])

end

def create
@follow_up_action = @complaint.follow_up_actions.new(params[:follow_up_action])
@follow_up_action.workbench = User.find params[:follow_up_action][:workbench_id]
respond_to do |format|
if @follow_up_action.save
flash[:notice] = 'FollowUpAction was successfully created.'
format.html { redirect_to([@complaint, @follow_up_actions]) }
format.xml { render :xml => @follow_up_actions, :status => :created, :location => @follow_up_actions }
else
format.html { render :action => "new" }
format.xml { render :xml => @follow_up_actions.errors, :status => :unprocessable_entity }
end
end
end

def update
@follow_up_action = Complaint.find(params[:complaint_id]).follow_up_actions.find(params[:id])
@follow_up_action.workbench = User.find params[:follow_up_action][:workbench_id]

respond_to do |format|
if @follow_up_action.update_attributes(params[:follow_up_action])
flash[:notice] = 'FollowUpAction was successfully updated.'
format.html { redirect_to(@follow_up_actions) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @follow_up_action.errors, :status => :unprocessable_entity }
end
end
end

def destroy
@follow_up_action = Complaint.find(params[:complaint_id]).follow_up_actions.find(params[:id])
@follow_up_action.destroy

respond_to do |format|
format.html { redirect_to(complaint_follow_up_actions_url(@complaint)) }
format.xml { head :ok }
end
end

def setup_complaint
@complaint = Complaint.find(params[:complaint_id])
end
end

0 comments on commit 77e84dc

Please sign in to comment.