M bin/process_pending_btc_transactions => bin/process_pending_btc_transactions +4 -17
@@ 25,6 25,7 @@ require "redis"
require_relative "../lib/blather_notify"
require_relative "../lib/electrum"
+require_relative "../lib/transaction"
CONFIG =
Dhall::Coder
@@ 98,19 99,6 @@ class Plan
@plan[:currency]
end
- def bonus_for(fiat_amount)
- return BigDecimal(0) if fiat_amount <= 15
-
- fiat_amount * case fiat_amount
- when (15..29.99)
- 0.01
- when (30..139.99)
- 0.03
- else
- 0.05
- end
- end
-
def price
BigDecimal(@plan[:monthly_price].to_i) * 0.0001
end
@@ 184,11 172,10 @@ class Customer
end
def add_btc_credit(txid, btc_amount, fiat_amount)
- return unless add_transaction(txid, fiat_amount, "Bitcoin payment")
+ tx = Transaction.new(txid, fiat_amount, "Bitcoin payment")
+ return unless tx.save
- if (bonus = plan.bonus_for(fiat_amount)).positive?
- add_transaction("bonus_for_#{txid}", bonus, "Bitcoin payment bonus")
- end
+ tx.bonus&.save
notify_btc_credit(txid, btc_amount, fiat_amount, bonus)
end
M config.ru => config.ru +89 -47
@@ 1,6 1,7 @@
# frozen_string_literal: true
require "braintree"
+require "bigdecimal/util"
require "date"
require "delegate"
require "dhall"
@@ 16,8 17,10 @@ if ENV["RACK_ENV"] == "development"
end
require_relative "lib/auto_top_up_repo"
+require_relative "lib/customer"
require_relative "lib/three_d_secure_repo"
require_relative "lib/electrum"
+require_relative "lib/transaction"
require "sentry-ruby"
Sentry.init do |config|
@@ 70,25 73,14 @@ class CreditCardGateway
end
def customer_id
- customer_id = REDIS.get(redis_key_jid)
+ customer_id = Customer.new(nil, @jid).customer_id
return customer_id if check_customer_id(customer_id)
result = @gateway.customer.create
raise "Braintree customer create failed" unless result.success?
@customer_id = result.customer.id
- save_customer_id!
- end
-
- def save_customer_id!
- unless REDIS.set(redis_key_jid, @customer_id) == "OK"
- raise "Saving new jid,customer to redis failed"
- end
-
- unless REDIS.set(redis_key_customer_id, @jid) == "OK"
- raise "Saving new customer,jid to redis failed"
- end
-
+ Customer.new(@customer_id, @jid).save!
@customer_id
end
@@ 127,46 119,45 @@ class CreditCardGateway
)
end
- def incr_antifraud!
+ def with_antifraud
+ result = antifraud || yield
+ return result if result.success?
+
@antifraud.each do |k|
REDIS.incr("jmp_antifraud-#{k}")
REDIS.expire("jmp_antifraud-#{k}", 60 * 60 * 24)
end
+
+ raise ErrorResult.for(result)
end
- def payment_method_create_options
- options = { verify_card: true, make_default: true }
- if merchant_account
- options[:verification_merchant_account_id] = merchant_account.to_s
+ def sale(nonce, amount)
+ with_antifraud do
+ @gateway.transaction.sale(
+ customer_id: customer_id, payment_method_nonce: nonce,
+ amount: amount, merchant_account_id: merchant_account.to_s,
+ options: {
+ store_in_vault_on_success: true, submit_for_settlement: true
+ }
+ )
end
- options
end
def default_method(nonce)
- result = antifraud || @gateway.payment_method.create(
- customer_id: customer_id, payment_method_nonce: nonce,
- options: payment_method_create_options
- )
-
- return result if result.success?
-
- incr_antifraud!
- raise ErrorResult.for(result)
+ with_antifraud do
+ @gateway.payment_method.create(
+ customer_id: customer_id, payment_method_nonce: nonce,
+ options: {
+ verify_card: true, make_default: true,
+ verification_merchant_account_id: merchant_account.to_s
+ }
+ )
+ end
end
def remove_method(token)
@gateway.payment_method.delete(token)
end
-
-protected
-
- def redis_key_jid
- "jmp_customer_id-#{@jid}"
- end
-
- def redis_key_customer_id
- "jmp_customer_jid-#{@customer_id}"
- end
end
class UnknownTransactions
@@ 214,6 205,61 @@ class UnknownTransactions
end
end
+class CardVault
+ def self.for(gateway, nonce, amount=nil)
+ if amount&.positive?
+ CardDeposit.new(gateway, nonce, amount)
+ else
+ new(gateway, nonce)
+ end
+ end
+
+ def initialize(gateway, nonce)
+ @gateway = gateway
+ @nonce = nonce
+ end
+
+ def call(auto_top_up_amount)
+ result = vault!
+ ThreeDSecureRepo.new.put_from_result(result)
+ AutoTopUpRepo.new.put(
+ @gateway.customer_id,
+ auto_top_up_amount
+ )
+ result
+ end
+
+ def vault!
+ @gateway.default_method(@nonce)
+ end
+
+ class CardDeposit < self
+ def initialize(gateway, nonce, amount)
+ super(gateway, nonce)
+ @amount = amount
+
+ return unless @amount < 15 || @amount > 35
+
+ raise CreditCardGateway::ErrorResult, "amount too low or too high"
+ end
+
+ def call(*)
+ result = super
+ Transaction.new(
+ @gateway.customer_id,
+ result.transaction.id,
+ @amount,
+ "Credit card payment"
+ ).save
+ result
+ end
+
+ def vault!
+ @gateway.sale(@nonce, @amount)
+ end
+ end
+end
+
class JmpPay < Roda
SENTRY_DSN = ENV["SENTRY_DSN"] && URI(ENV["SENTRY_DSN"])
plugin :render, engine: "slim"
@@ 290,15 336,11 @@ class JmpPay < Roda
end
r.post do
- result = gateway.default_method(params["braintree_nonce"])
- ThreeDSecureRepo.new.put_from_payment_method(
- gateway.customer_id,
- result.payment_method
- )
- topup.put(
- gateway.customer_id,
- params["auto_top_up_amount"].to_i
- )
+ CardVault
+ .for(
+ gateway, params["braintree_nonce"],
+ params["amount"].to_d
+ ).call(params["auto_top_up_amount"].to_i)
"OK"
rescue ThreeDSecureRepo::Failed
gateway.remove_method($!.message)
A lib/customer.rb => lib/customer.rb +32 -0
@@ 0,0 1,32 @@
+# frozen_string_literal: true
+
+class Customer
+ def initialize(customer_id, jid)
+ @customer_id = customer_id
+ @jid = jid
+ end
+
+ def customer_id
+ @customer_id = REDIS.get(redis_key_jid)
+ end
+
+ def save!
+ unless REDIS.set(redis_key_jid, @customer_id) == "OK"
+ raise "Saving new jid,customer to redis failed"
+ end
+
+ return if REDIS.set(redis_key_customer_id, @jid) == "OK"
+
+ raise "Saving new customer,jid to redis failed"
+ end
+
+protected
+
+ def redis_key_jid
+ "jmp_customer_id-#{@jid}"
+ end
+
+ def redis_key_customer_id
+ "jmp_customer_jid-#{@customer_id}"
+ end
+end
M lib/three_d_secure_repo.rb => lib/three_d_secure_repo.rb +8 -3
@@ 3,10 3,15 @@
class ThreeDSecureRepo
class Failed < StandardError; end
- def put_from_payment_method(_customer_id, method)
- return unless method.verification # Already vaulted
+ def put_from_result(result)
+ three_d = if result.payment_method
+ return unless result.payment_method.verification # Already vaulted
+
+ result.payment_method.verification.three_d_secure_info
+ else
+ result.transaction.three_d_secure_info
+ end
- three_d = method.verification.three_d_secure_info
if !three_d ||
(three_d.liability_shift_possible && !three_d.liability_shifted)
raise Failed, method.token
A lib/transaction.rb => lib/transaction.rb +40 -0
@@ 0,0 1,40 @@
+# frozen_string_literal: true
+
+class Transaction
+ def initialize(customer_id, id, amount, note)
+ @customer_id = customer_id
+ @id = id
+ @amount = amount
+ @note = note
+ end
+
+ def bonus
+ return unless bonus_amount.positive?
+
+ new(@customer_id, "bonus_for_#{@id}", bonus_amount, "#{@note} bonus")
+ end
+
+ def bonus_amount
+ return BigDecimal(0) if @amount <= 15
+
+ @amount * case @amount
+ when (15..29.99)
+ 0.01
+ when (30..139.99)
+ 0.03
+ else
+ 0.05
+ end
+ end
+
+ def save
+ args = [@customer_id, @id, @amount, @note]
+ DB.exec_params(<<-SQL, args).cmd_tuples.positive?
+ INSERT INTO transactions
+ (customer_id, transaction_id, settled_after, amount, note)
+ VALUES
+ ($1, $2, LOCALTIMESTAMP, $3, $4)
+ ON CONFLICT (transaction_id) DO NOTHING
+ SQL
+ end
+end
M views/credit_cards.slim => views/credit_cards.slim +6 -2
@@ 24,11 24,15 @@ form method="post" action=""
#braintree
| Unfortunately, our credit card processor requires JavaScript.
+ label#amount style="#{'display:none;' unless params['amount']}"
+ div Amount of initial deposit (minimum $15)
+ input type="number" name="amount" min="15" value="#{params.fetch('amount', '')}"
+
fieldset
legend Auto top-up when account balance is low?
label
| When balance drops below $5, add $
- input type="number" name="auto_top_up_amount" min="15" value=auto_top_up
+ input type="number" name="auto_top_up_amount" min="15" max="35" value=auto_top_up
small Leave blank for no auto top-up.
input type="hidden" name="customer_id" value=customer_id
@@ 93,7 97,7 @@ javascript:
instance.requestPaymentMethod({
threeDSecure: {
- amount: "0.0",
+ amount: document.querySelector("input[name=amount]").value || "0.0",
requireChallenge: true
}
}, function(err, payload) {