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