Browse Source

CLDC-3857 add new questions to organisation setup (#3075)

* Add questions to page, wip

* Add select as conditional question in radio like in logs

* Add existing value

* Add blank default

* Add values to about this organisation page

* Move grabbing values out of view

* Update new org page with js

* Selecting the profit status dependent on the provider type

* Add to org edit page for support users

* Add group logic

* Fix bug on existing orgs when group remains unchanged

* Clear group details if no longer part of a group

* Lint

* JS Lint

* Restore schema

* Remove manual_address_selected

* Revert "Remove manual_address_selected"

This reverts commit 812787fd09.

* Revert "Revert "Remove manual_address_selected""

This reverts commit 8ebd56036f.

* Update migration

* Remove file

* Move update after save

* Move methods to controller

* Update xml export

* Lint

* Update test xml file

* Update current user logic

* Lint

* Lint

* Reorder org js controller

* Lint

* Add tests to helper

* Some tests on the organisation pages

* Fix label text for profit status question in forms

* Add CSV task to update organisation profit status and group member fields

* only associate group on update if validation passes

* Update import raketask to use provided csv

looking into it I couldn't see a use for skip_group_member_validation. it temporarily disables validation but since it's not saved to database it'll make the organisation invalid in the future

opted instead to make all properties valid when importing

* Update tests to check more cases of organisation import

* Make XML tests check unordered file contents

stops tests flakily failing on the order of cols which is not important in xml

* Fix final failing tests

* restore manual_address_selected migration

* fix it's to its

* show the user the group they originally selected if possible when editing group

else fallback to any (oldest) org in the group

* update csv for ingest

---------

Co-authored-by: Manny Dinssa <44172848+Dinssa@users.noreply.github.com>
pull/3091/merge
Samuel Young 1 week ago committed by GitHub
parent
commit
a3ad64f6dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 21
      app/controllers/organisations_controller.rb
  2. 3
      app/frontend/controllers/index.js
  3. 27
      app/frontend/controllers/organisations_controller.js
  4. 52
      app/helpers/organisations_helper.rb
  5. 61
      app/models/organisation.rb
  6. 3
      app/services/exports/organisation_export_service.rb
  7. 27
      app/views/organisations/edit.html.erb
  8. 27
      app/views/organisations/new.html.erb
  9. 2
      app/views/organisations/show.html.erb
  10. 948
      config/csv/organisations_structure/20250710_organisation_profit_status_and_group.csv
  11. 4
      config/locales/en.yml
  12. 10
      db/migrate/20250227085622_add_new_question_fields_to_organisation.rb
  13. 4
      db/schema.rb
  14. 45
      lib/tasks/update_organisations_group_profit_status.rake
  15. 38
      spec/features/organisation_spec.rb
  16. 4
      spec/fixtures/exports/organisation.xml
  17. 2
      spec/fixtures/files/organisations_group_profit_status_invalid.csv
  18. 4
      spec/fixtures/files/organisations_group_profit_status_valid.csv
  19. 62
      spec/helpers/organisations_helper_spec.rb
  20. 63
      spec/lib/tasks/update_organisations_group_profit_status_spec.rb
  21. 20
      spec/services/exports/lettings_log_export_service_spec.rb
  22. 14
      spec/services/exports/organisation_export_service_spec.rb
  23. 16
      spec/services/exports/sales_log_export_service_spec.rb
  24. 10
      spec/services/exports/user_export_service_spec.rb
  25. 22
      spec/spec_helper.rb

21
app/controllers/organisations_controller.rb

@ -91,6 +91,7 @@ class OrganisationsController < ApplicationController
selected_rent_periods = rent_period_params[:rent_periods].compact_blank
@organisation = Organisation.new(org_params)
if @organisation.save
@organisation.update!(group: assign_group_number(@organisation.id, org_params[:group_member_id])) if org_params[:group_member]
OrganisationRentPeriod.transaction do
selected_rent_periods.each { |period| OrganisationRentPeriod.create!(organisation: @organisation, rent_period: period) }
end
@ -142,6 +143,9 @@ class OrganisationsController < ApplicationController
end
flash[:notice] = I18n.t("organisation.reactivated", organisation: @organisation.name)
else
if org_params[:group_member] && org_params[:group_member_id]
@organisation.group = assign_group_number(@organisation.id, org_params[:group_member_id])
end
flash[:notice] = I18n.t("organisation.updated")
end
if rent_period_params[:rent_periods].present?
@ -341,7 +345,7 @@ private
end
def org_params
params.require(:organisation).permit(:name, :address_line1, :address_line2, :postcode, :phone, :holds_own_stock, :provider_type, :housing_registration_no, :active)
params.require(:organisation).permit(:name, :address_line1, :address_line2, :postcode, :phone, :holds_own_stock, :provider_type, :housing_registration_no, :active, :profit_status, :group_member, :group_member_id)
end
def rent_period_params
@ -385,4 +389,19 @@ private
end
end
end
def assign_group_number(current_org_id, selected_org_id)
selected_org = Organisation.find_by(id: selected_org_id)
if selected_org&.group.present?
selected_org.group
else
next_group_number = next_available_group_number
selected_org.update!(group_member: true, group_member_id: current_org_id, group: next_group_number) if selected_org
next_group_number
end
end
def next_available_group_number
Organisation.maximum(:group).to_i + 1
end
end

3
app/frontend/controllers/index.js

@ -21,6 +21,8 @@ import TabsController from './tabs_controller.js'
import AddressSearchController from './address_search_controller.js'
import OrganisationsController from './organisations_controller.js'
application.register('accessible-autocomplete', AccessibleAutocompleteController)
application.register('conditional-filter', ConditionalFilterController)
application.register('conditional-question', ConditionalQuestionController)
@ -30,3 +32,4 @@ application.register('filter-layout', FilterLayoutController)
application.register('search', SearchController)
application.register('tabs', TabsController)
application.register('address-search', AddressSearchController)
application.register('organisations', OrganisationsController)

27
app/frontend/controllers/organisations_controller.js

@ -0,0 +1,27 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
updateProfitStatusOptions (event) {
const profitStatusSelect = document.getElementById('organisation-profit-status-field')
if (!profitStatusSelect) return
const localAuthorityOption = profitStatusSelect.querySelector('option[value="local_authority"]')
const nonProfitOption = profitStatusSelect.querySelector('option[value="non_profit"]')
const profitOption = profitStatusSelect.querySelector('option[value="profit"]')
const providerType = event.target.value
profitStatusSelect.disabled = false
localAuthorityOption.hidden = false
nonProfitOption.hidden = false
profitOption.hidden = false
if (providerType === 'LA') {
profitStatusSelect.value = 'local_authority'
nonProfitOption.hidden = true
profitOption.hidden = true
} else if (providerType === 'PRP') {
profitStatusSelect.value = ''
localAuthorityOption.hidden = true
}
}
}

52
app/helpers/organisations_helper.rb

@ -9,18 +9,27 @@ module OrganisationsHelper
end
end
def display_organisation_attributes(organisation)
[
def display_organisation_attributes(user, organisation)
attributes = [
{ name: "Organisation ID", value: "ORG#{organisation.id}", editable: false },
{ name: "Address", value: organisation.address_string, editable: true },
{ name: "Telephone number", value: organisation.phone, editable: true },
{ name: "Registration number", value: organisation.housing_registration_no || "", editable: false },
{ name: "Type of provider", value: organisation.display_provider_type, editable: false },
{ name: "Owns housing stock", value: organisation.holds_own_stock ? "Yes" : "No", editable: false },
{ name: "Rent periods", value: organisation.rent_period_labels, editable: true, format: :bullet },
{ name: "Data Sharing Agreement" },
{ name: "Status", value: status_tag(organisation.status) + delete_organisation_text(organisation), editable: false },
{ name: "Part of group", value: organisation.group_member ? "Yes" : "No", editable: user.support? },
]
if organisation.group_member
attributes << { name: "Group number", value: "GROUP#{organisation.group}", editable: user.support? }
end
attributes << { name: "For profit", value: organisation.display_profit_status, editable: user.support? }
attributes << { name: "Rent periods", value: organisation.rent_period_labels, editable: true, format: :bullet }
attributes << { name: "Data Sharing Agreement" }
attributes << { name: "Status", value: status_tag(organisation.status) + delete_organisation_text(organisation), editable: false }
attributes
end
def organisation_name_row(user:, organisation:, summary_list:)
@ -64,7 +73,40 @@ module OrganisationsHelper
def organisation_details_link_message(attribute)
text = lowercase_first_letter(attribute[:name])
return "Add #{text}" if attribute[:name] == "Rent periods"
return "Select profit status" if attribute[:name] == "For profit"
"Enter #{text}"
end
def group_organisation_options
null_option = [OpenStruct.new(id: "", name: "Select an option", group: nil)]
organisations = Organisation.visible.map { |org| OpenStruct.new(id: org.id, name: group_organisation_options_name(org)) }
null_option + organisations
end
def group_organisation_options_name(org)
"#{org.name}#{group_organisation_options_group_text(org)}"
end
def group_organisation_options_group_text(org)
return "" unless org.oldest_group_member
" (GROUP#{org.oldest_group_member&.group})"
end
def profit_status_options(provider_type = nil)
null_option = [OpenStruct.new(id: "", name: "Select an option")]
profit_statuses = Organisation::PROFIT_STATUS.map do |key, _value|
OpenStruct.new(id: key, name: Organisation::DISPLAY_PROFIT_STATUS[key])
end
case provider_type
when "LA"
profit_statuses.select! { |option| option.id == :local_authority }
when "PRP"
profit_statuses.reject! { |option| option.id == :local_authority }
end
null_option + profit_statuses
end
end

61
app/models/organisation.rb

@ -54,13 +54,26 @@ class Organisation < ApplicationRecord
PRP: 2,
}.freeze
PROFIT_STATUS = {
non_profit: 1,
profit: 2,
local_authority: 3,
}.freeze
enum :provider_type, PROVIDER_TYPE
enum :profit_status, PROFIT_STATUS
attribute :group_member, :boolean
before_save :clear_group_member_fields_if_not_group_member
alias_method :la?, :LA?
validates :name, presence: { message: I18n.t("validations.organisation.name_missing") }
validates :name, uniqueness: { case_sensitive: false, message: I18n.t("validations.organisation.name_not_unique") }
validates :provider_type, presence: { message: I18n.t("validations.organisation.provider_type_missing") }
validates :group_member_id, presence: { message: I18n.t("validations.organisation.group_missing") }, if: -> { group_member? }
validate :validate_profit_status
def self.find_by_id_on_multiple_fields(id)
return if id.nil?
@ -142,6 +155,12 @@ class Organisation < ApplicationRecord
DISPLAY_PROVIDER_TYPE[provider_type.to_sym]
end
DISPLAY_PROFIT_STATUS = { "non_profit": "Non-profit", "profit": "Profit", "local_authority": "Local authority" }.freeze
def display_profit_status
DISPLAY_PROFIT_STATUS.fetch(profit_status&.to_sym, "")
end
def has_managing_agents?
managing_agents.count.positive?
end
@ -232,4 +251,46 @@ class Organisation < ApplicationRecord
def has_visible_schemes?
owned_schemes.visible.count.positive?
end
def oldest_group_member
return nil if group.blank?
Organisation.visible.where(group:).order(:created_at).first
end
# Shown on the edit organisation page to indicate which group member the user initially entered, if possible
def selected_group_member
return nil if oldest_group_member.nil?
# find organisation by group_member_id if it exists
selected_group = Organisation.visible.find_by(id: group_member_id)
# ensure that its still in the same group
if selected_group.present? && selected_group.group == group
selected_group
else
oldest_group_member
end
end
private
def validate_profit_status
return if profit_status.nil?
if provider_type == "LA" && profit_status != "local_authority"
errors.add(:profit_status, I18n.t("validations.organisation.profit_status.must_be_LA"))
end
if provider_type == "PRP" && profit_status == "local_authority"
errors.add(:profit_status, I18n.t("validations.organisation.profit_status.must_not_be_LA"))
end
end
def clear_group_member_fields_if_not_group_member
unless group_member
self.group_member_id = nil
self.group = nil
end
end
end

3
app/services/exports/organisation_export_service.rb

@ -74,8 +74,7 @@ module Exports
attribute_hash["provider_type"] = organisation.provider_type_before_type_cast
attribute_hash["merge_date"] = organisation.merge_date&.iso8601
attribute_hash["available_from"] = organisation.available_from&.iso8601
attribute_hash["profit_status"] = nil # will need update when we add the field to the org
attribute_hash["group"] = nil # will need update when we add the field to the org
attribute_hash["profit_status"] = organisation.profit_status_before_type_cast
attribute_hash["status"] = organisation.status
attribute_hash["active"] = attribute_hash["status"] == :active

27
app/views/organisations/edit.html.erb

@ -29,6 +29,33 @@
autocomplete: "tel",
width: 20 %>
<% if current_user.support? %>
<%= f.govuk_radio_buttons_fieldset :group_member,
legend: { text: "Is this organisation part of a housing provider group structure?", size: "m" } do %>
<%= f.govuk_radio_button :group_member, true,
label: { text: "Yes" },
"data-controller": "conditional-question",
"data-action": "click->conditional-question#displayConditional",
"data-info": { conditional_questions: { group: [true] }, type: "organisation" }.to_json do %>
<%= f.govuk_collection_select :group_member_id,
group_organisation_options,
:id,
:name,
label: { text: "Search for an organisation that is part of the same group as this organisation", size: "m" },
options: { disabled: [""], selected: @organisation.selected_group_member&.id || "" },
"data-controller": %w[accessible-autocomplete conditional-filter] %>
<% end %>
<%= f.govuk_radio_button :group_member, false, label: { text: "No" } %>
<% end %>
<%= f.govuk_collection_select :profit_status,
profit_status_options(@organisation.provider_type),
:id,
:name,
label: { text: "Is the organisation for profit?", size: "m" },
options: { disabled: [""], selected: @organisation.profit_status || "" } %>
<% end %>
<%= f.govuk_check_boxes_fieldset :rent_periods,
legend: { text: "What are the rent periods for the organisation?" },
hint: { text: "It is not possible to deselect rent periods that are used in logs" } do %>

27
app/views/organisations/new.html.erb

@ -48,6 +48,8 @@
:id,
:name,
label: { text: "Organisation type", size: "m" },
"data-controller": "organisations",
"data-action": "change->organisations#updateProfitStatusOptions",
options: { disabled: [""], selected: @organisation.provider_type || "" } %>
<%= f.govuk_collection_radio_buttons :holds_own_stock,
@ -56,6 +58,31 @@
:name,
legend: { text: "Does the organisation hold its own stock?", size: "m" } %>
<%= f.govuk_radio_buttons_fieldset :group_member,
legend: { text: "Is this organisation part of a housing provider group structure?", size: "m" } do %>
<%= f.govuk_radio_button :group_member, true,
label: { text: "Yes" },
"data-controller": "conditional-question",
"data-action": "click->conditional-question#displayConditional",
"data-info": { conditional_questions: { group: [true] }, type: "organisation" }.to_json do %>
<%= f.govuk_collection_select :group_member_id,
group_organisation_options,
:id,
:name,
label: { text: "Search for an organisation that is part of the same group as this organisation", size: "m" },
options: { disabled: [""], selected: @organisation.oldest_group_member&.id || "" },
"data-controller": %w[accessible-autocomplete conditional-filter] %>
<% end %>
<%= f.govuk_radio_button :group_member, false, label: { text: "No" } %>
<% end %>
<%= f.govuk_collection_select :profit_status,
profit_status_options,
:id,
:name,
label: { text: "Is the organisation for profit?", size: "m" },
options: { disabled: [""], selected: @organisation.profit_status || "" } %>
<%= f.govuk_check_boxes_fieldset :rent_periods,
legend: { text: "What are the rent periods for the organisation?" } do %>
<% @rent_periods.map do |key, period| %>

2
app/views/organisations/show.html.erb

@ -15,7 +15,7 @@
<div class="govuk-grid-column-two-thirds-from-desktop">
<%= govuk_summary_list do |summary_list| %>
<%= organisation_name_row(user: current_user, organisation: @organisation, summary_list:) %>
<% display_organisation_attributes(@organisation).each do |attr| %>
<% display_organisation_attributes(current_user, @organisation).each do |attr| %>
<% if attr[:name] == "Data Sharing Agreement" %>
<%= data_sharing_agreement_row(organisation: @organisation, user: current_user, summary_list:) %>
<% else %>

948
config/csv/organisations_structure/20250710_organisation_profit_status_and_group.csv

@ -0,0 +1,948 @@
id,provider_type,profit_status,group
701,PRP,Non-profit,
1036,PRP,Non-profit,
1040,PRP,Profit,
950,PRP,Non-profit,
808,PRP,Non-profit,2
952,PRP,Non-profit,2
815,PRP,Non-profit,2
311,PRP,Non-profit,
1038,PRP,Non-profit,
610,PRP,Non-profit,
743,PRP,Non-profit,
750,PRP,Non-profit,
642,PRP,Profit,
181,PRP,Non-profit,
568,PRP,Non-profit,
954,PRP,Non-profit,
994,PRP,Non-profit,
763,PRP,Non-profit,
878,PRP,Non-profit,
961,PRP,Non-profit,
908,LA,Non-profit,
723,PRP,Non-profit,
198,PRP,Non-profit,
895,PRP,Non-profit,
420,LA,Local authority,
470,PRP,Non-profit,
541,PRP,Non-profit,
82,PRP,Non-profit,
857,PRP,Non-profit,
807,PRP,Non-profit,
239,PRP,Non-profit,
83,PRP,Non-profit,
39,PRP,Non-profit,
707,PRP,Non-profit,
1037,PRP,Non-profit,
1006,PRP,Local authority,
765,PRP,Non-profit,
1033,PRP,Non-profit,
649,PRP,Non-profit,
805,PRP,Non-profit,
960,PRP,Non-profit,
985,PRP,Non-profit,
531,PRP,Non-profit,
84,PRP,Non-profit,
629,PRP,Non-profit,
199,PRP,Non-profit,
273,LA,Local authority,
827,PRP,Profit,
442,LA,Local authority,
409,LA,Local authority,
772,PRP,Non-profit,
314,PRP,Non-profit,
240,PRP,Non-profit,
624,PRP,Non-profit,
918,PRP,Non-profit,4
888,PRP,Non-profit,4
955,PRP,Non-profit,4
907,PRP,Non-profit,
464,PRP,Profit,
468,PRP,Non-profit,
450,LA,Local authority,
790,PRP,Local authority,
1025,PRP,Non-profit,
874,PRP,Non-profit,
739,PRP,Non-profit,
556,PRP,Non-profit,
923,PRP,Non-profit,
200,PRP,Non-profit,
534,LA,Local authority,
806,LA,Local authority,
668,LA,Local authority,
934,PRP,Non-profit,
85,PRP,Non-profit,
2,PRP,Non-profit,
905,PRP,Non-profit,
524,PRP,Non-profit,
791,PRP,Non-profit,
86,PRP,Non-profit,
201,PRP,Non-profit,
640,PRP,Non-profit,
323,PRP,Non-profit,
693,LA,Local authority,
496,PRP,Non-profit,
481,PRP,Non-profit,
579,PRP,Non-profit,
322,LA,Local authority,
298,PRP,Local authority,
4,LA,Local authority,
490,PRP,Non-profit,
438,LA,Non-profit,
87,PRP,Non-profit,
88,PRP,Non-profit,
527,LA,Local authority,
877,PRP,Local authority,
818,PRP,Non-profit,
670,PRP,Non-profit,
766,PRP,Non-profit,
639,PRP,Non-profit,
940,PRP,Non-profit,
626,PRP,Non-profit,
460,LA,Local authority,
713,PRP,Non-profit,
89,PRP,Non-profit,
609,PRP,Non-profit,
422,LA,Local authority,
315,PRP,Non-profit,
90,PRP,Non-profit,
529,LA,Local authority,
999,PRP,Non-profit,
462,PRP,Non-profit,
914,PRP,Non-profit,
182,PRP,Non-profit,
933,PRP,Non-profit,
241,PRP,Non-profit,5
959,PRP,Non-profit,5
465,PRP,Non-profit,
518,LA,Local authority,
25,PRP,Non-profit,
34,LA,Local authority,
800,PRP,Local authority,
202,PRP,Non-profit,
544,PRP,Non-profit,
953,PRP,Non-profit,
637,PRP,Non-profit,
461,LA,Local authority,
880,PRP,Local authority,
510,PRP,Non-profit,
40,PRP,Non-profit,
401,LA,Local authority,
1015,PRP,Local authority,
429,LA,Local authority,
983,PRP,Non-profit,
652,PRP,Non-profit,
279,LA,Local authority,
275,PRP,Non-profit,
1054,PRP,Non-profit,
665,PRP,Non-profit,
704,PRP,Non-profit,
734,PRP,Non-profit,
408,LA,Local authority,
854,PRP,Non-profit,
700,PRP,Non-profit,
31,PRP,Non-profit,
191,PRP,Non-profit,
733,PRP,Non-profit,
870,PRP,Non-profit,
23,PRP,Non-profit,
430,LA,Local authority,
831,PRP,Non-profit,
844,PRP,Non-profit,
302,LA,Local authority,
286,PRP,Non-profit,
41,PRP,Non-profit,
858,PRP,Non-profit,
284,LA,Local authority,
493,PRP,Non-profit,
798,LA,Local authority,
335,LA,Local authority,
91,PRP,Non-profit,
661,PRP,Non-profit,
850,PRP,Non-profit,
66,PRP,Non-profit,
558,PRP,Non-profit,
511,PRP,Non-profit,
1071,PRP,Non-profit,
721,LA,Local authority,
307,LA,Local authority,
911,PRP,Non-profit,
454,LA,Local authority,
319,LA,Local authority,
295,LA,Local authority,
92,LA,Local authority,
659,PRP,Non-profit,
747,PRP,Non-profit,
5,PRP,Non-profit,
292,LA,Local authority,
316,PRP,Non-profit,
42,PRP,Non-profit,
242,PRP,Non-profit,
931,PRP,Non-profit,
759,PRP,Non-profit,
612,PRP,Non-profit,
658,PRP,Profit,
243,PRP,Non-profit,
1007,PRP,Non-profit,
43,PRP,Non-profit,
203,PRP,Non-profit,
497,LA,Local authority,
514,PRP,Non-profit,
24,PRP,Non-profit,
44,PRP,Non-profit,
204,PRP,Non-profit,
635,PRP,Non-profit,
813,PRP,Non-profit,
865,PRP,Non-profit,
715,PRP,Non-profit,
93,LA,Local authority,
817,PRP,Non-profit,
557,PRP,Non-profit,
746,PRP,Non-profit,
282,PRP,Non-profit,
618,PRP,Non-profit,
663,PRP,Non-profit,
821,PRP,Non-profit,
741,PRP,Non-profit,
403,LA,Local authority,
78,PRP,Non-profit,
415,LA,Local authority,
405,LA,Local authority,
977,PRP,Non-profit,
675,PRP,Non-profit,
525,LA,Local authority,
519,PRP,Non-profit,
94,PRP,Non-profit,
67,PRP,Non-profit,
485,PRP,Non-profit,
930,PRP,Non-profit,
95,PRP,Non-profit,
656,PRP,Local authority,
288,LA,Local authority,
192,LA,Local authority,
96,PRP,Non-profit,
205,PRP,Non-profit,
97,PRP,Non-profit,
413,LA,Local authority,
760,LA,Local authority,
447,LA,Local authority,
280,LA,Local authority,
300,PRP,Local authority,
6,PRP,Non-profit,
317,LA,Local authority,
512,PRP,Non-profit,
98,PRP,Non-profit,
70,PRP,Non-profit,
99,PRP,Non-profit,
29,PRP,Non-profit,
100,PRP,Non-profit,
1004,PRP,Non-profit,
101,PRP,Non-profit,
1011,LA,Local authority,
722,PRP,Non-profit,
1028,PRP,Profit,
559,PRP,Non-profit,
206,PRP,Non-profit,
965,LA,Local authority,
495,PRP,Non-profit,
207,PRP,Non-profit,
102,LA,Local authority,
912,PRP,Non-profit,
561,PRP,Non-profit,
103,PRP,Non-profit,
644,PRP,Non-profit,
452,LA,Local authority,
608,PRP,Non-profit,
472,PRP,Non-profit,
466,PRP,Non-profit,
104,PRP,Non-profit,
208,PRP,Non-profit,
105,PRP,Non-profit,
580,PRP,Non-profit,
835,PRP,Non-profit,
970,LA,Local authority,
276,PRP,Non-profit,
486,PRP,Non-profit,
951,PRP,Non-profit,
1003,PRP,Non-profit,
797,PRP,Non-profit,
548,PRP,Profit,
414,LA,Local authority,
748,PRP,Non-profit,
872,PRP,Non-profit,
777,PRP,Non-profit,
16,PRP,Non-profit,
263,PRP,Non-profit,
245,PRP,Non-profit,
630,PRP,Profit,
515,PRP,Non-profit,3
244,PRP,Non-profit,3
968,PRP,Non-profit,3
634,PRP,Non-profit,
538,PRP,Non-profit,
521,LA,Local authority,
728,PRP,Non-profit,
799,PRP,Non-profit,
246,PRP,Non-profit,
1055,PRP,Non-profit,
889,PRP,Non-profit,
1034,PRP,Profit,
614,PRP,Non-profit,
893,PRP,Non-profit,
106,PRP,Non-profit,
247,PRP,Non-profit,
424,LA,Local authority,
45,PRP,Non-profit,
209,PRP,Profit,
716,PRP,Non-profit,
1062,PRP,Profit,
657,PRP,Non-profit,
833,PRP,Non-profit,
494,LA,Local authority,
473,PRP,Non-profit,
1012,PRP,Non-profit,
28,PRP,Non-profit,
572,PRP,Non-profit,
856,PRP,Non-profit,
969,PRP,Non-profit,6
643,PRP,Non-profit,
1041,PRP,Non-profit,
418,LA,Local authority,
210,PRP,Non-profit,
551,PRP,Profit,
564,PRP,Non-profit,
476,PRP,Non-profit,
788,PRP,Non-profit,
46,PRP,Non-profit,
107,PRP,Non-profit,
881,PRP,Non-profit,
419,LA,Local authority,
653,PRP,Non-profit,
972,PRP,Non-profit,
814,PRP,Non-profit,
327,LA,Local authority,
502,PRP,Non-profit,
108,PRP,Non-profit,
769,PRP,Non-profit,
921,PRP,Non-profit,
731,PRP,Non-profit,
21,PRP,Profit,
65,PRP,Non-profit,
109,PRP,Non-profit,
47,PRP,Non-profit,
847,PRP,Non-profit,
860,PRP,Non-profit,
504,PRP,Profit,
265,PRP,Profit,
287,LA,Local authority,
980,PRP,Non-profit,
824,PRP,Non-profit,
546,PRP,Non-profit,
211,PRP,Non-profit,
416,LA,Local authority,
785,PRP,Local authority,
578,PRP,Non-profit,
71,PRP,Non-profit,
48,PRP,Non-profit,
672,PRP,Non-profit,
684,PRP,Non-profit,
650,PRP,Non-profit,
500,PRP,Non-profit,
332,LA,Non-profit,
266,PRP,Non-profit,
752,PRP,Non-profit,
702,PRP,Non-profit,
110,PRP,Non-profit,
694,PRP,Non-profit,
819,PRP,Non-profit,
620,PRP,Non-profit,
1010,PRP,Non-profit,
111,PRP,Non-profit,
853,PRP,Non-profit,7
732,PRP,Non-profit,
112,PRP,Non-profit,
755,PRP,Non-profit,
655,PRP,Non-profit,
49,PRP,Non-profit,
574,PRP,Non-profit,
113,PRP,Non-profit,
823,PRP,Non-profit,
775,PRP,Non-profit,
706,PRP,Non-profit,
780,PRP,Non-profit,
822,PRP,Non-profit,
264,PRP,Non-profit,
887,PRP,Non-profit,
114,PRP,Non-profit,
115,PRP,Non-profit,
212,PRP,Non-profit,
1051,PRP,Non-profit,
290,LA,Local authority,
982,PRP,Local authority,
72,PRP,Non-profit,
575,PRP,Non-profit,
778,PRP,Non-profit,
1042,PRP,Non-profit,
477,PRP,Non-profit,
183,PRP,Non-profit,
767,PRP,Non-profit,
837,PRP,Non-profit,
272,PRP,Non-profit,
248,PRP,Non-profit,
990,PRP,Non-profit,
736,PRP,Non-profit,
988,PRP,Non-profit,
939,PRP,Non-profit,
116,PRP,Non-profit,
1017,PRP,Non-profit,
526,PRP,Non-profit,
214,PRP,Non-profit,
520,LA,Local authority,
117,PRP,Non-profit,
583,PRP,Non-profit,7
118,LA,Local authority,
50,PRP,Non-profit,
709,PRP,Non-profit,
26,PRP,Non-profit,
11,PRP,Non-profit,
781,PRP,Non-profit,
421,LA,Local authority,
119,PRP,Non-profit,
73,PRP,Non-profit,
926,PRP,Non-profit,
51,PRP,Non-profit,
1001,PRP,Non-profit,
848,PRP,Non-profit,
679,PRP,Non-profit,
328,PRP,Non-profit,
428,LA,Local authority,
971,PRP,Local authority,
892,PRP,Non-profit,
553,PRP,Non-profit,
121,PRP,Non-profit,
758,PRP,Non-profit,
1019,PRP,Profit,
1020,PRP,Profit,
1064,PRP,Profit,
1065,PRP,Profit,
1066,PRP,Profit,
1067,PRP,Profit,
1068,PRP,Non-profit,
1063,PRP,Profit,
735,PRP,Profit,
1058,PRP,Non-profit,
435,LA,Local authority,
678,PRP,Local authority,
691,PRP,Local authority,
329,LA,Local authority,
836,PRP,Non-profit,
120,PRP,Non-profit,
250,PRP,Non-profit,
14,PRP,Non-profit,
927,LA,Local authority,
74,PRP,Non-profit,
122,PRP,Non-profit,
816,PRP,Non-profit,
184,PRP,Non-profit,
185,PRP,Non-profit,
978,PRP,Non-profit,
52,PRP,Non-profit,
621,PRP,Non-profit,
333,LA,Local authority,
193,LA,Local authority,
1053,LA,Non-profit,
308,LA,Local authority,
436,LA,Local authority,
310,LA,Local authority,
535,LA,Local authority,
517,LA,Local authority,
123,LA,Local authority,
79,LA,Local authority,
492,LA,Local authority,
859,LA,Local authority,
262,LA,Local authority,
498,LA,Local authority,
633,LA,Local authority,
437,LA,Local authority,
443,LA,Local authority,
124,LA,Local authority,
523,LA,Local authority,
278,LA,Local authority,
309,LA,Local authority,
194,LA,Local authority,
125,PRP,Non-profit,
761,PRP,Non-profit,
186,PRP,Non-profit,
811,PRP,Non-profit,
216,PRP,Non-profit,
425,LA,Local authority,
17,PRP,Non-profit,
942,PRP,Profit,
251,PRP,Non-profit,
919,LA,Local authority,
289,LA,Local authority,
787,PRP,Non-profit,
631,PRP,Non-profit,
771,PRP,Non-profit,
217,PRP,Non-profit,
68,PRP,Non-profit,
904,PRP,Profit,
126,LA,Local authority,
445,LA,Local authority,
963,PRP,Local authority,
1045,PRP,Non-profit,
573,PRP,Non-profit,5
127,PRP,Non-profit,
613,PRP,Non-profit,
632,PRP,Non-profit,
1030,PRP,Non-profit,
730,PRP,Non-profit,
509,LA,Local authority,
417,LA,Local authority,
616,PRP,Non-profit,
997,PRP,Non-profit,
689,LA,Local authority,
862,PRP,Non-profit,
682,PRP,Non-profit,
964,PRP,Non-profit,
681,PRP,Non-profit,
1056,PRP,Non-profit,
796,PRP,Non-profit,
719,PRP,Non-profit,
505,PRP,Non-profit,
1049,PRP,Non-profit,
128,PRP,Non-profit,
487,PRP,Non-profit,
1043,PRP,Non-profit,
932,PRP,Non-profit,
846,PRP,Non-profit,
998,PRP,Non-profit,
129,PRP,Non-profit,
1027,PRP,Non-profit,
130,PRP,Non-profit,
867,PRP,Non-profit,
407,LA,Local authority,
625,PRP,Non-profit,
53,PRP,Non-profit,
131,PRP,Non-profit,
549,PRP,Non-profit,
915,PRP,Non-profit,
132,PRP,Non-profit,
710,PRP,Profit,
439,LA,Local authority,
325,LA,Local authority,
738,PRP,Non-profit,
727,PRP,Non-profit,
906,PRP,Non-profit,
690,PRP,Non-profit,
469,PRP,Non-profit,
296,LA,Local authority,
793,PRP,Non-profit,
336,LA,Local authority,
218,PRP,Non-profit,
133,LA,Local authority,
973,PRP,Local authority,
885,PRP,Non-profit,
536,LA,Local authority,
80,LA,Local authority,
440,LA,Local authority,
456,LA,Local authority,
789,PRP,Local authority,
297,PRP,Local authority,
677,PRP,Non-profit,
277,PRP,Local authority,
38,PRP,Local authority,
830,PRP,Non-profit,
301,LA,Local authority,
75,PRP,Non-profit,
770,PRP,Non-profit,
686,PRP,Non-profit,
537,LA,Local authority,
533,PRP,Non-profit,
698,PRP,Non-profit,
717,PRP,Non-profit,
69,PRP,Non-profit,
948,PRP,Non-profit,
444,LA,Local authority,
402,LA,Local authority,
986,PRP,Non-profit,
252,PRP,Non-profit,
253,PRP,Non-profit,
219,PRP,Non-profit,
820,LA,Local authority,
254,PRP,Non-profit,
324,PRP,Non-profit,
845,PRP,Non-profit,
705,PRP,Non-profit,
554,PRP,Non-profit,
712,PRP,Non-profit,
255,PRP,Non-profit,
651,PRP,Non-profit,
699,PRP,Non-profit,
987,PRP,Non-profit,
779,PRP,Non-profit,
886,PRP,Non-profit,
195,LA,Local authority,
869,PRP,Non-profit,
683,PRP,Non-profit,
917,PRP,Non-profit,
688,PRP,Non-profit,
654,PRP,Non-profit,
134,PRP,Non-profit,
326,PRP,Non-profit,
555,PRP,Profit,
1069,PRP,Non-profit,
1060,PRP,Non-profit,
696,PRP,Non-profit,
782,PRP,Non-profit,
839,PRP,Non-profit,
946,PRP,Profit,
220,PRP,Non-profit,
221,PRP,Non-profit,
1000,PRP,Non-profit,7
222,PRP,Non-profit,
294,PRP,Profit,
863,PRP,Non-profit,
562,PRP,Non-profit,
223,PRP,Local authority,
135,PRP,Non-profit,
669,PRP,Non-profit,
136,PRP,Non-profit,
137,PRP,Non-profit,
611,PRP,Profit,
891,PRP,Profit,
864,PRP,Profit,
989,PRP,Non-profit,1
724,PRP,Non-profit,1
680,PRP,Non-profit,1
187,PRP,Non-profit,
479,PRP,Profit,
224,PRP,Non-profit,
501,PRP,Non-profit,
499,PRP,Non-profit,
225,PRP,Non-profit,
852,PRP,Non-profit,
913,PRP,Non-profit,
451,LA,Local authority,
20,PRP,Non-profit,
958,PRP,Non-profit,
674,PRP,Profit,
10,PRP,Non-profit,
774,PRP,Non-profit,
54,PRP,Non-profit,
55,PRP,Non-profit,
944,PRP,Non-profit,
627,PRP,Non-profit,
949,PRP,Non-profit,
1005,PRP,Non-profit,
920,PRP,Non-profit,
1050,PRP,Non-profit,
138,PRP,Non-profit,
139,PRP,Non-profit,
729,PRP,Non-profit,
1018,PRP,Non-profit,
530,LA,Local authority,
1016,PRP,Non-profit,
140,PRP,Non-profit,
426,LA,Local authority,
226,PRP,Non-profit,
812,PRP,Non-profit,
809,PRP,Non-profit,
545,LA,Local authority,
1052,PRP,Local authority,
1035,PRP,Non-profit,
873,PRP,Profit,6
764,PRP,Profit,6
141,PRP,Non-profit,
884,PRP,Non-profit,
896,PRP,Non-profit,
142,PRP,Non-profit,
1013,PRP,Non-profit,
890,PRP,Non-profit,
330,PRP,Non-profit,
482,PRP,Non-profit,
615,PRP,Non-profit,
1057,PRP,Profit,
227,PRP,Non-profit,
459,LA,Local authority,
305,LA,Local authority,
648,PRP,Local authority,
607,PRP,Non-profit,
803,LA,Local authority,
423,LA,Local authority,
448,LA,Local authority,
507,LA,Local authority,
412,LA,Local authority,
834,PRP,Local authority,
979,PRP,Non-profit,
306,PRP,Non-profit,
563,PRP,Non-profit,
996,PRP,Non-profit,
143,PRP,Non-profit,
646,PRP,Non-profit,
695,PRP,Non-profit,
188,PRP,Non-profit,
334,PRP,Profit,
993,PRP,Profit,
30,PRP,Non-profit,
671,PRP,Non-profit,
883,PRP,Non-profit,
776,PRP,Non-profit,
619,PRP,Non-profit,
714,PRP,Non-profit,
144,PRP,Non-profit,
196,LA,Local authority,
516,PRP,Non-profit,
664,PRP,Non-profit,
744,PRP,Non-profit,
1047,LA,Non-profit,
467,PRP,Non-profit,
189,PRP,Non-profit,
483,PRP,Non-profit,
506,PRP,Non-profit,
751,LA,Local authority,
567,PRP,Non-profit,
560,PRP,Non-profit,
56,PRP,Non-profit,
299,PRP,Non-profit,
37,LA,Local authority,
36,PRP,Non-profit,
35,PRP,Non-profit,
145,PRP,Non-profit,
1023,PRP,Local authority,
754,PRP,Profit,
749,PRP,Profit,
941,PRP,Non-profit,
33,PRP,Non-profit,
331,LA,Local authority,
956,PRP,Non-profit,
995,PRP,Non-profit,
27,PRP,Non-profit,
916,PRP,Non-profit,
882,PRP,Non-profit,
992,PRP,Non-profit,
925,PRP,Non-profit,
966,LA,Local authority,
910,PRP,Non-profit,
312,LA,Local authority,
146,LA,Local authority,
478,LA,Local authority,
228,PRP,Non-profit,
453,LA,Local authority,
539,LA,Local authority,
147,PRP,Non-profit,
256,PRP,Non-profit,
976,LA,Local authority,
318,PRP,Local authority,
285,LA,Local authority,
565,PRP,Non-profit,
427,LA,Local authority,
922,PRP,Non-profit,
641,PRP,Non-profit,
455,LA,Local authority,
57,PRP,Non-profit,
938,PRP,Non-profit,
840,PRP,Non-profit,
623,PRP,Non-profit,
257,PRP,Non-profit,
569,PRP,Non-profit,
552,LA,Local authority,
876,PRP,Local authority,
19,PRP,Non-profit,
1059,PRP,Non-profit,
58,PRP,Non-profit,
293,PRP,Profit,
148,PRP,Non-profit,
433,LA,Local authority,
149,PRP,Non-profit,
150,PRP,Non-profit,
1044,PRP,Local authority,
843,PRP,Non-profit,
1031,PRP,Non-profit,
281,PRP,Non-profit,
991,PRP,Non-profit,
929,PRP,Non-profit,
795,PRP,Non-profit,
742,PRP,Non-profit,
151,PRP,Non-profit,
1029,PRP,Non-profit,
685,PRP,Non-profit,
291,PRP,Non-profit,
152,PRP,Non-profit,
153,LA,Local authority,
463,PRP,Non-profit,
491,LA,Local authority,
871,PRP,Local authority,
532,LA,Local authority,
229,PRP,Non-profit,
1002,PRP,Non-profit,
666,PRP,Non-profit,
829,PRP,Non-profit,
697,PRP,Non-profit,
936,PRP,Profit,
570,PRP,Local authority,
410,LA,Local authority,
76,PRP,Non-profit,
855,PRP,Non-profit,
155,PRP,Non-profit,
304,PRP,Non-profit,
156,PRP,Non-profit,
1008,PRP,Non-profit,
875,PRP,Non-profit,
446,LA,Local authority,
879,PRP,Local authority,
786,PRP,Non-profit,4
1032,PRP,Non-profit,
434,LA,Local authority,
157,PRP,Non-profit,
441,LA,Local authority,
230,PRP,Non-profit,
628,PRP,Non-profit,
828,PRP,Non-profit,
231,PRP,Non-profit,
158,PRP,Non-profit,
489,PRP,Non-profit,
1021,LA,Local authority,
159,PRP,Non-profit,
825,PRP,Non-profit,
708,PRP,Non-profit,
81,LA,Local authority,
197,PRP,Non-profit,
810,PRP,Non-profit,
962,PRP,Non-profit,
1070,PRP,Non-profit,
160,PRP,Non-profit,
22,PRP,Non-profit,
258,PRP,Non-profit,
984,PRP,Non-profit,
232,PRP,Non-profit,
1046,PRP,Local authority,
622,PRP,Non-profit,
943,PRP,Non-profit,
638,PRP,Non-profit,
660,PRP,Non-profit,
161,PRP,Non-profit,
12,PRP,Non-profit,
162,PRP,Non-profit,
1014,PRP,Non-profit,
667,PRP,Non-profit,
673,PRP,Non-profit,
480,PRP,Non-profit,
937,PRP,Non-profit,
543,PRP,Non-profit,
841,PRP,Non-profit,
1026,PRP,Non-profit,
794,PRP,Non-profit,
725,PRP,Non-profit,
59,PRP,Non-profit,
522,PRP,Non-profit,
945,PRP,Non-profit,
163,PRP,Non-profit,
897,PRP,Non-profit,
636,PRP,Non-profit,
475,PRP,Non-profit,
768,LA,Local authority,
645,PRP,Non-profit,
726,PRP,Non-profit,
1024,LA,Non-profit,
259,PRP,Non-profit,
547,PRP,Non-profit,
566,PRP,Non-profit,
1061,PRP,Non-profit,
550,PRP,Non-profit,
861,PRP,Non-profit,
832,PRP,Non-profit,
508,PRP,Non-profit,
233,PRP,Non-profit,
234,PRP,Non-profit,
488,PRP,Non-profit,
773,PRP,Non-profit,
164,PRP,Non-profit,
740,PRP,Non-profit,
804,PRP,Non-profit,
647,PRP,Non-profit,
32,PRP,Non-profit,
851,PRP,Non-profit,
703,PRP,Non-profit,
826,PRP,Non-profit,
577,PRP,Non-profit,
432,LA,Local authority,
571,PRP,Local authority,
909,PRP,Non-profit,
581,PRP,Non-profit,
617,PRP,Non-profit,
801,PRP,Non-profit,
935,PRP,Non-profit,
320,PRP,Non-profit,
165,PRP,Non-profit,
757,PRP,Non-profit,
166,PRP,Non-profit,
431,LA,Local authority,
235,PRP,Non-profit,
236,PRP,Non-profit,
167,PRP,Non-profit,
237,PRP,Non-profit,
313,LA,Local authority,
838,PRP,Local authority,
457,LA,Local authority,
474,PRP,Non-profit,
9,PRP,Non-profit,
60,PRP,Non-profit,
168,PRP,Non-profit,
406,LA,Local authority,
61,PRP,Non-profit,
484,PRP,Non-profit,
169,LA,Local authority,
967,PRP,Local authority,
274,LA,Local authority,
8,PRP,Non-profit,
792,PRP,Non-profit,
404,LA,Local authority,
974,LA,Local authority,
62,PRP,Non-profit,
190,PRP,Non-profit,
260,PRP,Non-profit,
18,PRP,Non-profit,
77,PRP,Non-profit,
458,LA,Local authority,
975,PRP,Non-profit,
449,LA,Local authority,
411,LA,Local authority,
13,PRP,Non-profit,
1072,PRP,Non-profit,
171,PRP,Non-profit,
261,PRP,Non-profit,
172,LA,Local authority,
283,LA,Local authority,
737,LA,Local authority,
718,PRP,Local authority,
692,PRP,Non-profit,
753,PRP,Local authority,
842,PRP,Non-profit,
173,PRP,Non-profit,
267,PRP,Non-profit,
174,PRP,Non-profit,
784,PRP,Non-profit,
238,PRP,Non-profit,
720,PRP,Non-profit,
1039,PRP,Non-profit,
576,PRP,Non-profit,
176,PRP,Non-profit,
1009,PRP,Non-profit,
762,PRP,Non-profit,
957,PRP,Non-profit,
1048,PRP,Profit,
154,PRP,Non-profit,
947,PRP,Non-profit,
177,PRP,Non-profit,
178,PRP,Non-profit,
179,PRP,Non-profit,
981,PRP,Non-profit,
180,PRP,Non-profit,
584,PRP,Non-profit,
662,PRP,Non-profit,
849,PRP,Non-profit,
582,PRP,Non-profit,
1022,PRP,Non-profit,
868,PRP,Non-profit,
903,PRP,Profit,
1 id provider_type profit_status group
2 701 PRP Non-profit
3 1036 PRP Non-profit
4 1040 PRP Profit
5 950 PRP Non-profit
6 808 PRP Non-profit 2
7 952 PRP Non-profit 2
8 815 PRP Non-profit 2
9 311 PRP Non-profit
10 1038 PRP Non-profit
11 610 PRP Non-profit
12 743 PRP Non-profit
13 750 PRP Non-profit
14 642 PRP Profit
15 181 PRP Non-profit
16 568 PRP Non-profit
17 954 PRP Non-profit
18 994 PRP Non-profit
19 763 PRP Non-profit
20 878 PRP Non-profit
21 961 PRP Non-profit
22 908 LA Non-profit
23 723 PRP Non-profit
24 198 PRP Non-profit
25 895 PRP Non-profit
26 420 LA Local authority
27 470 PRP Non-profit
28 541 PRP Non-profit
29 82 PRP Non-profit
30 857 PRP Non-profit
31 807 PRP Non-profit
32 239 PRP Non-profit
33 83 PRP Non-profit
34 39 PRP Non-profit
35 707 PRP Non-profit
36 1037 PRP Non-profit
37 1006 PRP Local authority
38 765 PRP Non-profit
39 1033 PRP Non-profit
40 649 PRP Non-profit
41 805 PRP Non-profit
42 960 PRP Non-profit
43 985 PRP Non-profit
44 531 PRP Non-profit
45 84 PRP Non-profit
46 629 PRP Non-profit
47 199 PRP Non-profit
48 273 LA Local authority
49 827 PRP Profit
50 442 LA Local authority
51 409 LA Local authority
52 772 PRP Non-profit
53 314 PRP Non-profit
54 240 PRP Non-profit
55 624 PRP Non-profit
56 918 PRP Non-profit 4
57 888 PRP Non-profit 4
58 955 PRP Non-profit 4
59 907 PRP Non-profit
60 464 PRP Profit
61 468 PRP Non-profit
62 450 LA Local authority
63 790 PRP Local authority
64 1025 PRP Non-profit
65 874 PRP Non-profit
66 739 PRP Non-profit
67 556 PRP Non-profit
68 923 PRP Non-profit
69 200 PRP Non-profit
70 534 LA Local authority
71 806 LA Local authority
72 668 LA Local authority
73 934 PRP Non-profit
74 85 PRP Non-profit
75 2 PRP Non-profit
76 905 PRP Non-profit
77 524 PRP Non-profit
78 791 PRP Non-profit
79 86 PRP Non-profit
80 201 PRP Non-profit
81 640 PRP Non-profit
82 323 PRP Non-profit
83 693 LA Local authority
84 496 PRP Non-profit
85 481 PRP Non-profit
86 579 PRP Non-profit
87 322 LA Local authority
88 298 PRP Local authority
89 4 LA Local authority
90 490 PRP Non-profit
91 438 LA Non-profit
92 87 PRP Non-profit
93 88 PRP Non-profit
94 527 LA Local authority
95 877 PRP Local authority
96 818 PRP Non-profit
97 670 PRP Non-profit
98 766 PRP Non-profit
99 639 PRP Non-profit
100 940 PRP Non-profit
101 626 PRP Non-profit
102 460 LA Local authority
103 713 PRP Non-profit
104 89 PRP Non-profit
105 609 PRP Non-profit
106 422 LA Local authority
107 315 PRP Non-profit
108 90 PRP Non-profit
109 529 LA Local authority
110 999 PRP Non-profit
111 462 PRP Non-profit
112 914 PRP Non-profit
113 182 PRP Non-profit
114 933 PRP Non-profit
115 241 PRP Non-profit 5
116 959 PRP Non-profit 5
117 465 PRP Non-profit
118 518 LA Local authority
119 25 PRP Non-profit
120 34 LA Local authority
121 800 PRP Local authority
122 202 PRP Non-profit
123 544 PRP Non-profit
124 953 PRP Non-profit
125 637 PRP Non-profit
126 461 LA Local authority
127 880 PRP Local authority
128 510 PRP Non-profit
129 40 PRP Non-profit
130 401 LA Local authority
131 1015 PRP Local authority
132 429 LA Local authority
133 983 PRP Non-profit
134 652 PRP Non-profit
135 279 LA Local authority
136 275 PRP Non-profit
137 1054 PRP Non-profit
138 665 PRP Non-profit
139 704 PRP Non-profit
140 734 PRP Non-profit
141 408 LA Local authority
142 854 PRP Non-profit
143 700 PRP Non-profit
144 31 PRP Non-profit
145 191 PRP Non-profit
146 733 PRP Non-profit
147 870 PRP Non-profit
148 23 PRP Non-profit
149 430 LA Local authority
150 831 PRP Non-profit
151 844 PRP Non-profit
152 302 LA Local authority
153 286 PRP Non-profit
154 41 PRP Non-profit
155 858 PRP Non-profit
156 284 LA Local authority
157 493 PRP Non-profit
158 798 LA Local authority
159 335 LA Local authority
160 91 PRP Non-profit
161 661 PRP Non-profit
162 850 PRP Non-profit
163 66 PRP Non-profit
164 558 PRP Non-profit
165 511 PRP Non-profit
166 1071 PRP Non-profit
167 721 LA Local authority
168 307 LA Local authority
169 911 PRP Non-profit
170 454 LA Local authority
171 319 LA Local authority
172 295 LA Local authority
173 92 LA Local authority
174 659 PRP Non-profit
175 747 PRP Non-profit
176 5 PRP Non-profit
177 292 LA Local authority
178 316 PRP Non-profit
179 42 PRP Non-profit
180 242 PRP Non-profit
181 931 PRP Non-profit
182 759 PRP Non-profit
183 612 PRP Non-profit
184 658 PRP Profit
185 243 PRP Non-profit
186 1007 PRP Non-profit
187 43 PRP Non-profit
188 203 PRP Non-profit
189 497 LA Local authority
190 514 PRP Non-profit
191 24 PRP Non-profit
192 44 PRP Non-profit
193 204 PRP Non-profit
194 635 PRP Non-profit
195 813 PRP Non-profit
196 865 PRP Non-profit
197 715 PRP Non-profit
198 93 LA Local authority
199 817 PRP Non-profit
200 557 PRP Non-profit
201 746 PRP Non-profit
202 282 PRP Non-profit
203 618 PRP Non-profit
204 663 PRP Non-profit
205 821 PRP Non-profit
206 741 PRP Non-profit
207 403 LA Local authority
208 78 PRP Non-profit
209 415 LA Local authority
210 405 LA Local authority
211 977 PRP Non-profit
212 675 PRP Non-profit
213 525 LA Local authority
214 519 PRP Non-profit
215 94 PRP Non-profit
216 67 PRP Non-profit
217 485 PRP Non-profit
218 930 PRP Non-profit
219 95 PRP Non-profit
220 656 PRP Local authority
221 288 LA Local authority
222 192 LA Local authority
223 96 PRP Non-profit
224 205 PRP Non-profit
225 97 PRP Non-profit
226 413 LA Local authority
227 760 LA Local authority
228 447 LA Local authority
229 280 LA Local authority
230 300 PRP Local authority
231 6 PRP Non-profit
232 317 LA Local authority
233 512 PRP Non-profit
234 98 PRP Non-profit
235 70 PRP Non-profit
236 99 PRP Non-profit
237 29 PRP Non-profit
238 100 PRP Non-profit
239 1004 PRP Non-profit
240 101 PRP Non-profit
241 1011 LA Local authority
242 722 PRP Non-profit
243 1028 PRP Profit
244 559 PRP Non-profit
245 206 PRP Non-profit
246 965 LA Local authority
247 495 PRP Non-profit
248 207 PRP Non-profit
249 102 LA Local authority
250 912 PRP Non-profit
251 561 PRP Non-profit
252 103 PRP Non-profit
253 644 PRP Non-profit
254 452 LA Local authority
255 608 PRP Non-profit
256 472 PRP Non-profit
257 466 PRP Non-profit
258 104 PRP Non-profit
259 208 PRP Non-profit
260 105 PRP Non-profit
261 580 PRP Non-profit
262 835 PRP Non-profit
263 970 LA Local authority
264 276 PRP Non-profit
265 486 PRP Non-profit
266 951 PRP Non-profit
267 1003 PRP Non-profit
268 797 PRP Non-profit
269 548 PRP Profit
270 414 LA Local authority
271 748 PRP Non-profit
272 872 PRP Non-profit
273 777 PRP Non-profit
274 16 PRP Non-profit
275 263 PRP Non-profit
276 245 PRP Non-profit
277 630 PRP Profit
278 515 PRP Non-profit 3
279 244 PRP Non-profit 3
280 968 PRP Non-profit 3
281 634 PRP Non-profit
282 538 PRP Non-profit
283 521 LA Local authority
284 728 PRP Non-profit
285 799 PRP Non-profit
286 246 PRP Non-profit
287 1055 PRP Non-profit
288 889 PRP Non-profit
289 1034 PRP Profit
290 614 PRP Non-profit
291 893 PRP Non-profit
292 106 PRP Non-profit
293 247 PRP Non-profit
294 424 LA Local authority
295 45 PRP Non-profit
296 209 PRP Profit
297 716 PRP Non-profit
298 1062 PRP Profit
299 657 PRP Non-profit
300 833 PRP Non-profit
301 494 LA Local authority
302 473 PRP Non-profit
303 1012 PRP Non-profit
304 28 PRP Non-profit
305 572 PRP Non-profit
306 856 PRP Non-profit
307 969 PRP Non-profit 6
308 643 PRP Non-profit
309 1041 PRP Non-profit
310 418 LA Local authority
311 210 PRP Non-profit
312 551 PRP Profit
313 564 PRP Non-profit
314 476 PRP Non-profit
315 788 PRP Non-profit
316 46 PRP Non-profit
317 107 PRP Non-profit
318 881 PRP Non-profit
319 419 LA Local authority
320 653 PRP Non-profit
321 972 PRP Non-profit
322 814 PRP Non-profit
323 327 LA Local authority
324 502 PRP Non-profit
325 108 PRP Non-profit
326 769 PRP Non-profit
327 921 PRP Non-profit
328 731 PRP Non-profit
329 21 PRP Profit
330 65 PRP Non-profit
331 109 PRP Non-profit
332 47 PRP Non-profit
333 847 PRP Non-profit
334 860 PRP Non-profit
335 504 PRP Profit
336 265 PRP Profit
337 287 LA Local authority
338 980 PRP Non-profit
339 824 PRP Non-profit
340 546 PRP Non-profit
341 211 PRP Non-profit
342 416 LA Local authority
343 785 PRP Local authority
344 578 PRP Non-profit
345 71 PRP Non-profit
346 48 PRP Non-profit
347 672 PRP Non-profit
348 684 PRP Non-profit
349 650 PRP Non-profit
350 500 PRP Non-profit
351 332 LA Non-profit
352 266 PRP Non-profit
353 752 PRP Non-profit
354 702 PRP Non-profit
355 110 PRP Non-profit
356 694 PRP Non-profit
357 819 PRP Non-profit
358 620 PRP Non-profit
359 1010 PRP Non-profit
360 111 PRP Non-profit
361 853 PRP Non-profit 7
362 732 PRP Non-profit
363 112 PRP Non-profit
364 755 PRP Non-profit
365 655 PRP Non-profit
366 49 PRP Non-profit
367 574 PRP Non-profit
368 113 PRP Non-profit
369 823 PRP Non-profit
370 775 PRP Non-profit
371 706 PRP Non-profit
372 780 PRP Non-profit
373 822 PRP Non-profit
374 264 PRP Non-profit
375 887 PRP Non-profit
376 114 PRP Non-profit
377 115 PRP Non-profit
378 212 PRP Non-profit
379 1051 PRP Non-profit
380 290 LA Local authority
381 982 PRP Local authority
382 72 PRP Non-profit
383 575 PRP Non-profit
384 778 PRP Non-profit
385 1042 PRP Non-profit
386 477 PRP Non-profit
387 183 PRP Non-profit
388 767 PRP Non-profit
389 837 PRP Non-profit
390 272 PRP Non-profit
391 248 PRP Non-profit
392 990 PRP Non-profit
393 736 PRP Non-profit
394 988 PRP Non-profit
395 939 PRP Non-profit
396 116 PRP Non-profit
397 1017 PRP Non-profit
398 526 PRP Non-profit
399 214 PRP Non-profit
400 520 LA Local authority
401 117 PRP Non-profit
402 583 PRP Non-profit 7
403 118 LA Local authority
404 50 PRP Non-profit
405 709 PRP Non-profit
406 26 PRP Non-profit
407 11 PRP Non-profit
408 781 PRP Non-profit
409 421 LA Local authority
410 119 PRP Non-profit
411 73 PRP Non-profit
412 926 PRP Non-profit
413 51 PRP Non-profit
414 1001 PRP Non-profit
415 848 PRP Non-profit
416 679 PRP Non-profit
417 328 PRP Non-profit
418 428 LA Local authority
419 971 PRP Local authority
420 892 PRP Non-profit
421 553 PRP Non-profit
422 121 PRP Non-profit
423 758 PRP Non-profit
424 1019 PRP Profit
425 1020 PRP Profit
426 1064 PRP Profit
427 1065 PRP Profit
428 1066 PRP Profit
429 1067 PRP Profit
430 1068 PRP Non-profit
431 1063 PRP Profit
432 735 PRP Profit
433 1058 PRP Non-profit
434 435 LA Local authority
435 678 PRP Local authority
436 691 PRP Local authority
437 329 LA Local authority
438 836 PRP Non-profit
439 120 PRP Non-profit
440 250 PRP Non-profit
441 14 PRP Non-profit
442 927 LA Local authority
443 74 PRP Non-profit
444 122 PRP Non-profit
445 816 PRP Non-profit
446 184 PRP Non-profit
447 185 PRP Non-profit
448 978 PRP Non-profit
449 52 PRP Non-profit
450 621 PRP Non-profit
451 333 LA Local authority
452 193 LA Local authority
453 1053 LA Non-profit
454 308 LA Local authority
455 436 LA Local authority
456 310 LA Local authority
457 535 LA Local authority
458 517 LA Local authority
459 123 LA Local authority
460 79 LA Local authority
461 492 LA Local authority
462 859 LA Local authority
463 262 LA Local authority
464 498 LA Local authority
465 633 LA Local authority
466 437 LA Local authority
467 443 LA Local authority
468 124 LA Local authority
469 523 LA Local authority
470 278 LA Local authority
471 309 LA Local authority
472 194 LA Local authority
473 125 PRP Non-profit
474 761 PRP Non-profit
475 186 PRP Non-profit
476 811 PRP Non-profit
477 216 PRP Non-profit
478 425 LA Local authority
479 17 PRP Non-profit
480 942 PRP Profit
481 251 PRP Non-profit
482 919 LA Local authority
483 289 LA Local authority
484 787 PRP Non-profit
485 631 PRP Non-profit
486 771 PRP Non-profit
487 217 PRP Non-profit
488 68 PRP Non-profit
489 904 PRP Profit
490 126 LA Local authority
491 445 LA Local authority
492 963 PRP Local authority
493 1045 PRP Non-profit
494 573 PRP Non-profit 5
495 127 PRP Non-profit
496 613 PRP Non-profit
497 632 PRP Non-profit
498 1030 PRP Non-profit
499 730 PRP Non-profit
500 509 LA Local authority
501 417 LA Local authority
502 616 PRP Non-profit
503 997 PRP Non-profit
504 689 LA Local authority
505 862 PRP Non-profit
506 682 PRP Non-profit
507 964 PRP Non-profit
508 681 PRP Non-profit
509 1056 PRP Non-profit
510 796 PRP Non-profit
511 719 PRP Non-profit
512 505 PRP Non-profit
513 1049 PRP Non-profit
514 128 PRP Non-profit
515 487 PRP Non-profit
516 1043 PRP Non-profit
517 932 PRP Non-profit
518 846 PRP Non-profit
519 998 PRP Non-profit
520 129 PRP Non-profit
521 1027 PRP Non-profit
522 130 PRP Non-profit
523 867 PRP Non-profit
524 407 LA Local authority
525 625 PRP Non-profit
526 53 PRP Non-profit
527 131 PRP Non-profit
528 549 PRP Non-profit
529 915 PRP Non-profit
530 132 PRP Non-profit
531 710 PRP Profit
532 439 LA Local authority
533 325 LA Local authority
534 738 PRP Non-profit
535 727 PRP Non-profit
536 906 PRP Non-profit
537 690 PRP Non-profit
538 469 PRP Non-profit
539 296 LA Local authority
540 793 PRP Non-profit
541 336 LA Local authority
542 218 PRP Non-profit
543 133 LA Local authority
544 973 PRP Local authority
545 885 PRP Non-profit
546 536 LA Local authority
547 80 LA Local authority
548 440 LA Local authority
549 456 LA Local authority
550 789 PRP Local authority
551 297 PRP Local authority
552 677 PRP Non-profit
553 277 PRP Local authority
554 38 PRP Local authority
555 830 PRP Non-profit
556 301 LA Local authority
557 75 PRP Non-profit
558 770 PRP Non-profit
559 686 PRP Non-profit
560 537 LA Local authority
561 533 PRP Non-profit
562 698 PRP Non-profit
563 717 PRP Non-profit
564 69 PRP Non-profit
565 948 PRP Non-profit
566 444 LA Local authority
567 402 LA Local authority
568 986 PRP Non-profit
569 252 PRP Non-profit
570 253 PRP Non-profit
571 219 PRP Non-profit
572 820 LA Local authority
573 254 PRP Non-profit
574 324 PRP Non-profit
575 845 PRP Non-profit
576 705 PRP Non-profit
577 554 PRP Non-profit
578 712 PRP Non-profit
579 255 PRP Non-profit
580 651 PRP Non-profit
581 699 PRP Non-profit
582 987 PRP Non-profit
583 779 PRP Non-profit
584 886 PRP Non-profit
585 195 LA Local authority
586 869 PRP Non-profit
587 683 PRP Non-profit
588 917 PRP Non-profit
589 688 PRP Non-profit
590 654 PRP Non-profit
591 134 PRP Non-profit
592 326 PRP Non-profit
593 555 PRP Profit
594 1069 PRP Non-profit
595 1060 PRP Non-profit
596 696 PRP Non-profit
597 782 PRP Non-profit
598 839 PRP Non-profit
599 946 PRP Profit
600 220 PRP Non-profit
601 221 PRP Non-profit
602 1000 PRP Non-profit 7
603 222 PRP Non-profit
604 294 PRP Profit
605 863 PRP Non-profit
606 562 PRP Non-profit
607 223 PRP Local authority
608 135 PRP Non-profit
609 669 PRP Non-profit
610 136 PRP Non-profit
611 137 PRP Non-profit
612 611 PRP Profit
613 891 PRP Profit
614 864 PRP Profit
615 989 PRP Non-profit 1
616 724 PRP Non-profit 1
617 680 PRP Non-profit 1
618 187 PRP Non-profit
619 479 PRP Profit
620 224 PRP Non-profit
621 501 PRP Non-profit
622 499 PRP Non-profit
623 225 PRP Non-profit
624 852 PRP Non-profit
625 913 PRP Non-profit
626 451 LA Local authority
627 20 PRP Non-profit
628 958 PRP Non-profit
629 674 PRP Profit
630 10 PRP Non-profit
631 774 PRP Non-profit
632 54 PRP Non-profit
633 55 PRP Non-profit
634 944 PRP Non-profit
635 627 PRP Non-profit
636 949 PRP Non-profit
637 1005 PRP Non-profit
638 920 PRP Non-profit
639 1050 PRP Non-profit
640 138 PRP Non-profit
641 139 PRP Non-profit
642 729 PRP Non-profit
643 1018 PRP Non-profit
644 530 LA Local authority
645 1016 PRP Non-profit
646 140 PRP Non-profit
647 426 LA Local authority
648 226 PRP Non-profit
649 812 PRP Non-profit
650 809 PRP Non-profit
651 545 LA Local authority
652 1052 PRP Local authority
653 1035 PRP Non-profit
654 873 PRP Profit 6
655 764 PRP Profit 6
656 141 PRP Non-profit
657 884 PRP Non-profit
658 896 PRP Non-profit
659 142 PRP Non-profit
660 1013 PRP Non-profit
661 890 PRP Non-profit
662 330 PRP Non-profit
663 482 PRP Non-profit
664 615 PRP Non-profit
665 1057 PRP Profit
666 227 PRP Non-profit
667 459 LA Local authority
668 305 LA Local authority
669 648 PRP Local authority
670 607 PRP Non-profit
671 803 LA Local authority
672 423 LA Local authority
673 448 LA Local authority
674 507 LA Local authority
675 412 LA Local authority
676 834 PRP Local authority
677 979 PRP Non-profit
678 306 PRP Non-profit
679 563 PRP Non-profit
680 996 PRP Non-profit
681 143 PRP Non-profit
682 646 PRP Non-profit
683 695 PRP Non-profit
684 188 PRP Non-profit
685 334 PRP Profit
686 993 PRP Profit
687 30 PRP Non-profit
688 671 PRP Non-profit
689 883 PRP Non-profit
690 776 PRP Non-profit
691 619 PRP Non-profit
692 714 PRP Non-profit
693 144 PRP Non-profit
694 196 LA Local authority
695 516 PRP Non-profit
696 664 PRP Non-profit
697 744 PRP Non-profit
698 1047 LA Non-profit
699 467 PRP Non-profit
700 189 PRP Non-profit
701 483 PRP Non-profit
702 506 PRP Non-profit
703 751 LA Local authority
704 567 PRP Non-profit
705 560 PRP Non-profit
706 56 PRP Non-profit
707 299 PRP Non-profit
708 37 LA Local authority
709 36 PRP Non-profit
710 35 PRP Non-profit
711 145 PRP Non-profit
712 1023 PRP Local authority
713 754 PRP Profit
714 749 PRP Profit
715 941 PRP Non-profit
716 33 PRP Non-profit
717 331 LA Local authority
718 956 PRP Non-profit
719 995 PRP Non-profit
720 27 PRP Non-profit
721 916 PRP Non-profit
722 882 PRP Non-profit
723 992 PRP Non-profit
724 925 PRP Non-profit
725 966 LA Local authority
726 910 PRP Non-profit
727 312 LA Local authority
728 146 LA Local authority
729 478 LA Local authority
730 228 PRP Non-profit
731 453 LA Local authority
732 539 LA Local authority
733 147 PRP Non-profit
734 256 PRP Non-profit
735 976 LA Local authority
736 318 PRP Local authority
737 285 LA Local authority
738 565 PRP Non-profit
739 427 LA Local authority
740 922 PRP Non-profit
741 641 PRP Non-profit
742 455 LA Local authority
743 57 PRP Non-profit
744 938 PRP Non-profit
745 840 PRP Non-profit
746 623 PRP Non-profit
747 257 PRP Non-profit
748 569 PRP Non-profit
749 552 LA Local authority
750 876 PRP Local authority
751 19 PRP Non-profit
752 1059 PRP Non-profit
753 58 PRP Non-profit
754 293 PRP Profit
755 148 PRP Non-profit
756 433 LA Local authority
757 149 PRP Non-profit
758 150 PRP Non-profit
759 1044 PRP Local authority
760 843 PRP Non-profit
761 1031 PRP Non-profit
762 281 PRP Non-profit
763 991 PRP Non-profit
764 929 PRP Non-profit
765 795 PRP Non-profit
766 742 PRP Non-profit
767 151 PRP Non-profit
768 1029 PRP Non-profit
769 685 PRP Non-profit
770 291 PRP Non-profit
771 152 PRP Non-profit
772 153 LA Local authority
773 463 PRP Non-profit
774 491 LA Local authority
775 871 PRP Local authority
776 532 LA Local authority
777 229 PRP Non-profit
778 1002 PRP Non-profit
779 666 PRP Non-profit
780 829 PRP Non-profit
781 697 PRP Non-profit
782 936 PRP Profit
783 570 PRP Local authority
784 410 LA Local authority
785 76 PRP Non-profit
786 855 PRP Non-profit
787 155 PRP Non-profit
788 304 PRP Non-profit
789 156 PRP Non-profit
790 1008 PRP Non-profit
791 875 PRP Non-profit
792 446 LA Local authority
793 879 PRP Local authority
794 786 PRP Non-profit 4
795 1032 PRP Non-profit
796 434 LA Local authority
797 157 PRP Non-profit
798 441 LA Local authority
799 230 PRP Non-profit
800 628 PRP Non-profit
801 828 PRP Non-profit
802 231 PRP Non-profit
803 158 PRP Non-profit
804 489 PRP Non-profit
805 1021 LA Local authority
806 159 PRP Non-profit
807 825 PRP Non-profit
808 708 PRP Non-profit
809 81 LA Local authority
810 197 PRP Non-profit
811 810 PRP Non-profit
812 962 PRP Non-profit
813 1070 PRP Non-profit
814 160 PRP Non-profit
815 22 PRP Non-profit
816 258 PRP Non-profit
817 984 PRP Non-profit
818 232 PRP Non-profit
819 1046 PRP Local authority
820 622 PRP Non-profit
821 943 PRP Non-profit
822 638 PRP Non-profit
823 660 PRP Non-profit
824 161 PRP Non-profit
825 12 PRP Non-profit
826 162 PRP Non-profit
827 1014 PRP Non-profit
828 667 PRP Non-profit
829 673 PRP Non-profit
830 480 PRP Non-profit
831 937 PRP Non-profit
832 543 PRP Non-profit
833 841 PRP Non-profit
834 1026 PRP Non-profit
835 794 PRP Non-profit
836 725 PRP Non-profit
837 59 PRP Non-profit
838 522 PRP Non-profit
839 945 PRP Non-profit
840 163 PRP Non-profit
841 897 PRP Non-profit
842 636 PRP Non-profit
843 475 PRP Non-profit
844 768 LA Local authority
845 645 PRP Non-profit
846 726 PRP Non-profit
847 1024 LA Non-profit
848 259 PRP Non-profit
849 547 PRP Non-profit
850 566 PRP Non-profit
851 1061 PRP Non-profit
852 550 PRP Non-profit
853 861 PRP Non-profit
854 832 PRP Non-profit
855 508 PRP Non-profit
856 233 PRP Non-profit
857 234 PRP Non-profit
858 488 PRP Non-profit
859 773 PRP Non-profit
860 164 PRP Non-profit
861 740 PRP Non-profit
862 804 PRP Non-profit
863 647 PRP Non-profit
864 32 PRP Non-profit
865 851 PRP Non-profit
866 703 PRP Non-profit
867 826 PRP Non-profit
868 577 PRP Non-profit
869 432 LA Local authority
870 571 PRP Local authority
871 909 PRP Non-profit
872 581 PRP Non-profit
873 617 PRP Non-profit
874 801 PRP Non-profit
875 935 PRP Non-profit
876 320 PRP Non-profit
877 165 PRP Non-profit
878 757 PRP Non-profit
879 166 PRP Non-profit
880 431 LA Local authority
881 235 PRP Non-profit
882 236 PRP Non-profit
883 167 PRP Non-profit
884 237 PRP Non-profit
885 313 LA Local authority
886 838 PRP Local authority
887 457 LA Local authority
888 474 PRP Non-profit
889 9 PRP Non-profit
890 60 PRP Non-profit
891 168 PRP Non-profit
892 406 LA Local authority
893 61 PRP Non-profit
894 484 PRP Non-profit
895 169 LA Local authority
896 967 PRP Local authority
897 274 LA Local authority
898 8 PRP Non-profit
899 792 PRP Non-profit
900 404 LA Local authority
901 974 LA Local authority
902 62 PRP Non-profit
903 190 PRP Non-profit
904 260 PRP Non-profit
905 18 PRP Non-profit
906 77 PRP Non-profit
907 458 LA Local authority
908 975 PRP Non-profit
909 449 LA Local authority
910 411 LA Local authority
911 13 PRP Non-profit
912 1072 PRP Non-profit
913 171 PRP Non-profit
914 261 PRP Non-profit
915 172 LA Local authority
916 283 LA Local authority
917 737 LA Local authority
918 718 PRP Local authority
919 692 PRP Non-profit
920 753 PRP Local authority
921 842 PRP Non-profit
922 173 PRP Non-profit
923 267 PRP Non-profit
924 174 PRP Non-profit
925 784 PRP Non-profit
926 238 PRP Non-profit
927 720 PRP Non-profit
928 1039 PRP Non-profit
929 576 PRP Non-profit
930 176 PRP Non-profit
931 1009 PRP Non-profit
932 762 PRP Non-profit
933 957 PRP Non-profit
934 1048 PRP Profit
935 154 PRP Non-profit
936 947 PRP Non-profit
937 177 PRP Non-profit
938 178 PRP Non-profit
939 179 PRP Non-profit
940 981 PRP Non-profit
941 180 PRP Non-profit
942 584 PRP Non-profit
943 662 PRP Non-profit
944 849 PRP Non-profit
945 582 PRP Non-profit
946 1022 PRP Non-profit
947 868 PRP Non-profit
948 903 PRP Profit

4
config/locales/en.yml

@ -220,6 +220,10 @@ en:
name_missing: "Enter the name of the organisation."
name_not_unique: "An organisation with this name already exists. Use the organisation that already exists or add a location or other identifier to the name. For example: Organisation name (City)."
provider_type_missing: "Select the organisation type."
group_missing: "Select a group member."
profit_status:
must_be_LA: "This organisation is a local authority, its profit status must also be local authority."
must_not_be_LA: "This organisation is a private registered provider, its profit status cannot be local authority."
stock_owner:
blank: "You must choose a stock owner."
already_added: "You have already added this stock owner."

10
db/migrate/20250227085622_add_new_question_fields_to_organisation.rb

@ -0,0 +1,10 @@
class AddNewQuestionFieldsToOrganisation < ActiveRecord::Migration[7.2]
def change
change_table :organisations, bulk: true do |t|
t.integer :profit_status
t.boolean :group_member
t.integer :group_member_id
t.integer :group
end
end
end

4
db/schema.rb

@ -559,6 +559,10 @@ ActiveRecord::Schema[7.2].define(version: 2025_04_16_111741) do
t.datetime "available_from"
t.datetime "discarded_at"
t.datetime "schemes_deduplicated_at"
t.integer "profit_status"
t.boolean "group_member"
t.integer "group_member_id"
t.integer "group"
t.index ["absorbing_organisation_id"], name: "index_organisations_on_absorbing_organisation_id"
t.index ["name"], name: "index_organisations_on_name", unique: true
t.index ["old_visible_id"], name: "index_organisations_on_old_visible_id", unique: true

45
lib/tasks/update_organisations_group_profit_status.rake

@ -0,0 +1,45 @@
namespace :data_update do
desc "Update organisations with data from a CSV file"
task :update_organisations, [:csv_path] => :environment do |_task, args|
require "csv"
csv_path = args[:csv_path]
unless csv_path
Rails.logger.error "Please provide the path to the CSV file. Example: rake data_update:update_organisations[csv_path]"
exit
end
CSV.foreach(csv_path, headers: true) do |row|
organisation = Organisation.find_by(id: row["id"].to_i)
if organisation
organisation.update!(
profit_status: map_profit_status(row["profit_status"]),
)
# not all orgs have a group
if row["group"].present?
organisation.update!(
group_member: true,
group: row["group"].to_i,
# normally set to the ID of the other organisation you picked on the group form
# we can't set that here so we default to its own org ID
group_member_id: organisation.id,
)
end
Rails.logger.info "Updated ORG#{row['id']}"
else
Rails.logger.warn "Organisation with ID #{row['id']} not found"
end
end
Rails.logger.info "Organisation update task completed"
end
end
def map_profit_status(profit_status)
return :non_profit if profit_status == "Non-profit"
return :profit if profit_status == "Profit"
return :local_authority if profit_status == "Local authority"
nil
end

38
spec/features/organisation_spec.rb

@ -43,6 +43,11 @@ RSpec.describe "User Features" do
expect(page).to have_current_path("/organisations/#{org_id}/details")
end
it "does not allow coordinator users to edit their organisation's group and profit status" do
expect(page).to have_no_link("Change part of group")
expect(page).to have_no_link("Select profit status")
end
context "and the organisation does not hold housing stock" do
before do
organisation.update(holds_own_stock: false)
@ -365,5 +370,38 @@ RSpec.describe "User Features" do
expect(page).not_to have_link("Schemes", href: "/schemes", count: 2)
end
end
context "and is creating a new organisation" do
before do
visit("/organisations")
click_link("Create a new organisation")
end
it "displays the new organisation form" do
expect(page).to have_content("Create a new organisation")
expect(page).to have_field("organisation[name]", type: "text")
expect(page).to have_field("organisation[address_line1]", type: "text")
expect(page).to have_field("organisation[address_line2]", type: "text")
expect(page).to have_field("organisation[postcode]", type: "text")
expect(page).to have_field("organisation[phone]")
expect(page).to have_field("organisation[housing_registration_no]", type: "text")
expect(page).to have_select("organisation[provider_type]")
expect(page).to have_field("organisation[holds_own_stock]", type: "radio")
expect(page).to have_field("organisation[group_member]", type: "radio")
expect(page).to have_select("organisation[profit_status]")
expect(page).to have_button("Create organisation")
end
end
context "when viewing a specific organisation's details page" do
before do
visit("/organisations/#{org_id}/details")
end
it "allows the support user to edit the organisation's group and profit status" do
expect(page).to have_link("Change part of group")
expect(page).to have_link("Select profit status")
end
end
end
end

4
spec/fixtures/exports/organisation.xml vendored

@ -16,12 +16,12 @@
<merge_date/>
<absorbing_organisation_id/>
<available_from/>
<profit_status/>
<group/>
<deleted_at/>
<dsa_signed>true</dsa_signed>
<dsa_signed_at>{dsa_signed_at}</dsa_signed_at>
<dpo_email>{dpo_email}</dpo_email>
<profit_status/>
<group/>
<status>active</status>
</form>
</forms>

2
spec/fixtures/files/organisations_group_profit_status_invalid.csv vendored

@ -0,0 +1,2 @@
id,provider_type,profit_status,group
2000,Profit,4
1 id,provider_type,profit_status,group
2 2000,Profit,4

4
spec/fixtures/files/organisations_group_profit_status_valid.csv vendored

@ -0,0 +1,4 @@
id,provider_type,profit_status,group
234,LA,Local authority,2
750,PRP,Non-profit,2
642,PRP,Profit,
1 id provider_type profit_status group
2 234 LA Local authority 2
3 750 PRP Non-profit 2
4 642 PRP Profit

62
spec/helpers/organisations_helper_spec.rb

@ -3,10 +3,12 @@ require "rails_helper"
RSpec.describe OrganisationsHelper do
include TagHelper
describe "display_organisation_attributes" do
let(:organisation) { create(:organisation, :la, :holds_own_stock, address_line1: "2 Marsham Street", address_line2: "London", postcode: "SW1P 4DF", housing_registration_no: 1234, organisation_rent_periods: []) }
let(:support_user) { create(:user, :support) }
let(:coordinator_user) { create(:user, :data_coordinator) }
let(:organisation) { create(:organisation, :la, :holds_own_stock, address_line1: "2 Marsham Street", address_line2: "London", postcode: "SW1P 4DF", housing_registration_no: 1234, organisation_rent_periods: [], group_member: true, group_member_id: 99, group: 1) }
it "has the correct values" do
expect(display_organisation_attributes(organisation)).to eq(
it "has the correct values and editable status for support users" do
expect(display_organisation_attributes(support_user, organisation)).to eq(
[{ editable: false, name: "Organisation ID", value: "ORG#{organisation.id}" },
{ editable: true,
name: "Address",
@ -15,6 +17,28 @@ RSpec.describe OrganisationsHelper do
{ editable: false, name: "Registration number", value: "1234" },
{ editable: false, name: "Type of provider", value: "Local authority" },
{ editable: false, name: "Owns housing stock", value: "Yes" },
{ editable: true, name: "Part of group", value: "Yes" },
{ editable: true, name: "Group number", value: "GROUP1" },
{ editable: true, name: "For profit", value: "" },
{ editable: true, format: :bullet, name: "Rent periods", value: [] },
{ name: "Data Sharing Agreement" },
{ editable: false, name: "Status", value: status_tag(organisation.status) }],
)
end
it "has the correct values and editable status for non-support users" do
expect(display_organisation_attributes(coordinator_user, organisation)).to eq(
[{ editable: false, name: "Organisation ID", value: "ORG#{organisation.id}" },
{ editable: true,
name: "Address",
value: "2 Marsham Street\nLondon\nSW1P 4DF" },
{ editable: true, name: "Telephone number", value: nil },
{ editable: false, name: "Registration number", value: "1234" },
{ editable: false, name: "Type of provider", value: "Local authority" },
{ editable: false, name: "Owns housing stock", value: "Yes" },
{ editable: false, name: "Part of group", value: "Yes" },
{ editable: false, name: "Group number", value: "GROUP1" },
{ editable: false, name: "For profit", value: "" },
{ editable: true, format: :bullet, name: "Rent periods", value: [] },
{ name: "Data Sharing Agreement" },
{ editable: false, name: "Status", value: status_tag(organisation.status) }],
@ -56,4 +80,36 @@ RSpec.describe OrganisationsHelper do
end
end
end
describe "#group_organisation_options_name" do
let(:older_org) { create(:organisation, group_member: true, group_member_id: 123, group: 9) }
let(:org) { create(:organisation, name: "Org1", group_member: true, group_member_id: older_org.id) }
it "returns the organisation name with group text" do
allow(org).to receive(:oldest_group_member).and_return(older_org)
name = helper.group_organisation_options_name(org)
expect(name).to eq("Org1 (GROUP9)")
end
end
describe "#profit_status_options" do
it "returns a list of profit statuses with a null option" do
options = helper.profit_status_options
expect(options.map(&:name)).to include("Select an option")
end
context "when provider type is LA" do
it "returns only the local authority option" do
options = helper.profit_status_options("LA")
expect(options.map(&:id)).to eq(["", :local_authority])
end
end
context "when provider type is PRP" do
it "excludes the local authority option" do
options = helper.profit_status_options("PRP")
expect(options.map(&:id)).not_to include(:local_authority)
end
end
end
end

63
spec/lib/tasks/update_organisations_group_profit_status_spec.rb

@ -0,0 +1,63 @@
require "rails_helper"
require "rake"
RSpec.describe "data_update:update_organisations", type: :task do
let(:task) { Rake::Task["data_update:update_organisations"] }
before do
Rake.application.rake_require("tasks/update_organisations_group_profit_status")
Rake::Task.define_task(:environment)
task.reenable
end
context "when the CSV file is valid" do
let!(:organisation_la) { create(:organisation, id: 234, provider_type: "LA") }
let!(:organisation_prp_non_profit) { create(:organisation, id: 750, provider_type: "PRP") }
let!(:organisation_prp_profit) { create(:organisation, id: 642, provider_type: "PRP") }
let(:csv_path) { Rails.root.join("spec/fixtures/files/organisations_group_profit_status_valid.csv") }
it "updates an organisation profit status field" do
expect {
task.invoke(csv_path.to_s)
}
.to change { organisation_la.reload.profit_status }.to("local_authority")
.and change { organisation_prp_non_profit.reload.profit_status }.to("non_profit")
.and change { organisation_prp_profit.reload.profit_status }.to("profit")
end
it "updates an organisation group fields" do
task.invoke(csv_path.to_s)
organisation_la.reload
organisation_prp_non_profit.reload
organisation_prp_profit.reload
expect(organisation_la.group).to eq(2)
expect(organisation_la.group_member).to be_truthy
expect(organisation_la.group_member_id).to eq(organisation_la.id)
expect(organisation_prp_non_profit.group).to eq(2)
expect(organisation_prp_non_profit.group_member).to be_truthy
expect(organisation_prp_non_profit.group_member_id).to eq(organisation_prp_non_profit.id)
expect(organisation_prp_profit.group).to be_nil
expect(organisation_prp_profit.group_member).to be_falsy
expect(organisation_prp_profit.group_member_id).to be_nil
end
end
context "when the organisation is not found" do
let(:csv_path) { Rails.root.join("spec/fixtures/files/organisations_group_profit_status_invalid.csv") }
it "logs a warning" do
expect(Rails.logger).to receive(:warn).with("Organisation with ID 2000 not found")
task.invoke(csv_path.to_s)
end
end
context "when the CSV path is not provided" do
it "logs an error and exits" do
expect(Rails.logger).to receive(:error).with("Please provide the path to the CSV file. Example: rake data_update:update_organisations[csv_path]")
expect { task.invoke }.to raise_error(SystemExit)
end
end
end

20
spec/services/exports/lettings_log_export_service_spec.rb

@ -102,7 +102,7 @@ RSpec.describe Exports::LettingsLogExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_lettings_logs
@ -113,7 +113,7 @@ RSpec.describe Exports::LettingsLogExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_lettings_logs
@ -143,7 +143,7 @@ RSpec.describe Exports::LettingsLogExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_lettings_logs
@ -188,7 +188,7 @@ RSpec.describe Exports::LettingsLogExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_lettings_logs
@ -271,7 +271,7 @@ RSpec.describe Exports::LettingsLogExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_lettings_logs
@ -390,7 +390,7 @@ RSpec.describe Exports::LettingsLogExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
expect(export_service.export_xml_lettings_logs).to eq({ expected_zip_filename.gsub(".zip", "") => start_time })
@ -410,7 +410,7 @@ RSpec.describe Exports::LettingsLogExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_lettings_logs
@ -441,7 +441,7 @@ RSpec.describe Exports::LettingsLogExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_lettings_logs
@ -473,7 +473,7 @@ RSpec.describe Exports::LettingsLogExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_lettings_logs
@ -504,7 +504,7 @@ RSpec.describe Exports::LettingsLogExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_lettings_logs(full_update: true)
end

14
spec/services/exports/organisation_export_service_spec.rb

@ -65,7 +65,7 @@ RSpec.describe Exports::OrganisationExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_organisations
@ -76,7 +76,7 @@ RSpec.describe Exports::OrganisationExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_organisations
@ -100,7 +100,7 @@ RSpec.describe Exports::OrganisationExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_organisations
@ -121,7 +121,7 @@ RSpec.describe Exports::OrganisationExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_organisations
@ -145,7 +145,7 @@ RSpec.describe Exports::OrganisationExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_organisations
@ -164,7 +164,7 @@ RSpec.describe Exports::OrganisationExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_organisations
@ -277,7 +277,7 @@ RSpec.describe Exports::OrganisationExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
expect(export_service.export_xml_organisations).to eq({ expected_zip_filename.gsub(".zip", "") => start_time })

16
spec/services/exports/sales_log_export_service_spec.rb

@ -87,7 +87,7 @@ RSpec.describe Exports::SalesLogExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_sales_logs
@ -98,7 +98,7 @@ RSpec.describe Exports::SalesLogExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_sales_logs
@ -183,7 +183,7 @@ RSpec.describe Exports::SalesLogExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_sales_logs
@ -304,7 +304,7 @@ RSpec.describe Exports::SalesLogExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
expect(export_service.export_xml_sales_logs).to eq({ expected_zip_filename.gsub(".zip", "") => start_time })
@ -324,7 +324,7 @@ RSpec.describe Exports::SalesLogExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_sales_logs
@ -355,7 +355,7 @@ RSpec.describe Exports::SalesLogExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_sales_logs(full_update: true, collection_year: 2024)
@ -377,7 +377,7 @@ RSpec.describe Exports::SalesLogExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_sales_logs
@ -414,7 +414,7 @@ RSpec.describe Exports::SalesLogExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_sales_logs

10
spec/services/exports/user_export_service_spec.rb

@ -64,7 +64,7 @@ RSpec.describe Exports::UserExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_users
@ -75,7 +75,7 @@ RSpec.describe Exports::UserExportService 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.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_users
@ -97,7 +97,7 @@ RSpec.describe Exports::UserExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_users
@ -210,7 +210,7 @@ RSpec.describe Exports::UserExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
expect(export_service.export_xml_users).to eq({ expected_zip_filename.gsub(".zip", "") => start_time })
@ -229,7 +229,7 @@ RSpec.describe Exports::UserExportService 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_manifest_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
expect(export_service.export_xml_users).to eq({ expected_zip_filename.gsub(".zip", "") => start_time })

22
spec/spec_helper.rb

@ -126,3 +126,25 @@ RSpec.configure do |config|
end
RSpec::Matchers.define_negated_matcher :not_change, :change
def fix_xml_content_order(xml_file_content)
# split the content up my lines, sort them, then join together again
# this ensures the order of the output elements doesn't matter for the tests
# as the tests care about the content of the XML, not the order
xml_file_content.split("\n").sort.join("\n")
end
# write a matcher that checks if two strings are equal after apply fix_xml_content_order to both
RSpec::Matchers.define :have_same_xml_contents_as do |expected|
match do |actual|
fix_xml_content_order(actual) == fix_xml_content_order(expected)
end
failure_message do |actual|
"expected that unordered #{fix_xml_content_order(actual)} would be equal to unordered #{fix_xml_content_order(expected)}"
end
failure_message_when_negated do |actual|
"expected that unordered #{fix_xml_content_order(actual)} would not be equal to unordered #{fix_xml_content_order(expected)}"
end
end

Loading…
Cancel
Save