From 737b8acab9d5ace84e86009f55d3de049cb8c370 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 22 Mar 2021 21:59:10 -0500 Subject: [PATCH] Allow activating an account via credit card on web This is designed to work with current jmp-register flows pending new-register existing. Link a user to https://pay.jmp.chat//activate?return_to=... and they can choose to buy 5 months of service in either USD or CAD on a supported credit card. The card will be vaulted onto their newly-minted customer_id and the amount immediately billed. No account balance will be set or used, but rather a plan_log row created starting now and expiring in 5 months. --- .rubocop.yml | 1 + config.ru | 84 +++++++++++++++++++++++++++++++++++++++--- views/activate.slim | 89 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 views/activate.slim diff --git a/.rubocop.yml b/.rubocop.yml index 7f9dd6e..5f876f4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,6 +7,7 @@ Metrics/LineLength: Metrics/BlockLength: ExcludedMethods: - route + - "on" Layout/Tab: Enabled: false diff --git a/config.ru b/config.ru index 9d99dd2..e73d002 100644 --- a/config.ru +++ b/config.ru @@ -1,6 +1,7 @@ # frozen_string_literal: true require "braintree" +require "date" require "delegate" require "dhall" require "pg" @@ -15,6 +16,7 @@ end require_relative "lib/electrum" REDIS = Redis.new +PLANS = Dhall.load("env:PLANS").sync BRAINTREE_CONFIG = Dhall.load("env:BRAINTREE_CONFIG").sync ELECTRUM = Electrum.new( **Dhall::Coder.load("env:ELECTRUM_CONFIG", transform_keys: :to_sym) @@ -24,6 +26,35 @@ DB = PG.connect(dbname: "jmp") DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB) DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB) +class Plan + def self.for(plan_name) + new(PLANS.find { |p| p[:name].to_s == plan_name }) + end + + def initialize(plan) + @plan = plan + end + + def price(months=1) + (BigDecimal.new(@plan[:monthly_price].to_i) * months) / 10000 + end + + def currency + @plan[:currency].to_s.to_sym + end + + def merchant_account + BRAINTREE_CONFIG[:merchant_accounts][currency] + end + + def activate(customer_id, months) + DB.exec_params( + "INSERT INTO plan_log VALUES ($1, $2, $3, $4)", + [customer_id, @plan[:name], Time.now, Date.today >> months] + ) + end +end + class CreditCardGateway def initialize(jid, customer_id=nil) @jid = jid @@ -81,6 +112,19 @@ class CreditCardGateway ) end + def buy_plan(plan_name, months, nonce) + plan = Plan.for(plan_name) + result = @gateway.transaction.sale( + amount: plan.price(months), + payment_method_nonce: nonce, + merchant_account_id: plan.merchant_account, + options: {submit_for_settlement: true} + ) + return false unless result.success? + plan.activate(@customer_id, months) + true + end + protected def redis_key_jid @@ -154,12 +198,42 @@ class JmpPay < Roda end r.on :jid do |jid| - r.on "credit_cards" do - gateway = CreditCardGateway.new( - jid, - request.params["customer_id"] - ) + gateway = CreditCardGateway.new( + jid, + request.params["customer_id"] + ) + r.on "activate" do + render = lambda do |l={}| + view( + "activate", + locals: { + token: gateway.client_token, + customer_id: gateway.customer_id, + error: false + }.merge(l) + ) + end + + r.get do + render.call + end + + r.post do + result = gateway.buy_plan( + request.params["plan_name"], + 5, + request.params["braintree_nonce"] + ) + if result + r.redirect request.params["return_to"], 303 + else + render.call(error: true) + end + end + end + + r.on "credit_cards" do r.get do view( "credit_cards", diff --git a/views/activate.slim b/views/activate.slim new file mode 100644 index 0000000..97ae245 --- /dev/null +++ b/views/activate.slim @@ -0,0 +1,89 @@ +scss: + html, body { + font-family: sans-serif; + text-align: center; + } + + form { + margin: auto; + max-width: 40em; + + fieldset { + max-width: 20em; + margin: 2em auto; + label { + display: block; + } + } + + button { + display: block; + width: 10em; + margin: auto; + } + + } + + .error { + color: red; + max-width: 40em; + margin: 1em auto; + } + +h1 Activate New Account + +- if error + p.error + ' Your bank declined the transaction. + ' Often this happens when a person's credit card is a US card + ' that does not support international transactions, as JMP is + ' not based in the USA, though we do support transactions in USD. + p.error + ' If you were trying a prepaid card, you may wish to use + a href="https://privacy.com/" Privacy.com + | instead, as they do support international transactions. + +form method="post" action="" + #braintree + | Unfortunately, our credit card processor requires JavaScript. + + fieldset + legend Pay for 5 months of service + label + ' $14.95 USD + input type="radio" name="plan_name" value="usd_beta_unlimited-v20210223" required="required" + label + ' $17.95 CAD + input type="radio" name="plan_name" value="cad_beta_unlimited-v20210223" required="required" + + input type="hidden" name="customer_id" value=customer_id + input type="hidden" name="braintree_nonce" + +script src="https://js.braintreegateway.com/web/dropin/1.26.0/js/dropin.min.js" +javascript: + document.querySelector("#braintree").innerHTML = ""; + + var button = document.createElement("button"); + button.innerHTML = "Pay Now"; + document.querySelector("form").appendChild(button); + braintree.dropin.create({ + authorization: #{{token.to_json}}, + container: "#braintree", + vaultManager: false + }, function (createErr, instance) { + if(createErr) console.log(createErr); + + document.querySelector("form").addEventListener("submit", function(e) { + e.preventDefault(); + + instance.requestPaymentMethod(function(err, payload) { + if(err) { + console.log(err); + instance._mainView.showSheetError(); + } else { + e.target.braintree_nonce.value = payload.nonce; + e.target.submit(); + } + }); + }); + }); -- 2.38.4