Create initial monthly billing cronjob

Stephen Paul Weber created

Renews all expired plans where there is enough balance to do so.

Sets expires_at on all renewed plans to one month from today, so even if they
were very expired they are up to date the moment they pay.

Change summary

bin/billing_monthly_cronjob | 93 +++++++++++++++++++++++++++++++++++++++
1 file changed, 93 insertions(+)

Detailed changes

bin/billing_monthly_cronjob 🔗

@@ -0,0 +1,93 @@
+#!/usr/bin/ruby
+# frozen_string_literal: true
+
+# Usage: ./billing_monthly_cronjob \
+#      '{ healthchecks_url = "https://hc-ping.com/...", plans = ./plans.dhall }'
+
+require "bigdecimal"
+require "date"
+require "dhall"
+require "net/http"
+require "pg"
+
+CONFIG = Dhall.load(ARGV[0]).sync
+
+Net::HTTP.post_form(URI("#{CONFIG[:healthchecks_url]}/start"), {})
+
+db = PG.connect(dbname: "jmp")
+db.type_map_for_results = PG::BasicTypeMapForResults.new(db)
+db.type_map_for_queries = PG::BasicTypeMapForQueries.new(db)
+
+not_renewed = 0
+renewed = 0
+revenue = BigDecimal.new(0)
+
+RENEW_UNTIL = Date.today >> 1
+
+class Plan
+	def self.from_name(plan_name)
+		plan = CONFIG[:plans].find { |p| p[:name].to_s == plan_name }
+		new(plan) if plan
+	end
+
+	def initialize(plan)
+		@plan = plan
+	end
+
+	def price
+		BigDecimal.new(@plan["monthly_price"].to_i) * 0.0001
+	end
+
+	def bill_customer(db, customer_id)
+		transaction_id = "#{customer_id}-renew-until-#{RENEW_UNTIL}"
+		db.exec_params(<<-SQL, [customer_id, transaction_id, -price])
+			INSERT INTO transactions
+				(customer_id, transaction_id, amount, note)
+			VALUES
+				($1, $2, $3, 'Renew account plan')
+		SQL
+	end
+
+	def renew(db, customer_id, expires_at)
+		bill_customer(db, customer_id)
+
+		params = [RENEW_UNTIL, customer_id, expires_at]
+		db.exec_params(<<-SQL, params)
+		  UPDATE plan_log SET expires_at=$1
+		  WHERE customer_id=$2 AND expires_at=$3
+		SQL
+	end
+end
+
+db.transaction do
+	db.exec(
+		<<-SQL
+		SELECT customer_id, plan_name, expires_at, balance
+		FROM customer_plans INNER JOIN balances USING (customer_id)
+		WHERE expires_at <= NOW()
+		SQL
+	).each do |expired_customer|
+		plan = Plan.from_name(expired_customer["plan_name"])
+
+		if expired_customer["balance"] < plan.price
+			not_renewed += 1
+			next
+		end
+
+		plan.renew(
+			db,
+			expired_customer["customer_id"],
+			expired_customer["expires_at"]
+		)
+
+		renewed += 1
+		revenue += plan.price
+	end
+end
+
+Net::HTTP.post_form(
+	URI(CONFIG[:healthchecks_url].to_s),
+	renewed: renewed,
+	not_renewed: not_renewed,
+	revenue: revenue.to_s("F")
+)