From 464e1d5d6d22905049342bdc87266a57b9b9c63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Semp=C3=A9?= Date: Tue, 16 Aug 2022 13:52:50 +0200 Subject: [PATCH 1/8] feat(:free-units): Remove fixed amount target on Charge#properties --- .../charge_model_attributes_handler.rb | 2 - .../types/charges/fixed_amount_target_enum.rb | 11 ---- app/graphql/types/charges/input.rb | 1 - app/graphql/types/charges/object.rb | 7 --- app/models/charge.rb | 5 -- .../charge_models/percentage_service.rb | 8 +-- .../charges/validators/percentage_service.rb | 15 +---- config/locales/en.yml | 1 - schema.graphql | 7 --- schema.json | 49 ---------------- spec/factories/charge_factory.rb | 1 - spec/graphql/mutations/plans/create_spec.rb | 1 - spec/graphql/mutations/plans/update_spec.rb | 1 - spec/models/charge_spec.rb | 2 - .../charge_models/percentage_service_spec.rb | 30 +--------- .../validators/percentage_service_spec.rb | 57 +------------------ 16 files changed, 5 insertions(+), 193 deletions(-) delete mode 100644 app/graphql/types/charges/fixed_amount_target_enum.rb diff --git a/app/graphql/concerns/charge_model_attributes_handler.rb b/app/graphql/concerns/charge_model_attributes_handler.rb index 1922471588e..f8fd8e68d9b 100644 --- a/app/graphql/concerns/charge_model_attributes_handler.rb +++ b/app/graphql/concerns/charge_model_attributes_handler.rb @@ -27,7 +27,6 @@ def prepare_arguments(arguments) output[:properties] = { rate: output[:rate], fixed_amount: output[:fixed_amount], - fixed_amount_target: output[:fixed_amount_target], } end @@ -38,7 +37,6 @@ def prepare_arguments(arguments) output.delete(:package_size) output.delete(:rate) output.delete(:fixed_amount) - output.delete(:fixed_amount_target) output end diff --git a/app/graphql/types/charges/fixed_amount_target_enum.rb b/app/graphql/types/charges/fixed_amount_target_enum.rb deleted file mode 100644 index 3e68572171b..00000000000 --- a/app/graphql/types/charges/fixed_amount_target_enum.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Types - module Charges - class FixedAmountTargetEnum < Types::BaseEnum - Charge::FIXED_AMOUNT_TARGETS.each do |type| - value type - end - end - end -end diff --git a/app/graphql/types/charges/input.rb b/app/graphql/types/charges/input.rb index e269b3df1f2..41407c7eb5b 100644 --- a/app/graphql/types/charges/input.rb +++ b/app/graphql/types/charges/input.rb @@ -23,7 +23,6 @@ class Input < Types::BaseInputObject # NOTE: Percentage charge model argument :rate, String, required: false argument :fixed_amount, String, required: false - argument :fixed_amount_target, Types::Charges::FixedAmountTargetEnum, required: false end end end diff --git a/app/graphql/types/charges/object.rb b/app/graphql/types/charges/object.rb index 332c0edb60d..192fc7ea2f2 100644 --- a/app/graphql/types/charges/object.rb +++ b/app/graphql/types/charges/object.rb @@ -26,7 +26,6 @@ class Object < Types::BaseObject # NOTE: Percentage charge model field :rate, String, null: true field :fixed_amount, String, null: true - field :fixed_amount_target, Types::Charges::FixedAmountTargetEnum, null: true def amount return unless object.standard? || object.package? @@ -63,12 +62,6 @@ def fixed_amount object.properties['fixed_amount'] end - - def fixed_amount_target - return unless object.percentage? - - object.properties['fixed_amount_target'] - end end end end diff --git a/app/models/charge.rb b/app/models/charge.rb index 99d244cb9cf..42e73f835ba 100644 --- a/app/models/charge.rb +++ b/app/models/charge.rb @@ -15,11 +15,6 @@ class Charge < ApplicationRecord percentage ].freeze - FIXED_AMOUNT_TARGETS = %w[ - all_units - each_unit - ].freeze - enum charge_model: CHARGE_MODELS validates :amount_currency, inclusion: { in: currency_list } diff --git a/app/services/charges/charge_models/percentage_service.rb b/app/services/charges/charge_models/percentage_service.rb index 39e23858039..8f336df5b0b 100644 --- a/app/services/charges/charge_models/percentage_service.rb +++ b/app/services/charges/charge_models/percentage_service.rb @@ -15,9 +15,7 @@ def compute_percentage_amount(value) def compute_fixed_amount(value) return 0 if value.zero? - return 0 if (fixed_amount_target.nil? || fixed_amount.nil?) - - return fixed_amount if fixed_amount_target == 'all_units' + return 0 if fixed_amount.nil? value * fixed_amount end @@ -27,10 +25,6 @@ def rate BigDecimal(charge.properties['rate']) end - def fixed_amount_target - charge.properties['fixed_amount_target'] - end - def fixed_amount @fixed_amount ||= BigDecimal(charge.properties['fixed_amount'] || 0) end diff --git a/app/services/charges/validators/percentage_service.rb b/app/services/charges/validators/percentage_service.rb index dfd00ba1b89..51363f3c883 100644 --- a/app/services/charges/validators/percentage_service.rb +++ b/app/services/charges/validators/percentage_service.rb @@ -7,7 +7,6 @@ def validate errors = [] errors << :invalid_rate unless valid_rate? errors << :invalid_fixed_amount unless valid_fixed_amount? - errors << :invalid_fixed_amount_target unless valid_fixed_amount_target? return result.fail!(code: :invalid_properties, message: errors) if errors.present? @@ -29,22 +28,10 @@ def fixed_amount end def valid_fixed_amount? - return true if fixed_amount.nil? && fixed_amount_target.nil? + return true if fixed_amount.nil? ::Validators::DecimalAmountService.new(fixed_amount).valid_amount? end - - def fixed_amount_target - properties['fixed_amount_target'] - end - - def valid_fixed_amount_target? - return true if fixed_amount.nil? && fixed_amount_target.nil? - - fixed_amount_target.present? && - fixed_amount_target.is_a?(String) && - Charge::FIXED_AMOUNT_TARGETS.include?(fixed_amount_target) - end end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index aa71df5aba8..d1994e9586b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -19,7 +19,6 @@ en: invalid_package_size: invalid_package_size invalid_rate: invalid_rate invalid_fixed_amount: invalid_fixed_amount - invalid_fixed_amount_target: invalid_fixed_amount_target invalid_content_type: invalid_content_type invalid_size: invalid_size value_already_exists: value_already_exists diff --git a/schema.graphql b/schema.graphql index 32f2df4402c..f416bafe64d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -135,7 +135,6 @@ type Charge { chargeModel: ChargeModelEnum! createdAt: ISO8601DateTime! fixedAmount: String - fixedAmountTarget: FixedAmountTargetEnum freeUnits: Int graduatedRanges: [GraduatedRange!] id: ID! @@ -150,7 +149,6 @@ input ChargeInput { billableMetricId: ID! chargeModel: ChargeModelEnum! fixedAmount: String - fixedAmountTarget: FixedAmountTargetEnum freeUnits: Int graduatedRanges: [GraduatedRangeInput!] id: ID @@ -2626,11 +2624,6 @@ type EventCollection { metadata: CollectionMetadata! } -enum FixedAmountTargetEnum { - all_units - each_unit -} - type GraduatedRange { flatAmount: String! fromValue: Int! diff --git a/schema.json b/schema.json index 0b0acc4982a..a53440234ec 100644 --- a/schema.json +++ b/schema.json @@ -1322,20 +1322,6 @@ ] }, - { - "name": "fixedAmountTarget", - "description": null, - "type": { - "kind": "ENUM", - "name": "FixedAmountTargetEnum", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null, - "args": [ - - ] - }, { "name": "freeUnits", "description": null, @@ -1587,18 +1573,6 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null - }, - { - "name": "fixedAmountTarget", - "description": null, - "type": { - "kind": "ENUM", - "name": "FixedAmountTargetEnum", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null } ], "enumValues": null @@ -7970,29 +7944,6 @@ "inputFields": null, "enumValues": null }, - { - "kind": "ENUM", - "name": "FixedAmountTargetEnum", - "description": null, - "interfaces": null, - "possibleTypes": null, - "fields": null, - "inputFields": null, - "enumValues": [ - { - "name": "all_units", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "each_unit", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ] - }, { "kind": "SCALAR", "name": "Float", diff --git a/spec/factories/charge_factory.rb b/spec/factories/charge_factory.rb index cbf34de2d9d..b41bc07e0dd 100644 --- a/spec/factories/charge_factory.rb +++ b/spec/factories/charge_factory.rb @@ -36,7 +36,6 @@ { rate: '0.0555', fixed_amount: '2', - fixed_amount_target: 'each_unit', } end end diff --git a/spec/graphql/mutations/plans/create_spec.rb b/spec/graphql/mutations/plans/create_spec.rb index 4017db16eca..89325c12586 100644 --- a/spec/graphql/mutations/plans/create_spec.rb +++ b/spec/graphql/mutations/plans/create_spec.rb @@ -64,7 +64,6 @@ chargeModel: 'percentage', rate: '0.25', fixedAmount: '2', - fixedAmountTarget: 'all_units', }, { billableMetricId: billable_metrics.last.id, diff --git a/spec/graphql/mutations/plans/update_spec.rb b/spec/graphql/mutations/plans/update_spec.rb index 95c1f80b1e9..a1b99eb3353 100644 --- a/spec/graphql/mutations/plans/update_spec.rb +++ b/spec/graphql/mutations/plans/update_spec.rb @@ -65,7 +65,6 @@ chargeModel: 'percentage', rate: '0.25', fixedAmount: '2', - fixedAmountTarget: 'all_units', }, { billableMetricId: billable_metrics.last.id, diff --git a/spec/models/charge_spec.rb b/spec/models/charge_spec.rb index 34702d60155..bd88226a9c4 100644 --- a/spec/models/charge_spec.rb +++ b/spec/models/charge_spec.rb @@ -172,7 +172,6 @@ message: [ :invalid_rate, :invalid_fixed_amount, - :invalid_fixed_amount_target, ], ) end @@ -188,7 +187,6 @@ expect(charge.errors.messages.keys).to include(:properties) expect(charge.errors.messages[:properties]).to include('invalid_rate') expect(charge.errors.messages[:properties]).to include('invalid_fixed_amount') - expect(charge.errors.messages[:properties]).to include('invalid_fixed_amount_target') expect(Charges::Validators::PercentageService).to have_received(:new) .with(charge: charge) diff --git a/spec/services/charges/charge_models/percentage_service_spec.rb b/spec/services/charges/charge_models/percentage_service_spec.rb index a7b9828d3f2..f18e7e35f6e 100644 --- a/spec/services/charges/charge_models/percentage_service_spec.rb +++ b/spec/services/charges/charge_models/percentage_service_spec.rb @@ -11,7 +11,6 @@ properties: { rate: '5.55', fixed_amount: '0', - fixed_amount_target: 'each_unit', }, ) end @@ -29,7 +28,6 @@ properties: { rate: '5.55', fixed_amount: nil, - fixed_amount_target: nil, }, ) end @@ -39,43 +37,19 @@ end end - context 'when fixed amount value is NOT zero and should be applied on each unit' do + context 'when fixed amount value is NOT zero' do let(:charge) do create( :percentage_charge, properties: { rate: '5.55', fixed_amount: '2', - fixed_amount_target: 'each_unit', }, ) end - it 'applies the percentage rate and the additional charge on each init' do + it 'applies the percentage rate and the additional charge on each unit' do expect(percentage_service.apply(value: 100).amount).to eq(205.55) end end - - context 'when fixed amount value is NOT zero and should be applied on all units' do - let(:charge) do - create( - :percentage_charge, - properties: { - rate: '5.5555', - fixed_amount: '2', - fixed_amount_target: 'all_units', - }, - ) - end - - it 'applies the percentage rate and the additional charge on all inits' do - expect(percentage_service.apply(value: 100).amount).to eq(7.5555) - end - - context 'with value equal to zero' do - it 'applies the percentage rate and the additional charge on all inits' do - expect(percentage_service.apply(value: 0).amount).to eq(0) - end - end - end end diff --git a/spec/services/charges/validators/percentage_service_spec.rb b/spec/services/charges/validators/percentage_service_spec.rb index eca8299ece4..56fb685953e 100644 --- a/spec/services/charges/validators/percentage_service_spec.rb +++ b/spec/services/charges/validators/percentage_service_spec.rb @@ -11,7 +11,6 @@ { rate: '0.25', fixed_amount: '2', - fixed_amount_target: 'all_units', } end @@ -22,7 +21,6 @@ let(:percentage_properties) do { fixed_amount: '2', - fixed_amount_target: 'all_units', } end @@ -34,7 +32,6 @@ { rate: 0.25, fixed_amount: '2', - fixed_amount_target: 'all_units', } end @@ -46,7 +43,6 @@ { rate: 'bla', fixed_amount: '2', - fixed_amount_target: 'all_units', } end @@ -58,7 +54,6 @@ { rate: '-0.50', fixed_amount: '2', - fixed_amount_target: 'all_units', } end @@ -70,30 +65,17 @@ { rate: '0.00', fixed_amount: '2', - fixed_amount_target: 'all_units', } end it { expect(percentage_service.validate.error).to include(:invalid_rate) } end - context 'without fixed amount' do - let(:percentage_properties) do - { - rate: '0.25', - fixed_amount_target: 'all_units', - } - end - - it { expect(percentage_service.validate.error).to include(:invalid_fixed_amount) } - end - context 'when fixed amount cannot be converted to numeric format' do let(:percentage_properties) do { rate: '0.25', fixed_amount: 'bla', - fixed_amount_target: 'all_units', } end @@ -105,7 +87,6 @@ { rate: '0.25', fixed_amount: 2, - fixed_amount_target: 'all_units', } end @@ -117,25 +98,13 @@ { rate: '0.25', fixed_amount: '-2', - fixed_amount_target: 'all_units', } end it { expect(percentage_service.validate.error).to include(:invalid_fixed_amount) } end - context 'without fixed amount target' do - let(:percentage_properties) do - { - rate: '0.25', - fixed_amount: '2', - } - end - - it { expect(percentage_service.validate.error).to include(:invalid_fixed_amount_target) } - end - - context 'without fixed amount and fixed amount target' do + context 'without fixed amount' do let(:percentage_properties) do { rate: '0.25' @@ -144,29 +113,5 @@ it { expect(percentage_service.validate.error).to be nil } end - - context 'when fixed amount target is not string' do - let(:percentage_properties) do - { - rate: '0.25', - fixed_amount: '2', - fixed_amount_target: 5, - } - end - - it { expect(percentage_service.validate.error).to include(:invalid_fixed_amount_target) } - end - - context 'when fixed amount target is not either all_units or each_unit' do - let(:percentage_properties) do - { - rate: '0.25', - fixed_amount: '2', - fixed_amount_target: 'bbb', - } - end - - it { expect(percentage_service.validate.error).to include(:invalid_fixed_amount_target) } - end end end From a7108b07b5fd95ae8b911b30cff6799281dee229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Semp=C3=A9?= Date: Tue, 16 Aug 2022 14:17:38 +0200 Subject: [PATCH 2/8] feat(free-units): Migrate existing charges --- ...7_update_properties_on_percentage_charges.rb | 6 ++++++ db/schema.rb | 2 +- lib/tasks/charges.rake | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20220816120137_update_properties_on_percentage_charges.rb create mode 100644 lib/tasks/charges.rake diff --git a/db/migrate/20220816120137_update_properties_on_percentage_charges.rb b/db/migrate/20220816120137_update_properties_on_percentage_charges.rb new file mode 100644 index 00000000000..b9f1a967079 --- /dev/null +++ b/db/migrate/20220816120137_update_properties_on_percentage_charges.rb @@ -0,0 +1,6 @@ +class UpdatePropertiesOnPercentageCharges < ActiveRecord::Migration[7.0] + def change + LagoApi::Application.load_tasks + Rake::Task['charges:update_properties_for_free_units'].invoke + end +end diff --git a/db/schema.rb b/db/schema.rb index 9d68c597281..13c0602c526 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_08_11_155332) do +ActiveRecord::Schema[7.0].define(version: 2022_08_16_120137) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" diff --git a/lib/tasks/charges.rake b/lib/tasks/charges.rake new file mode 100644 index 00000000000..54e3bb08569 --- /dev/null +++ b/lib/tasks/charges.rake @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +namespace :charges do + desc 'Update Properties for Fixed Fee and Free Units' + task update_properties_for_free_units: :environment do + # Notes: We consider that we don’t have any clients with a percentage charge + # created containing a fixed_amount. All existing charges have fixed_amount + # and fixed_amount_target with a null value. + + Charge.percentage.where("properties -> 'fixed_amount_target' IS NOT NULL").find_each do |charge| + charge.properties.delete('fixed_amount_target') + charge.properties['free_units_per_events'] = nil + charge.properties['free_units_per_total_aggregation'] = nil + charge.save! + end + end +end From dc4eac4c9664b2b2cb05e1485a2c50aa74ed9b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Semp=C3=A9?= Date: Tue, 16 Aug 2022 14:42:42 +0200 Subject: [PATCH 3/8] feat(free-units): Add validations on free units --- .../charges/validators/percentage_service.rb | 22 +++++++ .../validators/percentage_service_spec.rb | 63 +++++++++++++++++-- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/app/services/charges/validators/percentage_service.rb b/app/services/charges/validators/percentage_service.rb index 51363f3c883..b43eded5041 100644 --- a/app/services/charges/validators/percentage_service.rb +++ b/app/services/charges/validators/percentage_service.rb @@ -7,6 +7,8 @@ def validate errors = [] errors << :invalid_rate unless valid_rate? errors << :invalid_fixed_amount unless valid_fixed_amount? + errors << :invalid_free_units_per_events unless valid_free_units_per_events? + errors << :invalid_free_units_per_total_aggregation unless valid_free_units_per_total_aggregation? return result.fail!(code: :invalid_properties, message: errors) if errors.present? @@ -32,6 +34,26 @@ def valid_fixed_amount? ::Validators::DecimalAmountService.new(fixed_amount).valid_amount? end + + def free_units_per_events + properties['free_units_per_events'] + end + + def valid_free_units_per_events? + return true if free_units_per_events.nil? + + free_units_per_events.is_a?(Integer) && free_units_per_events.positive? + end + + def free_units_per_total_aggregation + properties['free_units_per_total_aggregation'] + end + + def valid_free_units_per_total_aggregation? + return true if free_units_per_total_aggregation.nil? + + ::Validators::DecimalAmountService.new(free_units_per_total_aggregation).valid_amount? + end end end end diff --git a/spec/services/charges/validators/percentage_service_spec.rb b/spec/services/charges/validators/percentage_service_spec.rb index 56fb685953e..515acf0d289 100644 --- a/spec/services/charges/validators/percentage_service_spec.rb +++ b/spec/services/charges/validators/percentage_service_spec.rb @@ -71,15 +71,60 @@ it { expect(percentage_service.validate.error).to include(:invalid_rate) } end - context 'when fixed amount cannot be converted to numeric format' do + context 'when free_units_per_events is not an integer' do + let(:percentage_properties) do + { + rate: '0.25', + free_units_per_events: 'foo', + } + end + + it { expect(percentage_service.validate.error).to include(:invalid_free_units_per_events) } + end + + context 'when free_units_per_events is negative amount' do + let(:percentage_properties) do + { + rate: '0.25', + free_units_per_events: -1, + } + end + + it { expect(percentage_service.validate.error).to include(:invalid_free_units_per_events) } + end + + context 'when fixed amount and free_units_per_total_aggregation cannot be converted to numeric' do let(:percentage_properties) do { rate: '0.25', fixed_amount: 'bla', + free_units_per_total_aggregation: 'bla', } end - it { expect(percentage_service.validate.error).to include(:invalid_fixed_amount) } + it 'returns invalid amounts error' do + expect(percentage_service.validate.error).to include( + :invalid_fixed_amount, + :invalid_free_units_per_total_aggregation, + ) + end + end + + context 'when given fixed amount and free_units_per_total_aggregation are not string' do + let(:percentage_properties) do + { + rate: '0.25', + fixed_amount: 2, + free_units_per_total_aggregation: 1, + } + end + + it 'returns invalid amounts error' do + expect(percentage_service.validate.error).to include( + :invalid_fixed_amount, + :invalid_free_units_per_total_aggregation, + ) + end end context 'when given fixed amount is not string' do @@ -93,18 +138,26 @@ it { expect(percentage_service.validate.error).to include(:invalid_fixed_amount) } end - context 'with negative fixed amount' do + context 'with negative fixed amount, free_units_per_events and free_units_per_total_aggregation' do let(:percentage_properties) do { rate: '0.25', fixed_amount: '-2', + free_units_per_events: '-1', + free_units_per_total_aggregation: '-1', } end - it { expect(percentage_service.validate.error).to include(:invalid_fixed_amount) } + it 'returns invalid amounts error' do + expect(percentage_service.validate.error).to include( + :invalid_fixed_amount, + :invalid_free_units_per_events, + :invalid_free_units_per_total_aggregation, + ) + end end - context 'without fixed amount' do + context 'without fixed_amount, free_units_per_events and free_units_per_total_aggregation' do let(:percentage_properties) do { rate: '0.25' From 1ac53704c2c360c3c97d6d38b01984612369b529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Semp=C3=A9?= Date: Tue, 16 Aug 2022 15:06:42 +0200 Subject: [PATCH 4/8] feat(free-units): Add free units fields to GraphQL --- .../charge_model_attributes_handler.rb | 5 ++ app/graphql/types/charges/input.rb | 2 + app/graphql/types/charges/object.rb | 14 +++++ config/locales/en.yml | 2 + schema.graphql | 4 ++ schema.json | 52 +++++++++++++++++++ spec/factories/charge_factory.rb | 2 +- spec/graphql/mutations/plans/create_spec.rb | 2 + spec/graphql/mutations/plans/update_spec.rb | 2 + spec/models/charge_spec.rb | 4 ++ 10 files changed, 88 insertions(+), 1 deletion(-) diff --git a/app/graphql/concerns/charge_model_attributes_handler.rb b/app/graphql/concerns/charge_model_attributes_handler.rb index f8fd8e68d9b..827604f1c7e 100644 --- a/app/graphql/concerns/charge_model_attributes_handler.rb +++ b/app/graphql/concerns/charge_model_attributes_handler.rb @@ -6,6 +6,7 @@ module ChargeModelAttributesHandler # - Standard model only has one property `amount_cents` # - Graduated model relies on the the list of `GraduatedRange` # - Package model has properties `amount_cents`, `package_size` and `free_units` + # - Percentage model has properties `rate`, `fixed_amount`, `free_units_per_events`, `free_units_per_total_aggregation` def prepare_arguments(arguments) return arguments if arguments[:charges].blank? @@ -27,6 +28,8 @@ def prepare_arguments(arguments) output[:properties] = { rate: output[:rate], fixed_amount: output[:fixed_amount], + free_units_per_events: output[:free_units_per_events], + free_units_per_total_aggregation: output[:free_units_per_total_aggregation], } end @@ -37,6 +40,8 @@ def prepare_arguments(arguments) output.delete(:package_size) output.delete(:rate) output.delete(:fixed_amount) + output.delete(:free_units_per_events) + output.delete(:free_units_per_total_aggregation) output end diff --git a/app/graphql/types/charges/input.rb b/app/graphql/types/charges/input.rb index 41407c7eb5b..474e1da3d67 100644 --- a/app/graphql/types/charges/input.rb +++ b/app/graphql/types/charges/input.rb @@ -23,6 +23,8 @@ class Input < Types::BaseInputObject # NOTE: Percentage charge model argument :rate, String, required: false argument :fixed_amount, String, required: false + argument :free_units_per_events, Integer, required: false + argument :free_units_per_total_aggregation, String, required: false end end end diff --git a/app/graphql/types/charges/object.rb b/app/graphql/types/charges/object.rb index 192fc7ea2f2..001de65d1a8 100644 --- a/app/graphql/types/charges/object.rb +++ b/app/graphql/types/charges/object.rb @@ -26,6 +26,8 @@ class Object < Types::BaseObject # NOTE: Percentage charge model field :rate, String, null: true field :fixed_amount, String, null: true + field :free_units_per_events, Integer, null: true + field :free_units_per_total_aggregation, String, null: true def amount return unless object.standard? || object.package? @@ -62,6 +64,18 @@ def fixed_amount object.properties['fixed_amount'] end + + def free_units_per_events + return unless object.percentage? + + object.properties['free_units_per_events'] + end + + def free_units_per_total_aggregation + return unless object.percentage? + + object.properties['free_units_per_total_aggregation'] + end end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index d1994e9586b..506ed99185c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -19,6 +19,8 @@ en: invalid_package_size: invalid_package_size invalid_rate: invalid_rate invalid_fixed_amount: invalid_fixed_amount + invalid_free_units_per_events: invalid_free_units_per_events + invalid_free_units_per_total_aggregation: invalid_free_units_per_total_aggregation invalid_content_type: invalid_content_type invalid_size: invalid_size value_already_exists: value_already_exists diff --git a/schema.graphql b/schema.graphql index f416bafe64d..51742c7eb33 100644 --- a/schema.graphql +++ b/schema.graphql @@ -136,6 +136,8 @@ type Charge { createdAt: ISO8601DateTime! fixedAmount: String freeUnits: Int + freeUnitsPerEvents: Int + freeUnitsPerTotalAggregation: String graduatedRanges: [GraduatedRange!] id: ID! packageSize: Int @@ -150,6 +152,8 @@ input ChargeInput { chargeModel: ChargeModelEnum! fixedAmount: String freeUnits: Int + freeUnitsPerEvents: Int + freeUnitsPerTotalAggregation: String graduatedRanges: [GraduatedRangeInput!] id: ID packageSize: Int diff --git a/schema.json b/schema.json index a53440234ec..aabf579877c 100644 --- a/schema.json +++ b/schema.json @@ -1336,6 +1336,34 @@ ] }, + { + "name": "freeUnitsPerEvents", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "freeUnitsPerTotalAggregation", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, { "name": "graduatedRanges", "description": null, @@ -1573,6 +1601,30 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "freeUnitsPerEvents", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "freeUnitsPerTotalAggregation", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "enumValues": null diff --git a/spec/factories/charge_factory.rb b/spec/factories/charge_factory.rb index b41bc07e0dd..e2d214d21fb 100644 --- a/spec/factories/charge_factory.rb +++ b/spec/factories/charge_factory.rb @@ -35,7 +35,7 @@ properties do { rate: '0.0555', - fixed_amount: '2', + fixed_amount: '2' } end end diff --git a/spec/graphql/mutations/plans/create_spec.rb b/spec/graphql/mutations/plans/create_spec.rb index 89325c12586..3beed374d68 100644 --- a/spec/graphql/mutations/plans/create_spec.rb +++ b/spec/graphql/mutations/plans/create_spec.rb @@ -64,6 +64,8 @@ chargeModel: 'percentage', rate: '0.25', fixedAmount: '2', + freeUnitsPerEvents: 5, + freeUnitsPerTotalAggregation: '50', }, { billableMetricId: billable_metrics.last.id, diff --git a/spec/graphql/mutations/plans/update_spec.rb b/spec/graphql/mutations/plans/update_spec.rb index a1b99eb3353..78050b60586 100644 --- a/spec/graphql/mutations/plans/update_spec.rb +++ b/spec/graphql/mutations/plans/update_spec.rb @@ -65,6 +65,8 @@ chargeModel: 'percentage', rate: '0.25', fixedAmount: '2', + freeUnitsPerEvents: 5, + freeUnitsPerTotalAggregation: '50', }, { billableMetricId: billable_metrics.last.id, diff --git a/spec/models/charge_spec.rb b/spec/models/charge_spec.rb index bd88226a9c4..bce49de5d2c 100644 --- a/spec/models/charge_spec.rb +++ b/spec/models/charge_spec.rb @@ -172,6 +172,8 @@ message: [ :invalid_rate, :invalid_fixed_amount, + :invalid_free_units_per_events, + :invalid_free_units_per_total_aggregation, ], ) end @@ -187,6 +189,8 @@ expect(charge.errors.messages.keys).to include(:properties) expect(charge.errors.messages[:properties]).to include('invalid_rate') expect(charge.errors.messages[:properties]).to include('invalid_fixed_amount') + expect(charge.errors.messages[:properties]).to include('invalid_free_units_per_events') + expect(charge.errors.messages[:properties]).to include('invalid_free_units_per_total_aggregation') expect(Charges::Validators::PercentageService).to have_received(:new) .with(charge: charge) From 5fdbffe7bac1a6c2b6232b8f525c19e96f0cf379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Semp=C3=A9?= Date: Thu, 18 Aug 2022 11:22:25 +0200 Subject: [PATCH 5/8] feat(free-units): Calculate percentage based on fixed amount and free units --- .../charges/charge_models/base_service.rb | 15 ++-- .../charge_models/graduated_service.rb | 12 ++-- .../charges/charge_models/package_service.rb | 4 +- .../charge_models/percentage_service.rb | 42 ++++++++--- .../charges/charge_models/standard_service.rb | 4 +- app/services/fees/charge_service.rb | 10 +-- .../charge_models/graduated_service_spec.rb | 58 ++++++++++++---- .../charge_models/package_service_spec.rb | 17 +++-- .../charge_models/percentage_service_spec.rb | 69 +++++++++++-------- .../charge_models/standard_service_spec.rb | 15 +++- 10 files changed, 170 insertions(+), 76 deletions(-) diff --git a/app/services/charges/charge_models/base_service.rb b/app/services/charges/charge_models/base_service.rb index 0f44f749581..6861ddc9694 100644 --- a/app/services/charges/charge_models/base_service.rb +++ b/app/services/charges/charge_models/base_service.rb @@ -3,20 +3,25 @@ module Charges module ChargeModels class BaseService < ::BaseService - def initialize(charge:) + def self.apply(...) + new(...).apply + end + + def initialize(charge:, aggregation_result:) super(nil) @charge = charge + @aggregation_result = aggregation_result end - def apply(value:) - result.units = value - result.amount = compute_amount(value) + def apply + result.units = aggregation_result.aggregation + result.amount = compute_amount result end protected - attr_accessor :charge + attr_accessor :charge, :aggregation_result def compute_amount(value) raise NotImplementedError diff --git a/app/services/charges/charge_models/graduated_service.rb b/app/services/charges/charge_models/graduated_service.rb index f06299af833..c8420166a12 100644 --- a/app/services/charges/charge_models/graduated_service.rb +++ b/app/services/charges/charge_models/graduated_service.rb @@ -9,7 +9,7 @@ def ranges charge.properties.map(&:with_indifferent_access) end - def compute_amount(value) + def compute_amount ranges.reduce(0) do |result_amount, range| flat_amount = BigDecimal(range[:flat_amount]) per_unit_amount = BigDecimal(range[:per_unit_amount]) @@ -17,10 +17,10 @@ def compute_amount(value) # NOTE: Add flat amount to the total result_amount += flat_amount unless value.zero? - units = compute_range_units(range[:from_value], range[:to_value], value) + units = compute_range_units(range[:from_value], range[:to_value]) result_amount += units * per_unit_amount - # NOTE: value is between the bounds of the current range, + # NOTE: aggregation_result.aggregation is between the bounds of the current range, # we must stop the loop break result_amount if range[:to_value].nil? || range[:to_value] >= value @@ -29,7 +29,7 @@ def compute_amount(value) end # NOTE: compute how many units to bill in the range - def compute_range_units(from_value, to_value, value) + def compute_range_units(from_value, to_value) # NOTE: value is higher than the to_value of the range if to_value && value >= to_value return to_value - (from_value.zero? ? 1 : from_value) + 1 @@ -41,6 +41,10 @@ def compute_range_units(from_value, to_value, value) # NOTE: value is in the range value - from_value + 1 end + + def value + aggregation_result.aggregation + end end end end diff --git a/app/services/charges/charge_models/package_service.rb b/app/services/charges/charge_models/package_service.rb index c1edc94b512..003f8ddf9bc 100644 --- a/app/services/charges/charge_models/package_service.rb +++ b/app/services/charges/charge_models/package_service.rb @@ -5,9 +5,9 @@ module ChargeModels class PackageService < Charges::ChargeModels::BaseService protected - def compute_amount(value) + def compute_amount # NOTE: exclude free units from the count - billed_units = value - free_units + billed_units = aggregation_result.aggregation - free_units return 0 if billed_units.negative? # NOTE: Check how many packages (groups of units) are consumed diff --git a/app/services/charges/charge_models/percentage_service.rb b/app/services/charges/charge_models/percentage_service.rb index 8f336df5b0b..2f7c617b2b2 100644 --- a/app/services/charges/charge_models/percentage_service.rb +++ b/app/services/charges/charge_models/percentage_service.rb @@ -5,22 +5,48 @@ module ChargeModels class PercentageService < Charges::ChargeModels::BaseService protected - def compute_amount(value) - compute_percentage_amount(value) + compute_fixed_amount(value) + def compute_amount + compute_percentage_amount + compute_fixed_amount end - def compute_percentage_amount(value) - value * (rate.fdiv(100)) + def compute_percentage_amount + return 0 if free_units_value > aggregation_result.aggregation + + (aggregation_result.aggregation - free_units_value) * rate.fdiv(100) end - def compute_fixed_amount(value) - return 0 if value.zero? + def compute_fixed_amount + return 0 if aggregation_result.aggregation.zero? return 0 if fixed_amount.nil? - value * fixed_amount + (aggregation_result.count - free_units_count) * fixed_amount + end + + def free_units_value + return last_running_total unless last_running_total > free_units_per_total_aggregation + + free_units_per_total_aggregation.to_i + end + + def free_units_count + return free_units_per_events unless last_running_total > free_units_per_total_aggregation + + aggregation_result.options[:running_total].count { |e| e < free_units_per_total_aggregation } + end + + def last_running_total + @last_running_total ||= aggregation_result.options[:running_total].last + end + + def free_units_per_total_aggregation + @free_units_per_total_aggregation ||= BigDecimal(charge.properties['free_units_per_total_aggregation'] || 0) + end + + def free_units_per_events + @free_units_per_events ||= charge.properties['free_units_per_events'].to_i end - # NOTE: FE divides percentage rate with 100 and sends to BE + # NOTE: FE divides percentage rate with 100 and sends to BE. def rate BigDecimal(charge.properties['rate']) end diff --git a/app/services/charges/charge_models/standard_service.rb b/app/services/charges/charge_models/standard_service.rb index 35b25907264..de6340e1a79 100644 --- a/app/services/charges/charge_models/standard_service.rb +++ b/app/services/charges/charge_models/standard_service.rb @@ -5,8 +5,8 @@ module ChargeModels class StandardService < Charges::ChargeModels::BaseService protected - def compute_amount(value) - (value * BigDecimal(charge.properties['amount'])) + def compute_amount + (aggregation_result.aggregation * BigDecimal(charge.properties['amount'])) end end end diff --git a/app/services/fees/charge_service.rb b/app/services/fees/charge_service.rb index 562fa97efa1..8b8e5c29300 100644 --- a/app/services/fees/charge_service.rb +++ b/app/services/fees/charge_service.rb @@ -61,14 +61,14 @@ def init_fee end def compute_amount - aggregated_events = aggregator.aggregate( + aggregation_result = aggregator.aggregate( from_date: charges_from_date, to_date: boundaries.to_date, free_units_count: charge.properties.is_a?(Hash) ? charge.properties['free_units_per_events'].to_i : 0, ) - return aggregated_events unless aggregated_events.success? + return aggregation_result unless aggregation_result.success? - charge_model.apply(value: aggregated_events.aggregation) + apply_charge_model_service(aggregation_result) end def already_billed? @@ -98,7 +98,7 @@ def aggregator @aggregator = aggregator_service.new(billable_metric: billable_metric, subscription: subscription) end - def charge_model + def apply_charge_model_service(aggregation_result) return @charge_model if @charge_model model_service = case charge.charge_model.to_sym @@ -114,7 +114,7 @@ def charge_model raise NotImplementedError end - @charge_model = model_service.new(charge: charge) + @charge_model = model_service.apply(charge: charge, aggregation_result: aggregation_result) end def charges_from_date diff --git a/spec/services/charges/charge_models/graduated_service_spec.rb b/spec/services/charges/charge_models/graduated_service_spec.rb index 48d2a393801..61f74d27236 100644 --- a/spec/services/charges/charge_models/graduated_service_spec.rb +++ b/spec/services/charges/charge_models/graduated_service_spec.rb @@ -3,7 +3,15 @@ require 'rails_helper' RSpec.describe Charges::ChargeModels::GraduatedService, type: :service do - subject(:graduated_service) { described_class.new(charge: charge) } + subject(:apply_graduated_service) do + described_class.apply(charge: charge, aggregation_result: aggregation_result) + end + + before do + aggregation_result.aggregation = aggregation + end + + let(:aggregation_result) { BaseService::Result.new } let(:charge) do create( @@ -31,27 +39,51 @@ ) end - it 'does not apply the flat amount for 0' do - expect(graduated_service.apply(value: 0).amount).to eq(0) + context 'when aggregation is 0' do + let(:aggregation) { 0 } + + it 'does not apply the flat amount' do + expect(apply_graduated_service.amount).to eq(0) + end end - it 'applies a unit amount for 1 and the flat rate for 1' do - expect(graduated_service.apply(value: 1).amount).to eq(12) + context 'when aggregation is 1' do + let(:aggregation) { 1 } + + it 'applies a unit amount for 1 and the flat rate for 1' do + expect(apply_graduated_service.amount).to eq(12) + end end - it 'applies all unit amount for top bound' do - expect(graduated_service.apply(value: 10).amount).to eq(102) + context 'when aggregation is 10' do + let(:aggregation) { 10 } + + it 'applies all unit amount for top bound' do + expect(apply_graduated_service.amount).to eq(102) + end end - it 'applies next range flat amount for the next step' do - expect(graduated_service.apply(value: 11).amount).to eq(110) + context 'when aggregation is 11' do + let(:aggregation) { 11 } + + it 'applies next range flat amount for the next step' do + expect(apply_graduated_service.amount).to eq(110) + end end - it 'applies next unit amount for more unit in next step' do - expect(graduated_service.apply(value: 12).amount).to eq(115) + context 'when aggregation is 12' do + let(:aggregation) { 12 } + + it 'applies next unit amount for more unit in next step' do + expect(apply_graduated_service.amount).to eq(115) + end end - it 'applies last unit amount for more unit in last step' do - expect(graduated_service.apply(value: 21).amount).to eq(163) + context 'when aggregation is 21' do + let(:aggregation) { 21 } + + it 'applies last unit amount for more unit in last step' do + expect(apply_graduated_service.amount).to eq(163) + end end end diff --git a/spec/services/charges/charge_models/package_service_spec.rb b/spec/services/charges/charge_models/package_service_spec.rb index a1775abb57b..37a7d704c75 100644 --- a/spec/services/charges/charge_models/package_service_spec.rb +++ b/spec/services/charges/charge_models/package_service_spec.rb @@ -3,7 +3,16 @@ require 'rails_helper' RSpec.describe Charges::ChargeModels::PackageService, type: :service do - subject(:package_service) { described_class.new(charge: charge) } + subject(:apply_package_service) do + described_class.apply(charge: charge, aggregation_result: aggregation_result) + end + + before do + aggregation_result.aggregation = aggregation + end + + let(:aggregation_result) { BaseService::Result.new } + let(:aggregation) { 121 } let(:charge) do create( @@ -17,20 +26,20 @@ end it 'applies the package size to the value' do - expect(package_service.apply(value: 121).amount).to eq(1300) + expect(apply_package_service.amount).to eq(1300) end context 'with free_units' do before { charge.properties['free_units'] = 10 } it 'substracts the free units from the value' do - expect(package_service.apply(value: 121).amount).to eq(1200) + expect(apply_package_service.amount).to eq(1200) end context 'when free units is higher than the value' do before { charge.properties['free_units'] = 200 } - it { expect(package_service.apply(value: 121).amount).to eq(0) } + it { expect(apply_package_service.amount).to eq(0) } end end end diff --git a/spec/services/charges/charge_models/percentage_service_spec.rb b/spec/services/charges/charge_models/percentage_service_spec.rb index f18e7e35f6e..c154a85e26d 100644 --- a/spec/services/charges/charge_models/percentage_service_spec.rb +++ b/spec/services/charges/charge_models/percentage_service_spec.rb @@ -3,53 +3,62 @@ require 'rails_helper' RSpec.describe Charges::ChargeModels::PercentageService, type: :service do - subject(:percentage_service) { described_class.new(charge: charge) } + subject(:apply_percentage_service) do + described_class.apply(charge: charge, aggregation_result: aggregation_result) + end + + before do + aggregation_result.aggregation = aggregation + aggregation_result.count = 4 + aggregation_result.options = { running_total: [50, 150, 400] } + end + + let(:aggregation_result) { BaseService::Result.new } + let(:fixed_amount) { '2.0' } + let(:aggregation) { 800 } + let(:free_units_per_events) { 3 } + let(:free_units_per_total_aggregation) { '250.0' } + + let(:expected_percentage_amount) { (800 - 250) * (1.3 / 100) } + let(:expected_fixed_amount) { (4 - 2) * 2.0 } let(:charge) do create( :percentage_charge, properties: { - rate: '5.55', - fixed_amount: '0', + rate: '1.3', + fixed_amount: fixed_amount, + free_units_per_events: free_units_per_events, + free_units_per_total_aggregation: free_units_per_total_aggregation, }, ) end - context 'when fixed amount value is zero' do - it 'applies the percentage rate to the value' do - expect(percentage_service.apply(value: 100).amount).to eq(5.55) + context 'when aggregation value is 0' do + let(:aggregation) { 0 } + + it 'returns 0' do + expect(apply_percentage_service.amount).to eq(0) end end - context 'when fixed amount value is nil and fixed amount target is nil' do - let(:charge) do - create( - :percentage_charge, - properties: { - rate: '5.55', - fixed_amount: nil, - }, + context 'when fixed amount value is 0' do + it 'returns expected percentage amount' do + expect(apply_percentage_service.amount).to eq( + (expected_percentage_amount + expected_fixed_amount) ) end - - it 'applies the percentage rate to the value' do - expect(percentage_service.apply(value: 100).amount).to eq(5.55) - end end - context 'when fixed amount value is NOT zero' do - let(:charge) do - create( - :percentage_charge, - properties: { - rate: '5.55', - fixed_amount: '2', - }, - ) - end + context 'when free_units_per_total_aggregation > last running total' do + let(:free_units_per_total_aggregation) { '500.0' } + let(:expected_percentage_amount) { (800 - 400) * (1.3 / 100) } + let(:expected_fixed_amount) { (4 - 3) * 2.0 } - it 'applies the percentage rate and the additional charge on each unit' do - expect(percentage_service.apply(value: 100).amount).to eq(205.55) + it 'returns expected percentage amount based on last running total' do + expect(apply_percentage_service.amount).to eq( + (expected_percentage_amount + expected_fixed_amount) + ) end end end diff --git a/spec/services/charges/charge_models/standard_service_spec.rb b/spec/services/charges/charge_models/standard_service_spec.rb index b681ed3a844..d7f9667010d 100644 --- a/spec/services/charges/charge_models/standard_service_spec.rb +++ b/spec/services/charges/charge_models/standard_service_spec.rb @@ -3,7 +3,16 @@ require 'rails_helper' RSpec.describe Charges::ChargeModels::StandardService, type: :service do - subject(:standard_service) { described_class.new(charge: charge) } + subject(:apply_standard_service) do + described_class.apply(charge: charge, aggregation_result: aggregation_result) + end + + before do + aggregation_result.aggregation = aggregation + end + + let(:aggregation_result) { BaseService::Result.new } + let(:aggregation) { 10 } let(:charge) do create( @@ -15,7 +24,7 @@ ) end - it 'apply the charge model to the value' do - expect(standard_service.apply(value: 10).amount).to eq(5000) + it 'applies the charge model to the value' do + expect(apply_standard_service.amount).to eq(5000) end end From 38d6d7a53811f580b3f01080ee12fab32729abda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Semp=C3=A9?= Date: Fri, 19 Aug 2022 11:02:02 +0200 Subject: [PATCH 6/8] feat(free-units): Use delegation for aggregation_result.aggregation --- .../charges/charge_models/base_service.rb | 2 ++ .../charge_models/graduated_service.rb | 24 ++++++++----------- .../charges/charge_models/package_service.rb | 2 +- .../charge_models/percentage_service.rb | 6 ++--- .../charges/charge_models/standard_service.rb | 2 +- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/app/services/charges/charge_models/base_service.rb b/app/services/charges/charge_models/base_service.rb index 6861ddc9694..7fcfd91fb04 100644 --- a/app/services/charges/charge_models/base_service.rb +++ b/app/services/charges/charge_models/base_service.rb @@ -23,6 +23,8 @@ def apply attr_accessor :charge, :aggregation_result + delegate :units, to: :result + def compute_amount(value) raise NotImplementedError end diff --git a/app/services/charges/charge_models/graduated_service.rb b/app/services/charges/charge_models/graduated_service.rb index c8420166a12..75ffe51cce0 100644 --- a/app/services/charges/charge_models/graduated_service.rb +++ b/app/services/charges/charge_models/graduated_service.rb @@ -15,14 +15,14 @@ def compute_amount per_unit_amount = BigDecimal(range[:per_unit_amount]) # NOTE: Add flat amount to the total - result_amount += flat_amount unless value.zero? + result_amount += flat_amount unless units.zero? - units = compute_range_units(range[:from_value], range[:to_value]) - result_amount += units * per_unit_amount + range_units = compute_range_units(range[:from_value], range[:to_value]) + result_amount += range_units * per_unit_amount # NOTE: aggregation_result.aggregation is between the bounds of the current range, # we must stop the loop - break result_amount if range[:to_value].nil? || range[:to_value] >= value + break result_amount if range[:to_value].nil? || range[:to_value] >= units result_amount end @@ -30,20 +30,16 @@ def compute_amount # NOTE: compute how many units to bill in the range def compute_range_units(from_value, to_value) - # NOTE: value is higher than the to_value of the range - if to_value && value >= to_value + # NOTE: units is higher than the to_value of the range + if to_value && units >= to_value return to_value - (from_value.zero? ? 1 : from_value) + 1 end - return to_value - from_value if to_value && value >= to_value - return value if from_value.zero? + return to_value - from_value if to_value && units >= to_value + return units if from_value.zero? - # NOTE: value is in the range - value - from_value + 1 - end - - def value - aggregation_result.aggregation + # NOTE: units is in the range + units - from_value + 1 end end end diff --git a/app/services/charges/charge_models/package_service.rb b/app/services/charges/charge_models/package_service.rb index 003f8ddf9bc..1eb3209774c 100644 --- a/app/services/charges/charge_models/package_service.rb +++ b/app/services/charges/charge_models/package_service.rb @@ -7,7 +7,7 @@ class PackageService < Charges::ChargeModels::BaseService def compute_amount # NOTE: exclude free units from the count - billed_units = aggregation_result.aggregation - free_units + billed_units = units - free_units return 0 if billed_units.negative? # NOTE: Check how many packages (groups of units) are consumed diff --git a/app/services/charges/charge_models/percentage_service.rb b/app/services/charges/charge_models/percentage_service.rb index 2f7c617b2b2..6b0f8ebe104 100644 --- a/app/services/charges/charge_models/percentage_service.rb +++ b/app/services/charges/charge_models/percentage_service.rb @@ -10,13 +10,13 @@ def compute_amount end def compute_percentage_amount - return 0 if free_units_value > aggregation_result.aggregation + return 0 if free_units_value > units - (aggregation_result.aggregation - free_units_value) * rate.fdiv(100) + (units - free_units_value) * rate.fdiv(100) end def compute_fixed_amount - return 0 if aggregation_result.aggregation.zero? + return 0 if units.zero? return 0 if fixed_amount.nil? (aggregation_result.count - free_units_count) * fixed_amount diff --git a/app/services/charges/charge_models/standard_service.rb b/app/services/charges/charge_models/standard_service.rb index de6340e1a79..b88f91131e0 100644 --- a/app/services/charges/charge_models/standard_service.rb +++ b/app/services/charges/charge_models/standard_service.rb @@ -6,7 +6,7 @@ class StandardService < Charges::ChargeModels::BaseService protected def compute_amount - (aggregation_result.aggregation * BigDecimal(charge.properties['amount'])) + (units * BigDecimal(charge.properties['amount'])) end end end From 47c53ed5f59ee5ef8012acd08fd91b998287950c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Semp=C3=A9?= Date: Thu, 18 Aug 2022 16:37:42 +0200 Subject: [PATCH 7/8] feat(free-units): Add events_count on Fee and update invoice --- app/models/fee.rb | 1 + app/serializers/v1/fee_serializer.rb | 1 + app/services/charges/charge_models/base_service.rb | 1 + app/services/fees/charge_service.rb | 3 ++- app/views/templates/invoice.slim | 5 ++++- db/migrate/20220818141616_add_events_count_to_fees.rb | 7 +++++++ db/schema.rb | 3 ++- spec/services/fees/add_on_service_spec.rb | 1 + spec/services/fees/charge_service_spec.rb | 1 + spec/services/fees/subscription_service_spec.rb | 1 + 10 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20220818141616_add_events_count_to_fees.rb diff --git a/app/models/fee.rb b/app/models/fee.rb index c29440d4048..595416b7b3b 100644 --- a/app/models/fee.rb +++ b/app/models/fee.rb @@ -19,6 +19,7 @@ class Fee < ApplicationRecord validates :amount_currency, inclusion: { in: currency_list } validates :vat_amount_currency, inclusion: { in: currency_list } validates :units, numericality: { greated_than_or_equal_to: 0 } + validates :events_count, numericality: { greated_than_or_equal_to: 0 }, allow_nil: true scope :subscription_kind, -> { where(charge_id: nil, applied_add_on_id: nil) } scope :charge_kind, -> { where.not(charge_id: nil) } diff --git a/app/serializers/v1/fee_serializer.rb b/app/serializers/v1/fee_serializer.rb index 69278299b65..a358a09709f 100644 --- a/app/serializers/v1/fee_serializer.rb +++ b/app/serializers/v1/fee_serializer.rb @@ -14,6 +14,7 @@ def serialize vat_amount_cents: model.vat_amount_cents, vat_amount_currency: model.vat_amount_currency, units: model.units, + events_count: model.events_count, } end end diff --git a/app/services/charges/charge_models/base_service.rb b/app/services/charges/charge_models/base_service.rb index 7fcfd91fb04..0895cad6c22 100644 --- a/app/services/charges/charge_models/base_service.rb +++ b/app/services/charges/charge_models/base_service.rb @@ -15,6 +15,7 @@ def initialize(charge:, aggregation_result:) def apply result.units = aggregation_result.aggregation + result.count = aggregation_result.count result.amount = compute_amount result end diff --git a/app/services/fees/charge_service.rb b/app/services/fees/charge_service.rb index 8b8e5c29300..8a71258a8ef 100644 --- a/app/services/fees/charge_service.rb +++ b/app/services/fees/charge_service.rb @@ -52,7 +52,8 @@ def init_fee amount_currency: charge.amount_currency, vat_rate: customer.applicable_vat_rate, units: amount_result.units, - properties: boundaries.to_h + properties: boundaries.to_h, + events_count: amount_result.count, ) new_fee.compute_vat diff --git a/app/views/templates/invoice.slim b/app/views/templates/invoice.slim index 538de81f724..2427aac6745 100644 --- a/app/views/templates/invoice.slim +++ b/app/views/templates/invoice.slim @@ -315,7 +315,10 @@ html td width="70%" .body-1 = fee.billable_metric.name .body-3 - | Total unit: #{fee.units} + - if fee.charge.percentage? + | Total unit: #{fee.events_count} events for #{fee.units} + - else + | Total unit: #{fee.units} td.body-1 width="30%" = fee.amount.format table.total-table width="100%" diff --git a/db/migrate/20220818141616_add_events_count_to_fees.rb b/db/migrate/20220818141616_add_events_count_to_fees.rb new file mode 100644 index 00000000000..c504c8dfe03 --- /dev/null +++ b/db/migrate/20220818141616_add_events_count_to_fees.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddEventsCountToFees < ActiveRecord::Migration[7.0] + def change + add_column :fees, :events_count, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 13c0602c526..4a69627148e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_08_16_120137) do +ActiveRecord::Schema[7.0].define(version: 2022_08_18_141616) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -194,6 +194,7 @@ t.decimal "units", default: "0.0", null: false t.uuid "applied_add_on_id" t.jsonb "properties", default: {}, null: false + t.integer "events_count" t.index ["applied_add_on_id"], name: "index_fees_on_applied_add_on_id" t.index ["charge_id"], name: "index_fees_on_charge_id" t.index ["invoice_id"], name: "index_fees_on_invoice_id" diff --git a/spec/services/fees/add_on_service_spec.rb b/spec/services/fees/add_on_service_spec.rb index 1a1be6d898d..0552c1aa12b 100644 --- a/spec/services/fees/add_on_service_spec.rb +++ b/spec/services/fees/add_on_service_spec.rb @@ -28,6 +28,7 @@ expect(created_fee.vat_amount_cents).to eq(40) expect(created_fee.vat_rate).to eq(20.0) expect(created_fee.units).to eq(1) + expect(created_fee.events_count).to be_nil end end diff --git a/spec/services/fees/charge_service_spec.rb b/spec/services/fees/charge_service_spec.rb index d0aedd70123..54816099a13 100644 --- a/spec/services/fees/charge_service_spec.rb +++ b/spec/services/fees/charge_service_spec.rb @@ -57,6 +57,7 @@ expect(created_fee.vat_amount_cents).to eq(0) expect(created_fee.vat_rate).to eq(20.0) expect(created_fee.units).to eq(0) + expect(created_fee.events_count).to be_nil end end diff --git a/spec/services/fees/subscription_service_spec.rb b/spec/services/fees/subscription_service_spec.rb index 58ca0ba2b65..6404ab379b6 100644 --- a/spec/services/fees/subscription_service_spec.rb +++ b/spec/services/fees/subscription_service_spec.rb @@ -41,6 +41,7 @@ expect(created_fee.vat_amount_cents).to eq(20) expect(created_fee.vat_rate).to eq(20.0) expect(created_fee.units).to eq(1) + expect(created_fee.events_count).to be_nil end end From dfea40c3ff2d76f5a834a3e8f135bd0b532b1647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Semp=C3=A9?= Date: Fri, 19 Aug 2022 15:08:36 +0200 Subject: [PATCH 8/8] feat(free-units): Calculate percentage without free units --- .../charge_models/percentage_service.rb | 7 ++- .../charge_models/percentage_service_spec.rb | 44 ++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/app/services/charges/charge_models/percentage_service.rb b/app/services/charges/charge_models/percentage_service.rb index 6b0f8ebe104..fc2e1898788 100644 --- a/app/services/charges/charge_models/percentage_service.rb +++ b/app/services/charges/charge_models/percentage_service.rb @@ -23,19 +23,22 @@ def compute_fixed_amount end def free_units_value + return last_running_total if free_units_per_total_aggregation.zero? + return free_units_per_total_aggregation if last_running_total.zero? return last_running_total unless last_running_total > free_units_per_total_aggregation - free_units_per_total_aggregation.to_i + free_units_per_total_aggregation end def free_units_count + return free_units_per_events if free_units_per_total_aggregation.zero? return free_units_per_events unless last_running_total > free_units_per_total_aggregation aggregation_result.options[:running_total].count { |e| e < free_units_per_total_aggregation } end def last_running_total - @last_running_total ||= aggregation_result.options[:running_total].last + @last_running_total ||= aggregation_result.options[:running_total].last || 0 end def free_units_per_total_aggregation diff --git a/spec/services/charges/charge_models/percentage_service_spec.rb b/spec/services/charges/charge_models/percentage_service_spec.rb index c154a85e26d..1215eb68034 100644 --- a/spec/services/charges/charge_models/percentage_service_spec.rb +++ b/spec/services/charges/charge_models/percentage_service_spec.rb @@ -10,9 +10,10 @@ before do aggregation_result.aggregation = aggregation aggregation_result.count = 4 - aggregation_result.options = { running_total: [50, 150, 400] } + aggregation_result.options = { running_total: running_total } end + let(:running_total) { [50, 150, 400] } let(:aggregation_result) { BaseService::Result.new } let(:fixed_amount) { '2.0' } let(:aggregation) { 800 } @@ -50,6 +51,47 @@ end end + context 'when free_units_per_events is nil' do + let(:free_units_per_events) { nil } + let(:running_total) { [] } + + let(:expected_percentage_amount) { (800 - 250) * (1.3 / 100) } + let(:expected_fixed_amount) { (4 - 0) * 2.0 } + + it 'returns expected percentage amount' do + expect(apply_percentage_service.amount).to eq( + (expected_percentage_amount + expected_fixed_amount) + ) + end + end + + context 'when free_units_per_total_aggregation is nil' do + let(:free_units_per_total_aggregation) { nil } + let(:expected_percentage_amount) { (800 - 400) * (1.3 / 100) } + let(:expected_fixed_amount) { (4 - 3) * 2.0 } + + it 'returns expected percentage amount' do + expect(apply_percentage_service.amount).to eq( + (expected_percentage_amount + expected_fixed_amount) + ) + end + end + + context 'when free units are not set' do + let(:free_units_per_total_aggregation) { nil } + let(:free_units_per_events) { nil } + let(:running_total) { [] } + + let(:expected_percentage_amount) { 800 * (1.3 / 100) } + let(:expected_fixed_amount) { 4 * 2.0 } + + it 'returns expected percentage amount' do + expect(apply_percentage_service.amount).to eq( + (expected_percentage_amount + expected_fixed_amount) + ) + end + end + context 'when free_units_per_total_aggregation > last running total' do let(:free_units_per_total_aggregation) { '500.0' } let(:expected_percentage_amount) { (800 - 400) * (1.3 / 100) }