~singpolyma/sgx-jmp

sgx-jmp/lib/customer_plan.rb -rw-r--r-- 4.4 KiB
b808c03dStephen Paul Weber When two plan logs overlap current window 29 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# frozen_string_literal: true

require "forwardable"
require "value_semantics/monkey_patched"

require_relative "em"
require_relative "parented_domain"
require_relative "plan"

class CustomerPlan
	extend Forwardable

	def_delegator :plan, :name, :plan_name
	def_delegators :plan, :currency, :merchant_account,
	               :minute_limit, :message_limit

	value_semantics do
		customer_id           String
		plan                  Anything(), default: nil, coerce: true
		expires_at            Either(Time, nil), default_generator: -> { Time.now }
		auto_top_up_amount    Integer, default: 0
		monthly_overage_limit Integer, default: 0
		pending               Bool(), default: false
		parent_customer_id    Either(String, nil), default: nil
		parent_plan           Anything(), default: nil, coerce: true
	end

	class << self
		def default(customer_id, jid)
			new(customer_id, **ParentedDomain.for(jid)&.plan_kwargs || {})
		end

		def extract(**kwargs)
			new(**kwargs.slice(
				*value_semantics.attributes.map(&:name), :plan_name, :parent_plan_name
			))
		end

		def coerce_plan(plan_or_name_or_nil)
			return plan_or_name_or_nil if plan_or_name_or_nil.is_a?(OpenStruct)
			return OpenStruct.new unless plan_or_name_or_nil

			Plan.for(plan_or_name_or_nil)
		end
		alias coerce_parent_plan coerce_plan
	end

	def initialize(customer_id=nil, **kwargs)
		kwargs, customer_id = customer_id, nil if customer_id.is_a?(Hash)
		kwargs[:plan] = kwargs.delete(:plan_name) if kwargs.key?(:plan_name)
		if kwargs.key?(:parent_plan_name)
			kwargs[:parent_plan] = kwargs.delete(:parent_plan_name)
		end
		super(customer_id ? kwargs.merge(customer_id: customer_id) : kwargs)
	end

	def active?
		plan_name && expires_at > Time.now
	end

	def status
		return :active if active?
		return :pending if pending

		:expired
	end

	def monthly_price
		plan.monthly_price - (parent_plan&.subaccount_discount || 0)
	end

	def verify_parent!
		return unless parent_customer_id

		result = DB.query(<<~SQL, [parent_customer_id])
			SELECT plan_name FROM customer_plans WHERE customer_id=$1
		SQL

		raise "Invalid parent account" if !result || !result.first

		plan = Plan.for(result.first["plan_name"])
		raise "Parent currency mismatch" unless plan.currency == currency
	end

	def save_plan!
		verify_parent!
		DB.exec_defer(<<~SQL, [customer_id, plan_name, parent_customer_id])
			INSERT INTO plan_log
				(customer_id, plan_name, parent_customer_id, date_range)
			VALUES (
				$1,
				$2,
				$3,
				tsrange(
					LOCALTIMESTAMP - '2 seconds'::interval,
					LOCALTIMESTAMP - '1 second'::interval
				)
			)
		SQL
	end

	def bill_plan(note: nil)
		EMPromise.resolve(nil).then do
			DB.transaction do |db|
				next false unless !block_given? || yield(db)

				charge_for_plan(note)
				extend_plan
				true
			end
		end
	end

	def activate_plan_starting_now
		verify_parent!
		activated = DB.exec(<<~SQL, [customer_id, plan_name, parent_customer_id])
			INSERT INTO plan_log (customer_id, plan_name, date_range, parent_customer_id)
			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'), $3)
			ON CONFLICT DO NOTHING
		SQL
		activated = activated.cmd_tuples.positive?
		return false unless activated

		DB.exec(<<~SQL, [customer_id])
			DELETE FROM plan_log WHERE customer_id=$1 AND date_range << '[now,now]'
				AND upper(date_range) - lower(date_range) < '2 seconds'
		SQL
	end

	def extend_plan
		add_one_month_to_current_plan unless activate_plan_starting_now
	end

	def activation_date
		DB.query_one(<<~SQL, customer_id).then { |r| r[:start_date] }
			SELECT
				MIN(LOWER(date_range)) AS start_date
			FROM plan_log WHERE customer_id = $1;
		SQL
	end

	protected :customer_id, :plan, :pending, :[]

protected

	def charge_for_plan(note)
		raise "No plan setup" unless plan

		params = [
			customer_id,
			"#{customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
			-monthly_price,
			note
		]
		DB.exec(<<~SQL, params)
			INSERT INTO transactions
				(customer_id, transaction_id, created_at, settled_after, amount, note)
			VALUES ($1, $2, LOCALTIMESTAMP, LOCALTIMESTAMP, $3, $4)
		SQL
	end

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