credit_card_sale.rb

  1# frozen_string_literal: true
  2
  3require "bigdecimal/util"
  4require "delegate"
  5
  6require_relative "transaction"
  7require_relative "trust_level_repo"
  8
  9class TransactionDeclinedError < StandardError; end
 10
 11class AmountTooHighError < TransactionDeclinedError
 12	attr_reader :amount, :max_amount
 13
 14	def initialize(amount, max_amount)
 15		@amount = amount
 16		@max_amount = max_amount
 17		super("Amount $#{amount} exceeds maximum allowed amount of $#{max_amount}")
 18	end
 19end
 20
 21class AmountTooLowError < TransactionDeclinedError
 22	attr_reader :amount, :min_amount
 23
 24	def initialize(amount, min_amount)
 25		@amount = amount
 26		@min_amount = min_amount
 27		super("Amount $#{amount} is below minimum amount of $#{min_amount}")
 28	end
 29end
 30
 31class DeclinedError < TransactionDeclinedError
 32	attr_reader :declines, :max_declines
 33
 34	def initialize(declines, max_declines)
 35		@declines = declines
 36		@max_declines = max_declines
 37		super("Transaction declined")
 38	end
 39end
 40
 41class CreditCardSale
 42	def self.create(*args, transaction_class: Transaction, **kwargs)
 43		new(*args, **kwargs).sale.then do |response|
 44			tx = BraintreeTransaction.build(
 45				response,
 46				transaction_class: transaction_class
 47			)
 48			tx.insert.then { tx }
 49		end
 50	end
 51
 52	class BraintreeTransaction < SimpleDelegator
 53		def self.build(braintree_transaction, transaction_class: Transaction)
 54			new(braintree_transaction).to_transaction(transaction_class)
 55		end
 56
 57		def to_transaction(transaction_class)
 58			transaction_class.new(
 59				customer_id: customer_details.id,
 60				transaction_id: id,
 61				created_at: created_at,
 62				settled_after: created_at + (90 * 24 * 60 * 60),
 63				amount: amount,
 64				note: "Credit card payment"
 65			)
 66		end
 67	end
 68
 69	def initialize(
 70		customer, amount:, payment_method: nil,
 71		trust_repo: TrustLevelRepo.new
 72	)
 73		@customer = customer
 74		@amount = amount
 75		@payment_method = payment_method
 76		@trust_repo = trust_repo
 77	end
 78
 79	def sale
 80		EMPromise.all([validate!, resolve_payment_method]).then { |_, selected|
 81			BRAINTREE.transaction.sale(
 82				amount: @amount,
 83				merchant_account_id: @customer.merchant_account,
 84				options: { submit_for_settlement: true },
 85				payment_method_token: selected.token
 86			)
 87		}.then { |response| decline_guard(response) }
 88	end
 89
 90protected
 91
 92	def validate!
 93		EMPromise.all([
 94			REDIS.exists("jmp_customer_credit_card_lock-#{@customer.customer_id}"),
 95			@trust_repo.find(@customer), @customer.declines
 96		]).then do |(lock, tl, declines)|
 97			raise TransactionDeclinedError, "Too many payments recently" if lock == 1
 98
 99			tl.validate_credit_card_transaction!(@amount.to_d, declines)
100		end
101	end
102
103	def resolve_payment_method
104		EMPromise.all([
105			@payment_method ||
106				@customer.payment_methods.then(&:default_payment_method)
107		]).then do |(selected_method)|
108			raise "No valid payment method on file" unless selected_method
109
110			selected_method
111		end
112	end
113
114	def churnbuster_success(response)
115		Churnbuster.new.successful_payment(
116			@customer,
117			@amount,
118			response.transaction.id
119		)
120	end
121
122	def decline_guard(response)
123		if response.success?
124			churnbuster_success(response)
125			REDIS.setex(
126				"jmp_customer_credit_card_lock-#{@customer.customer_id}",
127				60 * 60 * 24, "1"
128			)
129			return response.transaction
130		end
131
132		@customer.mark_decline
133		raise BraintreeFailure, response
134	end
135end
136
137class BraintreeFailure < StandardError
138	attr_reader :response
139
140	def initialize(response)
141		super(response.message)
142		@response = response
143	end
144end