Browse Source

CLDC-3534 Export organisation data (#2599)

* Add organisation export service

* Call organisations export and write manifest

* Add some additional fields to export

* Add period to organisation export filename

* Update provider_type and add new fields

* Filter exports by the collection

* Update tests

* Update fields exported in lettings export (#2652)
pull/2661/head
kosiakkatrina 2 years ago committed by GitHub
parent
commit
40c79d2fa0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 11
      app/services/exports/export_service.rb
  2. 5
      app/services/exports/lettings_log_export_constants.rb
  3. 11
      app/services/exports/lettings_log_export_service.rb
  4. 27
      app/services/exports/organisation_export_constants.rb
  5. 72
      app/services/exports/organisation_export_service.rb
  6. 4
      app/services/exports/user_export_service.rb
  7. 9
      spec/fixtures/exports/general_needs_log.xml
  8. 9
      spec/fixtures/exports/general_needs_log_23_24.xml
  9. 9
      spec/fixtures/exports/general_needs_log_24_25.xml
  10. 26
      spec/fixtures/exports/organisation.xml
  11. 11
      spec/fixtures/exports/supported_housing_logs.xml
  12. 4
      spec/jobs/data_export_xml_job_spec.rb
  13. 115
      spec/services/exports/export_service_spec.rb
  14. 3
      spec/services/exports/lettings_log_export_service_spec.rb
  15. 219
      spec/services/exports/organisation_export_service_spec.rb
  16. 2
      spec/services/exports/user_export_service_spec.rb

11
app/services/exports/export_service.rb

@ -12,20 +12,24 @@ module Exports
daily_run_number = get_daily_run_number daily_run_number = get_daily_run_number
lettings_archives_for_manifest = {} lettings_archives_for_manifest = {}
users_archives_for_manifest = {} users_archives_for_manifest = {}
organisations_archives_for_manifest = {}
if collection.present? if collection.present?
case collection case collection
when "users" when "users"
users_archives_for_manifest = get_user_archives(start_time, full_update) users_archives_for_manifest = get_user_archives(start_time, full_update)
when "organisations"
organisations_archives_for_manifest = get_organisation_archives(start_time, full_update)
else else
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, collection) lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, collection)
end end
else else
users_archives_for_manifest = get_user_archives(start_time, full_update) users_archives_for_manifest = get_user_archives(start_time, full_update)
organisations_archives_for_manifest = get_organisation_archives(start_time, full_update)
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, collection) lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, collection)
end end
write_master_manifest(daily_run_number, lettings_archives_for_manifest.merge(users_archives_for_manifest)) write_master_manifest(daily_run_number, lettings_archives_for_manifest.merge(users_archives_for_manifest).merge(organisations_archives_for_manifest))
end end
private private
@ -61,6 +65,11 @@ module Exports
users_export_service.export_xml_users(full_update:) users_export_service.export_xml_users(full_update:)
end end
def get_organisation_archives(start_time, full_update)
organisations_export_service = Exports::OrganisationExportService.new(@storage_service, start_time)
organisations_export_service.export_xml_organisations(full_update:)
end
def get_lettings_archives(start_time, full_update, collection) def get_lettings_archives(start_time, full_update, collection)
lettings_export_service = Exports::LettingsLogExportService.new(@storage_service, start_time) lettings_export_service = Exports::LettingsLogExportService.new(@storage_service, start_time)
lettings_export_service.export_xml_lettings_logs(full_update:, collection_year: collection) lettings_export_service.export_xml_lettings_logs(full_update:, collection_year: collection)

5
app/services/exports/lettings_log_export_constants.rb

@ -29,7 +29,6 @@ module Exports::LettingsLogExportConstants
"has_benefits", "has_benefits",
"hb", "hb",
"hbrentshortfall", "hbrentshortfall",
"hcnum",
"hhmemb", "hhmemb",
"hhtype", "hhtype",
"homeless", "homeless",
@ -47,9 +46,7 @@ module Exports::LettingsLogExportConstants
"layear", "layear",
"leftreg", "leftreg",
"lettype", "lettype",
"manhcnum",
"maningorgid", "maningorgid",
"maningorgname",
"mantype", "mantype",
"mobstand", "mobstand",
"mrcdate", "mrcdate",
@ -60,7 +57,6 @@ module Exports::LettingsLogExportConstants
"nocharge", "nocharge",
"offered", "offered",
"owningorgid", "owningorgid",
"owningorgname",
"period", "period",
"uprn", "uprn",
"uprn_known", "uprn_known",
@ -76,7 +72,6 @@ module Exports::LettingsLogExportConstants
"prevloc", "prevloc",
"prevten", "prevten",
"propcode", "propcode",
"providertype",
"pscharge", "pscharge",
"reason", "reason",
"reasonother", "reasonother",

11
app/services/exports/lettings_log_export_service.rb

@ -57,13 +57,9 @@ module Exports
# Organisation fields # Organisation fields
if lettings_log.owning_organisation if lettings_log.owning_organisation
attribute_hash["owningorgid"] = lettings_log.owning_organisation.old_visible_id || (lettings_log.owning_organisation.id + LOG_ID_OFFSET) attribute_hash["owningorgid"] = lettings_log.owning_organisation.old_visible_id || (lettings_log.owning_organisation.id + LOG_ID_OFFSET)
attribute_hash["owningorgname"] = lettings_log.owning_organisation.name
attribute_hash["hcnum"] = lettings_log.owning_organisation.housing_registration_no
end end
if lettings_log.managing_organisation if lettings_log.managing_organisation
attribute_hash["maningorgid"] = lettings_log.managing_organisation.old_visible_id || (lettings_log.managing_organisation.id + LOG_ID_OFFSET) attribute_hash["maningorgid"] = lettings_log.managing_organisation.old_visible_id || (lettings_log.managing_organisation.id + LOG_ID_OFFSET)
attribute_hash["maningorgname"] = lettings_log.managing_organisation.name
attribute_hash["manhcnum"] = lettings_log.managing_organisation.housing_registration_no
end end
# Covert date times to ISO 8601 # Covert date times to ISO 8601
@ -85,9 +81,9 @@ module Exports
end end
attribute_hash["log_id"] = lettings_log.id attribute_hash["log_id"] = lettings_log.id
attribute_hash["assigned_to"] = lettings_log.assigned_to&.email attribute_hash["assigned_to"] = lettings_log.assigned_to_id
attribute_hash["created_by"] = lettings_log.created_by&.email attribute_hash["created_by"] = lettings_log.created_by_id
attribute_hash["amended_by"] = lettings_log.updated_by&.email attribute_hash["amended_by"] = lettings_log.updated_by_id
attribute_hash["la"] = lettings_log.la attribute_hash["la"] = lettings_log.la
attribute_hash["postcode_full"] = lettings_log.postcode_full attribute_hash["postcode_full"] = lettings_log.postcode_full
@ -164,7 +160,6 @@ module Exports
form << doc.create_element(key, value) form << doc.create_element(key, value)
end end
end end
form << doc.create_element("providertype", lettings_log.owning_organisation&.read_attribute_before_type_cast(:provider_type))
end end
xml_doc_to_temp_file(doc) xml_doc_to_temp_file(doc)

27
app/services/exports/organisation_export_constants.rb

@ -0,0 +1,27 @@
module Exports::OrganisationExportConstants
MAX_XML_RECORDS = 10_000
EXPORT_FIELDS = Set[
"id",
"name",
"phone",
"provider_type",
"address_line1",
"address_line2",
"postcode",
"holds_own_stock",
"housing_registration_no",
"active",
"old_org_id",
"old_visible_id",
"merge_date",
"absorbing_organisation_id",
"available_from",
"deleted_at",
"dsa_signed",
"dsa_signed_at",
"dpo_email",
"profit_status",
"group"
]
end

72
app/services/exports/organisation_export_service.rb

@ -0,0 +1,72 @@
module Exports
class OrganisationExportService < Exports::XmlExportService
include Exports::OrganisationExportConstants
include CollectionTimeHelper
def export_xml_organisations(full_update: false)
collection = "organisations"
recent_export = Export.where(collection:).order("started_at").last
base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1
export = build_export_run(collection, base_number, full_update)
archives_for_manifest = write_export_archive(export, collection, recent_export, full_update)
export.empty_export = archives_for_manifest.empty?
export.save!
archives_for_manifest
end
private
def get_archive_name(collection, base_number, increment)
return unless collection
base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.to_s.rjust(4, '0')}"
"#{collection}_2024_2025_apr_mar_#{base_number_str}_#{increment_str}".downcase
end
def retrieve_resources(recent_export, full_update, _collection)
if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time }
Organisation.where("(updated_at >= :from AND updated_at <= :to)", params)
else
params = { to: @start_time }
Organisation.where("updated_at <= :to", params)
end
end
def build_export_xml(organisations)
doc = Nokogiri::XML("<forms/>")
organisations.each do |organisation|
attribute_hash = apply_cds_transformation(organisation)
form = doc.create_element("form")
doc.at("forms") << form
attribute_hash.each do |key, value|
if !EXPORT_FIELDS.include?(key)
next
else
form << doc.create_element(key, value)
end
end
end
xml_doc_to_temp_file(doc)
end
def apply_cds_transformation(organisation)
attribute_hash = organisation.attributes
attribute_hash["deleted_at"] = organisation.discarded_at
attribute_hash["dsa_signed"] = organisation.data_protection_confirmed?
attribute_hash["dsa_signed_at"] = organisation.data_protection_confirmation&.signed_at
attribute_hash["dpo_email"] = organisation.data_protection_confirmation&.data_protection_officer_email
attribute_hash["provider_type"] = organisation.provider_type_before_type_cast
attribute_hash["profit_status"] = nil # will need update when we add the field to the org
attribute_hash["group"] = nil # will need update when we add the field to the org
attribute_hash
end
end
end

4
app/services/exports/user_export_service.rb

@ -4,9 +4,9 @@ module Exports
include CollectionTimeHelper include CollectionTimeHelper
def export_xml_users(full_update: false) def export_xml_users(full_update: false)
recent_export = Export.order("started_at").last
collection = "users" collection = "users"
recent_export = Export.where(collection:).order("started_at").last
base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1 base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1
export = build_export_run(collection, base_number, full_update) export = build_export_run(collection, base_number, full_update)
archives_for_manifest = write_export_archive(export, collection, recent_export, full_update) archives_for_manifest = write_export_archive(export, collection, recent_export, full_update)

9
spec/fixtures/exports/general_needs_log.xml vendored

@ -147,18 +147,13 @@
<duplicate_set_id/> <duplicate_set_id/>
<formid>{id}</formid> <formid>{id}</formid>
<owningorgid>{owning_org_id}</owningorgid> <owningorgid>{owning_org_id}</owningorgid>
<owningorgname>MHCLG</owningorgname>
<hcnum>1234</hcnum>
<maningorgid>{managing_org_id}</maningorgid> <maningorgid>{managing_org_id}</maningorgid>
<maningorgname>MHCLG</maningorgname>
<manhcnum>1234</manhcnum>
<createddate>2022-05-01T00:00:00+01:00</createddate> <createddate>2022-05-01T00:00:00+01:00</createddate>
<uploaddate>2022-05-01T00:00:00+01:00</uploaddate> <uploaddate>2022-05-01T00:00:00+01:00</uploaddate>
<log_id>{log_id}</log_id> <log_id>{log_id}</log_id>
<assigned_to>test1@example.com</assigned_to> <assigned_to>{assigned_to}</assigned_to>
<created_by>test1@example.com</created_by> <created_by>{created_by}</created_by>
<amended_by/> <amended_by/>
<renttype_detail>2</renttype_detail> <renttype_detail>2</renttype_detail>
<providertype>1</providertype>
</form> </form>
</forms> </forms>

9
spec/fixtures/exports/general_needs_log_23_24.xml vendored

@ -148,18 +148,13 @@
<duplicate_set_id/> <duplicate_set_id/>
<formid>{id}</formid> <formid>{id}</formid>
<owningorgid>{owning_org_id}</owningorgid> <owningorgid>{owning_org_id}</owningorgid>
<owningorgname>MHCLG</owningorgname>
<hcnum>1234</hcnum>
<maningorgid>{managing_org_id}</maningorgid> <maningorgid>{managing_org_id}</maningorgid>
<maningorgname>MHCLG</maningorgname>
<manhcnum>1234</manhcnum>
<createddate>2023-04-03T00:00:00+01:00</createddate> <createddate>2023-04-03T00:00:00+01:00</createddate>
<uploaddate>2023-04-03T00:00:00+01:00</uploaddate> <uploaddate>2023-04-03T00:00:00+01:00</uploaddate>
<log_id>{log_id}</log_id> <log_id>{log_id}</log_id>
<assigned_to>test1@example.com</assigned_to> <assigned_to>{assigned_to}</assigned_to>
<created_by>test1@example.com</created_by> <created_by>{created_by}</created_by>
<amended_by/> <amended_by/>
<renttype_detail>2</renttype_detail> <renttype_detail>2</renttype_detail>
<providertype>1</providertype>
</form> </form>
</forms> </forms>

9
spec/fixtures/exports/general_needs_log_24_25.xml vendored

@ -161,18 +161,13 @@
<la_as_entered>la as entered</la_as_entered> <la_as_entered>la as entered</la_as_entered>
<formid>{id}</formid> <formid>{id}</formid>
<owningorgid>{owning_org_id}</owningorgid> <owningorgid>{owning_org_id}</owningorgid>
<owningorgname>MHCLG</owningorgname>
<hcnum>1234</hcnum>
<maningorgid>{managing_org_id}</maningorgid> <maningorgid>{managing_org_id}</maningorgid>
<maningorgname>MHCLG</maningorgname>
<manhcnum>1234</manhcnum>
<createddate>2024-04-03T00:00:00+01:00</createddate> <createddate>2024-04-03T00:00:00+01:00</createddate>
<uploaddate>2024-04-03T00:00:00+01:00</uploaddate> <uploaddate>2024-04-03T00:00:00+01:00</uploaddate>
<log_id>{log_id}</log_id> <log_id>{log_id}</log_id>
<assigned_to>test1@example.com</assigned_to> <assigned_to>{assigned_to}</assigned_to>
<created_by>test1@example.com</created_by> <created_by>{created_by}</created_by>
<amended_by/> <amended_by/>
<renttype_detail>2</renttype_detail> <renttype_detail>2</renttype_detail>
<providertype>1</providertype>
</form> </form>
</forms> </forms>

26
spec/fixtures/exports/organisation.xml vendored

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<forms>
<form>
<id>{id}</id>
<name>MHCLG</name>
<phone/>
<provider_type>1</provider_type>
<address_line1>2 Marsham Street</address_line1>
<address_line2>London</address_line2>
<postcode>SW1P 4DF</postcode>
<holds_own_stock>true</holds_own_stock>
<active>true</active>
<housing_registration_no>1234</housing_registration_no>
<old_org_id/>
<old_visible_id/>
<merge_date/>
<absorbing_organisation_id/>
<available_from/>
<deleted_at/>
<dsa_signed>true</dsa_signed>
<dsa_signed_at>{dsa_signed_at}</dsa_signed_at>
<dpo_email>{dpo_email}</dpo_email>
<profit_status/>
<group/>
</form>
</forms>

11
spec/fixtures/exports/supported_housing_logs.xml vendored

@ -146,17 +146,13 @@
<duplicate_set_id/> <duplicate_set_id/>
<formid>{id}</formid> <formid>{id}</formid>
<owningorgid>{owning_org_id}</owningorgid> <owningorgid>{owning_org_id}</owningorgid>
<owningorgname>MHCLG</owningorgname>
<hcnum>1234</hcnum>
<maningorgid>{managing_org_id}</maningorgid> <maningorgid>{managing_org_id}</maningorgid>
<maningorgname>MHCLG</maningorgname>
<manhcnum>1234</manhcnum>
<createddate>2022-05-01T00:00:00+01:00</createddate> <createddate>2022-05-01T00:00:00+01:00</createddate>
<uploaddate>2022-05-01T00:00:00+01:00</uploaddate> <uploaddate>2022-05-01T00:00:00+01:00</uploaddate>
<log_id>{log_id}</log_id> <log_id>{log_id}</log_id>
<assigned_to>fake@email.com</assigned_to> <assigned_to>{assigned_to}</assigned_to>
<created_by>fake@email.com</created_by> <created_by>{created_by}</created_by>
<amended_by>other@email.com</amended_by> <amended_by>{amended_by}</amended_by>
<unittype_sh>7</unittype_sh> <unittype_sh>7</unittype_sh>
<confidential>1</confidential> <confidential>1</confidential>
<cligrp1>G</cligrp1> <cligrp1>G</cligrp1>
@ -175,6 +171,5 @@
<location_code>{location_id}</location_code> <location_code>{location_id}</location_code>
<location_status>active</location_status> <location_status>active</location_status>
<renttype_detail>2</renttype_detail> <renttype_detail>2</renttype_detail>
<providertype>1</providertype>
</form> </form>
</forms> </forms>

4
spec/jobs/data_export_xml_job_spec.rb

@ -5,17 +5,20 @@ describe DataExportXmlJob do
let(:env_config_service) { instance_double(Configuration::EnvConfigurationService) } let(:env_config_service) { instance_double(Configuration::EnvConfigurationService) }
let(:lettings_export_service) { instance_double(Exports::LettingsLogExportService, export_xml_lettings_logs: {}) } let(:lettings_export_service) { instance_double(Exports::LettingsLogExportService, export_xml_lettings_logs: {}) }
let(:user_export_service) { instance_double(Exports::UserExportService, export_xml_users: {}) } let(:user_export_service) { instance_double(Exports::UserExportService, export_xml_users: {}) }
let(:organisation_export_service) { instance_double(Exports::OrganisationExportService, export_xml_organisations: {}) }
before do before do
allow(Storage::S3Service).to receive(:new).and_return(storage_service) allow(Storage::S3Service).to receive(:new).and_return(storage_service)
allow(Configuration::EnvConfigurationService).to receive(:new).and_return(env_config_service) allow(Configuration::EnvConfigurationService).to receive(:new).and_return(env_config_service)
allow(Exports::LettingsLogExportService).to receive(:new).and_return(lettings_export_service) allow(Exports::LettingsLogExportService).to receive(:new).and_return(lettings_export_service)
allow(Exports::UserExportService).to receive(:new).and_return(user_export_service) allow(Exports::UserExportService).to receive(:new).and_return(user_export_service)
allow(Exports::OrganisationExportService).to receive(:new).and_return(organisation_export_service)
end end
it "calls the export services" do it "calls the export services" do
expect(lettings_export_service).to receive(:export_xml_lettings_logs) expect(lettings_export_service).to receive(:export_xml_lettings_logs)
expect(user_export_service).to receive(:export_xml_users) expect(user_export_service).to receive(:export_xml_users)
expect(organisation_export_service).to receive(:export_xml_organisations)
described_class.perform_now described_class.perform_now
end end
@ -24,6 +27,7 @@ describe DataExportXmlJob do
it "calls the export service" do it "calls the export service" do
expect(lettings_export_service).to receive(:export_xml_lettings_logs).with(full_update: true, collection_year: nil) expect(lettings_export_service).to receive(:export_xml_lettings_logs).with(full_update: true, collection_year: nil)
expect(user_export_service).to receive(:export_xml_users).with(full_update: true) expect(user_export_service).to receive(:export_xml_users).with(full_update: true)
expect(organisation_export_service).to receive(:export_xml_organisations).with(full_update: true)
described_class.perform_now(full_update: true) described_class.perform_now(full_update: true)
end end

115
spec/services/exports/export_service_spec.rb

@ -7,6 +7,8 @@ RSpec.describe Exports::ExportService do
let(:expected_master_manifest_filename) { "Manifest_2022_05_01_0001.csv" } let(:expected_master_manifest_filename) { "Manifest_2022_05_01_0001.csv" }
let(:start_time) { Time.zone.local(2022, 5, 1) } let(:start_time) { Time.zone.local(2022, 5, 1) }
let(:user) { FactoryBot.create(:user, email: "test1@example.com") } let(:user) { FactoryBot.create(:user, email: "test1@example.com") }
let(:organisations_export_service) { instance_double("Exports::OrganisationExportService", export_xml_organisations: {}) }
let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: {}) }
before do before do
Timecop.freeze(start_time) Timecop.freeze(start_time)
@ -14,6 +16,7 @@ RSpec.describe Exports::ExportService do
allow(storage_service).to receive(:write_file) allow(storage_service).to receive(:write_file)
allow(Exports::LettingsLogExportService).to receive(:new).and_return(lettings_logs_export_service) allow(Exports::LettingsLogExportService).to receive(:new).and_return(lettings_logs_export_service)
allow(Exports::UserExportService).to receive(:new).and_return(users_export_service) allow(Exports::UserExportService).to receive(:new).and_return(users_export_service)
allow(Exports::OrganisationExportService).to receive(:new).and_return(organisations_export_service)
end end
after do after do
@ -24,9 +27,7 @@ RSpec.describe Exports::ExportService do
context "and no lettings archives get created in lettings logs export" do context "and no lettings archives get created in lettings logs export" do
let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: {}) } let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: {}) }
context "and no user archives get created in user export" do context "and no user or organisation archives get created in user export" do
let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: {}) }
it "generates a master manifest with the correct name" do it "generates a master manifest with the correct name" do
expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args)
export_service.export_xml export_service.export_xml
@ -59,14 +60,49 @@ RSpec.describe Exports::ExportService do
expect(actual_content).to eq(expected_content) expect(actual_content).to eq(expected_content)
end end
end end
context "and one organisation archive gets created in organisation export" do
let(:organisations_export_service) { instance_double("Exports::OrganisationExportService", export_xml_organisations: { "some_organisation_file_base_name" => start_time }) }
it "generates a master manifest with the correct name" do
expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args)
export_service.export_xml
end
it "generates a master manifest with CSV headers and correct data" do
actual_content = nil
expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_organisation_file_base_name,2022-05-01 00:00:00 +0100,some_organisation_file_base_name.zip\n"
allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string }
export_service.export_xml
expect(actual_content).to eq(expected_content)
end
end
context "and user and organisation archive gets created in organisation export" do
let(:organisations_export_service) { instance_double("Exports::OrganisationExportService", export_xml_organisations: { "some_organisation_file_base_name" => start_time }) }
let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: { "some_user_file_base_name" => start_time }) }
it "generates a master manifest with the correct name" do
expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args)
export_service.export_xml
end
it "generates a master manifest with CSV headers and correct data" do
actual_content = nil
expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_user_file_base_name,2022-05-01 00:00:00 +0100,some_user_file_base_name.zip\nsome_organisation_file_base_name,2022-05-01 00:00:00 +0100,some_organisation_file_base_name.zip\n"
allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string }
export_service.export_xml
expect(actual_content).to eq(expected_content)
end
end
end end
context "and one lettings archive gets created in lettings logs export" do context "and one lettings archive gets created in lettings logs export" do
let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: { "some_file_base_name" => start_time }) } let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: { "some_file_base_name" => start_time }) }
context "and no user archives get created in user export" do context "and no user archives get created in user export" do
let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: {}) }
it "generates a master manifest with the correct name" do it "generates a master manifest with the correct name" do
expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args)
export_service.export_xml export_service.export_xml
@ -105,8 +141,6 @@ RSpec.describe Exports::ExportService do
let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: { "some_file_base_name" => start_time, "second_file_base_name" => start_time }) } let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: { "some_file_base_name" => start_time, "second_file_base_name" => start_time }) }
context "and no user archives get created in user export" do context "and no user archives get created in user export" do
let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: {}) }
it "generates a master manifest with the correct name" do it "generates a master manifest with the correct name" do
expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args)
export_service.export_xml export_service.export_xml
@ -139,6 +173,25 @@ RSpec.describe Exports::ExportService do
expect(actual_content).to eq(expected_content) expect(actual_content).to eq(expected_content)
end end
end end
context "and multiple user and organisation archives gets created in user export" do
let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: { "some_user_file_base_name" => start_time, "second_user_file_base_name" => start_time }) }
let(:organisations_export_service) { instance_double("Exports::OrganisationExportService", export_xml_organisations: { "some_organisation_file_base_name" => start_time, "second_organisation_file_base_name" => start_time }) }
it "generates a master manifest with the correct name" do
expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args)
export_service.export_xml
end
it "generates a master manifest with CSV headers and correct data" do
actual_content = nil
expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_file_base_name,2022-05-01 00:00:00 +0100,some_file_base_name.zip\nsecond_file_base_name,2022-05-01 00:00:00 +0100,second_file_base_name.zip\nsome_user_file_base_name,2022-05-01 00:00:00 +0100,some_user_file_base_name.zip\nsecond_user_file_base_name,2022-05-01 00:00:00 +0100,second_user_file_base_name.zip\nsome_organisation_file_base_name,2022-05-01 00:00:00 +0100,some_organisation_file_base_name.zip\nsecond_organisation_file_base_name,2022-05-01 00:00:00 +0100,second_organisation_file_base_name.zip\n"
allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string }
export_service.export_xml
expect(actual_content).to eq(expected_content)
end
end
end end
end end
@ -190,8 +243,6 @@ RSpec.describe Exports::ExportService do
context "when exporting user collection" do context "when exporting user collection" do
context "and no user archives get created in users export" do context "and no user archives get created in users export" do
let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: {}) }
context "and lettings log archive gets created in lettings logs export" do context "and lettings log archive gets created in lettings logs export" do
let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: { "some_file_base_name" => start_time }) } let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: { "some_file_base_name" => start_time }) }
@ -233,4 +284,50 @@ RSpec.describe Exports::ExportService do
end end
end end
end end
context "when exporting organisation collection" do
context "and no organisation archives get created in organisations export" do
let(:organisations_export_service) { instance_double("Exports::OrganisationExportService", export_xml_organisations: {}) }
context "and lettings log archive gets created in lettings logs export" do
let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: { "some_file_base_name" => start_time }) }
it "generates a master manifest with the correct name" do
expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args)
export_service.export_xml(full_update: true, collection: "organisations")
end
it "does not write lettings log data" do
actual_content = nil
expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\n"
allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string }
export_service.export_xml(full_update: true, collection: "organisations")
expect(actual_content).to eq(expected_content)
end
end
end
context "and organisations archive gets created in organisations export" do
let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: { "some_file_base_name" => start_time }) }
context "and lettings log archive gets created in lettings log export" do
let(:organisations_export_service) { instance_double("Exports::OrganisationExportService", export_xml_organisations: { "some_organisation_file_base_name" => start_time }) }
it "generates a master manifest with the correct name" do
expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args)
export_service.export_xml(full_update: true, collection: "organisations")
end
it "does not write lettings log data" do
actual_content = nil
expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_organisation_file_base_name,2022-05-01 00:00:00 +0100,some_organisation_file_base_name.zip\n"
allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string }
export_service.export_xml(full_update: true, collection: "organisations")
expect(actual_content).to eq(expected_content)
end
end
end
end
end end

3
spec/services/exports/lettings_log_export_service_spec.rb

@ -23,6 +23,9 @@ RSpec.describe Exports::LettingsLogExportService do
export_template.sub!(/\{managing_org_id\}/, (lettings_log["managing_organisation_id"] + Exports::LettingsLogExportService::LOG_ID_OFFSET).to_s) export_template.sub!(/\{managing_org_id\}/, (lettings_log["managing_organisation_id"] + Exports::LettingsLogExportService::LOG_ID_OFFSET).to_s)
export_template.sub!(/\{location_id\}/, (lettings_log["location_id"]).to_s) if lettings_log.needstype == 2 export_template.sub!(/\{location_id\}/, (lettings_log["location_id"]).to_s) if lettings_log.needstype == 2
export_template.sub!(/\{scheme_id\}/, (lettings_log["scheme_id"]).to_s) if lettings_log.needstype == 2 export_template.sub!(/\{scheme_id\}/, (lettings_log["scheme_id"]).to_s) if lettings_log.needstype == 2
export_template.sub!(/\{assigned_to\}/, lettings_log["assigned_to_id"].to_s)
export_template.sub!(/\{created_by\}/, lettings_log["created_by_id"].to_s)
export_template.sub!(/\{amended_by\}/, lettings_log["updated_by_id"].to_s)
export_template.sub!(/\{log_id\}/, lettings_log["id"].to_s) export_template.sub!(/\{log_id\}/, lettings_log["id"].to_s)
end end

219
spec/services/exports/organisation_export_service_spec.rb

@ -0,0 +1,219 @@
require "rails_helper"
RSpec.describe Exports::OrganisationExportService do
subject(:export_service) { described_class.new(storage_service, start_time) }
let(:storage_service) { instance_double(Storage::S3Service) }
let(:xml_export_file) { File.open("spec/fixtures/exports/organisation.xml", "r:UTF-8") }
let(:local_manifest_file) { File.open("spec/fixtures/exports/manifest.xml", "r:UTF-8") }
let(:expected_zip_filename) { "organisations_2024_2025_apr_mar_f0001_inc0001.zip" }
let(:expected_data_filename) { "organisations_2024_2025_apr_mar_f0001_inc0001_pt001.xml" }
let(:expected_manifest_filename) { "manifest.xml" }
let(:start_time) { Time.zone.local(2022, 5, 1) }
let(:organisation) { create(:organisation, with_dsa: false) }
def replace_entity_ids(organisation, export_template)
export_template.sub!(/\{id\}/, organisation["id"].to_s)
export_template.sub!(/\{dsa_signed_at\}/, organisation.data_protection_confirmation&.signed_at.to_s)
export_template.sub!(/\{dpo_email\}/, organisation.data_protection_confirmation&.data_protection_officer_email)
end
def replace_record_number(export_template, record_number)
export_template.sub!(/\{recno\}/, record_number.to_s)
end
before do
Timecop.freeze(start_time)
Singleton.__init__(FormHandler)
allow(storage_service).to receive(:write_file)
end
after do
Timecop.return
end
context "when exporting daily organisations in XML" do
context "and no organisations are available for export" do
it "returns an empty archives list" do
expect(export_service.export_xml_organisations).to eq({})
end
end
context "and one organisation is available for export" do
let!(:organisation) { create(:organisation) }
it "generates a ZIP export file with the expected filename" do
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args)
export_service.export_xml_organisations
end
it "generates an XML export file with the expected filename within the ZIP file" do
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content|
entry = Zip::File.open_buffer(content).find_entry(expected_data_filename)
expect(entry).not_to be_nil
expect(entry.name).to eq(expected_data_filename)
end
export_service.export_xml_organisations
end
it "generates an XML manifest file with the expected content within the ZIP file" do
expected_content = replace_record_number(local_manifest_file.read, 1)
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content|
entry = Zip::File.open_buffer(content).find_entry(expected_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
end
export_service.export_xml_organisations
end
it "generates an XML export file with the expected content within the ZIP file" do
expected_content = replace_entity_ids(organisation, xml_export_file.read)
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content|
entry = Zip::File.open_buffer(content).find_entry(expected_data_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
end
export_service.export_xml_organisations
end
it "returns the list with correct archive" do
expect(export_service.export_xml_organisations).to eq({ expected_zip_filename.gsub(".zip", "") => start_time })
end
end
context "and multiple organisations are available for export" do
before do
create(:organisation)
create(:organisation)
end
it "generates an XML manifest file with the expected content within the ZIP file" do
expected_content = replace_record_number(local_manifest_file.read, 2)
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content|
entry = Zip::File.open_buffer(content).find_entry(expected_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
end
export_service.export_xml_organisations
end
it "creates an export record in a database with correct time" do
expect { export_service.export_xml_organisations }
.to change(Export, :count).by(1)
expect(Export.last.started_at).to be_within(2.seconds).of(start_time)
end
context "when this is the first export (full)" do
it "returns a ZIP archive for the master manifest" do
expect(export_service.export_xml_organisations).to eq({ expected_zip_filename.gsub(".zip", "").gsub(".zip", "") => start_time })
end
end
context "and underlying data changes between getting the organisations and writting the manifest" do
def remove_organisations(organisations)
organisations.each(&:destroy)
file = Tempfile.new
doc = Nokogiri::XML("<forms/>")
doc.write_xml_to(file, encoding: "UTF-8")
file.rewind
file
end
def create_fake_maifest
file = Tempfile.new
doc = Nokogiri::XML("<forms/>")
doc.write_xml_to(file, encoding: "UTF-8")
file.rewind
file
end
it "maintains the same record number" do
# rubocop:disable RSpec/SubjectStub
allow(export_service).to receive(:build_export_xml) do |organisations|
remove_organisations(organisations)
end
allow(export_service).to receive(:build_manifest_xml) do
create_fake_maifest
end
expect(export_service).to receive(:build_manifest_xml).with(2)
# rubocop:enable RSpec/SubjectStub
export_service.export_xml_organisations
end
end
context "when this is a second export (partial)" do
before do
start_time = Time.zone.local(2022, 6, 1)
Export.new(started_at: start_time, collection: "organisations").save! # this should be organisation export
end
it "does not add any entry for the master manifest (no organisations)" do
expect(export_service.export_xml_organisations).to eq({})
end
end
end
context "and a previous export has run the same day having organisations" do
before do
create(:organisation)
export_service.export_xml_organisations
end
context "and we trigger another full update" do
it "increments the base number" do
export_service.export_xml_organisations(full_update: true)
expect(Export.last.base_number).to eq(2)
end
it "resets the increment number" do
export_service.export_xml_organisations(full_update: true)
expect(Export.last.increment_number).to eq(1)
end
it "returns a correct archives list for manifest file" do
expect(export_service.export_xml_organisations(full_update: true)).to eq({ "organisations_2024_2025_apr_mar_f0002_inc0001" => start_time })
end
it "generates a ZIP export file with the expected filename" do
expect(storage_service).to receive(:write_file).with("organisations_2024_2025_apr_mar_f0002_inc0001.zip", any_args)
export_service.export_xml_organisations(full_update: true)
end
end
end
context "and a previous export has run having no organisations" do
before { export_service.export_xml_organisations }
it "doesn't increment the manifest number by 1" do
export_service.export_xml_organisations
expect(Export.last.increment_number).to eq(1)
end
end
context "and an organisation has been migrated since the previous partial export" do
before do
create(:organisation, updated_at: Time.zone.local(2022, 4, 27))
create(:organisation, updated_at: Time.zone.local(2022, 4, 27))
Export.create!(started_at: Time.zone.local(2022, 4, 26), base_number: 1, increment_number: 1)
end
it "generates an XML manifest file with the expected content within the ZIP file" do
expected_content = replace_record_number(local_manifest_file.read, 2)
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content|
entry = Zip::File.open_buffer(content).find_entry(expected_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
end
expect(export_service.export_xml_organisations).to eq({ expected_zip_filename.gsub(".zip", "") => start_time })
end
end
end
end

2
spec/services/exports/user_export_service_spec.rb

@ -150,7 +150,7 @@ RSpec.describe Exports::UserExportService do
context "when this is a second export (partial)" do context "when this is a second export (partial)" do
before do before do
start_time = Time.zone.local(2022, 6, 1) start_time = Time.zone.local(2022, 6, 1)
Export.new(started_at: start_time).save! # this should be user export Export.new(started_at: start_time, collection: "users").save! # this should be user export
end end
it "does not add any entry for the master manifest (no users)" do it "does not add any entry for the master manifest (no users)" do

Loading…
Cancel
Save