Browse Source
Created a parent log class for sales log and lettings log. Any bits common to both sales and lettings can live in the parent class. As the sales log functionality is built up any commonalities with lettings log can be extracted into the parent log class. The sales log model is set up without a json form and instead the form is defined in code - like the setup section of the lettings log.pull/863/head
44 changed files with 867 additions and 170 deletions
@ -0,0 +1,64 @@
|
||||
class LogsController < ApplicationController |
||||
skip_before_action :verify_authenticity_token, if: :json_api_request? |
||||
before_action :authenticate, if: :json_api_request? |
||||
|
||||
private |
||||
|
||||
def create |
||||
log = yield |
||||
raise "Caller must pass a block that implements model creation" if log.blank? |
||||
|
||||
respond_to do |format| |
||||
format.html do |
||||
log.save! |
||||
redirect_to post_create_redirect_url(log) |
||||
end |
||||
format.json do |
||||
if log.save |
||||
render json: log, status: :created |
||||
else |
||||
render json: { errors: log.errors.messages }, status: :unprocessable_entity |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
def post_create_redirect_url |
||||
raise "implement in sub class" |
||||
end |
||||
|
||||
API_ACTIONS = %w[create show update destroy].freeze |
||||
|
||||
def json_api_request? |
||||
API_ACTIONS.include?(request["action"]) && request.format.json? |
||||
end |
||||
|
||||
def authenticate |
||||
http_basic_authenticate_or_request_with name: ENV["API_USER"], password: ENV["API_KEY"] |
||||
end |
||||
|
||||
def log_params |
||||
if current_user && !current_user.support? |
||||
org_params.merge(api_log_params) |
||||
else |
||||
api_log_params |
||||
end |
||||
end |
||||
|
||||
def api_log_params |
||||
return {} unless params[:lettings_log] || params[:sales_log] |
||||
|
||||
permitted = permitted_log_params |
||||
owning_id = permitted["owning_organisation_id"] |
||||
permitted["owning_organisation"] = Organisation.find(owning_id) if owning_id |
||||
permitted |
||||
end |
||||
|
||||
def org_params |
||||
{ |
||||
"owning_organisation_id" => current_user.organisation.id, |
||||
"managing_organisation_id" => current_user.organisation.id, |
||||
"created_by_id" => current_user.id, |
||||
} |
||||
end |
||||
end |
@ -0,0 +1,28 @@
|
||||
class SalesLogsController < LogsController |
||||
def create |
||||
super { SalesLog.new(log_params) } |
||||
end |
||||
|
||||
def show |
||||
respond_to do |format| |
||||
format.html { edit } |
||||
end |
||||
end |
||||
|
||||
def edit |
||||
@sales_log = current_user.sales_logs.find_by(id: params[:id]) |
||||
if @sales_log |
||||
render :edit, locals: { current_user: } |
||||
else |
||||
render_not_found |
||||
end |
||||
end |
||||
|
||||
def post_create_redirect_url(log) |
||||
sales_log_url(log) |
||||
end |
||||
|
||||
def permitted_log_params |
||||
params.require(:sales_log).permit(SalesLog.editable_fields) |
||||
end |
||||
end |
@ -0,0 +1,15 @@
|
||||
class Form::Sales::Setup::Pages::SaleDate < ::Form::Page |
||||
def initialize(id, hsh, subsection) |
||||
super |
||||
@id = "sale_date" |
||||
@header = "" |
||||
@description = "" |
||||
@subsection = subsection |
||||
end |
||||
|
||||
def questions |
||||
@questions ||= [ |
||||
Form::Sales::Setup::Questions::SaleDate.new(nil, nil, self), |
||||
] |
||||
end |
||||
end |
@ -0,0 +1,10 @@
|
||||
class Form::Sales::Setup::Questions::SaleDate < ::Form::Question |
||||
def initialize(id, hsh, page) |
||||
super |
||||
@id = "saledate" |
||||
@check_answer_label = "Sale completion date" |
||||
@header = "What is the sale completion date?" |
||||
@type = "date" |
||||
@page = page |
||||
end |
||||
end |
@ -0,0 +1,10 @@
|
||||
class Form::Sales::Setup::Sections::Setup < ::Form::Section |
||||
def initialize(id, hsh, form) |
||||
super |
||||
@id = "setup" |
||||
@label = "Before you start" |
||||
@description = "" |
||||
@form = form |
||||
@subsections = [Form::Sales::Setup::Subsections::Setup.new(nil, nil, self)] || [] |
||||
end |
||||
end |
@ -0,0 +1,14 @@
|
||||
class Form::Sales::Setup::Subsections::Setup < ::Form::Subsection |
||||
def initialize(id, hsh, section) |
||||
super |
||||
@id = "setup" |
||||
@label = "Set up this sales log" |
||||
@section = section |
||||
end |
||||
|
||||
def pages |
||||
@pages ||= [ |
||||
Form::Sales::Setup::Pages::SaleDate.new(nil, nil, self), |
||||
] |
||||
end |
||||
end |
@ -0,0 +1,7 @@
|
||||
class Log < ApplicationRecord |
||||
self.abstract_class = true |
||||
|
||||
belongs_to :owning_organisation, class_name: "Organisation", optional: true |
||||
belongs_to :managing_organisation, class_name: "Organisation", optional: true |
||||
belongs_to :created_by, class_name: "User", optional: true |
||||
end |
@ -0,0 +1,72 @@
|
||||
class SalesLogValidator < ActiveModel::Validator |
||||
def validate(record); end |
||||
end |
||||
|
||||
class SalesLog < Log |
||||
has_paper_trail |
||||
|
||||
validates_with SalesLogValidator |
||||
before_save :update_status! |
||||
|
||||
scope :filter_by_organisation, ->(org, _user = nil) { where(owning_organisation: org).or(where(managing_organisation: org)) } |
||||
|
||||
STATUS = { "not_started" => 0, "in_progress" => 1, "completed" => 2 }.freeze |
||||
enum status: STATUS |
||||
|
||||
def self.editable_fields |
||||
attribute_names |
||||
end |
||||
|
||||
def form_name |
||||
return unless saledate |
||||
|
||||
"#{collection_start_year}_#{collection_start_year + 1}_sales" |
||||
end |
||||
|
||||
def collection_start_year |
||||
return @sale_year if @sale_year |
||||
return unless saledate |
||||
|
||||
window_end_date = Time.zone.local(saledate.year, 4, 1) |
||||
@sale_year = saledate < window_end_date ? saledate.year - 1 : saledate.year |
||||
end |
||||
|
||||
def form |
||||
FormHandler.instance.get_form(form_name) || FormHandler.instance.get_form("2022_2023_sales") |
||||
end |
||||
|
||||
def optional_fields |
||||
[] |
||||
end |
||||
|
||||
def not_started? |
||||
status == "not_started" |
||||
end |
||||
|
||||
def completed? |
||||
status == "completed" |
||||
end |
||||
|
||||
private |
||||
|
||||
def update_status! |
||||
self.status = if all_fields_completed? && errors.empty? |
||||
"completed" |
||||
elsif all_fields_nil? |
||||
"not_started" |
||||
else |
||||
"in_progress" |
||||
end |
||||
end |
||||
|
||||
def all_fields_completed? |
||||
subsection_statuses = form.subsections.map { |subsection| subsection.status(self) }.uniq |
||||
subsection_statuses == [:completed] |
||||
end |
||||
|
||||
def all_fields_nil? |
||||
not_started_statuses = %i[not_started cannot_start_yet] |
||||
subsection_statuses = form.subsections.map { |subsection| subsection.status(self) }.uniq |
||||
subsection_statuses.all? { |status| not_started_statuses.include?(status) } |
||||
end |
||||
end |
@ -0,0 +1,25 @@
|
||||
<ol class="app-task-list govuk-!-margin-top-8"> |
||||
<% @sales_log.form.sections.map do |section| %> |
||||
<li> |
||||
<h2 class="app-task-list__section-heading"> |
||||
<%= section.label %> |
||||
</h2> |
||||
<% if section.description %> |
||||
<p class="govuk-body"><%= section.description.html_safe %></p> |
||||
<% end %> |
||||
<ul class="app-task-list__items"> |
||||
<% section.subsections.map do |subsection| %> |
||||
<% if subsection.applicable_questions(@sales_log).count > 0 || !subsection.enabled?(@sales_log) %> |
||||
<% subsection_status = subsection.status(@sales_log) %> |
||||
<li class="app-task-list__item"> |
||||
<span class="app-task-list__task-name" id="<%= subsection.id.dasherize %>"> |
||||
<%= subsection_link(subsection, @sales_log, current_user) %> |
||||
</span> |
||||
<%= status_tag(subsection_status, "app-task-list__tag") %> |
||||
</li> |
||||
<% end %> |
||||
<% end %> |
||||
</ul> |
||||
</li> |
||||
<% end %> |
||||
</ol> |
@ -0,0 +1,37 @@
|
||||
<% content_for :title, "Log #{@sales_log.id}" %> |
||||
<% content_for :breadcrumbs, govuk_breadcrumbs(breadcrumbs: { |
||||
"Logs" => "/logs", |
||||
content_for(:title) => "", |
||||
}) %> |
||||
|
||||
<div class="govuk-grid-row"> |
||||
<div class="govuk-grid-column-two-thirds-from-desktop"> |
||||
<h1 class="govuk-heading-xl"> |
||||
<%= content_for(:title) %> |
||||
</h1> |
||||
|
||||
<% if @sales_log.status == "in_progress" %> |
||||
<p class="govuk-body govuk-!-margin-bottom-7"><%= get_subsections_count(@sales_log, :completed) %> of <%= get_subsections_count(@sales_log, :all) %> sections completed.</p> |
||||
<p class="govuk-body govuk-!-margin-bottom-2"> |
||||
<% next_incomplete_section = get_next_incomplete_section(@sales_log) %> |
||||
</p> |
||||
<p> |
||||
<% if next_incomplete_section.present? %> |
||||
<a class="app-section-skip-link" href="#<%= next_incomplete_section.id.dasherize %>"> |
||||
Skip to next incomplete section: <%= next_incomplete_section.label %> |
||||
</a> |
||||
<% end %> |
||||
</p> |
||||
<% elsif @sales_log.status == "not_started" %> |
||||
<p class="govuk-body">This log has not been started.</p> |
||||
<% elsif @sales_log.status == "completed" %> |
||||
<p class="govuk-body"> |
||||
<%= status_tag(@sales_log.status) %> |
||||
</p> |
||||
<p class="govuk-body"> |
||||
You can <%= govuk_link_to "review and make changes to this log", "/logs/#{@sales_log.id}/review" %> until 2nd June <%= @sales_log.collection_start_year.present? ? @sales_log.collection_start_year + 1 : "" %>. |
||||
</p> |
||||
<% end %> |
||||
<%= render "tasklist" %> |
||||
</div> |
||||
</div> |
@ -0,0 +1,12 @@
|
||||
class AddSalesLog < ActiveRecord::Migration[7.0] |
||||
def change |
||||
create_table :sales_logs do |t| |
||||
t.integer :status, default: 0 |
||||
t.datetime :saledate |
||||
t.timestamps |
||||
t.references :owning_organisation, class_name: "Organisation", foreign_key: { to_table: :organisations, on_delete: :cascade } |
||||
t.references :managing_organisation, class_name: "Organisation", foreign_key: { to_table: :organisations } |
||||
t.references :created_by, class_name: "User", foreign_key: { to_table: :users } |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,12 @@
|
||||
FactoryBot.define do |
||||
factory :sales_log do |
||||
created_by { FactoryBot.create(:user) } |
||||
owning_organisation { created_by.organisation } |
||||
managing_organisation { created_by.organisation } |
||||
created_at { Time.utc(2022, 2, 8, 16, 52, 15) } |
||||
updated_at { Time.utc(2022, 2, 8, 16, 52, 15) } |
||||
trait :completed do |
||||
saledate { Time.utc(2022, 2, 2, 10, 36, 49) } |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,29 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe Form::Sales::Setup::Pages::SaleDate, type: :model do |
||||
subject(:page) { described_class.new(page_id, page_definition, subsection) } |
||||
|
||||
let(:page_id) { nil } |
||||
let(:page_definition) { nil } |
||||
let(:subsection) { instance_double(Form::Subsection) } |
||||
|
||||
it "has correct subsection" do |
||||
expect(page.subsection).to eq(subsection) |
||||
end |
||||
|
||||
it "has correct questions" do |
||||
expect(page.questions.map(&:id)).to eq(%w[saledate]) |
||||
end |
||||
|
||||
it "has the correct id" do |
||||
expect(page.id).to eq("sale_date") |
||||
end |
||||
|
||||
it "has the correct header" do |
||||
expect(page.header).to eq("") |
||||
end |
||||
|
||||
it "has the correct description" do |
||||
expect(page.description).to eq("") |
||||
end |
||||
end |
@ -0,0 +1,33 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe Form::Sales::Setup::Questions::SaleDate, type: :model do |
||||
subject(:question) { described_class.new(question_id, question_definition, page) } |
||||
|
||||
let(:question_id) { nil } |
||||
let(:question_definition) { nil } |
||||
let(:page) { instance_double(Form::Page) } |
||||
|
||||
it "has correct page" do |
||||
expect(question.page).to eq(page) |
||||
end |
||||
|
||||
it "has the correct id" do |
||||
expect(question.id).to eq("saledate") |
||||
end |
||||
|
||||
it "has the correct header" do |
||||
expect(question.header).to eq("What is the sale completion date?") |
||||
end |
||||
|
||||
it "has the correct check_answer_label" do |
||||
expect(question.check_answer_label).to eq("Sale completion date") |
||||
end |
||||
|
||||
it "has the correct type" do |
||||
expect(question.type).to eq("date") |
||||
end |
||||
|
||||
it "is not marked as derived" do |
||||
expect(question.derived?).to be false |
||||
end |
||||
end |
@ -0,0 +1,29 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe Form::Sales::Setup::Sections::Setup, type: :model do |
||||
subject(:setup) { described_class.new(section_id, section_definition, form) } |
||||
|
||||
let(:section_id) { nil } |
||||
let(:section_definition) { nil } |
||||
let(:form) { instance_double(Form) } |
||||
|
||||
it "has correct form" do |
||||
expect(setup.form).to eq(form) |
||||
end |
||||
|
||||
it "has correct subsections" do |
||||
expect(setup.subsections.map(&:id)).to eq(%w[setup]) |
||||
end |
||||
|
||||
it "has the correct id" do |
||||
expect(setup.id).to eq("setup") |
||||
end |
||||
|
||||
it "has the correct label" do |
||||
expect(setup.label).to eq("Before you start") |
||||
end |
||||
|
||||
it "has the correct description" do |
||||
expect(setup.description).to eq("") |
||||
end |
||||
end |
@ -0,0 +1,27 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe Form::Sales::Setup::Subsections::Setup, type: :model do |
||||
subject(:setup) { described_class.new(subsection_id, subsection_definition, section) } |
||||
|
||||
let(:subsection_id) { nil } |
||||
let(:subsection_definition) { nil } |
||||
let(:section) { instance_double(Form::Sales::Setup::Sections::Setup) } |
||||
|
||||
it "has correct section" do |
||||
expect(setup.section).to eq(section) |
||||
end |
||||
|
||||
it "has correct pages" do |
||||
expect(setup.pages.map(&:id)).to eq( |
||||
%w[sale_date], |
||||
) |
||||
end |
||||
|
||||
it "has the correct id" do |
||||
expect(setup.id).to eq("setup") |
||||
end |
||||
|
||||
it "has the correct label" do |
||||
expect(setup.label).to eq("Set up this sales log") |
||||
end |
||||
end |
@ -0,0 +1,8 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe Log, type: :model do |
||||
it "has two child log classes" do |
||||
expect(SalesLog).to be < described_class |
||||
expect(LettingsLog).to be < described_class |
||||
end |
||||
end |
@ -0,0 +1,85 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe SalesLog, type: :model do |
||||
let(:owning_organisation) { FactoryBot.create(:organisation) } |
||||
let(:created_by_user) { FactoryBot.create(:user) } |
||||
|
||||
it "inherits from log" do |
||||
expect(described_class).to be < Log |
||||
expect(described_class).to be < ApplicationRecord |
||||
end |
||||
|
||||
describe "#new" do |
||||
context "when creating a record" do |
||||
let(:sales_log) do |
||||
described_class.create |
||||
end |
||||
|
||||
it "attaches the correct custom validator" do |
||||
expect(sales_log._validators.values.flatten.map(&:class)) |
||||
.to include(SalesLogValidator) |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe "#form" do |
||||
let(:sales_log) { FactoryBot.build(:sales_log, created_by: created_by_user) } |
||||
let(:sales_log_2) { FactoryBot.build(:sales_log, saledate: Time.zone.local(2022, 5, 1), created_by: created_by_user) } |
||||
|
||||
it "has returns the correct form based on the start date" do |
||||
expect(sales_log.form_name).to be_nil |
||||
expect(sales_log.form).to be_a(Form) |
||||
expect(sales_log_2.form_name).to eq("2022_2023_sales") |
||||
expect(sales_log_2.form).to be_a(Form) |
||||
end |
||||
end |
||||
|
||||
describe "status" do |
||||
let!(:empty_sales_log) { FactoryBot.create(:sales_log) } |
||||
# let!(:in_progress_sales_log) { FactoryBot.create(:sales_log, :in_progress) } |
||||
let!(:completed_sales_log) { FactoryBot.create(:sales_log, :completed) } |
||||
|
||||
it "is set to not started for an empty sales log" do |
||||
expect(empty_sales_log.not_started?).to be(true) |
||||
# expect(empty_sales_log.in_progress?).to be(false) |
||||
expect(empty_sales_log.completed?).to be(false) |
||||
end |
||||
|
||||
# it "is set to in progress for a started sales log" do |
||||
# expect(in_progress_sales_log.in_progress?).to be(true) |
||||
# expect(in_progress_sales_log.not_started?).to be(false) |
||||
# expect(in_progress_sales_log.completed?).to be(false) |
||||
# end |
||||
|
||||
it "is set to completed for a completed sales log" do |
||||
# expect(completed_sales_log.in_progress?).to be(false) |
||||
expect(completed_sales_log.not_started?).to be(false) |
||||
expect(completed_sales_log.completed?).to be(true) |
||||
end |
||||
end |
||||
|
||||
context "when filtering by organisation" do |
||||
let(:organisation_1) { FactoryBot.create(:organisation) } |
||||
let(:organisation_2) { FactoryBot.create(:organisation) } |
||||
let(:organisation_3) { FactoryBot.create(:organisation) } |
||||
|
||||
before do |
||||
FactoryBot.create(:sales_log, :in_progress, owning_organisation: organisation_1, managing_organisation: organisation_1) |
||||
FactoryBot.create(:sales_log, :completed, owning_organisation: organisation_1, managing_organisation: organisation_2) |
||||
FactoryBot.create(:sales_log, :completed, owning_organisation: organisation_2, managing_organisation: organisation_1) |
||||
FactoryBot.create(:sales_log, :completed, owning_organisation: organisation_2, managing_organisation: organisation_2) |
||||
end |
||||
|
||||
it "filters by given organisation id" do |
||||
expect(described_class.filter_by_organisation([organisation_1.id]).count).to eq(3) |
||||
expect(described_class.filter_by_organisation([organisation_1.id, organisation_2.id]).count).to eq(4) |
||||
expect(described_class.filter_by_organisation([organisation_3.id]).count).to eq(0) |
||||
end |
||||
|
||||
it "filters by given organisation" do |
||||
expect(described_class.filter_by_organisation([organisation_1]).count).to eq(3) |
||||
expect(described_class.filter_by_organisation([organisation_1, organisation_2]).count).to eq(4) |
||||
expect(described_class.filter_by_organisation([organisation_3]).count).to eq(0) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,87 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe SalesLogsController, type: :request do |
||||
let(:user) { FactoryBot.create(:user) } |
||||
let(:owning_organisation) { user.organisation } |
||||
let(:managing_organisation) { owning_organisation } |
||||
let(:api_username) { "test_user" } |
||||
let(:api_password) { "test_password" } |
||||
let(:basic_credentials) do |
||||
ActionController::HttpAuthentication::Basic |
||||
.encode_credentials(api_username, api_password) |
||||
end |
||||
|
||||
let(:params) do |
||||
{ |
||||
"owning_organisation_id": owning_organisation.id, |
||||
"managing_organisation_id": managing_organisation.id, |
||||
"created_by_id": user.id, |
||||
} |
||||
end |
||||
|
||||
let(:headers) do |
||||
{ |
||||
"Content-Type" => "application/json", |
||||
"Accept" => "application/json", |
||||
"Authorization" => basic_credentials, |
||||
} |
||||
end |
||||
|
||||
before do |
||||
allow(ENV).to receive(:[]) |
||||
allow(ENV).to receive(:[]).with("API_USER").and_return(api_username) |
||||
allow(ENV).to receive(:[]).with("API_KEY").and_return(api_password) |
||||
end |
||||
|
||||
describe "POST #create" do |
||||
context "when API" do |
||||
before do |
||||
post "/sales-logs", headers:, params: params.to_json |
||||
end |
||||
|
||||
it "returns http success" do |
||||
expect(response).to have_http_status(:success) |
||||
end |
||||
|
||||
it "returns a serialized sales log" do |
||||
json_response = JSON.parse(response.body) |
||||
expect(json_response.keys).to match_array(SalesLog.new.attributes.keys) |
||||
end |
||||
|
||||
it "creates a sales log with the values passed" do |
||||
json_response = JSON.parse(response.body) |
||||
expect(json_response["owning_organisation_id"]).to eq(owning_organisation.id) |
||||
expect(json_response["managing_organisation_id"]).to eq(managing_organisation.id) |
||||
expect(json_response["created_by_id"]).to eq(user.id) |
||||
end |
||||
|
||||
context "with a request containing invalid credentials" do |
||||
let(:basic_credentials) do |
||||
ActionController::HttpAuthentication::Basic.encode_credentials(api_username, "Oops") |
||||
end |
||||
|
||||
it "returns 401" do |
||||
expect(response).to have_http_status(:unauthorized) |
||||
end |
||||
end |
||||
end |
||||
|
||||
context "when UI" do |
||||
let(:user) { FactoryBot.create(:user) } |
||||
let(:headers) { { "Accept" => "text/html" } } |
||||
|
||||
before do |
||||
RequestHelper.stub_http_requests |
||||
sign_in user |
||||
post "/sales-logs", headers: |
||||
end |
||||
|
||||
it "tracks who created the record" do |
||||
created_id = response.location.match(/[0-9]+/)[0] |
||||
whodunnit_actor = SalesLog.find_by(id: created_id).versions.last.actor |
||||
expect(whodunnit_actor).to be_a(User) |
||||
expect(whodunnit_actor.id).to eq(user.id) |
||||
end |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue