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 CreditCardSale
 10	def self.create(*args, transaction_class: Transaction, **kwargs)
 11		new(*args, **kwargs).sale.then do |response|
 12			tx = BraintreeTransaction.build(
 13				response,
 14				transaction_class: transaction_class
 15			)
 16			tx.insert.then { tx }
 17		end
 18	end
 19
 20	class BraintreeTransaction < SimpleDelegator
 21		def self.build(braintree_transaction, transaction_class: Transaction)
 22			new(braintree_transaction).to_transaction(transaction_class)
 23		end
 24
 25		def to_transaction(transaction_class)
 26			transaction_class.new(
 27				customer_id: customer_details.id,
 28				transaction_id: id,
 29				created_at: created_at,
 30				settled_after: created_at + (90 * 24 * 60 * 60),
 31				amount: amount,
 32				note: "Credit card payment"
 33			)
 34		end
 35	end
 36
 37	def initialize(
 38		customer, amount:, payment_method: nil,
 39		trust_repo: TrustLevelRepo.new
 40	)
 41		@customer = customer
 42		@amount = amount
 43		@payment_method = payment_method
 44		@trust_repo = trust_repo
 45	end
 46
 47	def sale
 48		EMPromise.all([validate!, resolve_payment_method]).then { |_, selected|
 49			BRAINTREE.transaction.sale(
 50				amount: @amount,
 51				merchant_account_id: @customer.merchant_account,
 52				options: { submit_for_settlement: true },
 53				payment_method_token: selected.token
 54			)
 55		}.then { |response| decline_guard(response) }
 56	end
 57
 58protected
 59
 60	def validate!
 61		EMPromise.all([
 62			REDIS.exists("jmp_customer_credit_card_lock-#{@customer.customer_id}"),
 63			@trust_repo.find(@customer), @customer.declines
 64		]).then do |(lock, tl, declines)|
 65			unless tl.credit_card_transaction?(@amount.to_d, declines)
 66				raise "Declined"
 67			end
 68			raise "Too many payments recently" if lock == 1
 69		end
 70	end
 71
 72	def resolve_payment_method
 73		EMPromise.all([
 74			@payment_method ||
 75				@customer.payment_methods.then(&:default_payment_method)
 76		]).then do |(selected_method)|
 77			raise "No valid payment method on file" unless selected_method
 78
 79			selected_method
 80		end
 81	end
 82
 83	def decline_guard(response)
 84		if response.success?
 85			REDIS.setex(
 86				"jmp_customer_credit_card_lock-#{@customer.customer_id}",
 87				60 * 60 * 24,
 88				"1"
 89			)
 90			return response.transaction
 91		end
 92
 93		@customer.mark_decline
 94		raise BraintreeFailure, response
 95	end
 96end
 97
 98class BraintreeFailure < StandardError
 99	attr_reader :response
100
101	def initialize(response)
102		super response.message
103		@response = response
104	end
105end