Copy in account activation logic from sgx-jmp

Stephen Paul Weber created

This is largely duplicated code, but the whole web-activation path should go
away soon.  This fixes web activation to produce the data we actually expect
instead of the hack previously produced.  Instead of an account activation for 5
months, we insert 5 months of balance and then bill for only one month as is reasonable.

Change summary

config.ru          | 65 +++++++++++++++++++++++++++++++++++--------
lib/transaction.rb | 70 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 122 insertions(+), 13 deletions(-)

Detailed changes

config.ru 🔗

@@ -61,12 +61,52 @@ class Plan
 		SQL
 	end
 
-	def activate(customer_id, months)
-		DB.exec_params(
-			"INSERT INTO plan_log VALUES ($1, $2, tsrange($3, $4))",
-			[customer_id, @plan[:name], Time.now, Date.today >> months]
-		)
-		true
+	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
+	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
 
@@ -140,18 +180,18 @@ class CreditCardGateway
 	end
 
 	def sale(ip:, **kwargs)
-		return false unless decline_guard(ip)
-		result = @gateway.transaction.sale(**kwargs)
-		return true if result.success?
+		return nil unless decline_guard(ip)
+		tx = Transaction.sale(**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)
-		false
+		nil
 	end
 
-	def buy_plan(plan_name, months, nonce, ip)
+	def buy_plan(plan_name, nonce, ip)
 		plan = Plan.for(plan_name)
 		sale(
 			ip: ip,
@@ -159,7 +199,7 @@ class CreditCardGateway
 			payment_method_nonce: nonce,
 			merchant_account_id: plan.merchant_account,
 			options: {submit_for_settlement: true}
-		) && plan.activate(@customer_id, months)
+		)&.insert && plan.bill_plan(@customer_id)
 	end
 
 protected
@@ -292,7 +332,6 @@ class JmpPay < Roda
 					result = DB.transaction do
 						Plan.active?(gateway.customer_id) || gateway.buy_plan(
 							request.params["plan_name"],
-							5,
 							request.params["braintree_nonce"],
 							request.ip
 						)

lib/transaction.rb 🔗

@@ -0,0 +1,70 @@
+# 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