Browse Source

[1716] [1717] Bulk upload part four (#1043)

* add start of bulk upload logs journey

* split upload controller into 2

* add year page to bulk upload journey

* bulk upload years now dynamic

* add placeholder for upload your file page

* handle bulk upload when not in crossover

* add bulk upload csv user journey

* bulk upload now persists and sends to S3

* swap ruby-filemagic for file command

* match csv or text file for validation

* in_crossover_period? now uses overlap of forms

- also moved from Form to FormHandler

* add production env var for CSV bucket

* stub kernel call methods

* remove duplicate env var

* hardcode env var for review apps

* move feature toggle to FeatureToggle

* crossover period checks now specific to the form

* fix typo in bulk upload journey
pull/1070/head v0.2.25
Phil Lee 2 years ago committed by GitHub
parent
commit
add39e9611
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/review_pipeline.yml
  2. 4
      app/controllers/bulk_upload_controller.rb
  3. 10
      app/controllers/bulk_upload_lettings_logs_controller.rb
  4. 10
      app/controllers/bulk_upload_sales_logs_controller.rb
  5. 198
      app/models/bulk_upload.rb
  6. 4
      app/models/form.rb
  7. 14
      app/models/form_handler.rb
  8. 27
      app/models/forms/bulk_upload_lettings/checking_file.rb
  9. 6
      app/models/forms/bulk_upload_lettings/prepare_your_file.rb
  10. 50
      app/models/forms/bulk_upload_lettings/upload_your_file.rb
  11. 4
      app/models/forms/bulk_upload_lettings/year.rb
  12. 27
      app/models/forms/bulk_upload_sales/checking_file.rb
  13. 6
      app/models/forms/bulk_upload_sales/prepare_your_file.rb
  14. 50
      app/models/forms/bulk_upload_sales/upload_your_file.rb
  15. 4
      app/models/forms/bulk_upload_sales/year.rb
  16. 197
      app/models/legacy_bulk_upload.rb
  17. 28
      app/views/bulk_upload_lettings_logs/forms/checking_file.html.erb
  18. 26
      app/views/bulk_upload_lettings_logs/forms/upload_your_file.html.erb
  19. 28
      app/views/bulk_upload_sales_logs/forms/checking_file.html.erb
  20. 26
      app/views/bulk_upload_sales_logs/forms/upload_your_file.html.erb
  21. 2
      app/views/logs/bulk_upload.html.erb
  22. 4
      config/initializers/feature_toggle.rb
  23. 10
      config/locales/en.yml
  24. 14
      db/migrate/20221128130843_create_bulk_uploads.rb
  25. 11
      db/schema.rb
  26. 36
      spec/features/bulk_upload_lettings_logs_spec.rb
  27. 36
      spec/features/bulk_upload_sales_logs_spec.rb
  28. 118
      spec/fixtures/files/blank_bulk_upload_sales.csv
  29. BIN
      spec/fixtures/files/excel_as_csv.csv
  30. 16
      spec/models/form_handler_spec.rb
  31. 34
      spec/models/form_spec.rb
  32. 47
      spec/models/forms/bulk_upload_lettings/upload_your_file_spec.rb
  33. 47
      spec/models/forms/bulk_upload_sales/upload_your_file_spec.rb
  34. 2
      spec/requests/bulk_upload_controller_spec.rb
  35. 2
      spec/requests/bulk_upload_lettings_logs_controller_spec.rb

2
.github/workflows/review_pipeline.yml

@ -120,7 +120,7 @@ jobs:
cf set-env $APP_NAME IMPORT_PAAS_INSTANCE $IMPORT_PAAS_INSTANCE cf set-env $APP_NAME IMPORT_PAAS_INSTANCE $IMPORT_PAAS_INSTANCE
cf set-env $APP_NAME EXPORT_PAAS_INSTANCE $EXPORT_PAAS_INSTANCE cf set-env $APP_NAME EXPORT_PAAS_INSTANCE $EXPORT_PAAS_INSTANCE
cf set-env $APP_NAME S3_CONFIG $S3_CONFIG cf set-env $APP_NAME S3_CONFIG $S3_CONFIG
cf set-env $APP_NAME CSV_DOWNLOAD_PAAS_INSTANCE $CSV_DOWNLOAD_PAAS_INSTANCE cf set-env $APP_NAME CSV_DOWNLOAD_PAAS_INSTANCE "dluhc-core-review-csv-bucket"
cf set-env $APP_NAME SENTRY_DSN $SENTRY_DSN cf set-env $APP_NAME SENTRY_DSN $SENTRY_DSN
cf set-env $APP_NAME APP_HOST "https://dluhc-core-review-${{ github.event.pull_request.number }}.london.cloudapps.digital" cf set-env $APP_NAME APP_HOST "https://dluhc-core-review-${{ github.event.pull_request.number }}.london.cloudapps.digital"

4
app/controllers/bulk_upload_controller.rb

@ -2,14 +2,14 @@ class BulkUploadController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
def show def show
@bulk_upload = BulkUpload.new(nil, nil) @bulk_upload = LegacyBulkUpload.new(nil, nil)
render "logs/bulk_upload" render "logs/bulk_upload"
end end
def bulk_upload def bulk_upload
file = upload_params.tempfile file = upload_params.tempfile
content_type = upload_params.content_type content_type = upload_params.content_type
@bulk_upload = BulkUpload.new(file, content_type) @bulk_upload = LegacyBulkUpload.new(file, content_type)
@bulk_upload.process(current_user) @bulk_upload.process(current_user)
if @bulk_upload.errors.present? if @bulk_upload.errors.present?
render "logs/bulk_upload", status: :unprocessable_entity render "logs/bulk_upload", status: :unprocessable_entity

10
app/controllers/bulk_upload_lettings_logs_controller.rb

@ -14,7 +14,7 @@ class BulkUploadLettingsLogsController < ApplicationController
end end
def update def update
if form.valid? if form.valid? && form.save!
redirect_to form.next_path redirect_to form.next_path
else else
render form.view_path render form.view_path
@ -28,7 +28,7 @@ private
end end
def in_crossover_period? def in_crossover_period?
FormHandler.instance.forms.values.any?(&:in_crossover_period?) FormHandler.instance.lettings_in_crossover_period?
end end
def form def form
@ -38,13 +38,15 @@ private
when "prepare-your-file" when "prepare-your-file"
Forms::BulkUploadLettings::PrepareYourFile.new(form_params) Forms::BulkUploadLettings::PrepareYourFile.new(form_params)
when "upload-your-file" when "upload-your-file"
Forms::BulkUploadLettings::UploadYourFile.new(form_params) Forms::BulkUploadLettings::UploadYourFile.new(form_params.merge(current_user:))
when "checking-file"
Forms::BulkUploadLettings::CheckingFile.new(form_params)
else else
raise "Page not found for path #{params[:id]}" raise "Page not found for path #{params[:id]}"
end end
end end
def form_params def form_params
params.fetch(:form, {}).permit(:year) params.fetch(:form, {}).permit(:year, :file)
end end
end end

10
app/controllers/bulk_upload_sales_logs_controller.rb

@ -14,7 +14,7 @@ class BulkUploadSalesLogsController < ApplicationController
end end
def update def update
if form.valid? if form.valid? && form.save!
redirect_to form.next_path redirect_to form.next_path
else else
render form.view_path render form.view_path
@ -28,7 +28,7 @@ private
end end
def in_crossover_period? def in_crossover_period?
FormHandler.instance.forms.values.any?(&:in_crossover_period?) FormHandler.instance.sales_in_crossover_period?
end end
def form def form
@ -38,13 +38,15 @@ private
when "prepare-your-file" when "prepare-your-file"
Forms::BulkUploadSales::PrepareYourFile.new(form_params) Forms::BulkUploadSales::PrepareYourFile.new(form_params)
when "upload-your-file" when "upload-your-file"
Forms::BulkUploadSales::UploadYourFile.new(form_params) Forms::BulkUploadSales::UploadYourFile.new(form_params.merge(current_user:))
when "checking-file"
Forms::BulkUploadSales::CheckingFile.new(form_params)
else else
raise "Page not found for path #{params[:id]}" raise "Page not found for path #{params[:id]}"
end end
end end
def form_params def form_params
params.fetch(:form, {}).permit(:year) params.fetch(:form, {}).permit(:year, :file)
end end
end end

198
app/models/bulk_upload.rb

@ -1,197 +1,13 @@
class BulkUpload class BulkUpload < ApplicationRecord
include ActiveModel::Model enum log_type: { lettings: "lettings", sales: "sales" }
include ActiveModel::Validations
include ActiveModel::Conversion
SPREADSHEET_CONTENT_TYPES = %w[ belongs_to :user
application/vnd.ms-excel
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
].freeze
FIRST_DATA_ROW = 7 after_initialize :generate_identifier, unless: :identifier
def initialize(file, content_type) private
@file = file
@content_type = content_type
end
def process(current_user)
return unless valid_content_type?
xlsx = Roo::Spreadsheet.open(@file, extension: :xlsx)
sheet = xlsx.sheet(0)
last_row = sheet.last_row
if last_row < FIRST_DATA_ROW
errors.add(:lettings_log_bulk_upload, "No data found")
else
data_range = FIRST_DATA_ROW..last_row
data_range.map do |row_num|
row = sheet.row(row_num)
# owning_organisation = Organisation.find(row[111])
# managing_organisation = Organisation.find(row[113])
lettings_log = LettingsLog.create!(
owning_organisation: current_user.organisation,
managing_organisation: current_user.organisation,
created_by: current_user,
)
map_row(row).each do |attr_key, attr_val|
_update = lettings_log.update(attr_key => attr_val)
rescue ArgumentError
# TODO: determine what we want to do when bulk upload contains totally invalid data for a field.
end
end
end
end
def valid_content_type?
if SPREADSHEET_CONTENT_TYPES.include?(@content_type)
true
else
errors.add(:lettings_log_bulk_upload, "Invalid file type")
false
end
end
def map_row(row)
{
lettype: row[1],
# reg_num_la_core_code: row[3],
# managementgroup: row[4],
# schemecode: row[5],
# firstletting: row[6],
tenancycode: row[7],
startertenancy: row[8],
tenancy: row[9],
tenancyother: row[10],
# tenancyduration: row[11],
hhmemb: hhmemb(row),
age1: row[12],
age2: row[13],
age3: row[14],
age4: row[15],
age5: row[16],
age6: row[17],
age7: row[18],
age8: row[19],
sex1: row[20],
sex2: row[21],
sex3: row[22],
sex4: row[23],
sex5: row[24],
sex6: row[25],
sex7: row[26],
sex8: row[27],
relat2: row[28],
relat3: row[29],
relat4: row[30],
relat5: row[31],
relat6: row[32],
relat7: row[33],
relat8: row[34],
ecstat1: row[35],
ecstat2: row[36],
ecstat3: row[37],
ecstat4: row[38],
ecstat5: row[39],
ecstat6: row[40],
ecstat7: row[41],
ecstat8: row[42],
ethnic: row[43],
national: row[44],
armedforces: row[45],
reservist: row[46],
preg_occ: row[47],
hb: row[48],
benefits: row[49],
net_income_known: row[50].present? ? 1 : nil,
earnings: row[50],
# increfused: row[51],
reason: row[52],
reasonother: row[53],
underoccupation_benefitcap: row[54],
housingneeds_a: row[55],
housingneeds_b: row[56],
housingneeds_c: row[57],
housingneeds_f: row[58],
housingneeds_g: row[59],
housingneeds_h: row[60],
prevten: row[61],
prevloc: row[62],
# prevpco_unknown: row[65],
layear: row[66],
waityear: row[67],
homeless: row[68],
reasonpref: row[69],
rp_homeless: row[70],
rp_insan_unsat: row[71],
rp_medwel: row[72],
rp_hardship: row[73],
rp_dontknow: row[74],
cbl: row[75],
chr: row[76],
cap: row[77],
# referral_source: row[78],
period: row[79],
brent: row[80],
scharge: row[81],
pscharge: row[82],
supcharg: row[83],
tcharge: row[84],
# tcharge_care_homes: row[85],
# no_rent_or_charge: row[86],
hbrentshortfall: row[87],
tshortfall: row[88],
voiddate: row[89].to_s + row[90].to_s + row[91].to_s,
majorrepairs: row[92].present? ? "1" : nil,
mrcdate: row[92].to_s + row[93].to_s + row[94].to_s,
# supported_scheme: row[95],
startdate: date_time(row[98], row[97], row[96]),
# startdate_day: row[96],
# startdate_month: row[97],
# startdate_year: row[98],
offered: row[99],
# property_reference: row[100],
beds: row[101],
unittype_gn: row[102],
builtype: row[103],
wchair: row[104],
property_relet: row[105],
rsnvac: row[106],
la: row[107],
# row[110] removed
# row[111] is owning organisation used above
# username: row[112],
# row[113] is managing organisation used above
leftreg: row[114],
# uprn: row[115],
incfreq: row[116],
# sheltered_accom: row[117],
illness: row[118],
illness_type_1: row[119],
illness_type_2: row[120],
illness_type_3: row[121],
illness_type_4: row[122],
illness_type_8: row[123],
illness_type_5: row[124],
illness_type_6: row[125],
illness_type_7: row[126],
illness_type_9: row[127],
illness_type_10: row[128],
# london_affordable: row[129],
rent_type: row[130],
irproduct_other: row[131],
# data_protection: row[132],
declaration: 1,
}
end
def date_time(year, month, day)
return unless year && month && day
Time.zone.local("20#{year}", month.to_s, day.to_s)
end
def hhmemb(row) def generate_identifier
[14, 15, 16, 17, 18, 19, 20].count { |idx| row[idx].present? } self.identifier ||= SecureRandom.uuid
end end
end end

4
app/models/form.rb

@ -231,10 +231,6 @@ class Form
end end
end end
def in_crossover_period?(now: Time.zone.now)
((end_date - 3.months) < now) && (now < end_date)
end
def inspect def inspect
"#<#{self.class} @type=#{type} @name=#{name}>" "#<#{self.class} @type=#{type} @name=#{name}>"
end end

14
app/models/form_handler.rb

@ -63,6 +63,20 @@ class FormHandler
form_mappings[current_collection_start_year - year] form_mappings[current_collection_start_year - year]
end end
def in_crossover_period?(now: Time.zone.now)
lettings_in_crossover_period?(now:) || sales_in_crossover_period?(now:)
end
def lettings_in_crossover_period?(now: Time.zone.now)
forms = lettings_forms.values
forms.count { |form| form.start_date < now && now < form.end_date } > 1
end
def sales_in_crossover_period?(now: Time.zone.now)
forms = sales_forms.values
forms.count { |form| form.start_date < now && now < form.end_date } > 1
end
private private
def get_all_forms def get_all_forms

27
app/models/forms/bulk_upload_lettings/checking_file.rb

@ -0,0 +1,27 @@
module Forms
module BulkUploadLettings
class CheckingFile
include ActiveModel::Model
include ActiveModel::Attributes
include Rails.application.routes.url_helpers
attribute :year, :integer
def view_path
"bulk_upload_lettings_logs/forms/checking_file"
end
def back_path
bulk_upload_lettings_log_path(id: "start")
end
def year_combo
"#{year}/#{year + 1 - 2000}"
end
def save!
true
end
end
end
end

6
app/models/forms/bulk_upload_lettings/prepare_your_file.rb

@ -31,10 +31,14 @@ module Forms
"#{year}/#{year + 1 - 2000}" "#{year}/#{year + 1 - 2000}"
end end
def save!
true
end
private private
def in_crossover_period? def in_crossover_period?
FormHandler.instance.forms.values.any?(&:in_crossover_period?) FormHandler.instance.lettings_in_crossover_period?
end end
end end
end end

50
app/models/forms/bulk_upload_lettings/upload_your_file.rb

@ -1,3 +1,5 @@
require "shellwords"
module Forms module Forms
module BulkUploadLettings module BulkUploadLettings
class UploadYourFile class UploadYourFile
@ -6,6 +8,11 @@ module Forms
include Rails.application.routes.url_helpers include Rails.application.routes.url_helpers
attribute :year, :integer attribute :year, :integer
attribute :file
attribute :current_user
validates :file, presence: true
validate :validate_file_is_csv
def view_path def view_path
"bulk_upload_lettings_logs/forms/upload_your_file" "bulk_upload_lettings_logs/forms/upload_your_file"
@ -14,6 +21,49 @@ module Forms
def back_path def back_path
bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year: }) bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year: })
end end
def year_combo
"#{year}/#{year + 1 - 2000}"
end
def next_path
bulk_upload_lettings_log_path(id: "checking-file", form: { year: })
end
def save!
bulk_upload = BulkUpload.create!(
user: current_user,
log_type: BulkUpload.log_types[:lettings],
year:,
)
if upload_enabled?
storage_service.write_file(bulk_upload.identifier, File.read(file.path))
end
true
end
private
def upload_enabled?
FeatureToggle.upload_enabled?
end
def storage_service
@storage_service ||= Storage::S3Service.new(Configuration::PaasConfigurationService.new, ENV["CSV_DOWNLOAD_PAAS_INSTANCE"])
end
def validate_file_is_csv
return unless file
argv = %W[file --brief --mime-type -- #{file.path}]
output = `#{argv.shelljoin}`
unless output.match?(/text\/csv|text\/plain/)
errors.add(:file, :not_csv)
end
end
end end
end end
end end

4
app/models/forms/bulk_upload_lettings/year.rb

@ -27,6 +27,10 @@ module Forms
bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year: }) bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year: })
end end
def save!
true
end
private private
def possible_years def possible_years

27
app/models/forms/bulk_upload_sales/checking_file.rb

@ -0,0 +1,27 @@
module Forms
module BulkUploadSales
class CheckingFile
include ActiveModel::Model
include ActiveModel::Attributes
include Rails.application.routes.url_helpers
attribute :year, :integer
def view_path
"bulk_upload_sales_logs/forms/checking_file"
end
def back_path
bulk_upload_sales_log_path(id: "start")
end
def year_combo
"#{year}/#{year + 1 - 2000}"
end
def save!
true
end
end
end
end

6
app/models/forms/bulk_upload_sales/prepare_your_file.rb

@ -31,10 +31,14 @@ module Forms
"#{year}/#{year + 1 - 2000}" "#{year}/#{year + 1 - 2000}"
end end
def save!
true
end
private private
def in_crossover_period? def in_crossover_period?
FormHandler.instance.forms.values.any?(&:in_crossover_period?) FormHandler.instance.sales_in_crossover_period?
end end
end end
end end

50
app/models/forms/bulk_upload_sales/upload_your_file.rb

@ -1,3 +1,5 @@
require "shellwords"
module Forms module Forms
module BulkUploadSales module BulkUploadSales
class UploadYourFile class UploadYourFile
@ -6,6 +8,11 @@ module Forms
include Rails.application.routes.url_helpers include Rails.application.routes.url_helpers
attribute :year, :integer attribute :year, :integer
attribute :file
attribute :current_user
validates :file, presence: true
validate :validate_file_is_csv
def view_path def view_path
"bulk_upload_sales_logs/forms/upload_your_file" "bulk_upload_sales_logs/forms/upload_your_file"
@ -14,6 +21,49 @@ module Forms
def back_path def back_path
bulk_upload_sales_log_path(id: "prepare-your-file", form: { year: }) bulk_upload_sales_log_path(id: "prepare-your-file", form: { year: })
end end
def year_combo
"#{year}/#{year + 1 - 2000}"
end
def next_path
bulk_upload_sales_log_path(id: "checking-file", form: { year: })
end
def save!
bulk_upload = BulkUpload.create!(
user: current_user,
log_type: BulkUpload.log_types[:sales],
year:,
)
if upload_enabled?
storage_service.write_file(bulk_upload.identifier, File.read(file.path))
end
true
end
private
def upload_enabled?
!Rails.env.development?
end
def storage_service
@storage_service ||= Storage::S3Service.new(Configuration::PaasConfigurationService.new, ENV["CSV_DOWNLOAD_PAAS_INSTANCE"])
end
def validate_file_is_csv
return unless file
argv = %W[file --brief --mime-type -- #{file.path}]
output = `#{argv.shelljoin}`
unless output.match?(/text\/csv|text\/plain/)
errors.add(:file, :not_csv)
end
end
end end
end end
end end

4
app/models/forms/bulk_upload_sales/year.rb

@ -27,6 +27,10 @@ module Forms
bulk_upload_sales_log_path(id: "prepare-your-file", form: { year: }) bulk_upload_sales_log_path(id: "prepare-your-file", form: { year: })
end end
def save!
true
end
private private
def possible_years def possible_years

197
app/models/legacy_bulk_upload.rb

@ -0,0 +1,197 @@
class LegacyBulkUpload
include ActiveModel::Model
include ActiveModel::Validations
include ActiveModel::Conversion
SPREADSHEET_CONTENT_TYPES = %w[
application/vnd.ms-excel
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
].freeze
FIRST_DATA_ROW = 7
def initialize(file, content_type)
@file = file
@content_type = content_type
end
def process(current_user)
return unless valid_content_type?
xlsx = Roo::Spreadsheet.open(@file, extension: :xlsx)
sheet = xlsx.sheet(0)
last_row = sheet.last_row
if last_row < FIRST_DATA_ROW
errors.add(:lettings_log_bulk_upload, "No data found")
else
data_range = FIRST_DATA_ROW..last_row
data_range.map do |row_num|
row = sheet.row(row_num)
# owning_organisation = Organisation.find(row[111])
# managing_organisation = Organisation.find(row[113])
lettings_log = LettingsLog.create!(
owning_organisation: current_user.organisation,
managing_organisation: current_user.organisation,
created_by: current_user,
)
map_row(row).each do |attr_key, attr_val|
_update = lettings_log.update(attr_key => attr_val)
rescue ArgumentError
# TODO: determine what we want to do when bulk upload contains totally invalid data for a field.
end
end
end
end
def valid_content_type?
if SPREADSHEET_CONTENT_TYPES.include?(@content_type)
true
else
errors.add(:lettings_log_bulk_upload, "Invalid file type")
false
end
end
def map_row(row)
{
lettype: row[1],
# reg_num_la_core_code: row[3],
# managementgroup: row[4],
# schemecode: row[5],
# firstletting: row[6],
tenancycode: row[7],
startertenancy: row[8],
tenancy: row[9],
tenancyother: row[10],
# tenancyduration: row[11],
hhmemb: hhmemb(row),
age1: row[12],
age2: row[13],
age3: row[14],
age4: row[15],
age5: row[16],
age6: row[17],
age7: row[18],
age8: row[19],
sex1: row[20],
sex2: row[21],
sex3: row[22],
sex4: row[23],
sex5: row[24],
sex6: row[25],
sex7: row[26],
sex8: row[27],
relat2: row[28],
relat3: row[29],
relat4: row[30],
relat5: row[31],
relat6: row[32],
relat7: row[33],
relat8: row[34],
ecstat1: row[35],
ecstat2: row[36],
ecstat3: row[37],
ecstat4: row[38],
ecstat5: row[39],
ecstat6: row[40],
ecstat7: row[41],
ecstat8: row[42],
ethnic: row[43],
national: row[44],
armedforces: row[45],
reservist: row[46],
preg_occ: row[47],
hb: row[48],
benefits: row[49],
net_income_known: row[50].present? ? 1 : nil,
earnings: row[50],
# increfused: row[51],
reason: row[52],
reasonother: row[53],
underoccupation_benefitcap: row[54],
housingneeds_a: row[55],
housingneeds_b: row[56],
housingneeds_c: row[57],
housingneeds_f: row[58],
housingneeds_g: row[59],
housingneeds_h: row[60],
prevten: row[61],
prevloc: row[62],
# prevpco_unknown: row[65],
layear: row[66],
waityear: row[67],
homeless: row[68],
reasonpref: row[69],
rp_homeless: row[70],
rp_insan_unsat: row[71],
rp_medwel: row[72],
rp_hardship: row[73],
rp_dontknow: row[74],
cbl: row[75],
chr: row[76],
cap: row[77],
# referral_source: row[78],
period: row[79],
brent: row[80],
scharge: row[81],
pscharge: row[82],
supcharg: row[83],
tcharge: row[84],
# tcharge_care_homes: row[85],
# no_rent_or_charge: row[86],
hbrentshortfall: row[87],
tshortfall: row[88],
voiddate: row[89].to_s + row[90].to_s + row[91].to_s,
majorrepairs: row[92].present? ? "1" : nil,
mrcdate: row[92].to_s + row[93].to_s + row[94].to_s,
# supported_scheme: row[95],
startdate: date_time(row[98], row[97], row[96]),
# startdate_day: row[96],
# startdate_month: row[97],
# startdate_year: row[98],
offered: row[99],
# property_reference: row[100],
beds: row[101],
unittype_gn: row[102],
builtype: row[103],
wchair: row[104],
property_relet: row[105],
rsnvac: row[106],
la: row[107],
# row[110] removed
# row[111] is owning organisation used above
# username: row[112],
# row[113] is managing organisation used above
leftreg: row[114],
# uprn: row[115],
incfreq: row[116],
# sheltered_accom: row[117],
illness: row[118],
illness_type_1: row[119],
illness_type_2: row[120],
illness_type_3: row[121],
illness_type_4: row[122],
illness_type_8: row[123],
illness_type_5: row[124],
illness_type_6: row[125],
illness_type_7: row[126],
illness_type_9: row[127],
illness_type_10: row[128],
# london_affordable: row[129],
rent_type: row[130],
irproduct_other: row[131],
# data_protection: row[132],
declaration: 1,
}
end
def date_time(year, month, day)
return unless year && month && day
Time.zone.local("20#{year}", month.to_s, day.to_s)
end
def hhmemb(row)
[14, 15, 16, 17, 18, 19, 20].count { |idx| row[idx].present? }
end
end

28
app/views/bulk_upload_lettings_logs/forms/checking_file.html.erb

@ -0,0 +1,28 @@
<% 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">
<%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "prepare-your-file"), method: :patch do |f| %>
<%= f.hidden_field :year %>
<span class="govuk-caption-l">Upload lettings logs in bulk (<%= @form.year_combo %>)</span>
<h1 class="govuk-heading-l">We’re checking the file</h1>
<h2 class="govuk-heading-m">Once this is done, we’ll email you the outcome.</h2>
<p class="govuk-body">
We’re checking for any missing data, issues or errors. Depending on the file size, this could take a few minutes.
</p>
<p class="govuk-body">
If there are errors we will email you a link to view all the errors.
</p>
<p class="govuk-body">
If there are no errors we will email you to let you know the upload is complete and that your logs have been created.
</p>
<% end %>
</div>
</div>

26
app/views/bulk_upload_lettings_logs/forms/upload_your_file.html.erb

@ -2,16 +2,24 @@
<%= govuk_back_link href: @form.back_path %> <%= govuk_back_link href: @form.back_path %>
<% end %> <% end %>
<%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "upload-your-file"), method: :patch do |f| %> <div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "upload-your-file"), method: :patch do |f| %>
<%= f.hidden_field :year %>
<%= f.govuk_error_summary %> <%= f.govuk_error_summary %>
<div> <span class="govuk-caption-l">Upload lettings logs in bulk (<%= @form.year_combo %>)</span>
Upload your file goes here <h1 class="govuk-heading-l">Upload your file</h1>
</div>
<div> <p class="govuk-body">
year selected <%= @form.year %> All data must be complete and formatted correctly (CSV <strong>only</strong>) - if not, we will reject the entire file.
</div> </p>
<%= f.govuk_submit %> <%= f.govuk_file_field :file,
<% end %> label: { text: "Upload file" } %>
<%= f.govuk_submit "Upload" %>
<% end %>
</div>
</div>

28
app/views/bulk_upload_sales_logs/forms/checking_file.html.erb

@ -0,0 +1,28 @@
<% 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">
<%= form_with model: @form, scope: :form, url: bulk_upload_sales_log_path(id: "prepare-your-file"), method: :patch do |f| %>
<%= f.hidden_field :year %>
<span class="govuk-caption-l">Upload sales logs in bulk (<%= @form.year_combo %>)</span>
<h1 class="govuk-heading-l">We’re checking the file</h1>
<h2 class="govuk-heading-m">Once this is done, we’ll email you the outcome.</h2>
<p class="govuk-body">
We’re checking for any missing data, issues or errors. Depending on the file size, this could take a few minutes.
</p>
<p class="govuk-body">
If there are errors we will email you a link to view all the errors.
</p>
<p class="govuk-body">
If there are no errors we will email you to let you know the upload is complete and that your logs have been created.
</p>
<% end %>
</div>
</div>

26
app/views/bulk_upload_sales_logs/forms/upload_your_file.html.erb

@ -2,16 +2,24 @@
<%= govuk_back_link href: @form.back_path %> <%= govuk_back_link href: @form.back_path %>
<% end %> <% end %>
<%= form_with model: @form, scope: :form, url: bulk_upload_sales_log_path(id: "upload-your-file"), method: :patch do |f| %> <div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= form_with model: @form, scope: :form, url: bulk_upload_sales_log_path(id: "upload-your-file"), method: :patch do |f| %>
<%= f.hidden_field :year %>
<%= f.govuk_error_summary %> <%= f.govuk_error_summary %>
<div> <span class="govuk-caption-l">Upload sales logs in bulk (<%= @form.year_combo %>)</span>
Upload your file goes here <h1 class="govuk-heading-l">Upload your file</h1>
</div>
<div> <p class="govuk-body">
year selected <%= @form.year %> All data must be complete and formatted correctly (CSV <strong>only</strong>) - if not, we will reject the entire file.
</div> </p>
<%= f.govuk_submit %> <%= f.govuk_file_field :file,
<% end %> label: { text: "Upload file" } %>
<%= f.govuk_submit "Upload" %>
<% end %>
</div>
</div>

2
app/views/logs/bulk_upload.html.erb

@ -1,6 +1,6 @@
<% content_for :title, "Bulk upload" %> <% content_for :title, "Bulk upload" %>
<%= form_for @bulk_upload, url: bulk_upload_lettings_logs_path, method: "post" do |f| %> <%= form_for @bulk_upload, scope: :bulk_upload, url: bulk_upload_lettings_logs_path, method: "post" do |f| %>
<%= f.govuk_error_summary %> <%= f.govuk_error_summary %>
<%= f.govuk_file_field :lettings_log_bulk_upload, <%= f.govuk_file_field :lettings_log_bulk_upload,

4
config/initializers/feature_toggle.rb

@ -26,4 +26,8 @@ class FeatureToggle
def self.bulk_upload_logs? def self.bulk_upload_logs?
!Rails.env.production? !Rails.env.production?
end end
def self.upload_enabled?
!Rails.env.development?
end
end end

10
config/locales/en.yml

@ -49,6 +49,16 @@ en:
attributes: attributes:
year: year:
blank: You must select a collection period to upload for blank: You must select a collection period to upload for
forms/bulk_upload_lettings/upload_your_file:
attributes:
file:
blank: Select which file to upload
not_csv: Your file must be in CSV format
forms/bulk_upload_sales/upload_your_file:
attributes:
file:
blank: Select which file to upload
not_csv: Your file must be in CSV format
activerecord: activerecord:
errors: errors:

14
db/migrate/20221128130843_create_bulk_uploads.rb

@ -0,0 +1,14 @@
class CreateBulkUploads < ActiveRecord::Migration[7.0]
def change
create_table :bulk_uploads do |t|
t.references :user
t.text :log_type, null: false
t.integer :year, null: false
t.uuid :identifier, null: false
t.timestamps
end
add_index :bulk_uploads, :identifier, unique: true
end
end

11
db/schema.rb

@ -14,6 +14,17 @@ ActiveRecord::Schema[7.0].define(version: 2022_12_06_081127) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
create_table "bulk_uploads", force: :cascade do |t|
t.bigint "user_id"
t.text "log_type", null: false
t.integer "year", null: false
t.uuid "identifier", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["identifier"], name: "index_bulk_uploads_on_identifier", unique: true
t.index ["user_id"], name: "index_bulk_uploads_on_user_id"
end
create_table "data_protection_confirmations", force: :cascade do |t| create_table "data_protection_confirmations", force: :cascade do |t|
t.bigint "organisation_id" t.bigint "organisation_id"
t.bigint "data_protection_officer_id" t.bigint "data_protection_officer_id"

36
spec/features/bulk_upload_lettings_logs_spec.rb

@ -3,13 +3,26 @@ require "rails_helper"
RSpec.describe "Bulk upload lettings log" do RSpec.describe "Bulk upload lettings log" do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:stub_file_upload) do
vcap_services = { "aws-s3-bucket" => {} }
mock_storage_service = instance_double("S3Service")
allow(ENV).to receive(:[])
allow(ENV).to receive(:[]).with("VCAP_SERVICES").and_return(vcap_services.to_json)
allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service)
allow(mock_storage_service).to receive(:write_file)
end
before do before do
stub_file_upload
sign_in user sign_in user
end end
# rubocop:disable RSpec/AnyInstance
context "when during crossover period" do context "when during crossover period" do
it "shows journey with year option" do it "shows journey with year option" do
Timecop.freeze(2023, 6, 1) do Timecop.freeze(2022, 6, 1) do
visit("/lettings-logs") visit("/lettings-logs")
expect(page).to have_link("Upload lettings logs in bulk") expect(page).to have_link("Upload lettings logs in bulk")
click_link("Upload lettings logs in bulk") click_link("Upload lettings logs in bulk")
@ -30,9 +43,30 @@ RSpec.describe "Bulk upload lettings log" do
click_button("Continue") click_button("Continue")
expect(page).to have_content("Upload your file") expect(page).to have_content("Upload your file")
click_button("Upload")
allow_any_instance_of(Forms::BulkUploadLettings::UploadYourFile).to receive(:`).and_return("not a csv")
expect(page).to have_content("Select which file to upload")
attach_file "file", file_fixture("2021_22_lettings_bulk_upload.xlsx")
click_button("Upload")
allow_any_instance_of(Forms::BulkUploadLettings::UploadYourFile).to receive(:`).and_return("text/csv")
expect(page).to have_content("Your file must be in CSV format")
attach_file "file", file_fixture("blank_bulk_upload_sales.csv")
expect {
click_button("Upload")
}.to change(BulkUpload, :count).by(1)
expect(page).to have_content("Once this is done")
click_link("Back")
expect(page).to have_content("Upload lettings logs in bulk")
end end
end end
end end
# rubocop:enable RSpec/AnyInstance
context "when not it crossover period" do context "when not it crossover period" do
it "shows journey with year option" do it "shows journey with year option" do

36
spec/features/bulk_upload_sales_logs_spec.rb

@ -3,13 +3,26 @@ require "rails_helper"
RSpec.describe "Bulk upload sales log" do RSpec.describe "Bulk upload sales log" do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:stub_file_upload) do
vcap_services = { "aws-s3-bucket" => {} }
mock_storage_service = instance_double("S3Service")
allow(ENV).to receive(:[])
allow(ENV).to receive(:[]).with("VCAP_SERVICES").and_return(vcap_services.to_json)
allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service)
allow(mock_storage_service).to receive(:write_file)
end
before do before do
stub_file_upload
sign_in user sign_in user
end end
# rubocop:disable RSpec/AnyInstance
context "when during crossover period" do context "when during crossover period" do
it "shows journey with year option" do it "shows journey with year option" do
Timecop.freeze(2023, 6, 1) do Timecop.freeze(2023, 5, 1) do
visit("/sales-logs") visit("/sales-logs")
expect(page).to have_link("Upload sales logs in bulk") expect(page).to have_link("Upload sales logs in bulk")
click_link("Upload sales logs in bulk") click_link("Upload sales logs in bulk")
@ -30,9 +43,30 @@ RSpec.describe "Bulk upload sales log" do
click_button("Continue") click_button("Continue")
expect(page).to have_content("Upload your file") expect(page).to have_content("Upload your file")
click_button("Upload")
allow_any_instance_of(Forms::BulkUploadSales::UploadYourFile).to receive(:`).and_return("not a csv")
expect(page).to have_content("Select which file to upload")
attach_file "file", file_fixture("2021_22_lettings_bulk_upload.xlsx")
click_button("Upload")
allow_any_instance_of(Forms::BulkUploadSales::UploadYourFile).to receive(:`).and_return("text/csv")
expect(page).to have_content("Your file must be in CSV format")
attach_file "file", file_fixture("blank_bulk_upload_sales.csv")
expect {
click_button("Upload")
}.to change(BulkUpload, :count).by(1)
expect(page).to have_content("Once this is done")
click_link("Back")
expect(page).to have_content("Upload sales logs in bulk")
end end
end end
end end
# rubocop:enable RSpec/AnyInstance
context "when not it crossover period" do context "when not it crossover period" do
it "shows journey with year option" do it "shows journey with year option" do

118
spec/fixtures/files/blank_bulk_upload_sales.csv vendored

@ -0,0 +1,118 @@
Question,What is the purchaser code?,What is the day of the sale completion date? - DD,What is the month of the sale completion date? - MM,What is the year of the sale completion date? - YY,[BLANK],Was the buyer interviewed for any of the answers you will provide on this log?,Age of Buyer 1,Age of Buyer 2 or Person 2,Age of Person 3,Age of Person 4,Age of Person 5,Age of Person 6,Gender identity of Buyer 1,Gender identity of Buyer 2 or Person 2,Gender identity of Person 3,Gender identity of Person 4,Gender identity of Person 5,Gender identity of Person 6,Person 2's relationship to lead tenant,Person 3's relationship to lead tenant,Person 4's relationship to lead tenant,Person 5's relationship to lead tenant,Person 6's relationship to lead tenant,Working situation of Buyer 1,Working situation of Buyer 2 or Person 2,Working situation of Person 3,Working situation of Person 4,Working situation of Person 5,Working situation of Person 6,What is the buyer 1's ethnic group?,What is buyer 1's nationality?,What is buyer 1's gross annual income?,What is buyer 2's gross annual income?,Was buyer 1's income used for a mortgage application?,Was buyer 2's income used for a mortgage application?,"What is the total amount the buyers had in savings before they paid any deposit for the property?
To the nearest £10",Have any of the buyers previously owned a property?,[BLANK],What was buyer 1's previous tenure?,What is the local authority of buyer 1's last settled home,Part 1 of postcode of buyer 1's last settled home,Part 2 of postcode of buyer 1's last settled home,Do you know the postcode of buyer 1's last settled home?,Was the buyer registered with their PRP (HA)?,Was the buyer registered with the local authority?,Was the buyer registered with a Help to Buy agent?,Was the buyer registered with another PRP (HA)?,Does anyone in the household consider themselves to have a disability?,Does anyone in the household use a wheelchair?,How many bedrooms does the property have?,What type of unit is the property?,Which type of building is the property?,What is the local authority of the property?,Part 1 of postcode of property,Part 2 of postcode of property,Is the property built or adapted to wheelchair-user standards?,What is the type of shared ownership sale?,"Is this a resale?
Shared ownership","What is the day of the practical completion or handover date? - DD
Shared ownership","What is the month of the practical completion or handover date? - MM
Shared ownership","What is the year of the practical completion or handover date? - YY
Shared ownership","What is the day of the exchange of contracts date? - DD
Shared ownership","What is the month of the exchange of contracts date? - MM
Shared ownership","What is the year of the exchange of contracts date? - YY
Shared ownership","Was the household re-housed under a local authority nominations agreement?
Shared ownership","How many bedrooms did the buyer's previous property have?
Shared ownership","What was the type of the buyer's previous property?
Shared ownership","What was the full purchase price?
Shared ownership","What was the initial percentage equity stake purchased?
Shared ownership","What is the mortgage amount?
Shared ownership","Does this include any extra borrowing?
Shared ownership","How much was the cash deposit paid on the property?
Shared ownership","How much cash discount was given through Social Homebuy?
Shared ownership","What is the basic monthly rent?
Shared ownership","What are the total monthly leasehold charges for the property?
Shared ownership",What is the type of discounted ownership sale?,"What was the full purchase price?
Discounted ownership","What was the amount of any loan, grant, discount or subsidy given?
Discounted ownership","What was the percentage discount?
Discounted ownership","What is the mortgage amount?
Discounted ownership","Does this include any extra borrowing?
Discounted ownership","How much was the cash deposit paid on the property?
Discounted ownership","What are the total monthly leasehold charges for the property?
Discounted ownership",What is the type of outright sale?,"What is the 'other' type of outright sale?
Outright sale",[BLANK],"What is the full purchase price?
Outright sale","What is the mortgage amount?
Outright sale","Does this include any extra borrowing?
Outright sale","How much was the cash deposit paid on the property?
Outright sale","What are the total monthly leasehold charges for the property?
Outright sale","Which organisation owned this property before the sale?
Organisation's CORE ID",Username,BLANK,Has the buyer ever served in the UK Armed Forces and for how long?,[BLANK],Are any of the buyers a spouse or civil partner of a UK Armed Forces regular who died in service within the last 2 years?,"What is the name of the mortgage lender?
Shared ownership","What is the name of the 'other' mortgage lender?
Shared ownership","What is the name of the mortgage lender?
Discounted ownership","What is the name of the 'other' mortgage lender?
Discounted ownership","What is the name of the mortgage lender?
Outright sale","What is the name of the 'other' mortgage lender?
Outright sale",Were the buyers receiving any of these housing-related benefits immediately before buying this property?,"What is the length of the mortgage in years?
Shared ownership","What is the length of the mortgage in years?
Discounted ownership","What is the length of the mortgage in years?
Outright sale","How long have the buyers been living in the property before the purchase?
Discounted ownership",Are there more than two joint purchasers of this property?,"How long have the buyers been living in the property before the purchase?
Shared ownership",Is this a staircasing transaction?,Data Protection question,Was this purchase made through an ownership scheme?,"Is the buyer a company?
Outright sale",Will the buyers live in the property?,Is this a joint purchase?,Will buyer 1 live in the property?,Will buyer 2 live in the property?,"Besides the buyers, how many people live in the property?","What percentage of the property has been bought in this staircasing transaction?
Shared ownership","What percentage of the property does the buyer now own in total?
Shared ownership","What was the rent type of the buyer's previous property?
Shared ownership","Was a mortgage used for the purchase of this property?
Shared ownership","Was a mortgage used for the purchase of this property?
Discounted ownership","Was a mortgage used for the purchase of this property?
Outright sale"
Values,Max 9 digits,1 - 31,1 - 12,19 - 23,,1 or null,"15 - 110
or R",1 - 110 or R,,,,,"M, F, X or R",,,,,,"P, C, X or R",,,,,0 - 10,,,,,,1 - 19,"12 -13, 17 -19",0 - 99999,,1 or 2,1 or 2,0 - 999990,1 - 3,,1 - 7 or 9,ONS CODE - E + 9 digits,XXX(X),XXX,1 or null,,,,,1 - 3,1 - 3,1 - 9,1 - 4 or 9,1 or 2,ONS CODE E + 9 digits,XXX(X),XXX,1 - 3,"2, 16, 18, 24, 28 or 30-31",1 or 2,1 - 31,1 - 12,19 - 23,1 - 31,1 - 12,19 - 23,1 - 3,1 - 9,1 - 4 or 9,0 - 999999,0 - 100,0 - 999999,1 - 3,0 - 999999,,0 - 999.99,,"8, 9, 14, 21, 22, 27 or 29",0 - 999999,,0 - 100,0 - 999999,1 - 3,0 - 999999,0 - 999.99,10 or 12,,,0 - 999999,,1-3,0 - 999999,0-999.99,Up to 7 digits,Username of CORE account this sales log should be assigned to,,3 - 8,,4 - 7,1 - 40,,1 - 40,,1 - 40,,1 - 4, Integer <=60, Integer <=60, Integer <=60, Integer <=80,1 - 3, Integer <=80,1 - 3,1,1 - 3,1 - 2,1 - 2,1 - 2,1 - 2,1 - 2,0 - 5,1 - 100,1 - 100,1-3 or 9-10,1 - 2,1 - 2,1 - 2
Can be Null?,No,,,,,No,No,"If fields 14, 19 and 25 are all also null","If fields 15, 20 and 26 are all also null","If fields 16, 21 and 27 are all also null","If fields 17, 22 and 28 are all also null","If fields 18, 23 and 29 are all also null",No,"If fields 8, 19 and 25 are all also null","If fields 9, 20 and 26 are also null","If fields 10, 21 and 27 are all also null","If fields 11, 22 and 28 are all also null","If fields 12, 23 and 29 are all also null","If fields 8, 14 and 25 are all also null","If fields 9, 15 and 26 are all also null","If fields 10, 16 and 27 are all also null","If fields 11, 17 and 28 are all also null","If fields 12, 18 and 29 are all also null",If field 6 = 1,"If fields 8, 14 and 19 are all also null","If fields 9, 15 and 20 are all also null","If fields 10, 16 and 21 are all also null","If fields 11, 17 and 22 are all also null","If fields 12, 18 and 23 are all also null",If field 6 = 1,,,If field 116 = 2,If field 32 is null,If field 116 = 2,If field 6 = 1,,,If field 6 = 1,No,If field 43 = 1,,If fields 41 and 42 BOTH have valid entries,Yes,,,,If field 6 = 1,,No,,,,,,,If field 113 = 2 or 3,,,,,,,,,"If field 113 = 2 or 3
OR
field 39 = 3 - 7 or 9",,If field 113 = 2 or 3,,,,,"If field 57 is null, 2, 16, 24 or 28",If field 113 = 2 or 3,,If field 113 = 1 or 3,If field 76 is null,"If field 76 is null, 9 or 14","If field 76 is null, 8, 21 or 22",If field 113 = 1 or 3,,,,If field 113 = 1 or 2,If field 84 is null or 10,,If field 113 = 1 or 2,,,,,No,Yes,,No,,No,If field 113 = 2 or 3,"If field 113 = 2 or 3
OR
If field 98 is not 40",If field 113 = 1 or 3,"If field 113 = 1 or 3
OR
If field 100 is not 40",If field 113 = 1 or 2,"If field 113 = 1 or 2
OR
If field 102 is not 40",No,If field 113 = 2 or 3,If field 113 = 1 or 3,If field 113 = 1 or 2,If field 113 = 1 or 3,If field 116 = 2,If field 113 = 2 or 3,If field 113 = 2 or 3,No,No,If field 113 = 1 or 2,If field 113 = 1 or 2,No,No,If field 116 = 2,No,If field 113 = 2 or 3,If field 113 = 2 or 3,"If field 113 = 1 or 2
OR
If field 39 = 3 - 9",If field 113 = 2 or 3,If field 113 = 1 or 3,If field 113 = 1 or 2
Bulk upload format and duplicate check,Yes,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Field number,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125
1 Question What is the purchaser code? What is the day of the sale completion date? - DD What is the month of the sale completion date? - MM What is the year of the sale completion date? - YY [BLANK] Was the buyer interviewed for any of the answers you will provide on this log? Age of Buyer 1 Age of Buyer 2 or Person 2 Age of Person 3 Age of Person 4 Age of Person 5 Age of Person 6 Gender identity of Buyer 1 Gender identity of Buyer 2 or Person 2 Gender identity of Person 3 Gender identity of Person 4 Gender identity of Person 5 Gender identity of Person 6 Person 2's relationship to lead tenant Person 3's relationship to lead tenant Person 4's relationship to lead tenant Person 5's relationship to lead tenant Person 6's relationship to lead tenant Working situation of Buyer 1 Working situation of Buyer 2 or Person 2 Working situation of Person 3 Working situation of Person 4 Working situation of Person 5 Working situation of Person 6 What is the buyer 1's ethnic group? What is buyer 1's nationality? What is buyer 1's gross annual income? What is buyer 2's gross annual income? Was buyer 1's income used for a mortgage application? Was buyer 2's income used for a mortgage application? What is the total amount the buyers had in savings before they paid any deposit for the property? To the nearest £10 Have any of the buyers previously owned a property? [BLANK] What was buyer 1's previous tenure? What is the local authority of buyer 1's last settled home Part 1 of postcode of buyer 1's last settled home Part 2 of postcode of buyer 1's last settled home Do you know the postcode of buyer 1's last settled home? Was the buyer registered with their PRP (HA)? Was the buyer registered with the local authority? Was the buyer registered with a Help to Buy agent? Was the buyer registered with another PRP (HA)? Does anyone in the household consider themselves to have a disability? Does anyone in the household use a wheelchair? How many bedrooms does the property have? What type of unit is the property? Which type of building is the property? What is the local authority of the property? Part 1 of postcode of property Part 2 of postcode of property Is the property built or adapted to wheelchair-user standards? What is the type of shared ownership sale? Is this a resale? Shared ownership What is the day of the practical completion or handover date? - DD Shared ownership What is the month of the practical completion or handover date? - MM Shared ownership What is the year of the practical completion or handover date? - YY Shared ownership What is the day of the exchange of contracts date? - DD Shared ownership What is the month of the exchange of contracts date? - MM Shared ownership What is the year of the exchange of contracts date? - YY Shared ownership Was the household re-housed under a local authority nominations agreement? Shared ownership How many bedrooms did the buyer's previous property have? Shared ownership What was the type of the buyer's previous property? Shared ownership What was the full purchase price? Shared ownership What was the initial percentage equity stake purchased? Shared ownership What is the mortgage amount? Shared ownership Does this include any extra borrowing? Shared ownership How much was the cash deposit paid on the property? Shared ownership How much cash discount was given through Social Homebuy? Shared ownership What is the basic monthly rent? Shared ownership What are the total monthly leasehold charges for the property? Shared ownership What is the type of discounted ownership sale? What was the full purchase price? Discounted ownership What was the amount of any loan, grant, discount or subsidy given? Discounted ownership What was the percentage discount? Discounted ownership What is the mortgage amount? Discounted ownership Does this include any extra borrowing? Discounted ownership How much was the cash deposit paid on the property? Discounted ownership What are the total monthly leasehold charges for the property? Discounted ownership What is the type of outright sale? What is the 'other' type of outright sale? Outright sale [BLANK] What is the full purchase price? Outright sale What is the mortgage amount? Outright sale Does this include any extra borrowing? Outright sale How much was the cash deposit paid on the property? Outright sale What are the total monthly leasehold charges for the property? Outright sale Which organisation owned this property before the sale? Organisation's CORE ID Username BLANK Has the buyer ever served in the UK Armed Forces and for how long? [BLANK] Are any of the buyers a spouse or civil partner of a UK Armed Forces regular who died in service within the last 2 years? What is the name of the mortgage lender? Shared ownership What is the name of the 'other' mortgage lender? Shared ownership What is the name of the mortgage lender? Discounted ownership What is the name of the 'other' mortgage lender? Discounted ownership What is the name of the mortgage lender? Outright sale What is the name of the 'other' mortgage lender? Outright sale Were the buyers receiving any of these housing-related benefits immediately before buying this property? What is the length of the mortgage in years? Shared ownership What is the length of the mortgage in years? Discounted ownership What is the length of the mortgage in years? Outright sale How long have the buyers been living in the property before the purchase? Discounted ownership Are there more than two joint purchasers of this property? How long have the buyers been living in the property before the purchase? Shared ownership Is this a staircasing transaction? Data Protection question Was this purchase made through an ownership scheme? Is the buyer a company? Outright sale Will the buyers live in the property? Is this a joint purchase? Will buyer 1 live in the property? Will buyer 2 live in the property? Besides the buyers, how many people live in the property? What percentage of the property has been bought in this staircasing transaction? Shared ownership What percentage of the property does the buyer now own in total? Shared ownership What was the rent type of the buyer's previous property? Shared ownership Was a mortgage used for the purchase of this property? Shared ownership Was a mortgage used for the purchase of this property? Discounted ownership Was a mortgage used for the purchase of this property? Outright sale
2 Values Max 9 digits 1 - 31 1 - 12 19 - 23 1 or null 15 - 110 or R 1 - 110 or R M, F, X or R P, C, X or R 0 - 10 1 - 19 12 -13, 17 -19 0 - 99999 1 or 2 1 or 2 0 - 999990 1 - 3 1 - 7 or 9 ONS CODE - E + 9 digits XXX(X) XXX 1 or null 1 - 3 1 - 3 1 - 9 1 - 4 or 9 1 or 2 ONS CODE E + 9 digits XXX(X) XXX 1 - 3 2, 16, 18, 24, 28 or 30-31 1 or 2 1 - 31 1 - 12 19 - 23 1 - 31 1 - 12 19 - 23 1 - 3 1 - 9 1 - 4 or 9 0 - 999999 0 - 100 0 - 999999 1 - 3 0 - 999999 0 - 999.99 8, 9, 14, 21, 22, 27 or 29 0 - 999999 0 - 100 0 - 999999 1 - 3 0 - 999999 0 - 999.99 10 or 12 0 - 999999 1-3 0 - 999999 0-999.99 Up to 7 digits Username of CORE account this sales log should be assigned to 3 - 8 4 - 7 1 - 40 1 - 40 1 - 40 1 - 4 Integer <=60 Integer <=60 Integer <=60 Integer <=80 1 - 3 Integer <=80 1 - 3 1 1 - 3 1 - 2 1 - 2 1 - 2 1 - 2 1 - 2 0 - 5 1 - 100 1 - 100 1-3 or 9-10 1 - 2 1 - 2 1 - 2
3 Can be Null? No No No If fields 14, 19 and 25 are all also null If fields 15, 20 and 26 are all also null If fields 16, 21 and 27 are all also null If fields 17, 22 and 28 are all also null If fields 18, 23 and 29 are all also null No If fields 8, 19 and 25 are all also null If fields 9, 20 and 26 are also null If fields 10, 21 and 27 are all also null If fields 11, 22 and 28 are all also null If fields 12, 23 and 29 are all also null If fields 8, 14 and 25 are all also null If fields 9, 15 and 26 are all also null If fields 10, 16 and 27 are all also null If fields 11, 17 and 28 are all also null If fields 12, 18 and 29 are all also null If field 6 = 1 If fields 8, 14 and 19 are all also null If fields 9, 15 and 20 are all also null If fields 10, 16 and 21 are all also null If fields 11, 17 and 22 are all also null If fields 12, 18 and 23 are all also null If field 6 = 1 If field 116 = 2 If field 32 is null If field 116 = 2 If field 6 = 1 If field 6 = 1 No If field 43 = 1 If fields 41 and 42 BOTH have valid entries Yes If field 6 = 1 No If field 113 = 2 or 3 If field 113 = 2 or 3 OR field 39 = 3 - 7 or 9 If field 113 = 2 or 3 If field 57 is null, 2, 16, 24 or 28 If field 113 = 2 or 3 If field 113 = 1 or 3 If field 76 is null If field 76 is null, 9 or 14 If field 76 is null, 8, 21 or 22 If field 113 = 1 or 3 If field 113 = 1 or 2 If field 84 is null or 10 If field 113 = 1 or 2 No Yes No No If field 113 = 2 or 3 If field 113 = 2 or 3 OR If field 98 is not 40 If field 113 = 1 or 3 If field 113 = 1 or 3 OR If field 100 is not 40 If field 113 = 1 or 2 If field 113 = 1 or 2 OR If field 102 is not 40 No If field 113 = 2 or 3 If field 113 = 1 or 3 If field 113 = 1 or 2 If field 113 = 1 or 3 If field 116 = 2 If field 113 = 2 or 3 If field 113 = 2 or 3 No No If field 113 = 1 or 2 If field 113 = 1 or 2 No No If field 116 = 2 No If field 113 = 2 or 3 If field 113 = 2 or 3 If field 113 = 1 or 2 OR If field 39 = 3 - 9 If field 113 = 2 or 3 If field 113 = 1 or 3 If field 113 = 1 or 2
4 Bulk upload format and duplicate check Yes
5 Field number 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125

BIN
spec/fixtures/files/excel_as_csv.csv vendored

Binary file not shown.
Can't render this file because it contains an unexpected character in line 3 and column 219.

16
spec/models/form_handler_spec.rb

@ -153,4 +153,20 @@ RSpec.describe FormHandler do
expect(form.type).to eq("lettings") expect(form.type).to eq("lettings")
expect(form.start_date.year).to eq(2022) expect(form.start_date.year).to eq(2022)
end end
# rubocop:disable RSpec/PredicateMatcher
describe "#in_crossover_period?" do
context "when not in overlapping period" do
it "returns false" do
expect(form_handler.in_crossover_period?(now: Date.new(2022, 1, 1))).to be_falsey
end
end
context "when in overlapping period" do
it "returns true" do
expect(form_handler.in_crossover_period?(now: Date.new(2022, 6, 1))).to be_truthy
end
end
end
# rubocop:enable RSpec/PredicateMatcher
end end

34
spec/models/form_spec.rb

@ -237,40 +237,6 @@ RSpec.describe Form, type: :model do
end end
end end
describe "#in_crossover_period?" do
context "when now not specified" do
context "when after end period" do
subject(:form) { described_class.new(nil, 2022, [], "sales") }
it "returns false" do
Timecop.freeze(2023, 8, 1) do
expect(form).not_to be_in_crossover_period
end
end
end
context "when during crossover" do
subject(:form) { described_class.new(nil, 2022, [], "sales") }
it "returns true" do
Timecop.freeze(2023, 6, 1) do
expect(form).to be_in_crossover_period
end
end
end
context "when before crossover" do
subject(:form) { described_class.new(nil, 2022, [], "sales") }
it "returns false" do
Timecop.freeze(2023, 1, 1) do
expect(form).not_to be_in_crossover_period
end
end
end
end
end
describe "when creating a lettings log", :aggregate_failures do describe "when creating a lettings log", :aggregate_failures do
it "creates a valid lettings form" do it "creates a valid lettings form" do
form = described_class.new("spec/fixtures/forms/2021_2022.json") form = described_class.new("spec/fixtures/forms/2021_2022.json")

47
spec/models/forms/bulk_upload_lettings/upload_your_file_spec.rb

@ -0,0 +1,47 @@
require "rails_helper"
RSpec.describe Forms::BulkUploadLettings::UploadYourFile do
subject(:form) { described_class.new(year:, file:, current_user:) }
let(:year) { 2022 }
let(:actual_file) { File.open(file_fixture("blank_bulk_upload_sales.csv")) }
let(:file) { ActionDispatch::Http::UploadedFile.new(tempfile: actual_file) }
let(:current_user) { create(:user) }
let(:mock_storage_service) { instance_double("S3Service") }
before do
vcap_services = { "aws-s3-bucket" => {} }
allow(ENV).to receive(:[])
allow(ENV).to receive(:[]).with("VCAP_SERVICES").and_return(vcap_services.to_json)
allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service)
allow(mock_storage_service).to receive(:write_file)
end
describe "#save" do
it "persists a BulkUpload" do
expect { form.save! }.to change(BulkUpload, :count).by(1)
end
it "persists a BulkUpload correctly" do
form.save!
bulk_upload = BulkUpload.last
expect(bulk_upload.user).to eql(current_user)
expect(bulk_upload.log_type).to eql("lettings")
expect(bulk_upload.year).to eql(year)
expect(bulk_upload.identifier).to be_present
end
it "uploads file via storage service" do
form.save!
bulk_upload = BulkUpload.last
expect(Storage::S3Service).to have_received(:new)
expect(mock_storage_service).to have_received(:write_file).with(bulk_upload.identifier, actual_file.read)
end
end
end

47
spec/models/forms/bulk_upload_sales/upload_your_file_spec.rb

@ -0,0 +1,47 @@
require "rails_helper"
RSpec.describe Forms::BulkUploadSales::UploadYourFile do
subject(:form) { described_class.new(year:, file:, current_user:) }
let(:year) { 2022 }
let(:actual_file) { File.open(file_fixture("blank_bulk_upload_sales.csv")) }
let(:file) { ActionDispatch::Http::UploadedFile.new(tempfile: actual_file) }
let(:current_user) { create(:user) }
let(:mock_storage_service) { instance_double("S3Service") }
before do
vcap_services = { "aws-s3-bucket" => {} }
allow(ENV).to receive(:[])
allow(ENV).to receive(:[]).with("VCAP_SERVICES").and_return(vcap_services.to_json)
allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service)
allow(mock_storage_service).to receive(:write_file)
end
describe "#save" do
it "persists a BulkUpload" do
expect { form.save! }.to change(BulkUpload, :count).by(1)
end
it "persists a BulkUpload correctly" do
form.save!
bulk_upload = BulkUpload.last
expect(bulk_upload.user).to eql(current_user)
expect(bulk_upload.log_type).to eql("sales")
expect(bulk_upload.year).to eql(year)
expect(bulk_upload.identifier).to be_present
end
it "uploads file via storage service" do
form.save!
bulk_upload = BulkUpload.last
expect(Storage::S3Service).to have_received(:new)
expect(mock_storage_service).to have_received(:write_file).with(bulk_upload.identifier, actual_file.read)
end
end
end

2
spec/requests/bulk_upload_controller_spec.rb

@ -45,7 +45,7 @@ RSpec.describe BulkUploadController, type: :request do
end end
it "returns a page with a file upload form" do it "returns a page with a file upload form" do
expect(response.body).to match(/<input id="bulk-upload-lettings-log-bulk-upload-field" class="govuk-file-upload"/) expect(response.body).to match(/<input id="legacy-bulk-upload-lettings-log-bulk-upload-field" class="govuk-file-upload"/)
expect(response.body).to match(/<button type="submit" formnovalidate="formnovalidate" class="govuk-button"/) expect(response.body).to match(/<button type="submit" formnovalidate="formnovalidate" class="govuk-button"/)
end end
end end

2
spec/requests/bulk_upload_lettings_logs_controller_spec.rb

@ -23,7 +23,7 @@ RSpec.describe BulkUploadLettingsLogsController, type: :request do
context "when in crossover period" do context "when in crossover period" do
it "redirects to /year" do it "redirects to /year" do
Timecop.freeze(2023, 6, 1) do Timecop.freeze(2022, 6, 1) do
get "/lettings-logs/bulk-upload-logs/start", params: {} get "/lettings-logs/bulk-upload-logs/start", params: {}
expect(response).to redirect_to("/lettings-logs/bulk-upload-logs/year") expect(response).to redirect_to("/lettings-logs/bulk-upload-logs/year")

Loading…
Cancel
Save