Browse Source

CLDC-3788 Export sales logs (#2943)

* Add sales export to the export service

* Update sales export

* Remove comments and redundant or statement

* Update sales export fields

* Add mrentprestaircasing, update factory and tests

* Enable sales export on test environments

* Update fees mappings for the export

* Upcase fields and rebase changes

* Update some more fields
CLDC-3939-test-tests-remotely
kosiakkatrina 4 days ago committed by GitHub
parent
commit
0d95025ab6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      app/models/export.rb
  2. 13
      app/services/exports/export_service.rb
  3. 146
      app/services/exports/sales_log_export_constants.rb
  4. 156
      app/services/exports/sales_log_export_service.rb
  5. 4
      app/services/feature_toggle.rb
  6. 99
      spec/factories/sales_log.rb
  7. 154
      spec/fixtures/exports/sales_log.xml
  8. 154
      spec/fixtures/exports/sales_log_2024.xml
  9. 247
      spec/services/exports/export_service_spec.rb
  10. 425
      spec/services/exports/sales_log_export_service_spec.rb

1
app/models/export.rb

@ -1,5 +1,6 @@
class Export < ApplicationRecord
scope :lettings, -> { where(collection: "lettings") }
scope :sales, -> { where(collection: "sales") }
scope :organisations, -> { where(collection: "organisations") }
scope :users, -> { where(collection: "users") }
end

13
app/services/exports/export_service.rb

@ -11,6 +11,7 @@ module Exports
start_time = Time.zone.now
daily_run_number = get_daily_run_number
lettings_archives_for_manifest = {}
sales_archives_for_manifest = {}
users_archives_for_manifest = {}
organisations_archives_for_manifest = {}
@ -20,16 +21,19 @@ module Exports
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
when "lettings"
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, year)
when "sales"
sales_archives_for_manifest = get_sales_archives(start_time, full_update, year)
end
else
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, year)
sales_archives_for_manifest = get_sales_archives(start_time, full_update, year) if FeatureToggle.sales_export_enabled?
end
write_master_manifest(daily_run_number, lettings_archives_for_manifest.merge(users_archives_for_manifest).merge(organisations_archives_for_manifest))
write_master_manifest(daily_run_number, lettings_archives_for_manifest.merge(sales_archives_for_manifest).merge(users_archives_for_manifest).merge(organisations_archives_for_manifest))
end
private
@ -74,5 +78,10 @@ module Exports
lettings_export_service = Exports::LettingsLogExportService.new(@storage_service, start_time)
lettings_export_service.export_xml_lettings_logs(full_update:, collection_year:)
end
def get_sales_archives(start_time, full_update, collection_year)
sales_export_service = Exports::SalesLogExportService.new(@storage_service, start_time)
sales_export_service.export_xml_sales_logs(full_update:, collection_year:)
end
end
end

146
app/services/exports/sales_log_export_constants.rb

@ -0,0 +1,146 @@
module Exports::SalesLogExportConstants
MAX_XML_RECORDS = 10_000
LOG_ID_OFFSET = 300_000_000_000
EXPORT_MODE = {
xml: 1,
csv: 2,
}.freeze
EXPORT_FIELDS = Set["ID",
"STATUS",
"DAY",
"MONTH",
"YEAR",
"DUPLICATESET",
"CREATEDDATE",
"UPLOADDATE",
"OWNINGORGID",
"OWNINGORGNAME",
"MANINGORGID",
"MANINGORGNAME",
"USERNAME",
"USERNAMEID",
"PURCHID",
"TYPE",
"OWNERSHIP",
"COLLECTIONYEAR",
"JOINTMORE",
"JOINT",
"BEDS",
"ETHNIC",
"ETHNICGROUP1",
"LIVEINBUYER1",
"BUILTYPE",
"PROPTYPE",
"NOINT",
"LIVEINBUYER2",
"PRIVACYNOTICE",
"WHEEL",
"HHOLDCOUNT",
"LA",
"INCOME1",
"INC1NK",
"INC1MORT",
"INCOME2",
"INC2NK",
"SAVINGSNK",
"SAVINGS",
"PREVOWN",
"AMENDEDBY",
"AMENDEDBYID",
"MORTGAGE",
"INC2MORT",
"HB",
"FROMBEDS",
"STAIRCASE",
"STAIRBOUGHT",
"STAIROWNED",
"MRENT",
"MRENTPRESTAIRCASING",
"RESALE",
"DEPOSIT",
"CASHDIS",
"DISABLED",
"VALUE",
"EQUITY",
"DISCOUNT",
"GRANT",
"PPCODENK",
"PPOSTC1",
"PPOSTC2",
"PREVLOC",
"PREVLOCNAME",
"PREVIOUSLAKNOWN",
"HHREGRES",
"HHREGRESSTILL",
"PROPLEN",
"HASMSCHARGE",
"MSCHARGE",
"PREVTEN",
"MORTGAGEUSED",
"WCHAIR",
"ARMEDFORCESSPOUSE",
"HODAY",
"HOMONTH",
"HOYEAR",
"FROMPROP",
"SOCPREVTEN",
"MORTLEN1",
"EXTRABOR",
"HHTYPE",
"POSTCODE",
"ISLAINFERRED",
"BULKUPLOADID",
"VALUE_VALUE_CHECK",
"PREVSHARED",
"STAIRCASETOSALE",
"ETHNICGROUP2",
"ETHNIC2",
"BUY2LIVING",
"PREVTEN2",
"UPRN",
"ADDRESS1",
"ADDRESS2",
"TOWNCITY",
"COUNTY",
"LANAME",
"CREATIONMETHOD",
"NATIONALITYALL1",
"NATIONALITYALL2",
"MSCHARGE_VALUE_CHECK",
"ADDRESS1INPUT",
"POSTCODEINPUT",
"ADDRESS_SEARCH_VALUE_CHECK",
"UPRNSELECTED",
"BULKADDRESS1",
"BULKADDRESS2",
"BULKTOWNCITY",
"BULKCOUNTY",
"BULKPOSTCODE",
"BULKLA",
"CREATEDBY",
"CREATEDBYID",
"HASESTATEFEE",
"ESTATEFEE",
"FIRSTSTAIR",
"NUMSTAIR",
"STAIRLASTDAY",
"STAIRLASTMONTH",
"STAIRLASTYEAR",
"STAIRINITIALYEAR",
"STAIRINITIALMONTH",
"STAIRINITIALDAY",
"HASSERVICECHARGES",
"SERVICECHARGES",]
(1..6).each do |index|
EXPORT_FIELDS << "AGE#{index}"
EXPORT_FIELDS << "ECSTAT#{index}"
EXPORT_FIELDS << "SEX#{index}"
end
(2..6).each do |index|
EXPORT_FIELDS << "RELAT#{index}"
end
end

156
app/services/exports/sales_log_export_service.rb

@ -0,0 +1,156 @@
module Exports
class SalesLogExportService < Exports::XmlExportService
include Exports::SalesLogExportConstants
include CollectionTimeHelper
def export_xml_sales_logs(full_update: false, collection_year: nil)
archives_for_manifest = {}
collection_years_to_export(collection_year).each do |year|
recent_export = Export.sales.where(year:).order("started_at").last
base_number = Export.sales.where(empty_export: false, year:).maximum(:base_number) || 1
export = build_export_run("sales", base_number, full_update, year)
archives = write_export_archive(export, year, recent_export, full_update)
archives_for_manifest.merge!(archives)
export.empty_export = archives.empty?
export.save!
end
archives_for_manifest
end
private
def get_archive_name(year, base_number, increment)
return unless year
base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.to_s.rjust(4, '0')}"
"core_sales_#{year}_#{year + 1}_apr_mar_#{base_number_str}_#{increment_str}".downcase
end
def retrieve_resources(recent_export, full_update, year)
if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time }
SalesLog.exportable.where("(updated_at >= :from AND updated_at <= :to) OR (values_updated_at IS NOT NULL AND values_updated_at >= :from AND values_updated_at <= :to)", params).filter_by_year(year)
else
params = { to: @start_time }
SalesLog.exportable.where("updated_at <= :to", params).filter_by_year(year)
end
end
def apply_cds_transformation(sales_log, _export_mode)
attribute_hash = sales_log.attributes_before_type_cast
attribute_hash["day"] = sales_log.saledate&.day
attribute_hash["month"] = sales_log.saledate&.month
attribute_hash["year"] = sales_log.saledate&.year
attribute_hash["createddate"] = sales_log.created_at&.iso8601
attribute_hash["createdby"] = sales_log.created_by&.email
attribute_hash["createdbyid"] = sales_log.created_by_id
attribute_hash["username"] = sales_log.assigned_to&.email
attribute_hash["usernameid"] = sales_log.assigned_to_id
attribute_hash["uploaddate"] = sales_log.updated_at&.iso8601
attribute_hash["amendedby"] = sales_log.updated_by&.email
attribute_hash["amendedbyid"] = sales_log.updated_by_id
attribute_hash["owningorgid"] = sales_log.owning_organisation&.id
attribute_hash["owningorgname"] = sales_log.owning_organisation&.name
attribute_hash["maningorgid"] = sales_log.managing_organisation&.id
attribute_hash["maningorgname"] = sales_log.managing_organisation&.name
attribute_hash["creationmethod"] = sales_log.creation_method_before_type_cast
attribute_hash["bulkuploadid"] = sales_log.bulk_upload_id
attribute_hash["collectionyear"] = sales_log.form.start_date.year
attribute_hash["ownership"] = sales_log.ownershipsch
attribute_hash["joint"] = sales_log.jointpur
attribute_hash["ethnicgroup1"] = sales_log.ethnic_group
attribute_hash["ethnicgroup2"] = sales_log.ethnic_group2
attribute_hash["previouslaknown"] = sales_log.previous_la_known
attribute_hash["hasmscharge"] = sales_log.discounted_ownership_sale? ? sales_log.has_mscharge : nil
attribute_hash["mscharge"] = sales_log.discounted_ownership_sale? ? sales_log.mscharge : nil
attribute_hash["hasservicecharges"] = sales_log.shared_ownership_scheme? ? sales_log.has_mscharge : nil
attribute_hash["servicecharges"] = sales_log.shared_ownership_scheme? ? sales_log.mscharge : nil
attribute_hash["hoday"] = sales_log.hodate&.day
attribute_hash["homonth"] = sales_log.hodate&.month
attribute_hash["hoyear"] = sales_log.hodate&.year
attribute_hash["inc1nk"] = sales_log.income1nk
attribute_hash["inc2nk"] = sales_log.income2nk
attribute_hash["postcode"] = sales_log.postcode_full
attribute_hash["islainferred"] = sales_log.is_la_inferred
attribute_hash["mortlen1"] = sales_log.mortlen
attribute_hash["ethnic2"] = sales_log.ethnicbuy2
attribute_hash["prevten2"] = sales_log.prevtenbuy2
attribute_hash["address1"] = sales_log.address_line1
attribute_hash["address2"] = sales_log.address_line2
attribute_hash["towncity"] = sales_log.town_or_city
attribute_hash["laname"] = LocalAuthority.find_by(code: sales_log.la)&.name
attribute_hash["address1input"] = sales_log.address_line1_input
attribute_hash["postcodeinput"] = sales_log.postcode_full_input
attribute_hash["uprnselected"] = sales_log.uprn_selection
attribute_hash["bulkaddress1"] = sales_log.address_line1_as_entered
attribute_hash["bulkaddress2"] = sales_log.address_line2_as_entered
attribute_hash["bulktowncity"] = sales_log.town_or_city_as_entered
attribute_hash["bulkcounty"] = sales_log.county_as_entered
attribute_hash["bulkpostcode"] = sales_log.postcode_full_as_entered
attribute_hash["bulkla"] = sales_log.la_as_entered
attribute_hash["nationalityall1"] = sales_log.nationality_all
attribute_hash["nationalityall2"] = sales_log.nationality_all_buyer2
attribute_hash["prevlocname"] = LocalAuthority.find_by(code: sales_log.prevloc)&.name
attribute_hash["liveinbuyer1"] = sales_log.buy1livein
attribute_hash["liveinbuyer2"] = sales_log.buy2livein
attribute_hash["hasestatefee"] = sales_log.has_management_fee
attribute_hash["estatefee"] = sales_log.management_fee
attribute_hash["stairlastday"] = sales_log.lasttransaction&.day
attribute_hash["stairlastmonth"] = sales_log.lasttransaction&.month
attribute_hash["stairlastyear"] = sales_log.lasttransaction&.year
attribute_hash["stairinitialday"] = sales_log.initialpurchase&.day
attribute_hash["stairinitialmonth"] = sales_log.initialpurchase&.month
attribute_hash["stairinitialyear"] = sales_log.initialpurchase&.year
attribute_hash["mscharge_value_check"] = sales_log.monthly_charges_value_check
attribute_hash["duplicateset"] = sales_log.duplicate_set_id
attribute_hash["staircasetosale"] = sales_log.staircasesale
attribute_hash.transform_keys!(&:upcase)
attribute_hash
end
def is_omitted_field?(field_name, _sales_log)
!EXPORT_FIELDS.include?(field_name)
end
def build_export_xml(sales_logs)
doc = Nokogiri::XML("<forms/>")
sales_logs.each do |sales_log|
attribute_hash = apply_cds_transformation(sales_log, EXPORT_MODE[:xml])
form = doc.create_element("form")
doc.at("forms") << form
attribute_hash.each do |key, value|
if is_omitted_field?(key, sales_log)
next
else
form << doc.create_element(key, value)
end
end
end
xml_doc_to_temp_file(doc)
end
def collection_years_to_export(collection_year)
return [collection_year] if collection_year.present?
FormHandler.instance.sales_forms.values.map { |f| f.start_date.year }.uniq.select { |year| year > 2024 }
end
end
end

4
app/services/feature_toggle.rb

@ -30,4 +30,8 @@ class FeatureToggle
def self.create_test_logs_enabled?
Rails.env.development? || Rails.env.review?
end
def self.sales_export_enabled?
Time.zone.now >= Time.zone.local(2025, 4, 1) || (Rails.env.review? || Rails.env.staging?)
end
end

99
spec/factories/sales_log.rb

@ -235,5 +235,104 @@ FactoryBot.define do
instance.save!(validate: false)
end
end
trait :export do
purchid { "123" }
ownershipsch { 2 }
type { 8 }
saledate_today
jointpur { 1 }
beds { 2 }
jointmore { 1 }
noint { 2 }
privacynotice { 1 }
age1_known { 0 }
age1 { 27 }
sex1 { "F" }
national { 18 }
buy1livein { 1 }
relat2 { "P" }
proptype { 1 }
age2_known { 0 }
age2 { 33 }
builtype { 1 }
ethnic { 3 }
ethnic_group { 17 }
sex2 { "X" }
buy2livein { "1" }
ecstat1 { "1" }
ecstat2 { "1" }
hholdcount { "4" }
wheel { 1 }
details_known_3 { 1 }
age3_known { 0 }
age3 { 14 }
details_known_4 { 1 }
age4_known { 0 }
age4 { 18 }
details_known_5 { 1 }
age5_known { 0 }
age5 { 40 }
details_known_6 { 1 }
age6_known { 0 }
age6 { 40 }
income1nk { 0 }
income1 { 10_000 }
inc1mort { 1 }
income2nk { 0 }
income2 { 10_000 }
inc2mort { 1 }
uprn_known { 0 }
address_line1 { "Address line 1" }
town_or_city { "City" }
la_known { 1 }
la { "E09000003" }
savingsnk { 1 }
prevown { 1 }
prevshared { 2 }
sex3 { "F" }
sex4 { "X" }
sex5 { "M" }
sex6 { "X" }
mortgage { 20_000 }
ecstat3 { 9 }
ecstat4 { 3 }
ecstat5 { 2 }
ecstat6 { 1 }
disabled { 1 }
deposit { 80_000 }
value { 110_000 }
value_value_check { 0 }
grant { 10_000 }
hhregres { 7 }
ppcodenk { 1 }
prevten { 1 }
previous_la_known { 0 }
relat3 { "X" }
relat4 { "X" }
relat5 { "R" }
relat6 { "R" }
hb { 4 }
mortgageused { 1 }
wchair { 1 }
armedforcesspouse { 5 }
has_mscharge { 1 }
mscharge { 100 }
mortlen { 10 }
pcodenk { 0 }
postcode_full { "SW1A 1AA" }
is_la_inferred { false }
mortgagelender { 5 }
extrabor { 1 }
ethnic_group2 { 17 }
nationalbuy2 { 13 }
buy2living { 3 }
proplen_asked { 1 }
address_line1_input { "Address line 1" }
postcode_full_input { "SW1A 1AA" }
nationality_all_group { 826 }
nationality_all_buyer2_group { 826 }
uprn { "10033558653" }
uprn_selection { 1 }
end
end
end

154
spec/fixtures/exports/sales_log.xml vendored

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<forms>
<form>
<ID>{id}</ID>
<STATUS>1</STATUS>
<PURCHID>123</PURCHID>
<TYPE>8</TYPE>
<JOINTMORE>1</JOINTMORE>
<BEDS>2</BEDS>
<AGE1>27</AGE1>
<SEX1>F</SEX1>
<ETHNIC>17</ETHNIC>
<BUILTYPE>1</BUILTYPE>
<PROPTYPE>1</PROPTYPE>
<AGE2>33</AGE2>
<RELAT2>P</RELAT2>
<SEX2>X</SEX2>
<NOINT>2</NOINT>
<ECSTAT2>1</ECSTAT2>
<PRIVACYNOTICE>1</PRIVACYNOTICE>
<ECSTAT1>1</ECSTAT1>
<WHEEL>1</WHEEL>
<HHOLDCOUNT>4</HHOLDCOUNT>
<AGE3>14</AGE3>
<LA>E09000033</LA>
<INCOME1>10000</INCOME1>
<AGE4>18</AGE4>
<AGE5>40</AGE5>
<AGE6>40</AGE6>
<INC1MORT>1</INC1MORT>
<INCOME2>10000</INCOME2>
<SAVINGSNK>1</SAVINGSNK>
<SAVINGS/>
<PREVOWN>1</PREVOWN>
<SEX3>F</SEX3>
<MORTGAGE>20000.0</MORTGAGE>
<INC2MORT>1</INC2MORT>
<ECSTAT3>9</ECSTAT3>
<ECSTAT4>3</ECSTAT4>
<ECSTAT5>2</ECSTAT5>
<ECSTAT6>1</ECSTAT6>
<RELAT3>X</RELAT3>
<RELAT4>X</RELAT4>
<RELAT5>R</RELAT5>
<RELAT6>R</RELAT6>
<HB>4</HB>
<SEX4>X</SEX4>
<SEX5>M</SEX5>
<SEX6>X</SEX6>
<FROMBEDS/>
<STAIRCASE/>
<STAIRBOUGHT/>
<STAIROWNED/>
<MRENT/>
<RESALE/>
<DEPOSIT>80000.0</DEPOSIT>
<CASHDIS/>
<DISABLED>1</DISABLED>
<VALUE>110000.0</VALUE>
<EQUITY/>
<DISCOUNT/>
<GRANT>10000.0</GRANT>
<PPCODENK>0</PPCODENK>
<PPOSTC1>SW1A</PPOSTC1>
<PPOSTC2>1AA</PPOSTC2>
<PREVLOC>E09000033</PREVLOC>
<HHREGRES>7</HHREGRES>
<HHREGRESSTILL/>
<PROPLEN/>
<MSCHARGE>100.0</MSCHARGE>
<PREVTEN>1</PREVTEN>
<MORTGAGEUSED>1</MORTGAGEUSED>
<WCHAIR>1</WCHAIR>
<ARMEDFORCESSPOUSE>5</ARMEDFORCESSPOUSE>
<HODAY/>
<HOMONTH/>
<HOYEAR/>
<FROMPROP/>
<SOCPREVTEN/>
<EXTRABOR>1</EXTRABOR>
<HHTYPE>6</HHTYPE>
<VALUE_VALUE_CHECK/>
<PREVSHARED>2</PREVSHARED>
<BUY2LIVING>3</BUY2LIVING>
<UPRN/>
<COUNTY/>
<ADDRESS_SEARCH_VALUE_CHECK/>
<FIRSTSTAIR/>
<NUMSTAIR/>
<MRENTPRESTAIRCASING/>
<DAY>1</DAY>
<MONTH>3</MONTH>
<YEAR>2026</YEAR>
<CREATEDDATE>2026-03-01T00:00:00+00:00</CREATEDDATE>
<CREATEDBY>{created_by_email}</CREATEDBY>
<CREATEDBYID>{created_by_id}</CREATEDBYID>
<USERNAME>{assigned_to_email}</USERNAME>
<USERNAMEID>{assigned_to_id}</USERNAMEID>
<UPLOADDATE>2026-03-01T00:00:00+00:00</UPLOADDATE>
<AMENDEDBY/>
<AMENDEDBYID/>
<OWNINGORGID>{owning_org_id}</OWNINGORGID>
<OWNINGORGNAME>{owning_org_name}</OWNINGORGNAME>
<MANINGORGID>{managing_org_id}</MANINGORGID>
<MANINGORGNAME>{managing_org_name}</MANINGORGNAME>
<CREATIONMETHOD>1</CREATIONMETHOD>
<BULKUPLOADID/>
<COLLECTIONYEAR>2025</COLLECTIONYEAR>
<OWNERSHIP>2</OWNERSHIP>
<JOINT>1</JOINT>
<ETHNICGROUP1>17</ETHNICGROUP1>
<ETHNICGROUP2>17</ETHNICGROUP2>
<PREVIOUSLAKNOWN>1</PREVIOUSLAKNOWN>
<HASMSCHARGE>1</HASMSCHARGE>
<HASSERVICECHARGES/>
<SERVICECHARGES/>
<INC1NK>0</INC1NK>
<INC2NK>0</INC2NK>
<POSTCODE>SW1A 1AA</POSTCODE>
<ISLAINFERRED>true</ISLAINFERRED>
<MORTLEN1>10</MORTLEN1>
<ETHNIC2/>
<PREVTEN2/>
<ADDRESS1>Address line 1</ADDRESS1>
<ADDRESS2/>
<TOWNCITY>City</TOWNCITY>
<LANAME>Westminster</LANAME>
<ADDRESS1INPUT>Address line 1</ADDRESS1INPUT>
<POSTCODEINPUT>SW1A 1AA</POSTCODEINPUT>
<UPRNSELECTED/>
<BULKADDRESS1/>
<BULKADDRESS2/>
<BULKTOWNCITY/>
<BULKCOUNTY/>
<BULKPOSTCODE/>
<BULKLA/>
<NATIONALITYALL1>826</NATIONALITYALL1>
<NATIONALITYALL2>826</NATIONALITYALL2>
<PREVLOCNAME>Westminster</PREVLOCNAME>
<LIVEINBUYER1>1</LIVEINBUYER1>
<LIVEINBUYER2>1</LIVEINBUYER2>
<HASESTATEFEE/>
<ESTATEFEE/>
<STAIRLASTDAY/>
<STAIRLASTMONTH/>
<STAIRLASTYEAR/>
<STAIRINITIALDAY/>
<STAIRINITIALMONTH/>
<STAIRINITIALYEAR/>
<MSCHARGE_VALUE_CHECK/>
<DUPLICATESET/>
<STAIRCASETOSALE/>
</form>
</forms>

154
spec/fixtures/exports/sales_log_2024.xml vendored

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<forms>
<form>
<ID>{id}</ID>
<STATUS>2</STATUS>
<PURCHID>123</PURCHID>
<TYPE>8</TYPE>
<JOINTMORE>1</JOINTMORE>
<BEDS>2</BEDS>
<AGE1>27</AGE1>
<SEX1>F</SEX1>
<ETHNIC>17</ETHNIC>
<BUILTYPE>1</BUILTYPE>
<PROPTYPE>1</PROPTYPE>
<AGE2>33</AGE2>
<RELAT2>P</RELAT2>
<SEX2>X</SEX2>
<NOINT>2</NOINT>
<ECSTAT2>1</ECSTAT2>
<PRIVACYNOTICE>1</PRIVACYNOTICE>
<ECSTAT1>1</ECSTAT1>
<WHEEL>1</WHEEL>
<HHOLDCOUNT>4</HHOLDCOUNT>
<AGE3>14</AGE3>
<LA>E09000033</LA>
<INCOME1>10000</INCOME1>
<AGE4>18</AGE4>
<AGE5>40</AGE5>
<AGE6>40</AGE6>
<INC1MORT>1</INC1MORT>
<INCOME2>10000</INCOME2>
<SAVINGSNK>1</SAVINGSNK>
<SAVINGS/>
<PREVOWN>1</PREVOWN>
<SEX3>F</SEX3>
<MORTGAGE>20000.0</MORTGAGE>
<INC2MORT>1</INC2MORT>
<ECSTAT3>9</ECSTAT3>
<ECSTAT4>3</ECSTAT4>
<ECSTAT5>2</ECSTAT5>
<ECSTAT6>1</ECSTAT6>
<RELAT3>X</RELAT3>
<RELAT4>X</RELAT4>
<RELAT5>R</RELAT5>
<RELAT6>R</RELAT6>
<HB>4</HB>
<SEX4>X</SEX4>
<SEX5>M</SEX5>
<SEX6>X</SEX6>
<FROMBEDS/>
<STAIRCASE/>
<STAIRBOUGHT/>
<STAIROWNED/>
<MRENT/>
<RESALE/>
<DEPOSIT>80000.0</DEPOSIT>
<CASHDIS/>
<DISABLED>1</DISABLED>
<VALUE>110000.0</VALUE>
<EQUITY/>
<DISCOUNT/>
<GRANT>10000.0</GRANT>
<PPCODENK>0</PPCODENK>
<PPOSTC1>SW1A</PPOSTC1>
<PPOSTC2>1AA</PPOSTC2>
<PREVLOC>E09000033</PREVLOC>
<HHREGRES>7</HHREGRES>
<HHREGRESSTILL/>
<PROPLEN/>
<MSCHARGE>100.0</MSCHARGE>
<PREVTEN>1</PREVTEN>
<MORTGAGEUSED>1</MORTGAGEUSED>
<WCHAIR>1</WCHAIR>
<ARMEDFORCESSPOUSE>5</ARMEDFORCESSPOUSE>
<HODAY/>
<HOMONTH/>
<HOYEAR/>
<FROMPROP/>
<SOCPREVTEN/>
<EXTRABOR>1</EXTRABOR>
<HHTYPE>6</HHTYPE>
<VALUE_VALUE_CHECK/>
<PREVSHARED>2</PREVSHARED>
<BUY2LIVING>3</BUY2LIVING>
<UPRN/>
<COUNTY/>
<ADDRESS_SEARCH_VALUE_CHECK/>
<FIRSTSTAIR/>
<NUMSTAIR/>
<MRENTPRESTAIRCASING/>
<DAY>3</DAY>
<MONTH>4</MONTH>
<YEAR>2024</YEAR>
<CREATEDDATE>2024-04-03T00:00:00+01:00</CREATEDDATE>
<CREATEDBY>{created_by_email}</CREATEDBY>
<CREATEDBYID>{created_by_id}</CREATEDBYID>
<USERNAME>{assigned_to_email}</USERNAME>
<USERNAMEID>{assigned_to_id}</USERNAMEID>
<UPLOADDATE>2024-04-03T00:00:00+01:00</UPLOADDATE>
<AMENDEDBY/>
<AMENDEDBYID/>
<OWNINGORGID>{owning_org_id}</OWNINGORGID>
<OWNINGORGNAME>{owning_org_name}</OWNINGORGNAME>
<MANINGORGID>{managing_org_id}</MANINGORGID>
<MANINGORGNAME>{managing_org_name}</MANINGORGNAME>
<CREATIONMETHOD>1</CREATIONMETHOD>
<BULKUPLOADID/>
<COLLECTIONYEAR>2024</COLLECTIONYEAR>
<OWNERSHIP>2</OWNERSHIP>
<JOINT>1</JOINT>
<ETHNICGROUP1>17</ETHNICGROUP1>
<ETHNICGROUP2>17</ETHNICGROUP2>
<PREVIOUSLAKNOWN>1</PREVIOUSLAKNOWN>
<HASMSCHARGE>1</HASMSCHARGE>
<HASSERVICECHARGES/>
<SERVICECHARGES/>
<INC1NK>0</INC1NK>
<INC2NK>0</INC2NK>
<POSTCODE>SW1A 1AA</POSTCODE>
<ISLAINFERRED>true</ISLAINFERRED>
<MORTLEN1>10</MORTLEN1>
<ETHNIC2/>
<PREVTEN2/>
<ADDRESS1>Address line 1</ADDRESS1>
<ADDRESS2/>
<TOWNCITY>City</TOWNCITY>
<LANAME>Westminster</LANAME>
<ADDRESS1INPUT>Address line 1</ADDRESS1INPUT>
<POSTCODEINPUT>SW1A 1AA</POSTCODEINPUT>
<UPRNSELECTED/>
<BULKADDRESS1/>
<BULKADDRESS2/>
<BULKTOWNCITY/>
<BULKCOUNTY/>
<BULKPOSTCODE/>
<BULKLA/>
<NATIONALITYALL1>826</NATIONALITYALL1>
<NATIONALITYALL2>826</NATIONALITYALL2>
<PREVLOCNAME>Westminster</PREVLOCNAME>
<LIVEINBUYER1>1</LIVEINBUYER1>
<LIVEINBUYER2>1</LIVEINBUYER2>
<HASESTATEFEE/>
<ESTATEFEE/>
<STAIRLASTDAY/>
<STAIRLASTMONTH/>
<STAIRLASTYEAR/>
<STAIRINITIALDAY/>
<STAIRINITIALMONTH/>
<STAIRINITIALYEAR/>
<MSCHARGE_VALUE_CHECK/>
<DUPLICATESET/>
<STAIRCASETOSALE/>
</form>
</forms>

247
spec/services/exports/export_service_spec.rb

@ -9,12 +9,14 @@ RSpec.describe Exports::ExportService do
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: {}) }
let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: {}) }
before do
Timecop.freeze(start_time)
Singleton.__init__(FormHandler)
allow(storage_service).to receive(:write_file)
allow(Exports::LettingsLogExportService).to receive(:new).and_return(lettings_logs_export_service)
allow(Exports::SalesLogExportService).to receive(:new).and_return(sales_logs_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
@ -23,7 +25,9 @@ RSpec.describe Exports::ExportService do
Timecop.return
end
context "when exporting daily XMLs" do
context "when exporting daily XMLs before 2025" do
let(:start_time) { Time.zone.local(2022, 5, 1) }
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: {}) }
@ -193,6 +197,64 @@ RSpec.describe Exports::ExportService do
end
end
end
context "and multiple sales archives get created in sales logs export" 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(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: { "some_sales_file_base_name" => start_time, "second_sales_file_base_name" => start_time }) }
context "and no user archives get created in user export" 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)
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\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 multiple user archive 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 }) }
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\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 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
context "when exporting specific lettings log collection" do
@ -204,7 +266,7 @@ RSpec.describe Exports::ExportService 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)
export_service.export_xml(full_update: true, collection: "2022")
export_service.export_xml(full_update: true, collection: "lettings", year: "2022")
end
it "does not write user data" do
@ -212,7 +274,7 @@ RSpec.describe Exports::ExportService do
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: "2022")
export_service.export_xml(full_update: true, collection: "lettings", year: "2022")
expect(actual_content).to eq(expected_content)
end
end
@ -226,7 +288,7 @@ RSpec.describe Exports::ExportService 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)
export_service.export_xml(full_update: true, collection: "2023")
export_service.export_xml(full_update: true, collection: "lettings", year: "2023")
end
it "does not write user data" do
@ -234,7 +296,7 @@ RSpec.describe Exports::ExportService do
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\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: "2023")
export_service.export_xml(full_update: true, collection: "lettings", year: "2023")
expect(actual_content).to eq(expected_content)
end
end
@ -330,4 +392,179 @@ RSpec.describe Exports::ExportService do
end
end
end
context "with date after 2025-04-01" do
let(:start_time) { Time.zone.local(2025, 5, 1) }
let(:expected_master_manifest_filename) { "Manifest_2025_05_01_0001.csv" }
context "when exporting daily XMLs" do
context "and no sales archives get created in sales logs export" do
let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: {}) }
let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: {}) }
context "and no user or organisation archives get created in user export" 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)
export_service.export_xml
end
it "generates a master manifest with CSV headers but no 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
expect(actual_content).to eq(expected_content)
end
end
end
context "and one sales archive gets created in sales logs export" do
let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: { "some_sales_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
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,2025-05-01 00:00:00 +0100,some_file_base_name.zip\nsome_sales_file_base_name,2025-05-01 00:00:00 +0100,some_sales_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 one user archive gets created in user export" do
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_file_base_name,2025-05-01 00:00:00 +0100,some_file_base_name.zip\nsome_sales_file_base_name,2025-05-01 00:00:00 +0100,some_sales_file_base_name.zip\nsome_user_file_base_name,2025-05-01 00:00:00 +0100,some_user_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
context "and multiple sales archives get created in sales logs export" do
let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: { "some_sales_file_base_name" => start_time, "second_sales_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
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,2025-05-01 00:00:00 +0100,some_file_base_name.zip\nsecond_file_base_name,2025-05-01 00:00:00 +0100,second_file_base_name.zip\nsome_sales_file_base_name,2025-05-01 00:00:00 +0100,some_sales_file_base_name.zip\nsecond_sales_file_base_name,2025-05-01 00:00:00 +0100,second_sales_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 multiple user archive 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 }) }
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,2025-05-01 00:00:00 +0100,some_file_base_name.zip\nsecond_file_base_name,2025-05-01 00:00:00 +0100,second_file_base_name.zip\nsome_sales_file_base_name,2025-05-01 00:00:00 +0100,some_sales_file_base_name.zip\nsecond_sales_file_base_name,2025-05-01 00:00:00 +0100,second_sales_file_base_name.zip\nsome_user_file_base_name,2025-05-01 00:00:00 +0100,some_user_file_base_name.zip\nsecond_user_file_base_name,2025-05-01 00:00:00 +0100,second_user_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 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,2025-05-01 00:00:00 +0100,some_file_base_name.zip\nsecond_file_base_name,2025-05-01 00:00:00 +0100,second_file_base_name.zip\nsome_sales_file_base_name,2025-05-01 00:00:00 +0100,some_sales_file_base_name.zip\nsecond_sales_file_base_name,2025-05-01 00:00:00 +0100,second_sales_file_base_name.zip\nsome_user_file_base_name,2025-05-01 00:00:00 +0100,some_user_file_base_name.zip\nsecond_user_file_base_name,2025-05-01 00:00:00 +0100,second_user_file_base_name.zip\nsome_organisation_file_base_name,2025-05-01 00:00:00 +0100,some_organisation_file_base_name.zip\nsecond_organisation_file_base_name,2025-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
context "when exporting specific sales log collection" do
let(:start_time) { Time.zone.local(2025, 5, 1) }
let(:expected_master_manifest_filename) { "Manifest_2025_05_01_0001.csv" }
let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: {}) }
context "and no sales archives get created in sales logs export" do
let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: {}) }
context "and user archive gets created in user export" do
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(full_update: true, collection: "sales", year: "2022")
end
it "does not write user 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: "sales", year: "2022")
expect(actual_content).to eq(expected_content)
end
end
end
context "and sales archive gets created in sales logs export" do
let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: { "some_sales_file_base_name" => start_time }) }
context "and user archive gets created in user export" do
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(full_update: true, collection: "sales", year: "2023")
end
it "does not write user data" do
actual_content = nil
expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_sales_file_base_name,2025-05-01 00:00:00 +0100,some_sales_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: "sales", year: "2023")
expect(actual_content).to eq(expected_content)
end
end
end
end
end

425
spec/services/exports/sales_log_export_service_spec.rb

@ -0,0 +1,425 @@
require "rails_helper"
RSpec.describe Exports::SalesLogExportService 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/sales_log.xml", "r:UTF-8") }
let(:local_manifest_file) { File.open("spec/fixtures/exports/manifest.xml", "r:UTF-8") }
let(:expected_zip_filename) { "core_sales_2025_2026_apr_mar_f0001_inc0001.zip" }
let(:expected_data_filename) { "core_sales_2025_2026_apr_mar_f0001_inc0001_pt001.xml" }
let(:expected_manifest_filename) { "manifest.xml" }
let(:start_time) { Time.zone.local(2026, 3, 1) }
let(:organisation) { create(:organisation, name: "MHCLG", housing_registration_no: 1234) }
let(:user) { FactoryBot.create(:user, email: "test1@example.com", organisation:) }
def replace_entity_ids(sales_log, export_template)
export_template.sub!(/\{owning_org_id\}/, sales_log["owning_organisation_id"].to_s)
export_template.sub!(/\{owning_org_name\}/, sales_log.owning_organisation.name.to_s)
export_template.sub!(/\{managing_org_id\}/, sales_log["managing_organisation_id"].to_s)
export_template.sub!(/\{managing_org_name\}/, sales_log.managing_organisation.name.to_s)
export_template.sub!(/\{assigned_to_id\}/, sales_log["assigned_to_id"].to_s)
export_template.sub!(/\{assigned_to_email\}/, sales_log.assigned_to&.email.to_s)
export_template.sub!(/\{created_by_id\}/, sales_log["created_by_id"].to_s)
export_template.sub!(/\{created_by_email\}/, sales_log.created_by&.email.to_s)
export_template.sub!(/\{id\}/, sales_log["id"].to_s)
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 sales logs in XML" do
context "and no sales logs are available for export" do
it "returns an empty archives list" do
expect(storage_service).not_to receive(:write_file)
expect(export_service.export_xml_sales_logs).to eq({})
end
end
context "when one pending sales log exists" do
before do
FactoryBot.create(
:sales_log,
:export,
status: "pending",
skip_update_status: true,
)
end
it "returns empty archives list for archives manifest" do
expect(storage_service).not_to receive(:write_file)
expect(export_service.export_xml_sales_logs).to eq({})
end
end
context "and one sales log is available for export" do
let!(:sales_log) { FactoryBot.create(:sales_log, :export, assigned_to: user) }
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_sales_logs
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_sales_logs
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_sales_logs
end
it "generates an XML export file with the expected content within the ZIP file" do
expected_content = replace_entity_ids(sales_log, 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_sales_logs
end
it "returns the list with correct archive" do
expect(export_service.export_xml_sales_logs).to eq({ expected_zip_filename.gsub(".zip", "") => start_time })
end
end
context "and multiple sales logs are available for export on different periods" do
let(:previous_zip_filename) { "core_sales_2024_2025_apr_mar_f0001_inc0001.zip" }
let(:next_zip_filename) { "core_sales_2026_2027_apr_mar_f0001_inc0001.zip" }
before do
FactoryBot.create(:sales_log, :ignore_validation_errors, saledate: Time.zone.local(2024, 5, 1))
FactoryBot.create(:sales_log, saledate: Time.zone.local(2025, 5, 1))
FactoryBot.create(:sales_log, :ignore_validation_errors, saledate: Time.zone.local(2026, 4, 1))
end
context "when sales logs are across multiple years" do
it "generates multiple ZIP export files with the expected filenames" do
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args)
expect(storage_service).not_to receive(:write_file).with(previous_zip_filename, any_args)
expect(storage_service).to receive(:write_file).with(next_zip_filename, any_args)
expect(Rails.logger).to receive(:info).with("Building export run for sales 2025")
expect(Rails.logger).to receive(:info).with("Creating core_sales_2025_2026_apr_mar_f0001_inc0001 - 1 resources")
expect(Rails.logger).to receive(:info).with("Added core_sales_2025_2026_apr_mar_f0001_inc0001_pt001.xml")
expect(Rails.logger).to receive(:info).with("Writing core_sales_2025_2026_apr_mar_f0001_inc0001.zip")
expect(Rails.logger).to receive(:info).with("Building export run for sales 2026")
expect(Rails.logger).to receive(:info).with("Creating core_sales_2026_2027_apr_mar_f0001_inc0001 - 1 resources")
expect(Rails.logger).to receive(:info).with("Added core_sales_2026_2027_apr_mar_f0001_inc0001_pt001.xml")
expect(Rails.logger).to receive(:info).with("Writing core_sales_2026_2027_apr_mar_f0001_inc0001.zip")
export_service.export_xml_sales_logs
end
it "generates zip export files only for specified year" do
expect(storage_service).to receive(:write_file).with(next_zip_filename, any_args)
expect(Rails.logger).to receive(:info).with("Building export run for sales 2026")
expect(Rails.logger).to receive(:info).with("Creating core_sales_2026_2027_apr_mar_f0001_inc0001 - 1 resources")
expect(Rails.logger).to receive(:info).with("Added core_sales_2026_2027_apr_mar_f0001_inc0001_pt001.xml")
expect(Rails.logger).to receive(:info).with("Writing core_sales_2026_2027_apr_mar_f0001_inc0001.zip")
export_service.export_xml_sales_logs(collection_year: 2026)
end
context "and previous full exports are different for previous years" do
let(:expected_zip_filename) { "core_sales_2025_2026_apr_mar_f0007_inc0004.zip" }
let(:next_zip_filename) { "core_sales_2026_2027_apr_mar_f0001_inc0001.zip" }
before do
Export.new(started_at: Time.zone.yesterday, base_number: 7, increment_number: 3, collection: "sales", year: 2025).save!
end
it "generates multiple ZIP export files with different base numbers in the filenames" do
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args)
expect(storage_service).to receive(:write_file).with(next_zip_filename, any_args)
expect(Rails.logger).to receive(:info).with("Building export run for sales 2025")
expect(Rails.logger).to receive(:info).with("Creating core_sales_2025_2026_apr_mar_f0007_inc0004 - 1 resources")
expect(Rails.logger).to receive(:info).with("Added core_sales_2025_2026_apr_mar_f0007_inc0004_pt001.xml")
expect(Rails.logger).to receive(:info).with("Writing core_sales_2025_2026_apr_mar_f0007_inc0004.zip")
expect(Rails.logger).to receive(:info).with("Building export run for sales 2026")
expect(Rails.logger).to receive(:info).with("Creating core_sales_2026_2027_apr_mar_f0001_inc0001 - 1 resources")
expect(Rails.logger).to receive(:info).with("Added core_sales_2026_2027_apr_mar_f0001_inc0001_pt001.xml")
expect(Rails.logger).to receive(:info).with("Writing core_sales_2026_2027_apr_mar_f0001_inc0001.zip")
export_service.export_xml_sales_logs
end
end
end
end
context "and multiple sales logs are available for export on same quarter" do
before do
FactoryBot.create(:sales_log, saledate: Time.zone.local(2025, 4, 1))
FactoryBot.create(:sales_log, saledate: Time.zone.local(2025, 4, 20))
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_sales_logs
end
it "creates a logs export record in a database with correct time" do
expect { export_service.export_xml_sales_logs }
.to change(Export, :count).by(2)
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 (existing sales logs)" do
expect(export_service.export_xml_sales_logs).to eq({ expected_zip_filename.gsub(".zip", "").gsub(".zip", "") => start_time })
end
end
context "and underlying data changes between getting the logs and writting the manifest" do
before do
FactoryBot.create(:sales_log, saledate: Time.zone.local(2026, 2, 1))
FactoryBot.create(:sales_log, :ignore_validation_errors, saledate: Time.zone.local(2026, 4, 1))
end
def remove_logs(logs)
logs.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 |logs|
remove_logs(logs)
end
allow(export_service).to receive(:build_manifest_xml) do
create_fake_maifest
end
expect(export_service).to receive(:build_manifest_xml).with(1)
# rubocop:enable RSpec/SubjectStub
export_service.export_xml_sales_logs
end
end
context "when this is a second export (partial)" do
before do
start_time = Time.zone.local(2026, 6, 1)
Export.new(started_at: start_time, collection: "sales", year: 2025).save!
end
it "does not add any entry for the master manifest (no sales logs)" do
expect(storage_service).not_to receive(:write_file)
expect(export_service.export_xml_sales_logs).to eq({})
end
end
end
context "and a previous export has run the same day having sales logs" do
before do
FactoryBot.create(:sales_log, saledate: Time.zone.local(2025, 5, 1))
export_service.export_xml_sales_logs
end
context "and we trigger another full update" do
it "increments the base number" do
export_service.export_xml_sales_logs(full_update: true)
expect(Export.last.base_number).to eq(2)
end
it "resets the increment number" do
export_service.export_xml_sales_logs(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_sales_logs(full_update: true)).to eq({ "core_sales_2025_2026_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("core_sales_2025_2026_apr_mar_f0002_inc0001.zip", any_args)
export_service.export_xml_sales_logs(full_update: true)
end
end
end
context "and a previous export has run having no sales logs" do
before { export_service.export_xml_sales_logs }
it "doesn't increment the manifest number by 1" do
export_service.export_xml_sales_logs
expect(Export.last.increment_number).to eq(1)
end
end
context "and a log has been manually updated since the previous partial export" do
let(:expected_zip_filename) { "core_sales_2025_2026_apr_mar_f0001_inc0002.zip" }
before do
FactoryBot.create(:sales_log, saledate: Time.zone.local(2026, 2, 1), updated_at: Time.zone.local(2026, 2, 27), values_updated_at: Time.zone.local(2026, 2, 29))
FactoryBot.create(:sales_log, saledate: Time.zone.local(2026, 2, 1), updated_at: Time.zone.local(2026, 2, 27), values_updated_at: Time.zone.local(2026, 2, 29))
Export.create!(started_at: Time.zone.local(2026, 2, 28), base_number: 1, increment_number: 1, collection: "sales", year: 2025)
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_sales_logs).to eq({ expected_zip_filename.gsub(".zip", "") => start_time })
end
end
context "and one sales log with duplicate reference is available for export" do
let!(:sales_log) { FactoryBot.create(:sales_log, :export, duplicate_set_id: 123) }
def replace_duplicate_set_id(export_file)
export_file.sub!("<DUPLICATESET/>", "<DUPLICATESET>123</DUPLICATESET>")
end
it "generates an XML export file with the expected content within the ZIP file" do
expected_content = replace_entity_ids(sales_log, xml_export_file.read)
expected_content = replace_duplicate_set_id(expected_content)
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_sales_logs
end
end
context "when exporting only 24/25 collection period" do
let(:start_time) { Time.zone.local(2024, 4, 3) }
before do
Timecop.freeze(start_time)
Singleton.__init__(FormHandler)
end
after do
Timecop.unfreeze
Singleton.__init__(FormHandler)
end
context "and one sales log is available for export" do
let!(:sales_log) { FactoryBot.create(:sales_log, :export) }
let(:expected_zip_filename) { "core_sales_2024_2025_apr_mar_f0001_inc0001.zip" }
let(:expected_data_filename) { "core_sales_2024_2025_apr_mar_f0001_inc0001_pt001.xml" }
let(:xml_export_file) { File.open("spec/fixtures/exports/sales_log_2024.xml", "r:UTF-8") }
it "generates an XML export file with the expected content within the ZIP file" do
expected_content = replace_entity_ids(sales_log, 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_sales_logs(full_update: true, collection_year: 2024)
end
end
end
context "when exporting various fees, correctly maps the values" do
context "with discounted ownership and mscharge" do
let!(:sales_log) { FactoryBot.create(:sales_log, :export, mscharge: 123) }
def replace_mscharge_value(export_file)
export_file.sub!("<MSCHARGE>100.0</MSCHARGE>", "<MSCHARGE>123.0</MSCHARGE>")
end
it "exports mscharge fields as hasmscharge and mscharge" do
expected_content = replace_entity_ids(sales_log, xml_export_file.read)
expected_content = replace_mscharge_value(expected_content)
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_sales_logs
end
end
context "with shared ownership and mscharge" do
let!(:sales_log) { FactoryBot.create(:sales_log, :export, ownershipsch: 1, staircase: 2, type: 30, mscharge: 321, has_management_fee: 1, management_fee: 222) }
def replace_mscharge_and_shared_ownership_values(export_file)
export_file.sub!("<HASSERVICECHARGES/>", "<HASSERVICECHARGES>1</HASSERVICECHARGES>")
export_file.sub!("<SERVICECHARGES/>", "<SERVICECHARGES>321.0</SERVICECHARGES>")
export_file.sub!("<HASESTATEFEE/>", "<HASESTATEFEE>1</HASESTATEFEE>")
export_file.sub!("<ESTATEFEE/>", "<ESTATEFEE>222.0</ESTATEFEE>")
export_file.sub!("<MSCHARGE>100.0</MSCHARGE>", "<MSCHARGE/>")
export_file.sub!("<HASMSCHARGE>1</HASMSCHARGE>", "<HASMSCHARGE/>")
export_file.sub!("<TYPE>8</TYPE>", "<TYPE>30</TYPE>")
export_file.sub!("<STAIRCASE/>", "<STAIRCASE>2</STAIRCASE>")
export_file.sub!("<GRANT>10000.0</GRANT>", "<GRANT/>")
export_file.sub!("<PPCODENK>0</PPCODENK>", "<PPCODENK>1</PPCODENK>")
export_file.sub!("<PPOSTC1>SW1A</PPOSTC1>", "<PPOSTC1/>")
export_file.sub!("<PPOSTC2>1AA</PPOSTC2>", "<PPOSTC2/>")
export_file.sub!("<PREVLOC>E09000033</PREVLOC>", "<PREVLOC/>")
export_file.sub!("<EXTRABOR>1</EXTRABOR>", "<EXTRABOR/>")
export_file.sub!("<OWNERSHIP>2</OWNERSHIP>", "<OWNERSHIP>1</OWNERSHIP>")
export_file.sub!("<PREVIOUSLAKNOWN>1</PREVIOUSLAKNOWN>", "<PREVIOUSLAKNOWN>0</PREVIOUSLAKNOWN>")
export_file.sub!("<PREVLOCNAME>Westminster</PREVLOCNAME>", "<PREVLOCNAME/>")
end
it "exports mscharge fields as hasmscharge and mscharge" do
expected_content = replace_entity_ids(sales_log, xml_export_file.read)
expected_content = replace_mscharge_and_shared_ownership_values(expected_content)
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_sales_logs
end
end
end
end
end
Loading…
Cancel
Save