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
 11# Error raised when a transaction amount exceeds the maximum allowed limit.
 12class AmountTooHighError < TransactionDeclinedError
 13	# @return [Numeric] The transaction amount that was too high.
 14	attr_reader :amount
 15	# @return [Numeric] The maximum amount allowed for the transaction.
 16	attr_reader :max_amount
 17
 18	# Initializes a new AmountTooHighError.
 19	# @param amount [Numeric] The transaction amount.
 20	# @param max_amount [Numeric] The maximum allowed amount.
 21	def initialize(amount, max_amount)
 22		@amount = amount
 23		@max_amount = max_amount
 24		super("Amount $#{amount} exceeds maximum allowed amount of $#{max_amount}")
 25	end
 26end
 27
 28# Error raised when a transaction amount is below the minimum required limit.
 29class AmountTooLowError < TransactionDeclinedError
 30	# @return [Numeric] The transaction amount that was too low.
 31	attr_reader :amount
 32	# @return [Numeric] The minimum amount required for the transaction.
 33	attr_reader :min_amount
 34
 35	# Initializes a new AmountTooLowError.
 36	# @param amount [Numeric] The transaction amount.
 37	# @param min_amount [Numeric] The minimum required amount.
 38	def initialize(amount, min_amount)
 39		@amount = amount
 40		@min_amount = min_amount
 41		super("Amount $#{amount} is below minimum amount of $#{min_amount}")
 42	end
 43end
 44
 45# Error raised when a transaction is declined, potentially due to exceeding decline limits.
 46class DeclinedError < TransactionDeclinedError
 47	# @return [Integer, nil] The number of declines the customer has.
 48	attr_reader :declines
 49	# @return [Integer, nil] The maximum number of declines allowed.
 50	attr_reader :max_declines
 51
 52	# Initializes a new DeclinedError.
 53	# @param declines [Integer, nil] (nil) The current number of declines.
 54	# @param max_declines [Integer, nil] (nil) The maximum allowed declines.
 55	def initialize(declines=nil, max_declines=nil)
 56		@declines = declines
 57		@max_declines = max_declines
 58		super("Transaction declined")
 59	end
 60end
 61
 62class CreditCardSale
 63	def self.create(*args, transaction_class: Transaction, **kwargs)
 64		new(*args, **kwargs).sale.then do |response|
 65			tx = BraintreeTransaction.build(
 66				response,
 67				transaction_class: transaction_class
 68			)
 69			tx.insert.then { tx }
 70		end
 71	end
 72
 73	class BraintreeTransaction < SimpleDelegator
 74		def self.build(braintree_transaction, transaction_class: Transaction)
 75			new(braintree_transaction).to_transaction(transaction_class)
 76		end
 77
 78		def to_transaction(transaction_class)
 79			transaction_class.new(
 80				customer_id: customer_details.id,
 81				transaction_id: id,
 82				created_at: created_at,
 83				settled_after: created_at + (90 * 24 * 60 * 60),
 84				amount: amount,
 85				note: "Credit card payment"
 86			)
 87		end
 88	end
 89
 90	def initialize(
 91		customer, amount:, payment_method: nil,
 92		trust_repo: TrustLevelRepo.new
 93	)
 94		@customer = customer
 95		@amount = amount
 96		@payment_method = payment_method
 97		@trust_repo = trust_repo
 98	end
 99
100	def sale
101		EMPromise.all([validate!, resolve_payment_method]).then { |_, selected|
102			BRAINTREE.transaction.sale(
103				amount: @amount,
104				merchant_account_id: @customer.merchant_account,
105				options: { submit_for_settlement: true },
106				payment_method_token: selected.token
107			)
108		}.then { |response| decline_guard(response) }
109	end
110
111protected
112
113	# Validates the transaction against customer locks, trust level, and decline history.
114	# @raise [TransactionDeclinedError] if the customer has made too many payments recently.
115	# @raise [AmountTooHighError] if the amount exceeds the trust level's maximum top-up amount.
116	# @raise [AmountTooLowError] if the amount is below any applicable minimum.
117	# @raise [DeclinedError] if the transaction is declined due to too many previous declines or other trust level restrictions.
118	# @return [EMPromise<void>] A promise that resolves if validation passes, or rejects with an error.
119	def validate!
120		EMPromise.all([
121			REDIS.exists("jmp_customer_credit_card_lock-#{@customer.customer_id}"),
122			@trust_repo.find(@customer), @customer.declines
123		]).then do |(lock, tl, declines)|
124			raise TransactionDeclinedError, "Too many payments recently" if lock == 1
125
126			tl.validate_credit_card_transaction!(@amount.to_d, declines)
127		end
128	end
129
130	def resolve_payment_method
131		EMPromise.all([
132			@payment_method ||
133				@customer.payment_methods.then(&:default_payment_method)
134		]).then do |(selected_method)|
135			raise "No valid payment method on file" unless selected_method
136
137			selected_method
138		end
139	end
140
141	def churnbuster_success(response)
142		Churnbuster.new.successful_payment(
143			@customer,
144			@amount,
145			response.transaction.id
146		)
147	end
148
149	def decline_guard(response)
150		if response.success?
151			churnbuster_success(response)
152			REDIS.setex(
153				"jmp_customer_credit_card_lock-#{@customer.customer_id}",
154				60 * 60 * 24, "1"
155			)
156			return response.transaction
157		end
158
159		@customer.mark_decline
160		raise BraintreeFailure, response
161	end
162end
163
164class BraintreeFailure < StandardError
165	attr_reader :response
166
167	def initialize(response)
168		super(response.message)
169		@response = response
170	end
171end