Skip to content

Commit

Permalink
Merge pull request #661 from trade-tariff/HOTT-3649-alcohol-calculati…
Browse files Browse the repository at this point in the history
…ons-spq

HOTT-3649: Adds evaluator for SPQ component
  • Loading branch information
willfish committed Aug 1, 2023
2 parents fb03e37 + b7303a4 commit f25fb32
Show file tree
Hide file tree
Showing 21 changed files with 411 additions and 80 deletions.
28 changes: 16 additions & 12 deletions app/decorators/confirmation_decorator.rb
Expand Up @@ -78,20 +78,24 @@ def format_measure_amount(value, _key)

formatted_values = value.map do |measure_unit_key, answer|
applicable_unit = applicable_measure_units[measure_unit_key.upcase]
has_coerced_unit = applicable_unit['coerced_measurement_unit_code'].present?
abbreviation = applicable_unit['abbreviation']
unit = applicable_unit['unit']

if measure_unit_key.upcase == Api::BaseComponent::RETAIL_PRICE_UNIT
tag.span(number_to_currency(answer, unit:), title: abbreviation)
elsif !has_coerced_unit
tag.span("#{answer} #{unit}", title: abbreviation)
else
"#{answer} #{unit}"
end
unit = if applicable_unit['coerced_measurement_unit_code'].present?
applicable_unit['unit']
else
applicable_unit['expansion'] || applicable_unit['abbreviation'] || applicable_unit['unit']
end

content = safe_join(
[
tag.span(tag.b(unit), title: applicable_unit['unit_question']),
tag.br,
answer,
],
'',
)
tag.div(content, id: "measure-#{measure_unit_key}")
end

formatted_values.join('<br>').html_safe
formatted_values.join('').html_safe
end

def format_customs_value(_value, _key)
Expand Down
4 changes: 4 additions & 0 deletions app/models/api/base_component.rb
Expand Up @@ -5,7 +5,10 @@ class BaseComponent < Api::Base

ALCOHOL_UNIT = 'ASV'.freeze
DECITONNE_UNIT = 'DTN'.freeze
LITERS_PURE_ALCOHOL_UNIT = 'LPA'.freeze
HECTOLITERS_UNIT = 'HLT'.freeze
RETAIL_PRICE_UNIT = 'RET'.freeze
SPR_DISCOUNT_UNIT = 'SPR'.freeze
SUCROSE_UNIT = 'BRX'.freeze
VOLUME_UNIT = 'HLT'.freeze

Expand Down Expand Up @@ -35,6 +38,7 @@ class BaseComponent < Api::Base
enum :unit, {
alcohol_volume: %w[ASVX],
sucrose: %w[DTNZ],
spq: %w[SPQ],
}

def ad_valorem?
Expand Down
2 changes: 2 additions & 0 deletions app/models/api/measure.rb
Expand Up @@ -66,6 +66,8 @@ def evaluator_for_compound_component(component)
ExpressionEvaluators::AlcoholVolumeMeasureUnit.new(self, component)
elsif component.sucrose?
ExpressionEvaluators::SucroseMeasureUnit.new(self, component)
elsif component.spq?
ExpressionEvaluators::Spq.new(self, component)
elsif component.specific_duty?
ExpressionEvaluators::MeasureUnit.new(self, component)
end
Expand Down
14 changes: 14 additions & 0 deletions app/models/api/measure_condition.rb
Expand Up @@ -75,6 +75,14 @@ def expresses_unit?
condition_measurement_unit_code.present?
end

def lpa_based?
measure_condition_component_units.include?('LPA')
end

def asvx_based?
measure_condition_component_units.include?('ASVX')
end

def document_code
return attributes[:document_code] if attributes[:document_code].present?

Expand All @@ -90,5 +98,11 @@ def certificate_description
def condition_measurement_unit
"#{condition_measurement_unit_code}#{condition_measurement_unit_qualifier_code}" if expresses_unit?
end

private

def measure_condition_component_units
@measure_condition_component_units ||= measure_condition_components.map(&:unit).compact
end
end
end
3 changes: 2 additions & 1 deletion app/models/steps/excise.rb
Expand Up @@ -19,10 +19,11 @@ def save
def options
available_additional_codes.map do |additional_code|
code = additional_code['code'].sub('X', '')
overlay = "#{additional_code['overlay']} (X#{code})".html_safe

OpenStruct.new(
id: code,
name: additional_code['overlay'].to_s.html_safe,
name: overlay,
disabled: code.in?(DISABLED_ADDITIONAL_CODES),
)
end
Expand Down
24 changes: 17 additions & 7 deletions app/services/concerns/measure_unit_presentable.rb
Expand Up @@ -2,22 +2,32 @@ module MeasureUnitPresentable
extend ActiveSupport::Concern

def presented_unit
@presented_unit ||= {
answer: unit_answer_for(applicable_unit),
unit: applicable_unit['unit'],
original_unit: applicable_unit['original_unit'],
multiplier: applicable_unit['multiplier'].presence || 1,
}
@presented_unit ||= presented_unit_for(applicable_unit)
end

def applicable_unit
applicable_units[component.unit]
end

def coerced_answer_for(unit)
presented_unit = presented_unit_for(unit)

presented_unit[:answer].to_f * presented_unit[:multiplier].to_f
end

def presented_unit_for(unit)
{
answer: unit_answer_for(unit),
unit: unit['unit'],
original_unit: unit['original_unit'],
multiplier: unit['multiplier'].presence || 1,
}
end

def unit_answer_for(unit)
key = unit['coerced_measurement_unit_code'].presence || "#{unit['measurement_unit_code']}#{unit['measurement_unit_qualifier_code']}"

user_session.measure_amount[key.downcase]
user_session.measure_amount_for(key)
end

private
Expand Down
6 changes: 4 additions & 2 deletions app/services/expression_evaluators/base.rb
Expand Up @@ -14,9 +14,11 @@ def initialize(measure, component, duty_total = nil)
attr_reader :measure, :component, :duty_total

def measure_condition
if component.belongs_to_measure_condition?
candidate_measure_condition_component = measure.applicable_components.first.presence || component

if candidate_measure_condition_component.belongs_to_measure_condition?
measure.measure_conditions.find do |measure_condition|
measure_condition.id == component.measure_condition_sid
measure_condition.id == candidate_measure_condition_component.measure_condition_sid
end
end
end
Expand Down
6 changes: 5 additions & 1 deletion app/services/expression_evaluators/compound.rb
Expand Up @@ -15,7 +15,11 @@ def sanitized_duty_expression
end

def duty_expression
base_duty_expression = measure.duty_expression&.formatted_base
base_duty_expression = if measure_condition.present?
measure_condition.duty_expression
else
measure.duty_expression&.formatted_base
end

if measure.resolved_duty_expression.present?
"#{base_duty_expression}<br>#{measure.resolved_duty_expression}"
Expand Down
59 changes: 59 additions & 0 deletions app/services/expression_evaluators/spq.rb
@@ -0,0 +1,59 @@
# SPQ is a complementary evaluator for the SPQ measurement unit on excise measure components
# It is the only evaluator that needs to reference the other components in the measure condition
#
# It cannot exist in a non-compound (non-multi-component form)
#
# It has two modes of operation:
# 1. LPA-based (where the other components are LPA we will use the LPA answer)
# 2. ASVX-based (where the other components are ASVX we will use the ASVX answer)
module ExpressionEvaluators
class Spq < ExpressionEvaluators::Base
include MeasureUnitPresentable

def call
{
calculation: measure_condition.duty_expression,
value:,
formatted_value: number_to_currency(value),
}
end

private

def value
@value ||= begin
candidate_value = if measure_condition.lpa_based?
lpa_value
elsif measure_condition.asvx_based?
asvx_value
end

candidate_value.present? ? candidate_value.to_f.floor(2) : 0.0
end
end

def lpa_value
component.duty_amount * lpa_answer * spr_answer
end

def asvx_value
component.duty_amount * asv_answer * hlt_answer * spr_answer
end

def asv_answer
coerced_answer_for(applicable_units[Api::BaseComponent::ALCOHOL_UNIT])
end

def hlt_answer
coerced_answer_for(applicable_units[Api::BaseComponent::HECTOLITERS_UNIT])
end

def spr_answer
coerced_answer_for(applicable_units[Api::BaseComponent::SPR_DISCOUNT_UNIT])
end

def lpa_answer
coerced_answer_for(applicable_units[Api::BaseComponent::LITERS_PURE_ALCOHOL_UNIT])
end
end
end
12 changes: 2 additions & 10 deletions spec/controllers/steps/confirmation_controller_spec.rb
Expand Up @@ -44,16 +44,8 @@
expect(assigns[:decorated_step]).to be_a(ConfirmationDecorator)
end

it 'contains the summary of all the previously given answers' do
expect(response.body).to include(expected_content)
end

it 'contains the links that allow users to go back and change their answers' do
expected_links.each do |link|
expect(response.body).to include(link)
end
end

it { expect(response.body).to include(expected_content) }
it { expected_links.each { |link| expect(response.body).to include(link) } }
it { expect(response).to have_http_status(:ok) }
it { expect(response).to render_template('confirmation/show') }
end
Expand Down
24 changes: 2 additions & 22 deletions spec/decorators/confirmation_decorator_spec.rb
Expand Up @@ -15,7 +15,7 @@
:with_planned_processing,
:with_certificate_of_origin,
:with_customs_value,
:with_measure_amount,
:with_coerced_measure_amount,
:with_additional_codes,
:with_excise_additional_codes,
:with_meursing_additional_code,
Expand Down Expand Up @@ -65,7 +65,7 @@
{ key: 'certificate_of_origin', label: 'Certificate of origin', value: 'Yes' },
{ key: 'meursing_additional_code', label: 'Meursing Code', value: '000' },
{ key: 'customs_value', label: 'Customs value', value: '£1,200.00' },
{ key: 'measure_amount', label: 'Import quantity', value: '<span title="100 kg">100 x 100 kg</span>' },
{ key: 'measure_amount', label: 'Import quantity', value: '<div id="measure-kgm"><span title="What is the weight of the goods you will be importing?"><b>kilograms</b></span><br>100</div>' },
{ key: 'excise', label: 'Excise additional code', value: '444, 369' },
{ key: 'vat', label: 'Applicable VAT rate', value: 'VAT zero rate (0.0)' },
]
Expand All @@ -88,26 +88,6 @@
expect(confirmation_decorator.user_answers).to eq(expected)
end
end

context 'when the measure amount is retail price' do
let(:user_session) do
build(
:user_session,
:with_retail_price_measure_amount,
commodity_code:,
)
end

let(:expected) do
[
{ key: 'measure_amount', label: 'Import quantity', value: '<span title="GBP">£1,000.00</span>' },
]
end

it 'reverses the unit and the answer' do
expect(confirmation_decorator.user_answers).to eq(expected)
end
end
end

describe '#path_for' do
Expand Down
6 changes: 6 additions & 0 deletions spec/factories/api/additional_code.rb
Expand Up @@ -25,4 +25,10 @@
description { 'CLIMATE CHANGE LEVY (CCL), 990, solid' }
formatted_description { 'CLIMATE CHANGE LEVY (CCL), 990, solid' }
end

trait :excise_spq do
code { 'X444' }
description { 'Spirits at least 3.5 but less than 8.5% &amp; eligible for SPR and DR' }
formatted_description { 'Spirits at least 3.5 but less than 8.5% &amp; eligible for SPR and DR' }
end
end
59 changes: 59 additions & 0 deletions spec/factories/api/commodity.rb
Expand Up @@ -363,6 +363,65 @@
end
end

trait :with_spq_measurement_units do
applicable_measure_units do
{
'HLT' => {
'measurement_unit_code' => 'HLT',
'measurement_unit_qualifier_code' => nil,
'abbreviation' => 'hl',
'expansion' => '100 litre (hl)',
'unit_question' => 'What is the volume of the goods that you will be importing?',
'unit_hint' => 'Enter the value in litres',
'unit' => 'litres',
'multiplier' => '0.01',
'coerced_measurement_unit_code' => 'LTR',
'original_unit' => 'x 100 litres',
'measurement_unit_type' => 'volume',
},
'LPA' => {
'measurement_unit_code' => 'LPA',
'measurement_unit_qualifier_code' => nil,
'abbreviation' => 'l alc. 100%',
'expansion' => 'litre of pure (100%) alcohol (l alc. 100%)',
'unit_question' => 'What is the volume of pure alcohol in the goods you will be importing?',
'unit_hint' => 'Enter the value in litres',
'unit' => 'litres',
'multiplier' => nil,
'coerced_measurement_unit_code' => nil,
'original_unit' => nil,
'measurement_unit_type' => 'volume',
},
'ASV' => {
'measurement_unit_code' => 'ASV',
'measurement_unit_qualifier_code' => nil,
'abbreviation' => '% vol',
'expansion' => 'percentage ABV (% vol)',
'unit_question' => 'What is the alcohol percentage (%) of the goods you are importing?',
'unit_hint' => 'Enter the alcohol by volume (ABV) percentage',
'unit' => 'percent',
'multiplier' => nil,
'coerced_measurement_unit_code' => nil,
'original_unit' => nil,
'measurement_unit_type' => 'percentage_abv',
},
'SPR' => {
'measurement_unit_code' => 'SPR',
'measurement_unit_qualifier_code' => nil,
'abbreviation' => 'SPR discount',
'expansion' => 'SPR discount (£)',
'unit_question' => 'What discount are you entitled to as a result of Small Producer Relief?',
'unit_hint' => 'Enter the SPR discount against the full rate, not the chargeable SPR rate. For example, if the full rate, before application of SPR is £10.00 / litre of pure alcohol, and you are entitled to pay £7.00, enter 3.00 as your SPR discount.',
'unit' => 'SPR discount (£)',
'multiplier' => nil,
'coerced_measurement_unit_code' => nil,
'original_unit' => nil,
'measurement_unit_type' => 'other',
},
}
end
end

trait :with_none_additional_code_measures do
applicable_additional_codes do
{
Expand Down
19 changes: 19 additions & 0 deletions spec/factories/api/measure_condition.rb
Expand Up @@ -34,6 +34,25 @@
end
end

trait :spq_positive do
action { 'Apply the amount of the action (see components)' }
action_code { '01' }
certificate_description {}
condition { 'E: The quantity or the price per unit declared, as appropriate, is equal or less than the specified maximum, or presentation of the required document' }
condition_code { 'E' }
condition_duty_amount { 3.49 }
condition_measurement_unit_code { 'ASV' }
condition_measurement_unit_qualifier_code {}
condition_monetary_unit_code {}
document_code { '' }
duty_expression { "<span>9.27</span> GBP / <abbr title='Litre pure (100%) alcohol'>l alc. 100%</abbr> - £1.00 / for each litre of pure alcohol, multiplied by the SPR discount" }
measure_condition_class { 'threshold' }
monetary_unit_abbreviation {}
requirement { "<span>3.49</span> <abbr title='%vol'>% vol</abbr>" }
requirement_operator { '=<' }
threshold_unit_type { 'percentage_abv' }
end

trait :stopping_negative do
action { 'Declared subheading not allowed' }
action_code { '08' }
Expand Down

0 comments on commit f25fb32

Please sign in to comment.