From 318f293d89b4efb1a50630f2ca6e3eeb9171b483 Mon Sep 17 00:00:00 2001 From: Amolith Date: Mon, 7 Apr 2025 13:24:26 -0600 Subject: [PATCH 01/10] feat: add upper limit to top-ups based on trust level --- lib/buy_account_credit_form.rb | 21 ++++---- lib/credit_card_sale.rb | 41 +++++++++++++-- lib/trust_level.rb | 58 +++++++++++++++++---- sgx_jmp.rb | 5 +- test/test_buy_account_credit_form.rb | 29 +++++++---- test/test_credit_card_sale.rb | 75 ++++++++++++++++++++++++++++ 6 files changed, 194 insertions(+), 35 deletions(-) diff --git a/lib/buy_account_credit_form.rb b/lib/buy_account_credit_form.rb index b6a1da671cb6c4c17886ed433c7dc2ff56e5a828..69b367672708066cd08fa84cd3f52d57236307a4 100644 --- a/lib/buy_account_credit_form.rb +++ b/lib/buy_account_credit_form.rb @@ -1,21 +1,25 @@ # frozen_string_literal: true +require_relative "trust_level_repo" +require_relative "credit_card_sale" + class BuyAccountCreditForm - class AmountValidationError < StandardError - def initialize(amount) - super("amount #{amount} must be more than $15") - end + def self.trust_level_repo(**kwargs) + kwargs[:trust_level_repo] || TrustLevelRepo.new(**kwargs) end def self.for(customer) - customer.payment_methods.then do |payment_methods| - new(customer.balance, payment_methods) + trust_level_repo.find(customer).then do |trust_level| + customer.payment_methods.then do |payment_methods| + new(customer.balance, payment_methods, trust_level.max_top_up_amount) + end end end - def initialize(balance, payment_methods) + def initialize(balance, payment_methods, max_top_up_amount) @balance = balance @payment_methods = payment_methods + @max_top_up_amount = max_top_up_amount end def form @@ -23,13 +27,12 @@ class BuyAccountCreditForm :top_up, balance: @balance, payment_methods: @payment_methods, - range: [15, nil] + range: [15, @max_top_up_amount] ) end def parse(form) amount = form.field("amount")&.value&.to_s - raise AmountValidationError, amount unless amount.to_i >= 15 { payment_method: @payment_methods.fetch( diff --git a/lib/credit_card_sale.rb b/lib/credit_card_sale.rb index 1c886774a31668cde8052e60fb8d9ae10aee7ed2..eeb830fb9681c4b107a7ff8685eee93d07db4a66 100644 --- a/lib/credit_card_sale.rb +++ b/lib/credit_card_sale.rb @@ -6,6 +6,38 @@ require "delegate" require_relative "transaction" require_relative "trust_level_repo" +class TransactionDeclinedError < StandardError; end + +class AmountTooHighError < TransactionDeclinedError + attr_reader :amount, :max_amount + + def initialize(amount, max_amount) + @amount = amount + @max_amount = max_amount + super("Amount $#{amount} exceeds maximum allowed amount of $#{max_amount}") + end +end + +class AmountTooLowError < TransactionDeclinedError + attr_reader :amount, :min_amount + + def initialize(amount, min_amount) + @amount = amount + @min_amount = min_amount + super("Amount $#{amount} is below minimum amount of $#{min_amount}") + end +end + +class DeclinedError < TransactionDeclinedError + attr_reader :declines, :max_declines + + def initialize(declines, max_declines) + @declines = declines + @max_declines = max_declines + super("Transaction declined") + end +end + class CreditCardSale def self.create(*args, transaction_class: Transaction, **kwargs) new(*args, **kwargs).sale.then do |response| @@ -62,10 +94,9 @@ protected REDIS.exists("jmp_customer_credit_card_lock-#{@customer.customer_id}"), @trust_repo.find(@customer), @customer.declines ]).then do |(lock, tl, declines)| - unless tl.credit_card_transaction?(@amount.to_d, declines) - raise "Declined" - end - raise "Too many payments recently" if lock == 1 + raise TransactionDeclinedError, "Too many payments recently" if lock == 1 + + tl.validate_credit_card_transaction!(@amount.to_d, declines) end end @@ -107,7 +138,7 @@ class BraintreeFailure < StandardError attr_reader :response def initialize(response) - super response.message + super(response.message) @response = response end end diff --git a/lib/trust_level.rb b/lib/trust_level.rb index 156fcf0cf9fb9963d09aafc77a40d48eb58ed003..7db31271943fcd366a0986c6af9bf2e5785d9d3a 100644 --- a/lib/trust_level.rb +++ b/lib/trust_level.rb @@ -32,6 +32,10 @@ module TrustLevel new if manual == "Tomb" end + def max_top_up_amount + 0 + end + def write_cdr? false end @@ -44,8 +48,9 @@ module TrustLevel false end - def credit_card_transaction?(*) - false + def validate_credit_card_transaction!(_amount, _declines) + # Give a more ambiguous error so they don't know they're tombed. + raise DeclinedError end def create_subaccount?(*) @@ -62,6 +67,14 @@ module TrustLevel new if manual == "Basement" || (!manual && settled_amount < 10) end + def max_top_up_amount + 35 + end + + def max_declines + 2 + end + def write_cdr? true end @@ -74,8 +87,11 @@ module TrustLevel messages_today < 40 end - def credit_card_transaction?(amount, declines) - amount <= 35 && declines <= 2 + def validate_credit_card_transaction!(amount, declines) + raise DeclinedError.new(declines, max_declines) if declines > max_declines + return unless amount > max_top_up_amount + + raise AmountTooHighError.new(amount, max_top_up_amount) end def create_subaccount?(already_have) @@ -92,6 +108,14 @@ module TrustLevel new if manual == "Paragon" || (!manual && settled_amount > 60) end + def max_top_up_amount + 500 + end + + def max_declines + 3 + end + def write_cdr? true end @@ -104,8 +128,11 @@ module TrustLevel messages_today < 700 end - def credit_card_transaction?(amount, declines) - amount <= 500 && declines <= 3 + def validate_credit_card_transaction!(amount, declines) + raise DeclinedError.new(declines, max_declines) if declines > max_declines + return unless amount > max_top_up_amount + + raise AmountTooHighError.new(amount, max_top_up_amount) end def create_subaccount?(already_have) @@ -134,9 +161,7 @@ module TrustLevel true end - def credit_card_transaction?(*) - true - end + def validate_credit_card_transaction!(*) end def create_subaccount?(*) true @@ -167,6 +192,14 @@ module TrustLevel @max_rate = EXPENSIVE_ROUTE.fetch(plan_name, 0.1) end + def max_top_up_amount + 130 + end + + def max_declines + 2 + end + def write_cdr? true end @@ -179,8 +212,11 @@ module TrustLevel messages_today < 500 end - def credit_card_transaction?(amount, declines) - amount <= 130 && declines <= 2 + def validate_credit_card_transaction!(amount, declines) + raise DeclinedError.new(declines, max_declines) if declines > max_declines + return unless amount > max_top_up_amount + + raise AmountTooHighError.new(amount, max_top_up_amount) end def create_subaccount?(already_have) diff --git a/sgx_jmp.rb b/sgx_jmp.rb index 588f5ba49eb29f4bce72639c956bde84e653869c..4e561b08cadccce35cd0821f94195239d58b53cf 100644 --- a/sgx_jmp.rb +++ b/sgx_jmp.rb @@ -670,7 +670,10 @@ Command.new( end }.then { |transaction| Command.finish("#{transaction} added to your account balance.") - }.catch_only(BuyAccountCreditForm::AmountValidationError) do |e| + }.catch_only( + BuyAccountCreditForm::AmountTooHighError, + BuyAccountCreditForm::AmountTooLowError + ) do |e| Command.finish(e.message, type: :error) end }.register(self).then(&CommandList.method(:register)) diff --git a/test/test_buy_account_credit_form.rb b/test/test_buy_account_credit_form.rb index 5a65387d6b0c2332743fd316dc962a8bad1afc8e..0c05519fdc4166f34b5e4126a3d56924f66f3cf6 100644 --- a/test/test_buy_account_credit_form.rb +++ b/test/test_buy_account_credit_form.rb @@ -3,16 +3,21 @@ require "test_helper" require "buy_account_credit_form" require "customer" +require "credit_card_sale" CustomerFinancials::BRAINTREE = Minitest::Mock.new CustomerFinancials::REDIS = Minitest::Mock.new +TrustLevelRepo::REDIS = Minitest::Mock.new +TrustLevelRepo::DB = Minitest::Mock.new class BuyAccountCreditFormTest < Minitest::Test def setup @payment_method = OpenStruct.new(card_type: "Test", last_4: "1234") + @max_top_up_amount = 130 @form = BuyAccountCreditForm.new( BigDecimal("15.1234"), - PaymentMethods.new([@payment_method]) + PaymentMethods.new([@payment_method]), + @max_top_up_amount ) end @@ -26,10 +31,24 @@ class BuyAccountCreditFormTest < Minitest::Test ["test"] ) + TrustLevelRepo::REDIS.expect( + :get, + EMPromise.resolve("Customer"), + ["jmp_customer_trust_level-test"] + ) + TrustLevelRepo::DB.expect( + :query_one, + EMPromise.resolve({}), + [String, "test"], default: {} + ) + assert_kind_of( BuyAccountCreditForm, BuyAccountCreditForm.for(customer).sync ) + + assert_mock TrustLevelRepo::REDIS + assert_mock TrustLevelRepo::DB end em :test_for @@ -67,14 +86,6 @@ class BuyAccountCreditFormTest < Minitest::Test assert_equal "123", @form.parse(iq_form)[:amount] end - def test_parse_bad_amount - iq_form = Blather::Stanza::X.new - iq_form.fields = [{ var: "amount", value: "1" }] - assert_raises(BuyAccountCreditForm::AmountValidationError) do - @form.parse(iq_form)[:amount] - end - end - def test_parse_payment_method iq_form = Blather::Stanza::X.new iq_form.fields = [ diff --git a/test/test_credit_card_sale.rb b/test/test_credit_card_sale.rb index f2e037b5ee2bd6f705472ff1db672eca9ecc3b1f..deded1504864f8b11fac0c7405bfbe6970c4a0bc 100644 --- a/test/test_credit_card_sale.rb +++ b/test/test_credit_card_sale.rb @@ -9,6 +9,7 @@ CreditCardSale::BRAINTREE = Minitest::Mock.new CreditCardSale::REDIS = Minitest::Mock.new TrustLevelRepo::REDIS = Minitest::Mock.new TrustLevelRepo::DB = Minitest::Mock.new +CustomerFinancials::REDIS = Minitest::Mock.new class CreditCardSaleTest < Minitest::Test FAKE_BRAINTREE_TRANSACTION = @@ -111,6 +112,80 @@ class CreditCardSaleTest < Minitest::Test end em :test_sale_locked + def test_sale_amount_too_high + CreditCardSale::REDIS.expect( + :exists, + EMPromise.resolve(0), + ["jmp_customer_credit_card_lock-test"] + ) + TrustLevelRepo::REDIS.expect( + :get, + EMPromise.resolve("Customer"), + ["jmp_customer_trust_level-test"] + ) + TrustLevelRepo::DB.expect( + :query_one, + EMPromise.resolve({}), + [String, "test"], default: {} + ) + CustomerFinancials::REDIS.expect( + :get, + EMPromise.resolve("0"), + ["jmp_pay_decline-test"] + ) + + assert_raises(AmountTooHighError) do + CreditCardSale.new( + customer(plan_name: "test_usd"), + amount: 131, + payment_method: OpenStruct.new(token: "token") + ).sale.sync + end + + assert_mock CustomerFinancials::REDIS + assert_mock CreditCardSale::REDIS + assert_mock TrustLevelRepo::REDIS + assert_mock TrustLevelRepo::DB + end + em :test_sale_amount_too_high + + def test_sale_too_many_declines + CreditCardSale::REDIS.expect( + :exists, + EMPromise.resolve(0), + ["jmp_customer_credit_card_lock-test"] + ) + TrustLevelRepo::REDIS.expect( + :get, + EMPromise.resolve("Customer"), + ["jmp_customer_trust_level-test"] + ) + TrustLevelRepo::DB.expect( + :query_one, + EMPromise.resolve({}), + [String, "test"], default: {} + ) + CustomerFinancials::REDIS.expect( + :get, + EMPromise.resolve("3"), + ["jmp_pay_decline-test"] + ) + + assert_raises(DeclinedError) do + CreditCardSale.new( + customer(plan_name: "test_usd"), + amount: 50, + payment_method: OpenStruct.new(token: "token") + ).sale.sync + end + + assert_mock CustomerFinancials::REDIS + assert_mock CreditCardSale::REDIS + assert_mock TrustLevelRepo::REDIS + assert_mock TrustLevelRepo::DB + end + em :test_sale_too_many_declines + def test_sale req = stub_request( :post, From fc6e0a2155671b3fc8be137bad0eba6e076de86d Mon Sep 17 00:00:00 2001 From: Amolith Date: Mon, 19 May 2025 14:09:48 -0600 Subject: [PATCH 02/10] docs: add Yard documentation to code we touched Document methods and attributes in `BuyAccountCreditForm`, `CreditCardSale` error classes, and the `TrustLevel` module subclasses. --- lib/buy_account_credit_form.rb | 17 ++++++++++++++ lib/credit_card_sale.rb | 39 ++++++++++++++++++++++++++----- lib/trust_level.rb | 42 ++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 6 deletions(-) diff --git a/lib/buy_account_credit_form.rb b/lib/buy_account_credit_form.rb index 69b367672708066cd08fa84cd3f52d57236307a4..367a95933a8fd36683842029c078489e6aa2b1f8 100644 --- a/lib/buy_account_credit_form.rb +++ b/lib/buy_account_credit_form.rb @@ -4,10 +4,19 @@ require_relative "trust_level_repo" require_relative "credit_card_sale" class BuyAccountCreditForm + # Returns a TrustLevelRepo instance, allowing for dependency injection. + # Either creates a new instance given kwargs or returns the existing instance. + # @param kwargs [Hash] keyword arguments. + # @option kwargs [TrustLevelRepo] :trust_level_repo An existing TrustLevelRepo instance. + # @return [TrustLevelRepo] An instance of TrustLevelRepo. def self.trust_level_repo(**kwargs) kwargs[:trust_level_repo] || TrustLevelRepo.new(**kwargs) end + # Factory method to create a BuyAccountCreditForm for a given customer. + # It fetches the customer's trust level to determine the maximum top-up amount. + # @param customer [Customer] The customer for whom the form is being created. + # @return [EMPromise] A promise that resolves with the new form instance. def self.for(customer) trust_level_repo.find(customer).then do |trust_level| customer.payment_methods.then do |payment_methods| @@ -16,12 +25,20 @@ class BuyAccountCreditForm end end + # Initializes a new BuyAccountCreditForm. + # @param balance [BigDecimal] The current balance of the customer. + # @param payment_methods [PaymentMethods] The available payment methods for the customer. + # @param max_top_up_amount [Numeric] The maximum amount the customer is allowed to top up, based on their trust level. def initialize(balance, payment_methods, max_top_up_amount) @balance = balance @payment_methods = payment_methods @max_top_up_amount = max_top_up_amount end + # Generates the form template for topping up account credit. + # The form will include a range for the amount field, constrained by a minimum of $15 + # and the customer's specific maximum top-up amount. + # @return [FormTemplate::OneRender] The rendered form template. def form FormTemplate.render( :top_up, diff --git a/lib/credit_card_sale.rb b/lib/credit_card_sale.rb index eeb830fb9681c4b107a7ff8685eee93d07db4a66..780efcc6cd9995ac318a5dca43455fc2528bd38b 100644 --- a/lib/credit_card_sale.rb +++ b/lib/credit_card_sale.rb @@ -8,9 +8,16 @@ require_relative "trust_level_repo" class TransactionDeclinedError < StandardError; end +# Error raised when a transaction amount exceeds the maximum allowed limit. class AmountTooHighError < TransactionDeclinedError - attr_reader :amount, :max_amount - + # @return [Numeric] The transaction amount that was too high. + attr_reader :amount + # @return [Numeric] The maximum amount allowed for the transaction. + attr_reader :max_amount + + # Initializes a new AmountTooHighError. + # @param amount [Numeric] The transaction amount. + # @param max_amount [Numeric] The maximum allowed amount. def initialize(amount, max_amount) @amount = amount @max_amount = max_amount @@ -18,9 +25,16 @@ class AmountTooHighError < TransactionDeclinedError end end +# Error raised when a transaction amount is below the minimum required limit. class AmountTooLowError < TransactionDeclinedError - attr_reader :amount, :min_amount - + # @return [Numeric] The transaction amount that was too low. + attr_reader :amount + # @return [Numeric] The minimum amount required for the transaction. + attr_reader :min_amount + + # Initializes a new AmountTooLowError. + # @param amount [Numeric] The transaction amount. + # @param min_amount [Numeric] The minimum required amount. def initialize(amount, min_amount) @amount = amount @min_amount = min_amount @@ -28,9 +42,16 @@ class AmountTooLowError < TransactionDeclinedError end end +# Error raised when a transaction is declined, potentially due to exceeding decline limits. class DeclinedError < TransactionDeclinedError - attr_reader :declines, :max_declines - + # @return [Integer, nil] The number of declines the customer has. + attr_reader :declines + # @return [Integer, nil] The maximum number of declines allowed. + attr_reader :max_declines + + # Initializes a new DeclinedError. + # @param declines [Integer, nil] The current number of declines. + # @param max_declines [Integer, nil] The maximum allowed declines. def initialize(declines, max_declines) @declines = declines @max_declines = max_declines @@ -89,6 +110,12 @@ class CreditCardSale protected + # Validates the transaction against customer locks, trust level, and decline history. + # @raise [TransactionDeclinedError] if the customer has made too many payments recently. + # @raise [AmountTooHighError] if the amount exceeds the trust level's maximum top-up amount. + # @raise [AmountTooLowError] if the amount is below any applicable minimum. + # @raise [DeclinedError] if the transaction is declined due to too many previous declines or other trust level restrictions. + # @return [EMPromise] A promise that resolves if validation passes, or rejects with an error. def validate! EMPromise.all([ REDIS.exists("jmp_customer_credit_card_lock-#{@customer.customer_id}"), diff --git a/lib/trust_level.rb b/lib/trust_level.rb index 7db31271943fcd366a0986c6af9bf2e5785d9d3a..cb48bf39a62dc3afcdcb9a3fd9d6c3a0c2e30251 100644 --- a/lib/trust_level.rb +++ b/lib/trust_level.rb @@ -32,6 +32,8 @@ module TrustLevel new if manual == "Tomb" end + # The maximum amount a user at Tomb trust level can top up. + # @return [Integer] Always 0 for Tomb level. def max_top_up_amount 0 end @@ -48,6 +50,11 @@ module TrustLevel false end + # Validates a credit card transaction for a Tomb trust level user. + # Users at this level cannot make credit card transactions. + # @param _amount [BigDecimal] The amount of the transaction (ignored). + # @param _declines [Integer] The number of recent declines (ignored). + # @raise [DeclinedError] Always raised to prevent transactions. def validate_credit_card_transaction!(_amount, _declines) # Give a more ambiguous error so they don't know they're tombed. raise DeclinedError @@ -67,10 +74,15 @@ module TrustLevel new if manual == "Basement" || (!manual && settled_amount < 10) end + # The maximum amount a user at Basement trust level can top up. + # @return [Integer] def max_top_up_amount 35 end + # The maximum number of credit card declines allowed for a Basement user + # before further transactions are blocked. + # @return [Integer] def max_declines 2 end @@ -87,6 +99,11 @@ module TrustLevel messages_today < 40 end + # Validates a credit card transaction for a Basement trust level user. + # @param amount [BigDecimal] The amount of the transaction. + # @param declines [Integer] The number of recent declines for the customer. + # @raise [DeclinedError] if the number of declines exceeds `max_declines`. + # @raise [AmountTooHighError] if the transaction amount exceeds `max_top_up_amount`. def validate_credit_card_transaction!(amount, declines) raise DeclinedError.new(declines, max_declines) if declines > max_declines return unless amount > max_top_up_amount @@ -108,10 +125,15 @@ module TrustLevel new if manual == "Paragon" || (!manual && settled_amount > 60) end + # The maximum amount a user at Paragon trust level can top up. + # @return [Integer] def max_top_up_amount 500 end + # The maximum number of credit card declines allowed for a Paragon user + # before further transactions are blocked. + # @return [Integer] def max_declines 3 end @@ -128,6 +150,11 @@ module TrustLevel messages_today < 700 end + # Validates a credit card transaction for a Paragon trust level user. + # @param amount [BigDecimal] The amount of the transaction. + # @param declines [Integer] The number of recent declines for the customer. + # @raise [DeclinedError] if the number of declines exceeds `max_declines`. + # @raise [AmountTooHighError] if the transaction amount exceeds `max_top_up_amount`. def validate_credit_card_transaction!(amount, declines) raise DeclinedError.new(declines, max_declines) if declines > max_declines return unless amount > max_top_up_amount @@ -161,6 +188,11 @@ module TrustLevel true end + # Validates a credit card transaction for an Olympias trust level user. + # Users at this level have no restrictions on credit card transactions through this method. + # @param _amount [BigDecimal] The amount of the transaction (ignored). + # @param _declines [Integer] The number of recent declines (ignored). + # @return [void] def validate_credit_card_transaction!(*) end def create_subaccount?(*) @@ -192,10 +224,15 @@ module TrustLevel @max_rate = EXPENSIVE_ROUTE.fetch(plan_name, 0.1) end + # The maximum amount a user at Customer trust level can top up. + # @return [Integer] def max_top_up_amount 130 end + # The maximum number of credit card declines allowed for a Customer user + # before further transactions are blocked. + # @return [Integer] def max_declines 2 end @@ -212,6 +249,11 @@ module TrustLevel messages_today < 500 end + # Validates a credit card transaction for a Customer trust level user. + # @param amount [BigDecimal] The amount of the transaction. + # @param declines [Integer] The number of recent declines for the customer. + # @raise [DeclinedError] if the number of declines exceeds `max_declines`. + # @raise [AmountTooHighError] if the transaction amount exceeds `max_top_up_amount`. def validate_credit_card_transaction!(amount, declines) raise DeclinedError.new(declines, max_declines) if declines > max_declines return unless amount > max_top_up_amount From e195e3de691b1dcd48d053c13cefef9e2cad4aad Mon Sep 17 00:00:00 2001 From: Amolith Date: Thu, 22 May 2025 12:27:29 -0600 Subject: [PATCH 03/10] fix: redo amount validation in the form --- lib/buy_account_credit_form.rb | 7 +++++++ test/test_buy_account_credit_form.rb | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/buy_account_credit_form.rb b/lib/buy_account_credit_form.rb index 367a95933a8fd36683842029c078489e6aa2b1f8..8084bb0579490230b3767a543f146524c586705f 100644 --- a/lib/buy_account_credit_form.rb +++ b/lib/buy_account_credit_form.rb @@ -50,6 +50,13 @@ class BuyAccountCreditForm def parse(form) amount = form.field("amount")&.value&.to_s + amount_value = amount.to_f + + if amount_value < 15 + raise CreditCardSale::TooLowError + elsif amount_value > @max_top_up_amount + raise CreditCardSale::TooHighError + end { payment_method: @payment_methods.fetch( diff --git a/test/test_buy_account_credit_form.rb b/test/test_buy_account_credit_form.rb index 0c05519fdc4166f34b5e4126a3d56924f66f3cf6..ef0ede190995c479b390098a813299e7dd28af65 100644 --- a/test/test_buy_account_credit_form.rb +++ b/test/test_buy_account_credit_form.rb @@ -94,4 +94,26 @@ class BuyAccountCreditFormTest < Minitest::Test ] assert_equal @payment_method, @form.parse(iq_form)[:payment_method] end + + def test_parse_amount_too_low + iq_form = Blather::Stanza::X.new + iq_form.fields = [ + { var: "payment_method", value: "0" }, + { var: "amount", value: "10" } + ] + assert_raises(CreditCardSale::TooLowError) do + @form.parse(iq_form) + end + end + + def test_parse_amount_too_high + iq_form = Blather::Stanza::X.new + iq_form.fields = [ + { var: "payment_method", value: "0" }, + { var: "amount", value: "200" } + ] + assert_raises(CreditCardSale::TooHighError) do + @form.parse(iq_form) + end + end end From 29c235344f1af2f6ccdae04a5c30f43c530593a7 Mon Sep 17 00:00:00 2001 From: Amolith Date: Thu, 22 May 2025 12:32:36 -0600 Subject: [PATCH 04/10] refactor: protect max_declines --- lib/trust_level.rb | 90 ++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/lib/trust_level.rb b/lib/trust_level.rb index cb48bf39a62dc3afcdcb9a3fd9d6c3a0c2e30251..63bcb64f72537ccd8fdbfe3d4f469f37e85353ed 100644 --- a/lib/trust_level.rb +++ b/lib/trust_level.rb @@ -32,12 +32,6 @@ module TrustLevel new if manual == "Tomb" end - # The maximum amount a user at Tomb trust level can top up. - # @return [Integer] Always 0 for Tomb level. - def max_top_up_amount - 0 - end - def write_cdr? false end @@ -67,6 +61,12 @@ module TrustLevel def to_s "Tomb" end + + # The maximum amount a user at Tomb trust level can top up. + # @return [Integer] Always 0 for Tomb level. + def max_top_up_amount + 0 + end end class Basement @@ -74,19 +74,6 @@ module TrustLevel new if manual == "Basement" || (!manual && settled_amount < 10) end - # The maximum amount a user at Basement trust level can top up. - # @return [Integer] - def max_top_up_amount - 35 - end - - # The maximum number of credit card declines allowed for a Basement user - # before further transactions are blocked. - # @return [Integer] - def max_declines - 2 - end - def write_cdr? true end @@ -118,24 +105,26 @@ module TrustLevel def to_s "Basement" end - end - class Paragon - TrustLevel.register do |manual:, settled_amount:, **| - new if manual == "Paragon" || (!manual && settled_amount > 60) - end - - # The maximum amount a user at Paragon trust level can top up. + # The maximum amount a user at Basement trust level can top up. # @return [Integer] def max_top_up_amount - 500 + 35 end - # The maximum number of credit card declines allowed for a Paragon user + protected + + # The maximum number of credit card declines allowed for a Basement user # before further transactions are blocked. # @return [Integer] def max_declines - 3 + 2 + end + end + + class Paragon + TrustLevel.register do |manual:, settled_amount:, **| + new if manual == "Paragon" || (!manual && settled_amount > 60) end def write_cdr? @@ -169,6 +158,21 @@ module TrustLevel def to_s "Paragon" end + + # The maximum amount a user at Paragon trust level can top up. + # @return [Integer] + def max_top_up_amount + 500 + end + + protected + + # The maximum number of credit card declines allowed for a Paragon user + # before further transactions are blocked. + # @return [Integer] + def max_declines + 3 + end end class Olympias @@ -224,19 +228,6 @@ module TrustLevel @max_rate = EXPENSIVE_ROUTE.fetch(plan_name, 0.1) end - # The maximum amount a user at Customer trust level can top up. - # @return [Integer] - def max_top_up_amount - 130 - end - - # The maximum number of credit card declines allowed for a Customer user - # before further transactions are blocked. - # @return [Integer] - def max_declines - 2 - end - def write_cdr? true end @@ -268,5 +259,20 @@ module TrustLevel def to_s "Customer" end + + # The maximum amount a user at Customer trust level can top up. + # @return [Integer] + def max_top_up_amount + 130 + end + + protected + + # The maximum number of credit card declines allowed for a Customer user + # before further transactions are blocked. + # @return [Integer] + def max_declines + 2 + end end end From d0ff2d5bc90ecfd447979bad88ee8cca1fbcae39 Mon Sep 17 00:00:00 2001 From: Amolith Date: Tue, 27 May 2025 13:37:44 -0600 Subject: [PATCH 05/10] fix: raise and check for errors correctly Replace incorrect `CreditCardSale::TooLowError` and `CreditCardSale::TooHighError` with correct `AmountTooLowError` and `AmountTooHighError` respectively. --- lib/buy_account_credit_form.rb | 4 ++-- test/test_buy_account_credit_form.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/buy_account_credit_form.rb b/lib/buy_account_credit_form.rb index 8084bb0579490230b3767a543f146524c586705f..9c511e44f669a0adc2a1613ec91a9332d443d932 100644 --- a/lib/buy_account_credit_form.rb +++ b/lib/buy_account_credit_form.rb @@ -53,9 +53,9 @@ class BuyAccountCreditForm amount_value = amount.to_f if amount_value < 15 - raise CreditCardSale::TooLowError + raise AmountTooLowError.new(amount_value, 15) elsif amount_value > @max_top_up_amount - raise CreditCardSale::TooHighError + raise AmountTooHighError.new(amount_value, @max_top_up_amount) end { diff --git a/test/test_buy_account_credit_form.rb b/test/test_buy_account_credit_form.rb index ef0ede190995c479b390098a813299e7dd28af65..20cd3bb3c4a2b6c06c8465d614be0d72893566b9 100644 --- a/test/test_buy_account_credit_form.rb +++ b/test/test_buy_account_credit_form.rb @@ -101,7 +101,7 @@ class BuyAccountCreditFormTest < Minitest::Test { var: "payment_method", value: "0" }, { var: "amount", value: "10" } ] - assert_raises(CreditCardSale::TooLowError) do + assert_raises(AmountTooLowError) do @form.parse(iq_form) end end @@ -112,7 +112,7 @@ class BuyAccountCreditFormTest < Minitest::Test { var: "payment_method", value: "0" }, { var: "amount", value: "200" } ] - assert_raises(CreditCardSale::TooHighError) do + assert_raises(AmountTooHighError) do @form.parse(iq_form) end end From 2185f8fe2abf1ea4bc184dc614b84b466973f0a6 Mon Sep 17 00:00:00 2001 From: Amolith Date: Tue, 27 May 2025 13:56:03 -0600 Subject: [PATCH 06/10] refactor: set DeclinedError defaults to nil --- lib/credit_card_sale.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/credit_card_sale.rb b/lib/credit_card_sale.rb index 780efcc6cd9995ac318a5dca43455fc2528bd38b..89f9d0f914e635832e3797ecef8510177a3facf6 100644 --- a/lib/credit_card_sale.rb +++ b/lib/credit_card_sale.rb @@ -50,9 +50,9 @@ class DeclinedError < TransactionDeclinedError attr_reader :max_declines # Initializes a new DeclinedError. - # @param declines [Integer, nil] The current number of declines. - # @param max_declines [Integer, nil] The maximum allowed declines. - def initialize(declines, max_declines) + # @param declines [Integer, nil] (nil) The current number of declines. + # @param max_declines [Integer, nil] (nil) The maximum allowed declines. + def initialize(declines=nil, max_declines=nil) @declines = declines @max_declines = max_declines super("Transaction declined") From 6ed0a2dc8de4447baef86352810b138dcf3333fa Mon Sep 17 00:00:00 2001 From: Amolith Date: Tue, 27 May 2025 14:10:17 -0600 Subject: [PATCH 07/10] refactor: remove duplicated logic from BuyAccountCreditForm --- lib/buy_account_credit_form.rb | 6 +----- test/test_buy_account_credit_form.rb | 11 ----------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/lib/buy_account_credit_form.rb b/lib/buy_account_credit_form.rb index 9c511e44f669a0adc2a1613ec91a9332d443d932..5675e68b0d40597b1bd7a6b662523a79065f1243 100644 --- a/lib/buy_account_credit_form.rb +++ b/lib/buy_account_credit_form.rb @@ -52,11 +52,7 @@ class BuyAccountCreditForm amount = form.field("amount")&.value&.to_s amount_value = amount.to_f - if amount_value < 15 - raise AmountTooLowError.new(amount_value, 15) - elsif amount_value > @max_top_up_amount - raise AmountTooHighError.new(amount_value, @max_top_up_amount) - end + raise AmountTooLowError.new(amount_value, 15) if amount_value < 15 { payment_method: @payment_methods.fetch( diff --git a/test/test_buy_account_credit_form.rb b/test/test_buy_account_credit_form.rb index 20cd3bb3c4a2b6c06c8465d614be0d72893566b9..8fc84c67a18f800040072b5b8c4b0ac81b7c181b 100644 --- a/test/test_buy_account_credit_form.rb +++ b/test/test_buy_account_credit_form.rb @@ -105,15 +105,4 @@ class BuyAccountCreditFormTest < Minitest::Test @form.parse(iq_form) end end - - def test_parse_amount_too_high - iq_form = Blather::Stanza::X.new - iq_form.fields = [ - { var: "payment_method", value: "0" }, - { var: "amount", value: "200" } - ] - assert_raises(AmountTooHighError) do - @form.parse(iq_form) - end - end end From 3e2a08fb88452c78d18c0fb890e132e5c7d8cdb5 Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 28 May 2025 14:02:12 -0600 Subject: [PATCH 08/10] style: wrap YARD comment lines --- lib/buy_account_credit_form.rb | 18 ++++++++++++------ lib/credit_card_sale.rb | 18 ++++++++++++------ lib/trust_level.rb | 14 ++++++++------ 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/lib/buy_account_credit_form.rb b/lib/buy_account_credit_form.rb index 5675e68b0d40597b1bd7a6b662523a79065f1243..ab5ff1f278e91a252e291d1b6605788f87f760ee 100644 --- a/lib/buy_account_credit_form.rb +++ b/lib/buy_account_credit_form.rb @@ -7,16 +7,19 @@ class BuyAccountCreditForm # Returns a TrustLevelRepo instance, allowing for dependency injection. # Either creates a new instance given kwargs or returns the existing instance. # @param kwargs [Hash] keyword arguments. - # @option kwargs [TrustLevelRepo] :trust_level_repo An existing TrustLevelRepo instance. + # @option kwargs [TrustLevelRepo] :trust_level_repo An existing TrustLevelRepo + # instance. # @return [TrustLevelRepo] An instance of TrustLevelRepo. def self.trust_level_repo(**kwargs) kwargs[:trust_level_repo] || TrustLevelRepo.new(**kwargs) end # Factory method to create a BuyAccountCreditForm for a given customer. - # It fetches the customer's trust level to determine the maximum top-up amount. + # It fetches the customer's trust level to determine the maximum top-up + # amount. # @param customer [Customer] The customer for whom the form is being created. - # @return [EMPromise] A promise that resolves with the new form instance. + # @return [EMPromise] A promise that resolves with the + # new form instance. def self.for(customer) trust_level_repo.find(customer).then do |trust_level| customer.payment_methods.then do |payment_methods| @@ -27,8 +30,10 @@ class BuyAccountCreditForm # Initializes a new BuyAccountCreditForm. # @param balance [BigDecimal] The current balance of the customer. - # @param payment_methods [PaymentMethods] The available payment methods for the customer. - # @param max_top_up_amount [Numeric] The maximum amount the customer is allowed to top up, based on their trust level. + # @param payment_methods [PaymentMethods] The available payment methods for + # the customer. + # @param max_top_up_amount [Numeric] The maximum amount the customer is + # allowed to top up, based on their trust level. def initialize(balance, payment_methods, max_top_up_amount) @balance = balance @payment_methods = payment_methods @@ -36,7 +41,8 @@ class BuyAccountCreditForm end # Generates the form template for topping up account credit. - # The form will include a range for the amount field, constrained by a minimum of $15 + # The form will include a range for the amount field, constrained by a minimum + # of $15 # and the customer's specific maximum top-up amount. # @return [FormTemplate::OneRender] The rendered form template. def form diff --git a/lib/credit_card_sale.rb b/lib/credit_card_sale.rb index 89f9d0f914e635832e3797ecef8510177a3facf6..da1b93651a259751ec74c887e8a74fd61fa7d953 100644 --- a/lib/credit_card_sale.rb +++ b/lib/credit_card_sale.rb @@ -42,7 +42,8 @@ class AmountTooLowError < TransactionDeclinedError end end -# Error raised when a transaction is declined, potentially due to exceeding decline limits. +# Error raised when a transaction is declined, potentially due to exceeding +# decline limits. class DeclinedError < TransactionDeclinedError # @return [Integer, nil] The number of declines the customer has. attr_reader :declines @@ -110,12 +111,17 @@ class CreditCardSale protected - # Validates the transaction against customer locks, trust level, and decline history. - # @raise [TransactionDeclinedError] if the customer has made too many payments recently. - # @raise [AmountTooHighError] if the amount exceeds the trust level's maximum top-up amount. + # Validates the transaction against customer locks, trust level, and decline + # history. + # @raise [TransactionDeclinedError] if the customer has made too many payments + # recently. + # @raise [AmountTooHighError] if the amount exceeds the trust level's maximum + # top-up amount. # @raise [AmountTooLowError] if the amount is below any applicable minimum. - # @raise [DeclinedError] if the transaction is declined due to too many previous declines or other trust level restrictions. - # @return [EMPromise] A promise that resolves if validation passes, or rejects with an error. + # @raise [DeclinedError] if the transaction is declined due to too many + # previous declines or other trust level restrictions. + # @return [EMPromise] A promise that resolves if validation passes, or + # rejects with an error. def validate! EMPromise.all([ REDIS.exists("jmp_customer_credit_card_lock-#{@customer.customer_id}"), diff --git a/lib/trust_level.rb b/lib/trust_level.rb index 63bcb64f72537ccd8fdbfe3d4f469f37e85353ed..e17e9af7ac1655888b3ff1692c22fd820b3925fb 100644 --- a/lib/trust_level.rb +++ b/lib/trust_level.rb @@ -90,7 +90,8 @@ module TrustLevel # @param amount [BigDecimal] The amount of the transaction. # @param declines [Integer] The number of recent declines for the customer. # @raise [DeclinedError] if the number of declines exceeds `max_declines`. - # @raise [AmountTooHighError] if the transaction amount exceeds `max_top_up_amount`. + # @raise [AmountTooHighError] if the transaction amount exceeds + # `max_top_up_amount`. def validate_credit_card_transaction!(amount, declines) raise DeclinedError.new(declines, max_declines) if declines > max_declines return unless amount > max_top_up_amount @@ -143,7 +144,8 @@ module TrustLevel # @param amount [BigDecimal] The amount of the transaction. # @param declines [Integer] The number of recent declines for the customer. # @raise [DeclinedError] if the number of declines exceeds `max_declines`. - # @raise [AmountTooHighError] if the transaction amount exceeds `max_top_up_amount`. + # @raise [AmountTooHighError] if the transaction amount exceeds + # `max_top_up_amount`. def validate_credit_card_transaction!(amount, declines) raise DeclinedError.new(declines, max_declines) if declines > max_declines return unless amount > max_top_up_amount @@ -193,9 +195,8 @@ module TrustLevel end # Validates a credit card transaction for an Olympias trust level user. - # Users at this level have no restrictions on credit card transactions through this method. - # @param _amount [BigDecimal] The amount of the transaction (ignored). - # @param _declines [Integer] The number of recent declines (ignored). + # Users at this level have no restrictions on credit card transactions + # through this method. # @return [void] def validate_credit_card_transaction!(*) end @@ -244,7 +245,8 @@ module TrustLevel # @param amount [BigDecimal] The amount of the transaction. # @param declines [Integer] The number of recent declines for the customer. # @raise [DeclinedError] if the number of declines exceeds `max_declines`. - # @raise [AmountTooHighError] if the transaction amount exceeds `max_top_up_amount`. + # @raise [AmountTooHighError] if the transaction amount exceeds + # `max_top_up_amount`. def validate_credit_card_transaction!(amount, declines) raise DeclinedError.new(declines, max_declines) if declines > max_declines return unless amount > max_top_up_amount From 09e0c80c51a1a0a475094452f790cdda88dd588d Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 28 May 2025 14:03:26 -0600 Subject: [PATCH 09/10] test(credit-card-sale): add missing openstruct require Not entirely sure what was changed, but the test started failing until I added the missing require. --- test/test_credit_card_sale.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_credit_card_sale.rb b/test/test_credit_card_sale.rb index deded1504864f8b11fac0c7405bfbe6970c4a0bc..2d460da513e4c39af7c23f5edc2776ec4c8462f8 100644 --- a/test/test_credit_card_sale.rb +++ b/test/test_credit_card_sale.rb @@ -4,6 +4,7 @@ require "test_helper" require "credit_card_sale" require "customer" require "transaction" +require "ostruct" CreditCardSale::BRAINTREE = Minitest::Mock.new CreditCardSale::REDIS = Minitest::Mock.new From 32f038a969c21022a14f117971a720149a94bc59 Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 28 May 2025 14:08:37 -0600 Subject: [PATCH 10/10] feat(form): conditionally display max top-up amount Remove range from the form, but pass max_top_up_amount through and only display it if the user isn't tombed. --- forms/top_up.rb | 5 ++++- lib/buy_account_credit_form.rb | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/forms/top_up.rb b/forms/top_up.rb index 9197c1641ff402f817688bb447a39d5c2e969236..5864326ebd50f44446e81852f62884763e913a25 100644 --- a/forms/top_up.rb +++ b/forms/top_up.rb @@ -1,6 +1,10 @@ form! title "Buy Account Credit" +if @max_top_up_amount & @max_top_up_amount.positive? + instructions "Max amount: $#{@max_top_up_amount}" +end + field( type: "fixed", label: "Current balance", @@ -11,7 +15,6 @@ field(**@payment_methods.to_list_single) field( datatype: "xs:decimal", - range: @range, var: "amount", label: "Amount of credit to buy", prefix: "$", diff --git a/lib/buy_account_credit_form.rb b/lib/buy_account_credit_form.rb index ab5ff1f278e91a252e291d1b6605788f87f760ee..e060ef6e78582aa3debd9d746a355b753c8f0f99 100644 --- a/lib/buy_account_credit_form.rb +++ b/lib/buy_account_credit_form.rb @@ -50,7 +50,7 @@ class BuyAccountCreditForm :top_up, balance: @balance, payment_methods: @payment_methods, - range: [15, @max_top_up_amount] + max_top_up_amount: @max_top_up_amount ) end