~singpolyma/jmp-pay

737b8acab9d5ace84e86009f55d3de049cb8c370 — Stephen Paul Weber 1 year, 10 months ago 4b6de2b
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/<jid>/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.
3 files changed, 169 insertions(+), 5 deletions(-)

M .rubocop.yml
M config.ru
A views/activate.slim
M .rubocop.yml => .rubocop.yml +1 -0
@@ 7,6 7,7 @@ Metrics/LineLength:
Metrics/BlockLength:
  ExcludedMethods:
    - route
    - "on"

Layout/Tab:
  Enabled: false

M config.ru => config.ru +79 -5
@@ 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",

A views/activate.slim => views/activate.slim +89 -0
@@ 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();
				}
			});
		});
	});