Skip to content

Commit

Permalink
Model a actual, refund and comment upload row
Browse files Browse the repository at this point in the history
We've already added the `Import::Csv:Financial` class for handling the
financial values from csv, actuals and refunds.

This commit adds a representation of the row as we expect it to be
supplied in the csv, we perform a set of validations adding to the
errors instance variable for use later.

Like financial values, we treat 'empty' values such as spaces as nothing
rather than included values that users would not see.

At this point a valid `Import::ActivityActualRefundComment::Row` still
may not result in a record being created as validation occurs later and
is collected together for presentation to the user.

We are doing more validation than the existing improter does, earlier, we
feel like this approach results in a number of benefits including:

- the code is more approachable, with as many validation happening as
  early as possible in one place
- performant, we don't have to waste resources during the import if we
  already know the row is invalid
- we can offer more helpful errors than the standard model validation might.

As part of this work we are introducing some better name and namespacing
for the code that relates to the importing processes. For related
models, services and any other we can use:

`import::csv:activity_refund_comment`

We settled on this as the code relates so many models, including:

- Actual Transactions
- Refund Transactions
- Comments on Activities

This model reflects the change to the expectation on users around what
values should be included in a csv row once it is in use.
  • Loading branch information
mec committed Jan 4, 2024
1 parent 73cec26 commit 1934128
Show file tree
Hide file tree
Showing 4 changed files with 851 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

- Display the budgets headings as "Activity budgets" for level C and D activities on the Financials tab
- model a financial value from a csv file
- model a single row of csv data that contains actual, refund and activity
comments

## Release 141 - 2023-12-04

Expand Down
208 changes: 208 additions & 0 deletions app/models/import/csv/activity_actual_refund_comment/row.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
class Import::Csv::ActivityActualRefundComment::Row
attr_reader :errors

def initialize(csv_row)
@row = csv_row
@errors = {}
@actual = Import::Csv::Financial.new(@row.field("Actual Value"))
@refund = Import::Csv::Financial.new(@row.field("Refund Value"))
end

def valid?
validate_financial_quarter
validate_financial_year
validate_roda_identifier
validate_receiving_organisation_name
validate_receiving_organisation_type

if validate_actual && validate_refund
validate_no_actual_and_refund
validate_refund_must_have_comment
end

@errors.empty?
end

def invalid?
!valid?
end

def empty?
return nil unless valid?

actual_value.zero? && refund_value.zero? && comment.nil?
end

def actual_value
@actual.decimal_value
end

def refund_value
@refund.decimal_value
end

def comment
@row.field("Comment").tap do |comment|
return nil if comment.blank?
end
end

def roda_identifier
@row.field("Activity RODA Identifier").tap do |identifier|
return nil if identifier.blank?
end
end

def financial_quarter
@row.field("Financial Quarter").tap do |quarter|
return nil if quarter.blank?
end
end

def financial_year
@row.field("Financial Year").tap do |year|
return nil if year.blank?
end
end

def receiving_organisation_name
@row.field("Receiving Organisation Name").tap do |name|
return nil if name.blank?
end
end

def receiving_organisation_type
@row.field("Receiving Organisation Type").tap do |type|
return nil if type.blank?
end
end

def receiving_organisation_iati_reference
@row.field("Receiving Organisation IATI Reference").tap do |reference|
return nil if reference.blank?
end
end

private def original_actual_value
@actual.original_value
end

private def original_refund_value
@refund.original_value
end

private def validate_roda_identifier
if roda_identifier.blank?
@errors["Activity RODA Identifier"] = [roda_identifier, I18n.t("import.csv.activity_actual_refund_comment.errors.default.required")]
return false
end

true
end

private def validate_financial_quarter
if financial_quarter.blank?
@errors["Financial Quarter"] = [financial_quarter, I18n.t("import.csv.activity_actual_refund_comment.errors.default.required")]
return false
end

if ["1", "2", "3", "4"].none?(financial_quarter)
@errors["Financial Quarter"] = [financial_quarter, I18n.t("import.csv.activity_actual_refund_comment.errors.financial_quarter")]
return false
end

true
end

private def validate_financial_year
if financial_year.blank?
@errors["Financial Year"] = [financial_year, I18n.t("import.csv.activity_actual_refund_comment.errors.default.required")]
return false
end

begin
FinancialYear.new(financial_year)
rescue ::FinancialYear::InvalidYear
@errors["Financial Year"] = [financial_year, I18n.t("import.csv.activity_actual_refund_comment.errors.financial_year")]
return false
end

true
end

private def validate_actual
if actual_value.nil?
@errors["Actual Value"] = [original_actual_value, I18n.t("import.csv.activity_actual_refund_comment.errors.financial_value")]
return false
end
true
end

private def validate_refund
if refund_value.nil?
@errors["Refund Value"] = [original_refund_value, I18n.t("import.csv.activity_actual_refund_comment.errors.financial_value")]
return false
end
true
end

private def validate_no_actual_and_refund
if actual_value.positive? && (refund_value.positive? || refund_value.negative?)
@errors["Actual Value"] = [original_actual_value, I18n.t("import.csv.activity_actual_refund_comment.errors.actual_value_with_refund")]
@errors["Refund Value"] = [original_refund_value, I18n.t("import.csv.activity_actual_refund_comment.errors.refund_value_with_actual")]
return false
end
true
end

private def validate_refund_must_have_comment
if actual_value.zero? && !refund_value.zero? && comment.nil?
@errors["Comment"] = [comment, I18n.t("import.csv.activity_actual_refund_comment.errors.refund_requires_comment")]
return false
end
true
end

private def validate_receiving_organisation_name
if receiving_organisation_name.blank? && receiving_organisation_type.present?
@errors["Receiving Organisation Name"] = [
receiving_organisation_name, I18n.t("import.csv.activity_actual_refund_comment.errors.receiving_organisation_name.type")
]
return false
end

if receiving_organisation_name.blank? && receiving_organisation_iati_reference.present?
@errors["Receiving Organisation Name"] = [
receiving_organisation_name, I18n.t("import.csv.activity_actual_refund_comment.errors.receiving_organisation_name.reference")
]
return false
end

true
end

private def validate_receiving_organisation_type
return true if receiving_organisation_name.blank? && receiving_organisation_type.blank?

if receiving_organisation_name.present? && receiving_organisation_type.blank?
@errors["Receiving Organisation Type"] = [
receiving_organisation_type, I18n.t("import.csv.activity_actual_refund_comment.errors.receiving_organisation_type.blank_name")
]
return false
end

unless value_in_code_list?("organisation_type", receiving_organisation_type)
@errors["Receiving Organisation Type"] = [
receiving_organisation_type, I18n.t("import.csv.activity_actual_refund_comment.errors.receiving_organisation_type.invalid_code")
]
return false
end

true
end

private def value_in_code_list?(code_list, value)
code_list = Codelist.new(type: code_list)
code_list.find_item_by_code(value) ? true : false
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
en:
import:
csv:
activity_actual_refund_comment:
errors:
default:
required: Is required
financial_quarter: Must be 1, 2, 3 or 4
financial_year: Must be a four digit year
financial_value: Must be a financial value
actual_value_with_refund: Actual and refund cannot be reported on the same row
refund_value_with_actual: Refund and actual cannot be reported on the same row
refund_requires_comment: Refund must have a comment
receiving_organisation_name:
type: Cannot be blank when Receiving Organisation Type is present
reference: Cannot be blank when Receiving Organisation IATI reference is present
receiving_organisation_type:
blank_name: Cannot be blank when Receiving Organisation Name is present
invalid_code: Is not a valid receiving organisation type code
Loading

0 comments on commit 1934128

Please sign in to comment.