~singpolyma/jmp-pay

4427fdffcfa8c48ea15d10e20fa479e82986b831 — Stephen Paul Weber 1 year, 2 days ago b30fd91
Removed web activation form

No longer used for either registrations or as a hack for payments, everything
handled by ad-hoc commands now and nothing links here any longer.
3 files changed, 0 insertions(+), 328 deletions(-)

M config.ru
D lib/transaction.rb
D views/activate.slim
M config.ru => config.ru +0 -158
@@ 25,7 25,6 @@ end
use Sentry::Rack::CaptureExceptions

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)


@@ 35,84 34,6 @@ 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(@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 self.active?(customer_id)
		DB.exec_params(<<~SQL, [customer_id]).first&.[]("count").to_i.positive?
			SELECT count(1) AS count FROM customer_plans
			WHERE customer_id=$1 AND expires_at > NOW()
		SQL
	end

	def bill_plan(customer_id)
		DB.transaction do
			charge_for_plan(customer_id)
			unless activate_plan_starting_now(customer_id)
				add_one_month_to_current_plan(customer_id)
			end
		end
		true
	end

	def activate_plan_starting_now(customer_id)
		DB.exec(<<~SQL, [customer_id, @plan[:name]]).cmd_tuples.positive?
			INSERT INTO plan_log
				(customer_id, plan_name, date_range)
			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'))
			ON CONFLICT DO NOTHING
		SQL
	end

protected

	def charge_for_plan(customer_id)
		params = [
			customer_id,
			"#{customer_id}-bill-#{@plan[:name]}-at-#{Time.now.to_i}",
			-price
		]
		DB.exec(<<~SQL, params)
			INSERT INTO transactions
				(customer_id, transaction_id, created_at, amount)
			VALUES ($1, $2, LOCALTIMESTAMP, $3)
		SQL
	end

	def add_one_month_to_current_plan(customer_id)
		DB.exec(<<~SQL, [customer_id])
			UPDATE plan_log SET date_range=range_merge(
				date_range,
				tsrange(
					LOCALTIMESTAMP,
					GREATEST(upper(date_range), LOCALTIMESTAMP) + '1 month'
				)
			)
			WHERE
				customer_id=$1 AND
				date_range && tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month')
		SQL
	end
end

class CreditCardGateway
	def initialize(jid, customer_id=nil)
		@jid = jid


@@ 175,38 96,6 @@ class CreditCardGateway
		)
	end

	def decline_guard(ip)
		customer_declines, ip_declines = REDIS.mget(
			"jmp_pay_decline-#{@customer_id}",
			"jmp_pay_decline-#{ip}"
		)
		customer_declines.to_i < 2 && ip_declines.to_i < 4
	end

	def sale(ip:, **kwargs)
		return nil unless decline_guard(ip)

		tx = Transaction.sale(@gateway, **kwargs)
		return tx if tx

		REDIS.incr("jmp_pay_decline-#{@customer_id}")
		REDIS.expire("jmp_pay_decline-#{@customer_id}", 60 * 60 * 24)
		REDIS.incr("jmp_pay_decline-#{ip}")
		REDIS.expire("jmp_pay_decline-#{ip}", 60 * 60 * 24)
		nil
	end

	def buy_plan(plan_name, nonce, ip)
		plan = Plan.for(plan_name)
		sale(
			ip: ip,
			amount: plan.price(5),
			payment_method_nonce: nonce,
			merchant_account_id: plan.merchant_account,
			options: { submit_for_settlement: true }
		)&.insert && plan.bill_plan(@customer_id)
	end

protected

	def redis_key_jid


@@ 307,53 196,6 @@ class JmpPay < Roda
			gateway = CreditCardGateway.new(jid, params["customer_id"])
			topup = "jmp_customer_auto_top_up_amount-#{gateway.customer_id}"

			r.on "activate" do
				Sentry.configure_scope do |scope|
					scope.set_transaction_name("activate")
					scope.set_context(
						"activate",
						plan_name: params["plan_name"]
					)
				end

				render = lambda do |l={}|
					view(
						"activate",
						locals: {
							token: gateway.client_token,
							customer_id: gateway.customer_id,
							error: false
						}.merge(l)
					)
				end

				r.get do
					if Plan.active?(gateway.customer_id)
						r.redirect params["return_to"], 303
					else
						render.call
					end
				end

				r.post do
					result = DB.transaction {
						Plan.active?(gateway.customer_id) || gateway.buy_plan(
							params["plan_name"],
							params["braintree_nonce"],
							request.ip
						)
					}
					if params["auto_top_up_amount"].to_i >= 15
						REDIS.set(topup, params["auto_top_up_amount"].to_i)
					end
					if result
						r.redirect params["return_to"], 303
					else
						render.call(error: true)
					end
				end
			end

			r.on "credit_cards" do
				r.get do
					view(

D lib/transaction.rb => lib/transaction.rb +0 -75
@@ 1,75 0,0 @@
# frozen_string_literal: true

require "bigdecimal"

# Largely copied from sgx-jmp to support web activation more properly
# Goes away when web activation goes away
class Transaction
	def self.sale(gateway, **kwargs)
		response = gateway.transaction.sale(**kwargs)
		response.success? ? new(response.transaction) : nil
	end

	attr_reader :amount

	def initialize(braintree_transaction)
		@customer_id = braintree_transaction.customer_details.id
		@transaction_id = braintree_transaction.id
		@created_at = braintree_transaction.created_at
		@amount = BigDecimal(braintree_transaction.amount, 4)
	end

	def insert
		DB.transaction do
			insert_tx
			insert_bonus
		end
		true
	end

	def bonus
		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 to_s
		plus = " + #{'%.4f' % bonus} bonus"
		"$#{'%.2f' % amount}#{plus if bonus.positive?}"
	end

protected

	def insert_tx
		params = [@customer_id, @transaction_id, @created_at, @amount]
		DB.exec(<<~SQL, params)
			INSERT INTO transactions
				(customer_id, transaction_id, created_at, amount, note)
			VALUES
				($1, $2, $3, $4, 'Credit card payment')
		SQL
	end

	def insert_bonus
		return if bonus <= 0

		params = [
			@customer_id, "bonus_for_#{@transaction_id}",
			@created_at, bonus
		]
		DB.exec(<<~SQL, params)
			INSERT INTO transactions
				(customer_id, transaction_id, created_at, amount, note)
			VALUES
				($1, $2, $3, $4, 'Credit card payment bonus')
		SQL
	end
end

D views/activate.slim => views/activate.slim +0 -95
@@ 1,95 0,0 @@
scss:
	html, body {
		font-family: sans-serif;
		text-align: center;
	}

	form {
		margin: auto;
		max-width: 40em;

		fieldset {
			max-width: 25em;
			margin: 2em auto;
			label { display: block; }
			input[type=number] { max-width: 3em; }
			small { 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"

	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="15"
			small Leave blank for no auto top-up.

	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);
				} else {
					e.target.braintree_nonce.value = payload.nonce;
					e.target.submit();
				}
			});
		});
	});