Browse Source

CLDC-4236: reset to e6962f3222

CLDC-4236-temp-revert-pipeline-updates
Nat Dean-Lewis 1 week ago
parent
commit
dff42ac2d5
  1. 15
      .github/workflows/aws_deploy.yml
  2. 34
      .github/workflows/manual_review_code_pipeline.yml
  3. 1
      .github/workflows/production_pipeline.yml
  4. 151
      .github/workflows/review_pipeline.yml
  5. 67
      .github/workflows/review_teardown_pipeline.yml
  6. 4
      app/helpers/bulk_upload/sales_log_to_csv.rb
  7. 1
      app/models/form/sales/subsections/shared_ownership_staircasing_transaction.rb
  8. 4
      app/models/sales_log.rb
  9. 9
      app/models/validations/sales/financial_validations.rb
  10. 4
      app/services/bulk_upload/sales/year2026/csv_parser.rb
  11. 12
      app/services/bulk_upload/sales/year2026/row_parser.rb
  12. 6
      app/services/exports/sales_log_export_constants.rb
  13. 3
      app/services/exports/sales_log_export_service.rb
  14. 13
      config/locales/forms/2026/sales/sale_information.en.yml
  15. 4
      config/locales/validations/sales/financial.en.yml
  16. 6
      db/schema.rb
  17. 2
      spec/fixtures/exports/sales_log_26_27.xml
  18. 10
      spec/fixtures/files/2026_27_sales_bulk_upload.csv
  19. 6
      spec/fixtures/files/sales_logs_csv_export_codes_26.csv
  20. 6
      spec/fixtures/files/sales_logs_csv_export_labels_26.csv
  21. 6
      spec/fixtures/files/sales_logs_csv_export_non_support_codes_26.csv
  22. 6
      spec/fixtures/files/sales_logs_csv_export_non_support_labels_26.csv
  23. 2
      spec/fixtures/variable_definitions/sales_download_26_27.csv
  24. 2
      spec/lib/tasks/log_variable_definitions_spec.rb
  25. 1
      spec/models/form/sales/subsections/shared_ownership_staircasing_transaction_spec.rb
  26. 59
      spec/models/validations/sales/financial_validations_spec.rb
  27. 2
      spec/services/bulk_upload/sales/year2026/row_parser_spec.rb
  28. 101
      spec/services/exports/sales_log_export_service_spec.rb

15
.github/workflows/aws_deploy.yml

@ -22,6 +22,10 @@ on:
release_tag:
required: false
type: string
ref:
required: false
type: string
default: ""
concurrency:
group: deploy-${{ inputs.environment }}${{ inputs.concurrency_tag }}
@ -42,6 +46,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
@ -53,16 +59,19 @@ jobs:
id: ecr-login
uses: aws-actions/amazon-ecr-login@v2
- name: Resolve commit SHA
run: echo "commit_sha=${{ inputs.ref || github.sha }}" >> $GITHUB_ENV
- name: Check if image with tag already exists
run: |
echo "image-exists=$(if aws ecr list-images --repository-name=$repository --query "imageIds[*].imageTag" | grep -q ${{ github.sha }}; then echo true; else echo false; fi)" >> $GITHUB_ENV
echo "image-exists=$(if aws ecr describe-images --repository-name=$repository --image-ids imageTag=${{ env.commit_sha }} > /dev/null 2>&1; then echo true; else echo false; fi)" >> $GITHUB_ENV
- name: Build, tag, and push docker image to ECR if there is no image, failing for releases
id: build-image
if: ${{ env.image-exists == 'false' }}
env:
registry: ${{ steps.ecr-login.outputs.registry }}
commit_tag: ${{ github.sha }}
commit_tag: ${{ env.commit_sha }}
run: |
if [[ ${{ inputs.environment }} == 'production' ]]; then
echo "Error: Deployment to production environment is not allowed as there is no docker image (i.e. the AWS deploy on staging was unsuccessful for this commit)."
@ -100,7 +109,7 @@ jobs:
id: update-image-tags
env:
registry: ${{ steps.ecr-login.outputs.registry }}
commit_tag: ${{ github.sha }}
commit_tag: ${{ inputs.ref || github.sha }}
readable_tag: ${{ inputs.environment }}-${{ env.additional-tag }}
run: |
manifest=$(aws ecr batch-get-image --repository-name $repository --image-ids imageTag=$commit_tag --output text --query images[].imageManifest)

34
.github/workflows/manual_review_code_pipeline.yml

@ -1,29 +1,51 @@
name: Manual review app code pipeline
name: Manual review app build and deploy
concurrency:
group: review-${{ inputs.review_app_key }}
group: deploy-review${{ inputs.pr_number }}
on:
workflow_dispatch:
inputs:
review_app_key:
pr_number:
required: true
type: string
description: "The review app ID to deploy code for."
description: "The PR number of the review app to deploy code for. Note: this is NOT the ticket number"
permissions: {}
defaults:
run:
shell: bash
jobs:
get_pr_head_sha:
name: Get PR HEAD SHA
runs-on: ubuntu-latest
outputs:
pr_head_sha: ${{ steps.get_sha.outputs.pr_head_sha }}
steps:
- name: Get PR HEAD SHA
id: get_sha
uses: actions/github-script@v7
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: parseInt('${{ inputs.pr_number }}'),
});
core.setOutput('pr_head_sha', pr.head.sha);
code:
name: Deploy review app code
needs: [get_pr_head_sha]
uses: ./.github/workflows/aws_deploy.yml
with:
aws_account_id: 837698168072
aws_role_prefix: core-dev
aws_task_prefix: core-review-${{ inputs.review_app_key }}
concurrency_tag: ${{ inputs.review_app_key }}
aws_task_prefix: core-review-${{ inputs.pr_number }}
concurrency_tag: ${{ inputs.pr_number }}
environment: review
ref: ${{ needs.get_pr_head_sha.outputs.pr_head_sha }}
permissions:
id-token: write

1
.github/workflows/production_pipeline.yml

@ -3,7 +3,6 @@ name: Production CI/CD Pipeline
on:
release:
types: [released]
workflow_dispatch:
defaults:
run:

151
.github/workflows/review_pipeline.yml

@ -1,57 +1,162 @@
name: Review app pipeline
concurrency:
group: review-${{ github.event.pull_request.number }}
on:
pull_request:
types:
- opened
- synchronize
- reopened
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
required: true
type: string
description: "The number of the PR for which to deploy a review app. Note: this is NOT the ticket number"
pull_request:
types: [synchronize]
defaults:
run:
shell: bash
concurrency:
group: deploy-review${{ github.event.pull_request.number || inputs.pr_number || github.event.issue.number }}
permissions: {}
jobs:
get_pr_details:
name: Get PR details
if: github.event_name == 'workflow_dispatch' || (github.event.issue.pull_request && startsWith(github.event.comment.body, '/deploy-review')) || github.event_name == 'pull_request'
runs-on: ubuntu-latest
outputs:
pr_number: ${{ steps.get_pr_details.outputs.pr_number }}
pr_head_sha: ${{ steps.get_pr_details.outputs.pr_head_sha }}
steps:
- name: Get PR number and HEAD SHA
id: get_pr_details
uses: actions/github-script@v7
with:
script: |
let prNumber;
if (context.eventName === 'workflow_dispatch') {
prNumber = '${{ inputs.pr_number }}';
} else if (context.eventName === 'pull_request') {
prNumber = context.payload.pull_request.number.toString();
} else {
prNumber = context.issue.number.toString();
}
core.setOutput('pr_number', prNumber);
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: parseInt(prNumber),
});
core.setOutput('pr_head_sha', pr.head.sha);
check_deployment_started:
name: Check if deployment has been started
if: github.event_name == 'pull_request'
needs: [get_pr_details]
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
started: ${{ steps.check.outputs.started }}
steps:
- name: Check for previous deployment workflow runs
id: check
uses: actions/github-script@v7
with:
script: |
const prNumber = '${{ needs.get_pr_details.outputs.pr_number }}';
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(prNumber),
});
const deployComment = comments.find(c => c.body === 'Starting review app deployment...');
core.setOutput('started', deployComment ? 'true' : 'false');
deployment_started_comment:
name: Comment deployment started
if: github.event_name != 'pull_request'
needs: [get_pr_details]
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ needs.get_pr_details.outputs.pr_number }},
body: 'Starting review app deployment...',
});
infra:
name: Deploy review app infrastructure
if: github.event_name != 'pull_request'
needs: [get_pr_details]
uses: communitiesuk/submit-social-housing-lettings-and-sales-data-infrastructure/.github/workflows/create_review_app_infra.yml@main
with:
key: ${{ github.event.pull_request.number }}
key: ${{ needs.get_pr_details.outputs.pr_number }}
app_repo_role: arn:aws:iam::815624722760:role/core-application-repo
permissions:
id-token: write
code:
name: Deploy review app code
needs: [infra]
if: github.event_name != 'pull_request'
needs: [get_pr_details, infra]
uses: ./.github/workflows/aws_deploy.yml
with:
aws_account_id: 837698168072
aws_role_prefix: core-dev
aws_task_prefix: core-review-${{ needs.get_pr_details.outputs.pr_number }}
concurrency_tag: ${{ needs.get_pr_details.outputs.pr_number }}
environment: review
ref: ${{ needs.get_pr_details.outputs.pr_head_sha }}
permissions:
id-token: write
auto_update_code:
name: Auto-update review app code
if: github.event_name == 'pull_request' && needs.check_deployment_started.outputs.started == 'true'
needs: [get_pr_details, check_deployment_started]
uses: ./.github/workflows/aws_deploy.yml
with:
aws_account_id: 837698168072
aws_role_prefix: core-dev
aws_task_prefix: core-review-${{ github.event.pull_request.number }}
concurrency_tag: ${{ github.event.pull_request.number }}
aws_task_prefix: core-review-${{ needs.get_pr_details.outputs.pr_number }}
concurrency_tag: ${{ needs.get_pr_details.outputs.pr_number }}
environment: review
ref: ${{ needs.get_pr_details.outputs.pr_head_sha }}
permissions:
id-token: write
comment:
name: Add link to PR
needs: [code]
if: github.event_name != 'pull_request'
needs: [get_pr_details, code]
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Comment on PR with URL
uses: unsplash/comment-on-pr@v1.3.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/github-script@v7
with:
msg: "Created review app at https://review.submit-social-housing-data.communities.gov.uk/${{ github.event.pull_request.number }}. Note that the review app will be automatically deprovisioned after 30 days and will need the review app pipeline running again."
check_for_duplicate_msg: true
duplicate_msg_pattern: Created review app at*
script: |
const prNumber = ${{ needs.get_pr_details.outputs.pr_number }};
const body = `Created review app at https://review.submit-social-housing-data.communities.gov.uk/${prNumber}. Note that the review app will be automatically deprovisioned after 30 days and will need the review app pipeline running again.`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const duplicate = comments.find(c => c.body.startsWith('Created review app at'));
if (!duplicate) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: body,
});
}

67
.github/workflows/review_teardown_pipeline.yml

@ -1,27 +1,85 @@
name: Review app teardown pipeline
concurrency:
group: review-${{ github.event.pull_request.number }}
group: deploy-review${{ github.event.pull_request.number || inputs.pr_number }}
on:
pull_request:
types:
- closed
workflow_dispatch:
inputs:
pr_number:
required: true
type: string
description: "The PR number of the review app to tear down. Note: this is NOT the ticket number"
permissions: {}
env:
app_repo_role: arn:aws:iam::815624722760:role/core-application-repo
aws_account_id: 837698168072
aws_region: eu-west-2
aws_role_prefix: core-dev
aws_task_prefix: core-review-${{ github.event.pull_request.number }}
jobs:
get_pr_number:
name: Get PR number
runs-on: ubuntu-latest
outputs:
pr_number: ${{ steps.get.outputs.pr_number }}
steps:
- name: Get PR number
id: get
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "pr_number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
else
echo "pr_number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
fi
check_review_app_exists:
name: Check if review app exists
needs: [get_pr_number]
runs-on: ubuntu-latest
permissions:
id-token: write
outputs:
exists: ${{ steps.check.outputs.exists }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: ${{ env.app_repo_role }}
- name: Configure AWS credentials for review environment
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/${{ env.aws_role_prefix }}-deployment
role-chaining: true
- name: Check if ECS service exists
id: check
env:
aws_task_prefix: core-review-${{ needs.get_pr_number.outputs.pr_number }}
run: |
if aws ecs describe-services --cluster ${{ env.aws_task_prefix }}-app --services ${{ env.aws_task_prefix }}-app --query "services[?status=='ACTIVE']" | grep -q 'serviceName'; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
database:
name: Drop database
if: needs.check_review_app_exists.outputs.exists == 'true'
needs: [get_pr_number, check_review_app_exists]
runs-on: ubuntu-latest
permissions:
id-token: write
env:
aws_task_prefix: core-review-${{ needs.get_pr_number.outputs.pr_number }}
steps:
- name: Configure AWS credentials
@ -55,10 +113,11 @@ jobs:
infra:
name: Teardown review app
needs: [database]
if: needs.check_review_app_exists.outputs.exists == 'true'
needs: [get_pr_number, check_review_app_exists, database]
uses: communitiesuk/submit-social-housing-lettings-and-sales-data-infrastructure/.github/workflows/destroy_review_app_infra.yml@main
with:
key: ${{ github.event.pull_request.number }}
key: ${{ needs.get_pr_number.outputs.pr_number }}
app_repo_role: arn:aws:iam::815624722760:role/core-application-repo
permissions:
id-token: write

4
app/helpers/bulk_upload/sales_log_to_csv.rb

@ -678,7 +678,9 @@ class BulkUpload::SalesLogToCsv
log.gender_same_as_sex5,
log.gender_description5,
log.gender_same_as_sex6,
log.gender_description6, # 134
log.gender_description6,
log.hasservicechargeschanged,
log.newservicecharges, # 136
]
end

1
app/models/form/sales/subsections/shared_ownership_staircasing_transaction.rb

@ -26,6 +26,7 @@ class Form::Sales::Subsections::SharedOwnershipStaircasingTransaction < ::Form::
Form::Sales::Pages::MonthlyRentStaircasingOwned.new(nil, nil, self),
Form::Sales::Pages::MonthlyRentStaircasing.new(nil, nil, self),
(Form::Sales::Pages::ServiceChargeStaircasing.new("service_charge_staircasing", nil, self) if form.start_year_2026_or_later?),
(Form::Sales::Pages::ServiceChargeChanged.new(nil, nil, self) if form.start_year_2026_or_later?),
Form::Sales::Pages::MonthlyChargesValueCheck.new("monthly_charges_shared_ownership_value_check", nil, self),
].compact
end

4
app/models/sales_log.rb

@ -583,4 +583,8 @@ class SalesLog < Log
def mscharge_value
mscharge if discounted_ownership_sale? || !form.start_year_2025_or_later?
end
def hasservicechargeschanged_label
form.get_question(:hasservicechargeschanged, self)&.label_from_value(hasservicechargeschanged)
end
end

9
app/models/validations/sales/financial_validations.rb

@ -139,6 +139,15 @@ module Validations::Sales::FinancialValidations
end
end
def validate_newservicecharges_different_from_mscharge(record)
return unless record.hasservicechargeschanged == 1 && record.newservicecharges && record.has_mscharge == 1 && record.mscharge
if record.newservicecharges == record.mscharge
record.errors.add :newservicecharges, I18n.t("validations.sales.financial.newservicecharges.same_as_previous")
record.errors.add :mscharge, I18n.t("validations.sales.financial.mscharge.same_as_new")
end
end
private
def is_relationship_child?(relationship)

4
app/services/bulk_upload/sales/year2026/csv_parser.rb

@ -4,7 +4,7 @@ class BulkUpload::Sales::Year2026::CsvParser
include CollectionTimeHelper
# TODO: CLDC-4162: Update when 2026 format is known
FIELDS = 134
FIELDS = 136
FORM_YEAR = 2026
attr_reader :path
@ -27,7 +27,7 @@ class BulkUpload::Sales::Year2026::CsvParser
def cols
# TODO: CLDC-4162: Update when 2026 format is known
@cols ||= ("A".."ED").to_a
@cols ||= ("A".."EF").to_a
end
def row_parsers

12
app/services/bulk_upload/sales/year2026/row_parser.rb

@ -149,6 +149,8 @@ class BulkUpload::Sales::Year2026::RowParser
field_132: "If 'No', enter person 5's gender identity",
field_133: "Is the gender person 6 identifies with the same as their sex registered at birth?",
field_134: "If 'No', enter person 6's gender identity",
field_135: "Will the service charge change after this staircasing transaction takes place?",
field_136: "What are the new total monthly service charges for the property?",
}.freeze
ERROR_BASE_KEY = "validations.sales.2026.bulk_upload".freeze
@ -328,6 +330,9 @@ class BulkUpload::Sales::Year2026::RowParser
attribute :field_133, :integer
attribute :field_134, :string
attribute :field_135, :integer
attribute :field_136, :decimal
validates :field_1,
presence: {
message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "sale completion date (day)."),
@ -863,6 +868,7 @@ private
sexrab4: %i[field_48],
sexrab5: %i[field_52],
sexrab6: %i[field_56],
buildheightclass: %i[field_122],
gender_same_as_sex1: %i[field_123],
@ -877,6 +883,9 @@ private
gender_description5: %i[field_132],
gender_same_as_sex6: %i[field_133],
gender_description6: %i[field_134],
hasservicechargeschanged: %i[field_135],
newservicecharges: %i[field_136],
}
end
@ -926,6 +935,9 @@ private
attributes["gender_same_as_sex6"] = field_133
attributes["gender_description6"] = field_134
attributes["hasservicechargeschanged"] = field_135
attributes["newservicecharges"] = field_136
attributes["relat2"] = relationship_from_is_partner(field_34)
attributes["relat3"] = relationship_from_is_partner(field_42)
attributes["relat4"] = relationship_from_is_partner(field_46)

6
app/services/exports/sales_log_export_constants.rb

@ -157,7 +157,11 @@ module Exports::SalesLogExportConstants
YEAR_2025_EXPORT_FIELDS << "SEX#{index}"
end
YEAR_2026_EXPORT_FIELDS = Set["BUILDHEIGHTCLASS"]
YEAR_2026_EXPORT_FIELDS = Set[
"BUILDHEIGHTCLASS",
"HASSERVICECHARGESCHANGED",
"NEWSERVICECHARGES",
]
(1..6).each do |index|
YEAR_2026_EXPORT_FIELDS << "SEXRAB#{index}"

3
app/services/exports/sales_log_export_service.rb

@ -135,6 +135,9 @@ module Exports
attribute_hash["hasestatefee"] = sales_log.has_management_fee
attribute_hash["estatefee"] = sales_log.management_fee
attribute_hash["hasservicechargeschanged"] = sales_log.hasservicechargeschanged
attribute_hash["newservicecharges"] = sales_log.newservicecharges
attribute_hash["stairlastday"] = sales_log.lasttransaction&.day
attribute_hash["stairlastmonth"] = sales_log.lasttransaction&.month
attribute_hash["stairlastyear"] = sales_log.lasttransaction&.year

13
config/locales/forms/2026/sales/sale_information.en.yml

@ -285,6 +285,19 @@ en:
hint_text: ""
question_text: "Enter the total monthly charge"
servicecharges_changed:
page_header: ""
has_service_charges_changed:
check_answer_label: "Service charge will change"
check_answer_prompt: "Tell us if the service charge will change"
hint_text: "This includes any charges for day-to-day maintenance and repairs, building insurance, and any contributions to a sinking or reserved fund. It does not include estate management fees."
question_text: "Will the service charge change after this staircasing transaction takes place?"
new_service_charges:
check_answer_label: "New monthly service charges"
check_answer_prompt: ""
hint_text: ""
question_text: "Enter the new total monthly charge"
purchase_price:
discounted_ownership:
page_header: "About the price of the property"

4
config/locales/validations/sales/financial.en.yml

@ -48,6 +48,10 @@ en:
mscharge:
monthly_leasehold_charges:
not_zero: "Monthly leasehold charges cannot be £0 if the property has monthly charges."
same_as_new: "You said that the service charge will change and you entered the same amount as the new service charge. If the service charge will not change, answer 'No' to that question before entering the service charge here."
newservicecharges:
same_as_previous: "You said that the service charge will change and you entered the same amount as the previous question. If the service charge will not change, answer 'No'."
resale:
equity_over_max: "The maximum initial equity stake is %{max_equity}%."

6
db/schema.rb

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2026_02_25_135309) do
ActiveRecord::Schema[7.2].define(version: 2026_03_05_095832) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -824,8 +824,10 @@ ActiveRecord::Schema[7.2].define(version: 2026_02_25_135309) do
t.string "sexrab4"
t.string "sexrab5"
t.string "sexrab6"
t.integer "mortlen_known"
t.integer "buildheightclass"
t.integer "mortlen_known"
t.integer "hasservicechargeschanged"
t.decimal "newservicecharges", precision: 10, scale: 2
t.integer "gender_same_as_sex1"
t.integer "gender_same_as_sex2"
t.integer "gender_same_as_sex3"

2
spec/fixtures/exports/sales_log_26_27.xml vendored

@ -163,5 +163,7 @@
<MSCHARGE_VALUE_CHECK/>
<DUPLICATESET/>
<STAIRCASETOSALE/>
<HASSERVICECHARGESCHANGED/>
<NEWSERVICECHARGES/>
</form>
</forms>

10
spec/fixtures/files/2026_27_sales_bulk_upload.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/sales_logs_csv_export_codes_26.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/sales_logs_csv_export_labels_26.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/sales_logs_csv_export_non_support_codes_26.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/sales_logs_csv_export_non_support_labels_26.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/variable_definitions/sales_download_26_27.csv vendored

@ -17,3 +17,5 @@ gender_same_as_sex5,Is the gender person 5 identifies with the same as their sex
gender_description5,If 'No', enter person 5's gender identity
gender_same_as_sex6,Is the gender person 6 identifies with the same as their sex registered at birth?
gender_description6,If 'No', enter person 6's gender identity
hasservicechargeschanged,Will the service charge change after this staircasing transaction takes place?
newservicecharges,What are the new total monthly service charges for the property?

1 sexrab1,What was buyer 1's sex at birth?
17 gender_description5,If 'No', enter person 5's gender identity
18 gender_same_as_sex6,Is the gender person 6 identifies with the same as their sex registered at birth?
19 gender_description6,If 'No', enter person 6's gender identity
20 hasservicechargeschanged,Will the service charge change after this staircasing transaction takes place?
21 newservicecharges,What are the new total monthly service charges for the property?

2
spec/lib/tasks/log_variable_definitions_spec.rb

@ -6,7 +6,7 @@ RSpec.describe "log_variable_definitions" do
subject(:task) { Rake::Task["data_import:add_variable_definitions"] }
let(:path) { "spec/fixtures/variable_definitions" }
let(:total_variable_definitions_count) { 463 }
let(:total_variable_definitions_count) { 465 }
before do
Rake.application.rake_require("tasks/log_variable_definitions")

1
spec/models/form/sales/subsections/shared_ownership_staircasing_transaction_spec.rb

@ -77,6 +77,7 @@ RSpec.describe Form::Sales::Subsections::SharedOwnershipStaircasingTransaction,
monthly_rent_staircasing_owned
monthly_rent_staircasing
service_charge_staircasing
service_charge_changed
monthly_charges_shared_ownership_value_check
],
)

59
spec/models/validations/sales/financial_validations_spec.rb

@ -478,4 +478,63 @@ RSpec.describe Validations::Sales::FinancialValidations do
expect(record.errors).to be_empty
end
end
describe "#validate_newservicecharges_different_from_mscharge" do
let(:record) { FactoryBot.build(:sales_log, ownershipsch: 1, staircase: 1) }
it "does not add errors when hasservicechargeschanged is nil" do
record.hasservicechargeschanged = nil
record.newservicecharges = 100
record.has_mscharge = 1
record.mscharge = 100
financial_validator.validate_newservicecharges_different_from_mscharge(record)
expect(record.errors).to be_empty
end
it "does not add errors when hasservicechargeschanged is 2 (No)" do
record.hasservicechargeschanged = 2
record.newservicecharges = 100
record.has_mscharge = 1
record.mscharge = 100
financial_validator.validate_newservicecharges_different_from_mscharge(record)
expect(record.errors).to be_empty
end
it "does not add errors when newservicecharges is nil" do
record.hasservicechargeschanged = 1
record.newservicecharges = nil
record.has_mscharge = 1
record.mscharge = 100
financial_validator.validate_newservicecharges_different_from_mscharge(record)
expect(record.errors).to be_empty
end
it "does not add errors when mscharge is nil" do
record.hasservicechargeschanged = 1
record.newservicecharges = 100
record.has_mscharge = 2
record.mscharge = nil
financial_validator.validate_newservicecharges_different_from_mscharge(record)
expect(record.errors).to be_empty
end
it "does not add errors when newservicecharges is different from mscharge" do
record.hasservicechargeschanged = 1
record.newservicecharges = 150
record.has_mscharge = 1
record.mscharge = 100
financial_validator.validate_newservicecharges_different_from_mscharge(record)
expect(record.errors).to be_empty
end
it "adds an error when hasservicechargeschanged is 1 (Yes) and newservicecharges equals mscharge" do
record.hasservicechargeschanged = 1
record.newservicecharges = 100
record.has_mscharge = 1
record.mscharge = 100
financial_validator.validate_newservicecharges_different_from_mscharge(record)
expect(record.errors["newservicecharges"]).to include(match I18n.t("validations.sales.financial.newservicecharges.same_as_previous"))
expect(record.errors["mscharge"]).to include(match I18n.t("validations.sales.financial.mscharge.same_as_new"))
end
end
end

2
spec/services/bulk_upload/sales/year2026/row_parser_spec.rb

@ -116,6 +116,8 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
field_123: "1",
field_125: "2",
field_126: "Non-binary",
field_135: "1",
field_136: "150",
}
end

101
spec/services/exports/sales_log_export_service_spec.rb

@ -451,18 +451,12 @@ RSpec.describe Exports::SalesLogExportService do
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)
def replace_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/>")
@ -474,16 +468,93 @@ RSpec.describe Exports::SalesLogExportService do
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 have_same_xml_contents_as(expected_content)
context "when not staircasing" 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_non_staircasing_values(export_file)
export_file.sub!("<HASESTATEFEE/>", "<HASESTATEFEE>1</HASESTATEFEE>")
export_file.sub!("<ESTATEFEE/>", "<ESTATEFEE>222.0</ESTATEFEE>")
export_file.sub!("<TYPE>8</TYPE>", "<TYPE>30</TYPE>")
export_file.sub!("<STAIRCASE/>", "<STAIRCASE>2</STAIRCASE>")
end
export_service.export_xml_sales_logs
it "exports mscharge fields as hasmscharge and mscharge" do
expected_content = replace_entity_ids(sales_log, xml_export_file.read)
replace_shared_ownership_values(expected_content)
replace_non_staircasing_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 have_same_xml_contents_as(expected_content)
end
export_service.export_xml_sales_logs
end
end
context "when staircasing" do
context "when exporting only 26/27 collection period", metadata: { year: 26 } do
let(:start_time) { collection_start_date_for_year(2026) }
let(:expected_zip_filename) { "core_sales_2026_2027_apr_mar_f0001_inc0001.zip" }
let(:expected_data_filename) { "core_sales_2026_2027_apr_mar_f0001_inc0001_pt001.xml" }
let(:xml_export_file) { File.open("spec/fixtures/exports/sales_log_26_27.xml", "r:UTF-8") }
let!(:sales_log) { FactoryBot.create(:sales_log, :export, ownershipsch: 1, staircase: 1, type: 2, mscharge: 321, has_management_fee: 1, management_fee: 222, hasservicechargeschanged: 1, newservicecharges: 150) }
def replace_staircasing_values(export_file)
export_file.sub!("<HASESTATEFEE/>", "<HASESTATEFEE>1</HASESTATEFEE>")
export_file.sub!("<ESTATEFEE/>", "<ESTATEFEE>222.0</ESTATEFEE>")
export_file.sub!("<TYPE>8</TYPE>", "<TYPE>2</TYPE>")
export_file.sub!("<STAIRCASE/>", "<STAIRCASE>1</STAIRCASE>")
export_file.sub!("<ARMEDFORCESSPOUSE>5</ARMEDFORCESSPOUSE>", "<ARMEDFORCESSPOUSE/>")
export_file.sub!("<BUILTYPE>1</BUILTYPE>", "<BUILTYPE/>")
export_file.sub!("<BUY2LIVING>3</BUY2LIVING>", "<BUY2LIVING/>")
export_file.sub!("<DEPOSIT>80000.0</DEPOSIT>", "<DEPOSIT/>")
export_file.sub!("<DISABLED>1</DISABLED>", "<DISABLED/>")
export_file.sub!("<ECSTAT1>1</ECSTAT1>", "<ECSTAT1/>")
export_file.sub!("<ECSTAT2>1</ECSTAT2>", "<ECSTAT2/>")
export_file.sub!("<ESTATEFEE>222.0</ESTATEFEE>", "<ESTATEFEE/>")
export_file.sub!("<ETHNIC>17</ETHNIC>", "<ETHNIC/>")
export_file.sub!("<ETHNICGROUP1>17</ETHNICGROUP1>", "<ETHNICGROUP1/>")
export_file.sub!("<ETHNICGROUP2>17</ETHNICGROUP2>", "<ETHNICGROUP2/>")
export_file.sub!("<HASESTATEFEE>1</HASESTATEFEE>", "<HASESTATEFEE/>")
export_file.sub!("<HB>4</HB>", "<HB/>")
export_file.sub!("<HHOLDCOUNT>4</HHOLDCOUNT>", "<HHOLDCOUNT/>")
export_file.sub!("<HHREGRES>7</HHREGRES>", "<HHREGRES/>")
export_file.sub!("<INC1MORT>1</INC1MORT>", "<INC1MORT/>")
export_file.sub!("<INC1NK>0</INC1NK>", "<INC1NK/>")
export_file.sub!("<INC2MORT>1</INC2MORT>", "<INC2MORT/>")
export_file.sub!("<INC2NK>0</INC2NK>", "<INC2NK/>")
export_file.sub!("<INCOME1>10000</INCOME1>", "<INCOME1/>")
export_file.sub!("<INCOME2>10000</INCOME2>", "<INCOME2/>")
export_file.sub!("<LIVEINBUYER1>1</LIVEINBUYER1>", "<LIVEINBUYER1/>")
export_file.sub!("<LIVEINBUYER2>1</LIVEINBUYER2>", "<LIVEINBUYER2/>")
export_file.sub!("<MORTGAGE>20000.0</MORTGAGE>", "<MORTGAGE/>")
export_file.sub!("<MORTLEN1>10</MORTLEN1>", "<MORTLEN1/>")
export_file.sub!("<NATIONALITYALL1>826</NATIONALITYALL1>", "<NATIONALITYALL1/>")
export_file.sub!("<NATIONALITYALL2>826</NATIONALITYALL2>", "<NATIONALITYALL2/>")
export_file.sub!("<PREVOWN>1</PREVOWN>", "<PREVOWN/>")
export_file.sub!("<PREVSHARED>2</PREVSHARED>", "<PREVSHARED/>")
export_file.sub!("<PREVTEN>1</PREVTEN>", "<PREVTEN/>")
export_file.sub!("<SAVINGSNK>1</SAVINGSNK>", "<SAVINGSNK/>")
export_file.sub!("<WCHAIR>1</WCHAIR>", "<WCHAIR/>")
export_file.sub!("<WHEEL>1</WHEEL>", "<WHEEL/>")
export_file.sub!("<HASSERVICECHARGESCHANGED/>", "<HASSERVICECHARGESCHANGED>1</HASSERVICECHARGESCHANGED>")
export_file.sub!("<NEWSERVICECHARGES/>", "<NEWSERVICECHARGES>150.0</NEWSERVICECHARGES>")
end
it "exports mscharge fields and hasservicechargeschanged and newservicecharges" do
expected_content = replace_entity_ids(sales_log, xml_export_file.read)
replace_shared_ownership_values(expected_content)
replace_staircasing_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 have_same_xml_contents_as(expected_content)
end
export_service.export_xml_sales_logs(full_update: true, collection_year: 2026)
end
end
end
end
end

Loading…
Cancel
Save