80 changed files with 1144 additions and 547 deletions
@ -0,0 +1,42 @@
|
||||
class BulkUploadLettingsResumeController < ApplicationController |
||||
before_action :authenticate_user! |
||||
|
||||
def start |
||||
@bulk_upload = current_user.bulk_uploads.find(params[:id]) |
||||
|
||||
redirect_to page_bulk_upload_lettings_resume_path(@bulk_upload, page: "fix-choice") |
||||
end |
||||
|
||||
def show |
||||
@bulk_upload = current_user.bulk_uploads.find(params[:id]) |
||||
|
||||
render form.view_path |
||||
end |
||||
|
||||
def update |
||||
@bulk_upload = current_user.bulk_uploads.find(params[:id]) |
||||
|
||||
if form.valid? && form.save! |
||||
redirect_to form.next_path |
||||
else |
||||
render form.view_path |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def form |
||||
@form ||= case params[:page] |
||||
when "fix-choice" |
||||
Forms::BulkUploadLettingsResume::FixChoice.new(form_params.merge(bulk_upload: @bulk_upload)) |
||||
when "confirm" |
||||
Forms::BulkUploadLettingsResume::Confirm.new(form_params.merge(bulk_upload: @bulk_upload)) |
||||
else |
||||
raise "invalid form" |
||||
end |
||||
end |
||||
|
||||
def form_params |
||||
params.fetch(:form, {}).permit(:choice) |
||||
end |
||||
end |
||||
@ -0,0 +1,23 @@
|
||||
module MoneyFormattingHelper |
||||
include ActionView::Helpers::NumberHelper |
||||
|
||||
def format_money_input(log:, question:) |
||||
value = log[question.id] |
||||
|
||||
return unless value |
||||
return value unless question.prefix == "£" |
||||
|
||||
number_with_precision( |
||||
value, |
||||
precision: 2, |
||||
) |
||||
end |
||||
|
||||
def format_as_currency(num_string) |
||||
number_to_currency( |
||||
num_string, |
||||
unit: "£", |
||||
precision: 2, |
||||
) |
||||
end |
||||
end |
||||
@ -0,0 +1,30 @@
|
||||
module Forms |
||||
module BulkUploadLettingsResume |
||||
class Confirm |
||||
include ActiveModel::Model |
||||
include ActiveModel::Attributes |
||||
include Rails.application.routes.url_helpers |
||||
|
||||
attribute :bulk_upload |
||||
|
||||
def view_path |
||||
"bulk_upload_lettings_resume/confirm" |
||||
end |
||||
|
||||
def back_path |
||||
page_bulk_upload_lettings_resume_path(bulk_upload, page: "fix-choice") |
||||
end |
||||
|
||||
def next_path |
||||
resume_bulk_upload_lettings_result_path(bulk_upload) |
||||
end |
||||
|
||||
def save! |
||||
processor = BulkUpload::Processor.new(bulk_upload:) |
||||
processor.approve |
||||
|
||||
true |
||||
end |
||||
end |
||||
end |
||||
end |
||||
@ -0,0 +1,53 @@
|
||||
module Forms |
||||
module BulkUploadLettingsResume |
||||
class FixChoice |
||||
include ActiveModel::Model |
||||
include ActiveModel::Attributes |
||||
include Rails.application.routes.url_helpers |
||||
|
||||
attribute :bulk_upload |
||||
attribute :choice, :string |
||||
|
||||
validates :choice, presence: true, |
||||
inclusion: { in: %w[create-fix-inline upload-again] } |
||||
|
||||
def options |
||||
[ |
||||
OpenStruct.new(id: "create-fix-inline", name: "Upload these logs and fix errors on CORE site"), |
||||
OpenStruct.new(id: "upload-again", name: "Fix errors in the CSV and re-upload"), |
||||
] |
||||
end |
||||
|
||||
def view_path |
||||
"bulk_upload_lettings_resume/fix_choice" |
||||
end |
||||
|
||||
def next_path |
||||
case choice |
||||
when "create-fix-inline" |
||||
page_bulk_upload_lettings_resume_path(bulk_upload, page: "confirm") |
||||
when "upload-again" |
||||
if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors? |
||||
summary_bulk_upload_lettings_result_path(bulk_upload) |
||||
else |
||||
bulk_upload_lettings_result_path(bulk_upload) |
||||
end |
||||
else |
||||
raise "invalid choice" |
||||
end |
||||
end |
||||
|
||||
def recommendation |
||||
if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors? |
||||
"For this many errors we recommend to fix errors in the CSV and re-upload as you may be able to edit many fields at once in a CSV." |
||||
else |
||||
"For this many errors we recommend to upload logs and fix errors on site as you can easily see the questions and select the appropriate answer." |
||||
end |
||||
end |
||||
|
||||
def save! |
||||
true |
||||
end |
||||
end |
||||
end |
||||
end |
||||
@ -0,0 +1,22 @@
|
||||
<% content_for :before_content do %> |
||||
<%= govuk_back_link href: @form.back_path %> |
||||
<% end %> |
||||
|
||||
<div class="govuk-grid-row"> |
||||
<div class="govuk-grid-column-two-thirds"> |
||||
<span class="govuk-caption-l">Bulk upload for lettings (<%= @bulk_upload.year_combo %>)</span> |
||||
<h1 class="govuk-heading-l">Are you sure you want to upload all logs from this bulk upload?</h1> |
||||
|
||||
<p class="govuk-body">There are <%= pluralize(@bulk_upload.logs.count, "log") %> in this bulk upload with <%= pluralize(@bulk_upload.bulk_upload_errors.count, "error") %> that still need to be fixed after upload.</p> |
||||
|
||||
<%= govuk_warning_text(icon_fallback_text: "Danger") do %> |
||||
You can not delete logs once you create them |
||||
<% end %> |
||||
|
||||
<%= form_with model: @form, scope: :form, url: page_bulk_upload_lettings_resume_path(@bulk_upload, page: "confirm"), method: :patch do |f| %> |
||||
<%= f.govuk_submit %> |
||||
|
||||
<%= govuk_button_link_to "Cancel", @form.back_path, secondary: true %> |
||||
<% end %> |
||||
</div> |
||||
</div> |
||||
@ -0,0 +1,36 @@
|
||||
<div class="govuk-grid-row"> |
||||
<div class="govuk-grid-column-two-thirds"> |
||||
<%= form_with model: @form, scope: :form, url: page_bulk_upload_lettings_resume_path(@bulk_upload, page: "fix-choice"), method: :patch do |f| %> |
||||
<%= f.govuk_error_summary %> |
||||
|
||||
<span class="govuk-caption-l">Bulk upload for lettings (<%= @bulk_upload.year_combo %>)</span> |
||||
<h1 class="govuk-heading-l">How would you like to fix <%= pluralize(@bulk_upload.bulk_upload_errors.count, "error") %>?</h1> |
||||
|
||||
<div class="govuk-body-l"> |
||||
<%= @bulk_upload.filename %> |
||||
</div> |
||||
|
||||
<div class="govuk-body"> |
||||
<%= @form.recommendation %> |
||||
</div> |
||||
|
||||
<%= govuk_details(summary_text: "How to choose between fixing errors on the CORE site or in the CSV") do %> |
||||
<p class="govuk-body">When it comes to fixing errors, there are pros and cons to doing it on a CSV versus doing it on a website.</p> |
||||
|
||||
<p class="govuk-body">Fixing errors on a CSV file can be beneficial because it allows you to easily make changes to multiple records at once, and you can use tools like Excel to quickly identify and correct errors. However, if the CSV file is not properly formatted, it can be difficult to identify which records contain errors.</p> |
||||
|
||||
<p class="govuk-body">Fixing errors on a website can be convenient because you can see the data in context and make changes in real-time. However, this approach can be time-consuming if you need to make changes to multiple records, and it may be more difficult to identify errors in a large dataset.</p> |
||||
|
||||
<p class="govuk-body">Ultimately, the best approach will depend on the specific situation and the nature of the errors that need to be fixed.</p> |
||||
<% end %> |
||||
|
||||
<%= f.govuk_collection_radio_buttons :choice, |
||||
@form.options, |
||||
:id, |
||||
:name, |
||||
legend: { hidden: true } %> |
||||
|
||||
<%= f.govuk_submit %> |
||||
<% end %> |
||||
</div> |
||||
</div> |
||||
@ -1,14 +1,19 @@
|
||||
<%= render partial: "form/guidance/#{question.guidance_partial}" if question.top_guidance? %> |
||||
|
||||
<%= f.govuk_number_field question.id.to_sym, |
||||
<%= f.govuk_number_field( |
||||
question.id.to_sym, |
||||
caption: caption(caption_text, page_header, conditional), |
||||
label: legend(question, page_header, conditional), |
||||
hint: { text: question.hint_text&.html_safe }, |
||||
min: question.min, max: question.max, step: question.step, |
||||
min: question.min, |
||||
max: question.max, |
||||
step: question.step, |
||||
width: question.width, |
||||
readonly: question.read_only?, |
||||
prefix_text: question.prefix.to_s, |
||||
suffix_text: question.suffix_label(@log), |
||||
**stimulus_html_attributes(question) %> |
||||
value: format_money_input(log: @log, question:), |
||||
**stimulus_html_attributes(question), |
||||
) %> |
||||
|
||||
<%= render partial: "form/guidance/#{question.guidance_partial}" if question.bottom_guidance? %> |
||||
|
||||
@ -0,0 +1,5 @@
|
||||
class AddStatusCacheToLettingsLog < ActiveRecord::Migration[7.0] |
||||
def change |
||||
add_column :lettings_logs, :status_cache, :integer, null: false, default: 0 |
||||
end |
||||
end |
||||
|
|
@ -0,0 +1,41 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe MoneyFormattingHelper do |
||||
describe "#format_money_input" do |
||||
let!(:log) { create(:lettings_log, :completed, brent: 1000) } |
||||
let(:question) { instance_double(Form::Question, id: "brent", prefix:) } |
||||
|
||||
context "with £ prefix" do |
||||
let(:prefix) { "£" } |
||||
|
||||
it "returns formatted input" do |
||||
expect(format_money_input(log:, question:)).to eq("1000.00") |
||||
end |
||||
end |
||||
|
||||
context "with other prefix" do |
||||
let(:prefix) { "other" } |
||||
|
||||
it "does not format the input" do |
||||
expect(format_money_input(log:, question:)).to eq(BigDecimal(1000)) |
||||
end |
||||
end |
||||
|
||||
context "without prefix" do |
||||
let(:prefix) { nil } |
||||
|
||||
it "does not format the input" do |
||||
expect(format_money_input(log:, question:)).to eq(BigDecimal(1000)) |
||||
end |
||||
end |
||||
|
||||
context "when value is nil" do |
||||
let(:prefix) { "£" } |
||||
let(:log) { create(:lettings_log, brent: nil) } |
||||
|
||||
it "does not format the input" do |
||||
expect(format_money_input(log:, question:)).to be_nil |
||||
end |
||||
end |
||||
end |
||||
end |
||||
@ -0,0 +1,83 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe LocationDeactivationPeriod do |
||||
let(:validator) { LocationDeactivationPeriodValidator.new } |
||||
let(:location) { FactoryBot.create(:location, startdate: now - 2.years) } |
||||
let(:record) { FactoryBot.create(:location_deactivation_period, deactivation_date: now, location:) } |
||||
|
||||
describe "#validate" do |
||||
around do |example| |
||||
Timecop.freeze(now) do |
||||
example.run |
||||
end |
||||
end |
||||
|
||||
context "when not in a crossover period" do |
||||
let(:now) { Time.utc(2023, 3, 1) } |
||||
|
||||
context "with a deactivation date before the current collection period" do |
||||
it "adds an error" do |
||||
record.deactivation_date = now - 1.year |
||||
location.location_deactivation_periods.clear |
||||
validator.validate(record) |
||||
expect(record.errors[:deactivation_date]).to include "The date must be on or after the 1 April 2022" |
||||
end |
||||
end |
||||
|
||||
context "with a deactivation date in the current collection period" do |
||||
it "does not add an error" do |
||||
record.deactivation_date = now - 1.day |
||||
location.location_deactivation_periods.clear |
||||
validator.validate(record) |
||||
expect(record.errors).to be_empty |
||||
end |
||||
end |
||||
end |
||||
|
||||
context "when in a crossover period" do |
||||
let(:now) { Time.utc(2023, 5, 1) } |
||||
|
||||
context "with a deactivation date before the previous collection period" do |
||||
it "does not add an error" do |
||||
record.deactivation_date = now - 2.years |
||||
location.location_deactivation_periods.clear |
||||
validator.validate(record) |
||||
expect(record.errors[:deactivation_date]).to include "The date must be on or after the 1 April 2022" |
||||
end |
||||
end |
||||
|
||||
context "with a deactivation date in the previous collection period" do |
||||
it "does not add an error" do |
||||
record.deactivation_date = now - 1.year |
||||
location.location_deactivation_periods.clear |
||||
validator.validate(record) |
||||
expect(record.errors).to be_empty |
||||
end |
||||
end |
||||
|
||||
context "with a deactivation date in the current collection period" do |
||||
it "does not add an error" do |
||||
record.deactivation_date = now - 1.day |
||||
location.location_deactivation_periods.clear |
||||
validator.validate(record) |
||||
expect(record.errors).to be_empty |
||||
end |
||||
end |
||||
|
||||
context "but the location was created in the current collection period" do |
||||
let(:location) { FactoryBot.create(:location, startdate:) } |
||||
let(:startdate) { now - 2.days } |
||||
|
||||
context "with a deactivation date in the previous collection period" do |
||||
it "adds an error" do |
||||
record.deactivation_date = now - 1.year |
||||
location.location_deactivation_periods.clear |
||||
validator.validate(record) |
||||
start_date = startdate.to_formatted_s(:govuk_date) |
||||
expect(record.errors[:deactivation_date]).to include "The location cannot be deactivated before #{start_date}, the date when it was first available" |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
||||
@ -0,0 +1,68 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe SchemeDeactivationPeriod do |
||||
let(:validator) { SchemeDeactivationPeriodValidator.new } |
||||
let(:scheme) { FactoryBot.create(:scheme, created_at: now - 2.years) } |
||||
let(:record) { FactoryBot.create(:scheme_deactivation_period, deactivation_date: now, scheme:) } |
||||
|
||||
describe "#validate" do |
||||
around do |example| |
||||
Timecop.freeze(now) do |
||||
example.run |
||||
end |
||||
end |
||||
|
||||
context "when not in a crossover period" do |
||||
let(:now) { Time.utc(2023, 3, 1) } |
||||
|
||||
context "with a deactivation date before the current collection period" do |
||||
it "adds an error" do |
||||
record.deactivation_date = now - 1.year |
||||
scheme.scheme_deactivation_periods.clear |
||||
validator.validate(record) |
||||
expect(record.errors[:deactivation_date]).to include("The date must be on or after the 1 April 2022") |
||||
end |
||||
end |
||||
|
||||
context "with a deactivation date in the current collection period" do |
||||
it "does not add an error" do |
||||
record.deactivation_date = now - 1.day |
||||
scheme.scheme_deactivation_periods.clear |
||||
validator.validate(record) |
||||
expect(record.errors[:deactivation_date]).to be_empty |
||||
end |
||||
end |
||||
end |
||||
|
||||
context "when in a crossover period" do |
||||
let(:now) { Time.utc(2023, 5, 1) } |
||||
|
||||
context "with a deactivation date before the previous collection period" do |
||||
it "does not add an error" do |
||||
record.deactivation_date = now - 2.years |
||||
scheme.scheme_deactivation_periods.clear |
||||
validator.validate(record) |
||||
expect(record.errors[:deactivation_date]).to include("The date must be on or after the 1 April 2022") |
||||
end |
||||
end |
||||
|
||||
context "with a deactivation date in the previous collection period" do |
||||
it "does not add an error" do |
||||
record.deactivation_date = now - 1.year |
||||
scheme.scheme_deactivation_periods.clear |
||||
validator.validate(record) |
||||
expect(record.errors[:deactivation_date]).to be_empty |
||||
end |
||||
end |
||||
|
||||
context "with a deactivation date in the current collection period" do |
||||
it "does not add an error" do |
||||
record.deactivation_date = now - 1.day |
||||
scheme.scheme_deactivation_periods.clear |
||||
validator.validate(record) |
||||
expect(record.errors[:deactivation_date]).to be_empty |
||||
end |
||||
end |
||||
end |
||||
end |
||||
end |
||||
@ -0,0 +1,84 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe BulkUploadLettingsResumeController, type: :request do |
||||
let(:user) { create(:user) } |
||||
let(:bulk_upload) { create(:bulk_upload, :lettings, user:, bulk_upload_errors:) } |
||||
let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2) } |
||||
|
||||
before do |
||||
sign_in user |
||||
end |
||||
|
||||
describe "GET /lettings-logs/bulk-upload-resume/:ID/start" do |
||||
it "redirects to choice page" do |
||||
get "/lettings-logs/bulk-upload-resume/#{bulk_upload.id}/start" |
||||
|
||||
expect(response).to redirect_to("/lettings-logs/bulk-upload-resume/#{bulk_upload.id}/fix-choice") |
||||
end |
||||
end |
||||
|
||||
describe "GET /lettings-logs/bulk-upload-resume/:ID/fix-choice" do |
||||
it "renders the page correctly" do |
||||
get "/lettings-logs/bulk-upload-resume/#{bulk_upload.id}/fix-choice" |
||||
|
||||
expect(response).to be_successful |
||||
|
||||
expect(response.body).to include("Bulk upload for lettings") |
||||
expect(response.body).to include("2022/23") |
||||
expect(response.body).to include("How would you like to fix 2 errors?") |
||||
expect(response.body).to include(bulk_upload.filename) |
||||
end |
||||
end |
||||
|
||||
describe "PATCH /lettings-logs/bulk-upload-resume/:ID/fix-choice" do |
||||
context "when no option selected" do |
||||
it "renders error message" do |
||||
patch "/lettings-logs/bulk-upload-resume/#{bulk_upload.id}/fix-choice" |
||||
|
||||
expect(response).to be_successful |
||||
|
||||
expect(response.body).to include("You must select") |
||||
end |
||||
end |
||||
|
||||
context "when upload again selected" do |
||||
it "sends them to relevant report" do |
||||
patch "/lettings-logs/bulk-upload-resume/#{bulk_upload.id}/fix-choice", params: { form: { choice: "upload-again" } } |
||||
|
||||
expect(response).to redirect_to("/lettings-logs/bulk-upload-results/#{bulk_upload.id}") |
||||
end |
||||
end |
||||
|
||||
context "when fix inline selected" do |
||||
it "sends them to confirm choice" do |
||||
patch "/lettings-logs/bulk-upload-resume/#{bulk_upload.id}/fix-choice", params: { form: { choice: "create-fix-inline" } } |
||||
|
||||
expect(response).to redirect_to("/lettings-logs/bulk-upload-resume/#{bulk_upload.id}/confirm") |
||||
end |
||||
end |
||||
end |
||||
|
||||
describe "GET /lettings-logs/bulk-upload-resume/:ID/confirm" do |
||||
it "renders page" do |
||||
get "/lettings-logs/bulk-upload-resume/#{bulk_upload.id}/confirm" |
||||
|
||||
expect(response).to be_successful |
||||
|
||||
expect(response.body).to include("Are you sure") |
||||
end |
||||
end |
||||
|
||||
describe "PATCH /lettings-logs/bulk-upload-resume/:ID/confirm" do |
||||
let(:mock_processor) { instance_double(BulkUpload::Processor, approve: nil) } |
||||
|
||||
it "approves logs for creation" do |
||||
allow(BulkUpload::Processor).to receive(:new).with(bulk_upload:).and_return(mock_processor) |
||||
|
||||
patch "/lettings-logs/bulk-upload-resume/#{bulk_upload.id}/confirm" |
||||
|
||||
expect(mock_processor).to have_received(:approve) |
||||
|
||||
expect(response).to redirect_to("/lettings-logs/bulk-upload-results/#{bulk_upload.id}/resume") |
||||
end |
||||
end |
||||
end |
||||
Loading…
Reference in new issue