Browse Source
* 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 fieldsCLDC-3939-test-tests-remotely
10 changed files with 1392 additions and 7 deletions
@ -1,5 +1,6 @@ |
|||||||
class Export < ApplicationRecord |
class Export < ApplicationRecord |
||||||
scope :lettings, -> { where(collection: "lettings") } |
scope :lettings, -> { where(collection: "lettings") } |
||||||
|
scope :sales, -> { where(collection: "sales") } |
||||||
scope :organisations, -> { where(collection: "organisations") } |
scope :organisations, -> { where(collection: "organisations") } |
||||||
scope :users, -> { where(collection: "users") } |
scope :users, -> { where(collection: "users") } |
||||||
end |
end |
||||||
|
@ -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 |
@ -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 |
@ -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> |
@ -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> |
@ -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…
Reference in new issue