diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index a455b8f..8e13430 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -2,6 +2,8 @@ require 'time' class PaymentsController < ApplicationController + MAX_PAYMENT_AMOUNT = 2000 + protect_from_forgery with: :exception skip_before_action :verify_authenticity_token, only: [:payment_receipt] before_action :verify_payment_callback, only: [:payment_receipt] @@ -41,7 +43,13 @@ def payment_receipt end def make_payment - processed_url = generate_hash(@current_user, params['amount']) + amount = validated_payment_amount(params['amount']) + if amount.nil? + redirect_to all_payments_path, alert: 'Please enter a valid payment amount.' + return + end + + processed_url = generate_hash(current_user, amount) redirect_to processed_url, allow_other_host: true end @@ -55,6 +63,7 @@ def payment_show @cost_subscription = current_application_settings.subscription_cost.to_f @total_cost = cost_lodging + cost_partner + (@has_subscription ? @cost_subscription : 0) @balance_due = @total_cost - @ttl_paid + @max_payment_amount = max_payment_amount_for(@balance_due) end def delete_manual_payment @@ -115,6 +124,36 @@ def generate_hash(current_user, amount=100) final_url = connection_hash[url_to_use] + '?' + url_for_payment + 'hash=' + encoded_hash end + def validated_payment_amount(raw_amount) + amount = Integer(raw_amount, exception: false) + return nil if amount.nil? || amount <= 0 + + balance_due = current_balance_due + return nil if balance_due <= 0 + + max_amount = max_payment_amount_for(balance_due) + return nil if amount > max_amount + + amount + end + + def max_payment_amount_for(balance_due) + [balance_due.floor, MAX_PAYMENT_AMOUNT].min + end + + def current_balance_due + current_application + return 0.0 if @current_application.nil? + + cost_lodging = Lodging.find_by(description: @current_application.lodging_selection)&.cost.to_f + cost_partner = @current_application.partner_registration&.cost.to_f + has_subscription = @current_application.subscription + cost_subscription = current_application_settings.subscription_cost.to_f + total_cost = cost_lodging + cost_partner + (has_subscription ? cost_subscription : 0) + total_paid = Payment.current_conference_payments.where(user_id: current_user, transaction_status: '1').pluck(:total_amount).map(&:to_f).sum / 100 + total_cost - total_paid + end + def url_params params.permit(:amount, :transactionType, :transactionStatus, :transactionId, :transactionTotalAmount, :transactionDate, :transactionAcountType, :transactionResultCode, :transactionResultMessage, :orderNumber, :timestamp, :hash, :conf_year) end diff --git a/app/views/payments/payment_show.html.erb b/app/views/payments/payment_show.html.erb index f51887e..0432cb2 100644 --- a/app/views/payments/payment_show.html.erb +++ b/app/views/payments/payment_show.html.erb @@ -61,10 +61,13 @@
$
- <%= f.number_field :amount, value: "#{@balance_due.to_i}", - within: 1..2000, required: true, class: 'form-control' %> + <%= f.number_field :amount, value: @max_payment_amount, + within: 1..@max_payment_amount, required: true, class: 'form-control' %> <%= f.submit "Pay Now", class: 'btn btn-sm btn-success ml-2' %> + + The amount is prefilled with your full balance due, but you can enter a different amount to make a partial payment. + <% end %> diff --git a/spec/controllers/payments_controller_spec.rb b/spec/controllers/payments_controller_spec.rb index 4a4faff..fea37df 100644 --- a/spec/controllers/payments_controller_spec.rb +++ b/spec/controllers/payments_controller_spec.rb @@ -168,14 +168,22 @@ end context 'with different amounts' do - it 'handles decimal amounts correctly' do + it 'rejects decimal amounts' do post :make_payment, params: { amount: '50.50' } - expect(response.location).to include('amountDue=5000') # 50.50.to_i * 100 = 50 * 100 + expect(response).to redirect_to(all_payments_path) + expect(flash[:alert]).to eq('Please enter a valid payment amount.') end - it 'handles zero amount' do + it 'rejects zero amount' do post :make_payment, params: { amount: '0' } - expect(response.location).to include('amountDue=0') + expect(response).to redirect_to(all_payments_path) + expect(flash[:alert]).to eq('Please enter a valid payment amount.') + end + + it 'rejects amount above current balance due' do + post :make_payment, params: { amount: '500' } + expect(response).to redirect_to(all_payments_path) + expect(flash[:alert]).to eq('Please enter a valid payment amount.') end end end diff --git a/spec/requests/payments_spec.rb b/spec/requests/payments_spec.rb index 1884763..99a008e 100644 --- a/spec/requests/payments_spec.rb +++ b/spec/requests/payments_spec.rb @@ -33,16 +33,22 @@ before do sign_in user allow_any_instance_of(PaymentsController).to receive(:user_has_payments?).and_return(true) + allow_any_instance_of(PaymentsController).to receive(:payments_open?).and_return(true) + allow_any_instance_of(PaymentsController).to receive(:current_application) do |controller| + controller.instance_variable_set(:@current_application, application) + end + create(:lodging, description: application.lodging_selection, cost: 100.0) + application.update!(partner_registration: create(:partner_registration, cost: 0.0), subscription: false) + create(:payment, :current_conference, user: user, transaction_status: '1', total_amount: '3000') - mock_application = instance_double(Application, - lodging_selection: "Standard", - partner_registration: instance_double(PartnerRegistration, cost: 0.0), - subscription: false + allow_any_instance_of(PaymentsController).to receive(:current_application_settings).and_return( + double(subscription_cost: 25.0, payments_directions: 'Payment instructions', allow_payments: true) ) - allow_any_instance_of(PaymentsController).to receive(:current_application).and_return(mock_application) + end - allow(Payment).to receive_message_chain(:current_conference_payments, :where, :pluck).and_return([1000, 2000]) - allow_any_instance_of(PaymentsController).to receive(:current_application_settings).and_return(double(subscription_cost: 25.0)) + it "renders helper text explaining partial payments" do + get all_payments_path + expect(response.body).to include("The amount is prefilled with your full balance due, but you can enter a different amount to make a partial payment.") end end end @@ -58,6 +64,15 @@ context "when user is signed in" do before do sign_in user + allow_any_instance_of(PaymentsController).to receive(:current_application) do |controller| + controller.instance_variable_set(:@current_application, application) + end + create(:lodging, description: application.lodging_selection, cost: 300.0) + application.update!(partner_registration: create(:partner_registration, cost: 0.0), subscription: false) + allow_any_instance_of(PaymentsController).to receive(:current_application_settings).and_return( + double(subscription_cost: 25.0) + ) + allow_any_instance_of(PaymentsController).to receive(:current_balance_due).and_return(300.0) allow_any_instance_of(PaymentsController).to receive(:generate_hash).and_return("https://payment-url.example.com") end @@ -65,6 +80,55 @@ post make_payment_path, params: { amount: "100" } expect(response).to redirect_to("https://payment-url.example.com") end + + it "supports paying the full balance amount" do + expect_any_instance_of(PaymentsController) + .to receive(:generate_hash).with(user, 200).and_return("https://payment-url.example.com") + + post make_payment_path, params: { amount: "200" } + expect(response).to redirect_to("https://payment-url.example.com") + end + + it "supports paying a partial balance amount" do + expect_any_instance_of(PaymentsController) + .to receive(:generate_hash).with(user, 75).and_return("https://payment-url.example.com") + + post make_payment_path, params: { amount: "75" } + expect(response).to redirect_to("https://payment-url.example.com") + end + + it "rejects missing amount input" do + post make_payment_path, params: { amount: "" } + expect(response).to redirect_to(all_payments_path) + expect(flash[:alert]).to eq("Please enter a valid payment amount.") + end + + it "rejects non-numeric amount input" do + post make_payment_path, params: { amount: "abc" } + expect(response).to redirect_to(all_payments_path) + expect(flash[:alert]).to eq("Please enter a valid payment amount.") + end + + it "rejects non-positive amount input" do + post make_payment_path, params: { amount: "0" } + expect(response).to redirect_to(all_payments_path) + expect(flash[:alert]).to eq("Please enter a valid payment amount.") + end + + it "rejects amount above balance due" do + post make_payment_path, params: { amount: "400" } + expect(response).to redirect_to(all_payments_path) + expect(flash[:alert]).to eq("Please enter a valid payment amount.") + end + + it "rejects amount above MAX_PAYMENT_AMOUNT even when balance due is higher" do + allow_any_instance_of(PaymentsController).to receive(:current_balance_due).and_return(3000.0) + + post make_payment_path, params: { amount: "2001" } + + expect(response).to redirect_to(all_payments_path) + expect(flash[:alert]).to eq("Please enter a valid payment amount.") + end end end