From ec5a2194562e3eece215dec228f99ab8316d24c5 Mon Sep 17 00:00:00 2001 From: Martin Lensment Date: Thu, 2 Jul 2015 13:06:58 +0300 Subject: [PATCH 1/5] Add method to return correct price for an operation #2741 --- app/models/pricelist.rb | 12 ++++- spec/models/pricelist_spec.rb | 95 +++++++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/app/models/pricelist.rb b/app/models/pricelist.rb index cfdb53776..1c47c1b83 100644 --- a/app/models/pricelist.rb +++ b/app/models/pricelist.rb @@ -1,6 +1,8 @@ class Pricelist < ActiveRecord::Base include Versions # version/pricelist_version.rb + scope :valid, -> { where("valid_from <= ? AND valid_to >= ? OR valid_to IS NULL", Time.zone.now, Time.zone.now) } + monetize :price_cents validates :price_cents, :price_currency, :price, @@ -13,10 +15,18 @@ class Pricelist < ActiveRecord::Base after_initialize :init_values def init_values return unless new_record? - self.valid_from = Time.zone.now.beginning_of_year + self.valid_from = Time.zone.now.beginning_of_year unless valid_from end def name "#{operation_category} #{category}" end + + class << self + def price_for(category, operation, duration) + lists = valid.where(category: category, operation_category: operation, duration: duration) + return lists.first.price if lists.count == 1 + lists.where('valid_to IS NOT NULL').order(valid_from: :desc).first.price + end + end end diff --git a/spec/models/pricelist_spec.rb b/spec/models/pricelist_spec.rb index f52b1aeeb..9bdabf1d8 100644 --- a/spec/models/pricelist_spec.rb +++ b/spec/models/pricelist_spec.rb @@ -15,8 +15,8 @@ describe Pricelist do it 'should not be valid' do @pricelist.valid? @pricelist.errors.full_messages.should match_array([ - "Category is missing", - "Duration is missing", + "Category is missing", + "Duration is missing", "Operation category is missing" ]) end @@ -36,7 +36,6 @@ describe Pricelist do it 'should not have name' do @pricelist.name.should == ' ' end - end context 'with valid attributes' do @@ -69,4 +68,94 @@ describe Pricelist do end end end + + it 'should return correct price' do + Fabricate(:pricelist, { + category: 'ee', + operation_category: 'create', + duration: '1year', + price: 1.50, + valid_from: Time.zone.parse('2198-01-01'), + valid_to: Time.zone.parse('2199-01-01') + }) + + expect { Pricelist.price_for('ee', 'create', '1year') }.to raise_error(NoMethodError) + + Fabricate(:pricelist, { + category: 'ee', + operation_category: 'create', + duration: '1year', + price: 1.50, + valid_from: Time.zone.parse('2015-01-01'), + valid_to: nil + }) + + Pricelist.price_for('ee', 'create', '1year').should == 1.50 + + Fabricate(:pricelist, { + category: 'ee', + operation_category: 'create', + duration: '1year', + price: 1.30, + valid_from: Time.zone.parse('2015-01-01'), + valid_to: Time.zone.parse('2999-01-01') + }) + + Pricelist.price_for('ee', 'create', '1year').should == 1.30 + + Fabricate.create(:pricelist, { + category: 'ee', + operation_category: 'create', + duration: '1year', + price: 1.20, + valid_from: Time.zone.parse('2015-06-01'), + valid_to: Time.zone.parse('2999-01-01') + }) + + Pricelist.price_for('ee', 'create', '1year').should == 1.20 + + Fabricate.create(:pricelist, { + category: 'ee', + operation_category: 'create', + duration: '1year', + price: 1.10, + valid_from: Time.zone.parse('2014-01-01'), + valid_to: Time.zone.parse('2999-01-01') + }) + + Pricelist.price_for('ee', 'create', '1year').should == 1.20 + + Fabricate.create(:pricelist, { + category: 'ee', + operation_category: 'create', + duration: '1year', + price: 1.10, + valid_from: Time.zone.parse('2999-02-01'), + valid_to: Time.zone.parse('2999-01-01') + }) + + Pricelist.price_for('ee', 'create', '1year').should == 1.20 + + Fabricate.create(:pricelist, { + category: 'ee', + operation_category: 'create', + duration: '1year', + price: 1.10, + valid_from: Time.zone.parse('2015-06-02'), + valid_to: nil + }) + + Pricelist.price_for('ee', 'create', '1year').should == 1.20 + + Fabricate.create(:pricelist, { + category: 'ee', + operation_category: 'create', + duration: '1year', + price: 1.10, + valid_from: Time.zone.parse('2015-07-01'), + valid_to: Time.zone.parse('2999-01-01') + }) + + Pricelist.price_for('ee', 'create', '1year').should == 1.10 + end end From e2374f9702fa561b0e8343bc885abc6264732afc Mon Sep 17 00:00:00 2001 From: Martin Lensment Date: Thu, 2 Jul 2015 13:08:05 +0300 Subject: [PATCH 2/5] Improve test #2741 --- spec/models/pricelist_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/models/pricelist_spec.rb b/spec/models/pricelist_spec.rb index 9bdabf1d8..9323960b7 100644 --- a/spec/models/pricelist_spec.rb +++ b/spec/models/pricelist_spec.rb @@ -70,6 +70,8 @@ describe Pricelist do end it 'should return correct price' do + expect { Pricelist.price_for('ee', 'create', '1year') }.to raise_error(NoMethodError) + Fabricate(:pricelist, { category: 'ee', operation_category: 'create', From 98439b896cdd4891d77b7cd5df7f9399450c1175 Mon Sep 17 00:00:00 2001 From: Martin Lensment Date: Thu, 2 Jul 2015 17:08:24 +0300 Subject: [PATCH 3/5] Debit account on domain create #2741 --- app/controllers/epp/domains_controller.rb | 27 ++++++++++++++++--- app/models/bank_transaction.rb | 4 ++- app/models/domain.rb | 6 +++++ app/models/pricelist.rb | 4 +-- app/models/registrar.rb | 10 +++++++ .../registrar/account_activities/index.haml | 2 +- config/locales/en.yml | 2 ++ 7 files changed, 47 insertions(+), 8 deletions(-) diff --git a/app/controllers/epp/domains_controller.rb b/app/controllers/epp/domains_controller.rb index a3977e8e2..3bb8f50e1 100644 --- a/app/controllers/epp/domains_controller.rb +++ b/app/controllers/epp/domains_controller.rb @@ -21,11 +21,18 @@ class Epp::DomainsController < EppController def create authorize! :create, Epp::Domain @domain = Epp::Domain.new_from_epp(params[:parsed_frame], current_user) + @domain.valid? - if @domain.errors.any? || !@domain.save - handle_errors(@domain) - else - render_epp_response '/epp/domains/create' + handle_errors(@domain) and return if @domain.errors.any? + handle_errors and return unless balance_ok?('create') + + ActiveRecord::Base.transaction do + if @domain.save + current_user.registrar.debit!(@domain_price, "#{I18n.t('create')} #{@domain.name}") + render_epp_response '/epp/domains/create' + else + handle_errors(@domain) + end end end @@ -185,4 +192,16 @@ class Epp::DomainsController < EppController msg: "#{I18n.t(:client_side_status_editing_error)}: status [status]" } end + + def balance_ok?(operation) + @domain_price = @domain.price(operation).amount + if current_user.registrar.balance < @domain_price + epp_errors << { + code: '2104', + msg: I18n.t('billing_failure_credit_balance_low') + } + return false + end + true + end end diff --git a/app/models/bank_transaction.rb b/app/models/bank_transaction.rb index 1a2ec5568..a5c8da94c 100644 --- a/app/models/bank_transaction.rb +++ b/app/models/bank_transaction.rb @@ -3,7 +3,9 @@ class BankTransaction < ActiveRecord::Base belongs_to :bank_statement has_one :account_activity - scope :unbinded, -> { where('id NOT IN (SELECT bank_transaction_id FROM account_activities)') } + scope :unbinded, -> { + where('id NOT IN (SELECT bank_transaction_id FROM account_activities where bank_transaction_id IS NOT NULL)') + } def binded? account_activity.present? diff --git a/app/models/domain.rb b/app/models/domain.rb index 2fa8e338e..6203fe2f4 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -372,6 +372,12 @@ class Domain < ActiveRecord::Base DomainMailer.pending_deleted(self).deliver_now end + def price(operation) + zone = name.split('.').drop(1).join('.') + p = "#{self.period}year" + Pricelist.price_for(zone, operation, p) + end + ### VALIDATIONS ### def validate_nameserver_ips diff --git a/app/models/pricelist.rb b/app/models/pricelist.rb index 1c47c1b83..4bd1847cd 100644 --- a/app/models/pricelist.rb +++ b/app/models/pricelist.rb @@ -23,8 +23,8 @@ class Pricelist < ActiveRecord::Base end class << self - def price_for(category, operation, duration) - lists = valid.where(category: category, operation_category: operation, duration: duration) + def price_for(zone, operation, period) + lists = valid.where(category: zone, operation_category: operation, duration: period) return lists.first.price if lists.count == 1 lists.where('valid_to IS NOT NULL').order(valid_from: :desc).first.price end diff --git a/app/models/registrar.rb b/app/models/registrar.rb index 1feb2caf4..e2a3f7e4d 100644 --- a/app/models/registrar.rb +++ b/app/models/registrar.rb @@ -12,6 +12,8 @@ class Registrar < ActiveRecord::Base has_many :priv_contacts, -> { privs }, class_name: 'Contact' has_many :white_ips, dependent: :destroy + delegate :balance, to: :cash_account + validates :name, :reg_no, :country_code, :email, :code, presence: true validates :name, :reg_no, :reference_no, :code, uniqueness: true validate :forbidden_codes @@ -121,6 +123,14 @@ class Registrar < ActiveRecord::Base accounts.find_by(account_type: Account::CASH) end + def debit!(sum, description) + cash_account.account_activities.create!( + sum: -sum, + currency: 'EUR', + description: description + ) + end + def domain_transfers at = DomainTransfer.arel_table DomainTransfer.where( diff --git a/app/views/registrar/account_activities/index.haml b/app/views/registrar/account_activities/index.haml index 45783d727..0efd0ab20 100644 --- a/app/views/registrar/account_activities/index.haml +++ b/app/views/registrar/account_activities/index.haml @@ -20,7 +20,7 @@ - if x.invoice %td= link_to(x.invoice, [:registrar, x.invoice]) - else - %td \- + %td - - c = x.sum > 0.0 ? 'text-success' : 'text-danger' - s = x.sum > 0.0 ? "+#{x.sum} #{x.currency}" : "#{x.sum} #{x.currency}" %td{class: c}= s diff --git a/config/locales/en.yml b/config/locales/en.yml index 679bb44a0..0faa3b0ef 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -854,3 +854,5 @@ en: registry_zip: 'Postcode' registry_country_code: 'Country' blocked_domains: 'Blocked domains' + billing_failure_credit_balance_low: 'Billing failure - credit balance low' + create: 'Create' From 61dfe6e1f3486e5c3506c7807cc7f91bd8fdbeee Mon Sep 17 00:00:00 2001 From: Martin Lensment Date: Thu, 2 Jul 2015 17:49:40 +0300 Subject: [PATCH 4/5] Add period unit support to domain price method #2741 --- app/models/domain.rb | 7 ++++++- spec/models/domain_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/models/domain.rb b/app/models/domain.rb index 6203fe2f4..1800c98ec 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -374,7 +374,12 @@ class Domain < ActiveRecord::Base def price(operation) zone = name.split('.').drop(1).join('.') - p = "#{self.period}year" + + p = period / 365 if period_unit == 'd' + p = period / 12 if period_unit == 'm' + p = period if period_unit == 'y' + + p = "#{p}year" Pricelist.price_for(zone, operation, p) end diff --git a/spec/models/domain_spec.rb b/spec/models/domain_spec.rb index 380b907e5..41b0970e7 100644 --- a/spec/models/domain_spec.rb +++ b/spec/models/domain_spec.rb @@ -182,6 +182,26 @@ describe Domain do @domain.force_delete_at.should be_nil end + it 'should know its price' do + Fabricate(:pricelist, { + category: 'ee', + operation_category: 'create', + duration: '1year', + price: 1.50, + valid_from: Time.zone.parse('2015-01-01'), + valid_to: nil + }) + + domain = Fabricate(:domain) + domain.price('create').should == 1.50 + + domain = Fabricate(:domain, period: 12, period_unit: 'm') + domain.price('create').should == 1.50 + + domain = Fabricate(:domain, period: 365, period_unit: 'd') + domain.price('create').should == 1.50 + end + context 'about registrant update confirm' do before :all do @domain.registrant_verification_token = 123 From 36e4f2ddc91958f234fd84ccf2c8bd2e7ee7eea1 Mon Sep 17 00:00:00 2001 From: Martin Lensment Date: Thu, 2 Jul 2015 17:57:49 +0300 Subject: [PATCH 5/5] Fix some tests #2741 --- app/models/registrar.rb | 8 ++++++++ spec/epp/domain_spec.rb | 3 +++ spec/fabricators/pricelist_fabricator.rb | 4 ++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/models/registrar.rb b/app/models/registrar.rb index e2a3f7e4d..5d89816d8 100644 --- a/app/models/registrar.rb +++ b/app/models/registrar.rb @@ -131,6 +131,14 @@ class Registrar < ActiveRecord::Base ) end + def credit!(sum, description) + cash_account.account_activities.create!( + sum: sum, + currency: 'EUR', + description: description + ) + end + def domain_transfers at = DomainTransfer.arel_table DomainTransfer.where( diff --git a/spec/epp/domain_spec.rb b/spec/epp/domain_spec.rb index c9bb99451..fae65e7f6 100644 --- a/spec/epp/domain_spec.rb +++ b/spec/epp/domain_spec.rb @@ -5,7 +5,9 @@ describe 'EPP Domain', epp: true do @xsd = Nokogiri::XML::Schema(File.read('doc/schemas/domain-eis-1.0.xsd')) @epp_xml = EppXml.new(cl_trid: 'ABC-12345') @registrar1 = Fabricate(:registrar1, code: 'REGDOMAIN1') + @registrar1.credit!(10000, '') @registrar2 = Fabricate(:registrar2, code: 'REGDOMAIN2') + @registrar2.credit!(10000, '') Fabricate(:api_user, username: 'registrar1', registrar: @registrar1) Fabricate(:api_user, username: 'registrar2', registrar: @registrar2) @@ -17,6 +19,7 @@ describe 'EPP Domain', epp: true do Fabricate(:contact, code: 'FIXED:JURIDICAL_1234', ident_type: 'bic') Fabricate(:reserved_domain) Fabricate(:blocked_domain) + Fabricate(:pricelist) @uniq_no = proc { @i ||= 0; @i += 1 } end diff --git a/spec/fabricators/pricelist_fabricator.rb b/spec/fabricators/pricelist_fabricator.rb index 296c3b5fb..36e24b0c8 100644 --- a/spec/fabricators/pricelist_fabricator.rb +++ b/spec/fabricators/pricelist_fabricator.rb @@ -1,8 +1,8 @@ Fabricator(:pricelist) do valid_from 1.year.ago valid_to 1.year.since - category '.ee' + category 'ee' duration '1year' - operation_category 'new' + operation_category 'create' price 10 end