invoicing_payments_processing 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/balance.rb +27 -0
- data/lib/bufferpaypalnotification.rb +391 -0
- data/lib/customplan.rb +33 -0
- data/lib/extend_client_by_invoicing_payments_processing.rb +128 -0
- data/lib/invoice.rb +617 -0
- data/lib/invoiceitem.rb +11 -0
- data/lib/invoicing_payments_processing.rb +127 -0
- data/lib/movement.rb +138 -0
- data/lib/paypalsubscription.rb +131 -0
- metadata +154 -0
@@ -0,0 +1,128 @@
|
|
1
|
+
module BlackStack
|
2
|
+
|
3
|
+
#
|
4
|
+
#
|
5
|
+
#
|
6
|
+
class Client < Sequel::Model(:client)
|
7
|
+
one_to_many :paypal_subscriptions, :class=>:'BlackStack::PayPalSubscription', :key=>:id_client
|
8
|
+
one_to_many :customplans, :class=>:'BlackStack::CustomPlan', :key=>:id_client
|
9
|
+
one_to_many :movements, :class=>:'BlackStack::Movement', :key=>:id_client
|
10
|
+
|
11
|
+
# crea/actualiza un registro en la tabla movment, reduciendo la cantidad de creditos y saldo que tiene el cliente, para el producto indicado en product_code.
|
12
|
+
def consume(product_code, number_of_credits=1, description=nil)
|
13
|
+
DB.execute("exec reduceDebt '#{product_code.to_sql}', '#{self.id}', #{number_of_credits.to_s}, '#{description.to_s.to_sql}'")
|
14
|
+
end
|
15
|
+
|
16
|
+
# TODO: el cliente deberia tener una FK a la tabla division. La relacion no puede ser N-N.
|
17
|
+
# TODO: se debe preguntar a la central
|
18
|
+
def division
|
19
|
+
q =
|
20
|
+
"SELECT d.id as id " +
|
21
|
+
"FROM division d " +
|
22
|
+
"JOIN user_division ud ON d.id=ud.id_division " +
|
23
|
+
"JOIN [user] u ON u.id=ud.id_user " +
|
24
|
+
"WHERE u.id_client = '#{self.id}' "
|
25
|
+
row = DB[q].first
|
26
|
+
BlackStack::Division.where(:id=>row[:id]).first
|
27
|
+
end
|
28
|
+
|
29
|
+
# retorna true si este cliente no tiene ninguna generada con productos LGB2
|
30
|
+
def deserve_trial()
|
31
|
+
self.disabled_for_trial_ssm != true
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
def deserve_trial?
|
36
|
+
self.deserve_trial()
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
def get_balance()
|
41
|
+
n = 0
|
42
|
+
BlackStack::InvoicingPaymentsProcessing::products_descriptor.each { |code|
|
43
|
+
n += BlackStack::Balance.new(self.id, code).amount
|
44
|
+
}
|
45
|
+
n
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
def get_movements(from_time, to_time, product_code=nil)
|
50
|
+
if from_time > to_time
|
51
|
+
raise "From time must be earlier than To time"
|
52
|
+
end
|
53
|
+
if to_time.prev_year > from_time
|
54
|
+
raise "There time frame cannot be longer than 1 year."
|
55
|
+
end
|
56
|
+
to_time += 1
|
57
|
+
ds = BlackStack::Movement.where(:id_client => self.id, :product_code=>product_code) if !product_code.nil?
|
58
|
+
ds = BlackStack::Movement.where(:id_client => self.id) if product_code.nil?
|
59
|
+
ds.where("create_time >= ? and create_time <= ?", from_time, to_time)
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
def add_bonus(id_user_creator, product_code, bonus_credits, description, expiration_time)
|
64
|
+
bonus_amount = 0
|
65
|
+
# balance = BlackStack::Balance.new(self.id, product_code)
|
66
|
+
# amount = balance.amount.to_f
|
67
|
+
# credits = balance.credits.to_f
|
68
|
+
# if amount>=0 && credits>=0
|
69
|
+
# bonus_amount = (amount / credits) * bonus_credits
|
70
|
+
## else
|
71
|
+
## h = BlackStack::InvoicingPaymentsProcessing.product_descriptor(product_code)
|
72
|
+
## bonus_amount = h[:default_fee_per_unit].to_f
|
73
|
+
# end
|
74
|
+
m = BlackStack::Movement.new(
|
75
|
+
:id_client => self.id,
|
76
|
+
:create_time => now(),
|
77
|
+
:type => BlackStack::Movement::MOVEMENT_TYPE_ADD_BONUS,
|
78
|
+
:id_user_creator => id_user_creator,
|
79
|
+
:description => description,
|
80
|
+
:paypal1_amount => 0,
|
81
|
+
:bonus_amount => bonus_amount,
|
82
|
+
:amount => 0-bonus_amount,
|
83
|
+
:credits => 0-bonus_credits,
|
84
|
+
:profits_amount => 0,
|
85
|
+
:product_code => product_code,
|
86
|
+
:expiration_time => expiration_time
|
87
|
+
)
|
88
|
+
m.id = guid()
|
89
|
+
m.save
|
90
|
+
end
|
91
|
+
|
92
|
+
# retorna true si existe algun item de factura relacionado al 'plan' ('item_number').
|
93
|
+
# si el atributo 'amount' ademas es distinto a nil, se filtran items por ese monto.
|
94
|
+
def has_item(item_number, amount=nil)
|
95
|
+
h = BlackStack::InvoicingPaymentsProcessing::plans_descriptor.select { |obj| obj[:item_number].to_s == item_number.to_s }.first
|
96
|
+
raise "Plan not found" if h.nil?
|
97
|
+
|
98
|
+
q =
|
99
|
+
"SELECT i.id " +
|
100
|
+
"FROM invoice i "
|
101
|
+
|
102
|
+
# si el plan tiene un trial, entnces se pregunta si ya existe un item de factura por el importe del trial.
|
103
|
+
# si el plan no tiene un trial, entnces se pregunta si ya existe un item de factura por el importe del plan.
|
104
|
+
if amount.nil?
|
105
|
+
q +=
|
106
|
+
"JOIN invoice_item t ON ( i.id=t.id_invoice AND t.item_number='#{item_number}' ) "
|
107
|
+
else
|
108
|
+
q +=
|
109
|
+
"JOIN invoice_item t ON ( i.id=t.id_invoice AND t.item_number='#{item_number}' AND t.amount=#{amount.to_s} ) "
|
110
|
+
end
|
111
|
+
|
112
|
+
q +=
|
113
|
+
"WHERE i.id_client='#{self.id}' "
|
114
|
+
|
115
|
+
return !DB[q].first.nil?
|
116
|
+
end
|
117
|
+
|
118
|
+
# retorna los planes estandar definidos en el array BlackStack::InvoicingPaymentsProcessing::plans_descriptor, y le concatena los arrays customizados de este cliente definidos en la tabla custom_plan
|
119
|
+
def plans
|
120
|
+
a = BlackStack::InvoicingPaymentsProcessing::plans_descriptor
|
121
|
+
self.customplans.each { |p|
|
122
|
+
a << p.to_hash
|
123
|
+
}
|
124
|
+
a
|
125
|
+
end
|
126
|
+
end # class Client
|
127
|
+
|
128
|
+
end # module BlackStack
|
data/lib/invoice.rb
ADDED
@@ -0,0 +1,617 @@
|
|
1
|
+
module BlackStack
|
2
|
+
class Invoice < Sequel::Model(:invoice)
|
3
|
+
STATUS_UNPAID = 0
|
4
|
+
STATUS_PAID = 1
|
5
|
+
STATUS_REFUNDED = 2
|
6
|
+
PARAMS_FLOAT_MULTIPLICATION_FACTOR = 10000
|
7
|
+
|
8
|
+
BlackStack::Invoice.dataset = BlackStack::Invoice.dataset.disable_insert_output
|
9
|
+
self.dataset = self.dataset.disable_insert_output
|
10
|
+
|
11
|
+
many_to_one :buffer_paypal_notification, :class=>:'BlackStack::BufferPayPalNotification', :key=>:id_buffer_paypal_notification
|
12
|
+
many_to_one :client, :class=>:'BlackStack::Client', :key=>:id_client
|
13
|
+
many_to_one :paypal_subscription, :class=>:'BlackStack::PayPalSubscription', :key=>:id_paypal_subscription
|
14
|
+
one_to_many :items, :class=>:'BlackStack::InvoiceItem', :key=>:id_invoice
|
15
|
+
|
16
|
+
# compara 2 planes, y retorna TRUE si ambos pueden coexistir en una misma facutra, con un mismo enlace de PayPal
|
17
|
+
def self.compatibility?(h, i)
|
18
|
+
return false if h[:type]!=i[:type]
|
19
|
+
return false if h[:type]=='S' && h[:type]==i[:type] && h[:period]!=i[:period]
|
20
|
+
return false if h[:type]=='S' && h[:type]==i[:type] && h[:units]!=i[:units]
|
21
|
+
return false if h[:type]=='S' && h[:type]==i[:type] && h[:trial_period]!=i[:trial_period]
|
22
|
+
return false if h[:type]=='S' && h[:type]==i[:type] && h[:trial_units]!=i[:trial_units]
|
23
|
+
return false if h[:type]=='S' && h[:type]==i[:type] && h[:trial2_period]!=i[:trial2_period]
|
24
|
+
return false if h[:type]=='S' && h[:type]==i[:type] && h[:trial2_units]!=i[:trial2_units]
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
# retorna un array con la lista de estados posibles de una factura
|
29
|
+
def self.statuses()
|
30
|
+
[STATUS_UNPAID, STATUS_PAID, STATUS_REFUNDED]
|
31
|
+
end
|
32
|
+
|
33
|
+
# retorna un string con el nombre descriptivo del estado
|
34
|
+
def self.statusDescription(status)
|
35
|
+
if status == STATUS_UNPAID || status == nil
|
36
|
+
return "UNPAID"
|
37
|
+
elsif status == STATUS_PAID
|
38
|
+
return "PAID"
|
39
|
+
elsif status == STATUS_REFUNDED
|
40
|
+
return "REFUNDED"
|
41
|
+
else
|
42
|
+
raise "Unknown Invoice Status (#{status.to_s})"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# retorna el valor del color HTML para un estado
|
47
|
+
def self.statusColor(status)
|
48
|
+
if status == STATUS_UNPAID || status == nil
|
49
|
+
return "red"
|
50
|
+
elsif status == STATUS_PAID
|
51
|
+
return "green"
|
52
|
+
elsif status == STATUS_REFUNDED
|
53
|
+
return "brown"
|
54
|
+
else
|
55
|
+
raise "Unknown Invoice Status (#{status.to_s})"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
def deserve_trial?
|
61
|
+
return self.disabled_for_trial_ssm == false || self.disabled_for_trial_ssm == nil
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
def allowedToAddRemoveItems?
|
66
|
+
return (self.status == STATUS_UNPAID || self.status == nil) && (self.disabled_for_add_remove_items == false || self.disabled_for_add_remove_items == nil)
|
67
|
+
end
|
68
|
+
|
69
|
+
# envia un email transaccional con informacion de facturacion, y pasos a seguir despues del pago
|
70
|
+
def notificationSubject()
|
71
|
+
"We Received Your Payment"
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
def notificationBody()
|
76
|
+
"<p>We received your payment for a total of $#{("%.2f" % self.total()).to_s}.</p>" +
|
77
|
+
"<p>You can find your invoice <a href='#{CS_HOME_PAGE}/member/invoice?iid=#{self.id.to_guid}'><b>here</b></a></p>"
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
def number()
|
82
|
+
self.id.to_guid
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
def dateDesc()
|
87
|
+
self.create_time.strftime('%b %d, %Y')
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
def dueDateDesc()
|
92
|
+
Date.strptime(self.billing_period_from.to_s, '%Y-%m-%d').strftime('%b %d, %Y')
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
def billingPeriodFromDesc()
|
97
|
+
Date.strptime(self.billing_period_from.to_s, '%Y-%m-%d').strftime('%b %d, %Y')
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
def billingPeriodToDesc()
|
102
|
+
Date.strptime(self.billing_period_to.to_s, '%Y-%m-%d').strftime('%b %d, %Y')
|
103
|
+
end
|
104
|
+
|
105
|
+
#
|
106
|
+
def total()
|
107
|
+
ret = 0
|
108
|
+
self.items.each { |item|
|
109
|
+
ret += item.amount.to_f
|
110
|
+
# libero recursos
|
111
|
+
DB.disconnect
|
112
|
+
GC.start
|
113
|
+
}
|
114
|
+
ret
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
def totalDesc()
|
119
|
+
("%.2f" % self.total.to_s)
|
120
|
+
end
|
121
|
+
|
122
|
+
# TODO: refactor me
|
123
|
+
def paypal_link()
|
124
|
+
item = self.items.first
|
125
|
+
if item.nil?
|
126
|
+
raise "Invoice has no items"
|
127
|
+
end
|
128
|
+
|
129
|
+
plan_descriptor = self.client.plans.select { |j| j[:item_number].to_s == item.item_number.to_s }.first
|
130
|
+
if plan_descriptor.nil?
|
131
|
+
raise "Plan not found"
|
132
|
+
end
|
133
|
+
|
134
|
+
product_descriptor = BlackStack::InvoicingPaymentsProcessing::products_descriptor.select { |j| j[:code].to_s == plan_descriptor[:product_code].to_s }.first
|
135
|
+
if product_descriptor.nil?
|
136
|
+
raise "Product not found"
|
137
|
+
end
|
138
|
+
|
139
|
+
item_name = ""
|
140
|
+
n = 0
|
141
|
+
self.items.each { |t|
|
142
|
+
h = BlackStack::InvoicingPaymentsProcessing::plans_descriptor.select { |obj| obj[:item_number].to_s == t.item_number.to_s }.first
|
143
|
+
item_name += '; ' if n>0
|
144
|
+
item_name += h[:name]
|
145
|
+
if item_name.size >= 65
|
146
|
+
item_name += "..."
|
147
|
+
break
|
148
|
+
end
|
149
|
+
n+=1
|
150
|
+
}
|
151
|
+
|
152
|
+
return_path = product_descriptor[:return_path]
|
153
|
+
id_invoice = self.id
|
154
|
+
id_client = self.client.id
|
155
|
+
allow_trials = self.deserve_trial?
|
156
|
+
|
157
|
+
bIsSubscription = false
|
158
|
+
bIsSubscription = true if plan_descriptor[:type]=="S"
|
159
|
+
|
160
|
+
# generating the invoice number
|
161
|
+
invoice_id = "#{id_client.to_guid}.#{id_invoice.to_guid}"
|
162
|
+
|
163
|
+
values = {}
|
164
|
+
|
165
|
+
# common parameters for all the situations
|
166
|
+
values[:business] = BlackStack::InvoicingPaymentsProcessing::paypal_business_email
|
167
|
+
values[:lc] = "en_US"
|
168
|
+
|
169
|
+
if bIsSubscription
|
170
|
+
values[:cmd] = "_xclick-subscriptions"
|
171
|
+
else
|
172
|
+
values[:cmd] = "_xclick"
|
173
|
+
end
|
174
|
+
|
175
|
+
values[:upload] = 1
|
176
|
+
values[:no_shipping] = 1
|
177
|
+
values[:return] = BlackStack::Netting::add_param(return_path, "track_object_id", id_invoice.to_guid)
|
178
|
+
values[:return_url] = BlackStack::Netting::add_param(return_path, "track_object_id", id_invoice.to_guid)
|
179
|
+
values[:rm] = 1
|
180
|
+
values[:notify_url] = BlackStack::InvoicingPaymentsProcessing::paypal_ipn_listener
|
181
|
+
|
182
|
+
values[:invoice] = id_invoice
|
183
|
+
values[:item_name] = item_name
|
184
|
+
values[:item_number] = id_invoice.to_s
|
185
|
+
values[:src] = '1'
|
186
|
+
|
187
|
+
# si es una suscripcion
|
188
|
+
if (bIsSubscription)
|
189
|
+
|
190
|
+
trial1 = allow_trials && plan_descriptor[:trial_fee]!=nil && plan_descriptor[:trial_period]!=nil && plan_descriptor[:trial_units]!=nil
|
191
|
+
trial2 = allow_trials && plan_descriptor[:trial2_fee]!=nil && plan_descriptor[:trial2_period]!=nil && plan_descriptor[:trial2_units]!=nil
|
192
|
+
|
193
|
+
values[:a3] = 0
|
194
|
+
self.items.each { |i|
|
195
|
+
if trial1 && i.units!=i.plan_descriptor[:trial_credits]
|
196
|
+
raise 'Cannot order more than 1 package and trial in the same invoice'
|
197
|
+
elsif trial1
|
198
|
+
values[:a3] += i.plan_descriptor[:fee].to_f
|
199
|
+
else # !trial1
|
200
|
+
values[:a3] += ( i.units.to_f * ( i.plan_descriptor[:fee].to_f / i.plan_descriptor[:credits].to_f ).to_f )
|
201
|
+
end
|
202
|
+
}
|
203
|
+
values[:p3] = plan_descriptor[:units] # every 1
|
204
|
+
values[:t3] = plan_descriptor[:period] # per month
|
205
|
+
|
206
|
+
# si tiene un primer periodo de prueba
|
207
|
+
if trial1
|
208
|
+
values[:a1] = 0 # $1 fee
|
209
|
+
self.items.each { |i| values[:a1] += i.plan_descriptor[:trial_fee].to_i }
|
210
|
+
values[:p1] = plan_descriptor[:trial_units] # 15
|
211
|
+
values[:t1] = plan_descriptor[:trial_period] # days
|
212
|
+
|
213
|
+
# si tiene un segundo periodo de prueba
|
214
|
+
if trial2
|
215
|
+
values[:a2] = 0 # $50 fee
|
216
|
+
self.items.each { |i| values[:a2] += i.plan_descriptor[:trial2_fee].to_i }
|
217
|
+
values[:p2] = plan_descriptor[:trial2_units] # first 1
|
218
|
+
values[:t2] = plan_descriptor[:trial2_period] # month
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# sino, entonces es un pago por unica vez
|
223
|
+
else
|
224
|
+
values[:amount] = 0
|
225
|
+
self.items.each { |i| values[:amount] += i.amount.to_f }
|
226
|
+
end
|
227
|
+
|
228
|
+
# return url
|
229
|
+
"#{BlackStack::InvoicingPaymentsProcessing::PAYPAL_ORDERS_URL}/cgi-bin/webscr?" + URI.encode_www_form(values)
|
230
|
+
end
|
231
|
+
|
232
|
+
# retorna true si el estado de la factura sea NULL o UNPAID
|
233
|
+
def canBePaid?
|
234
|
+
self.status == nil || self.status == BlackStack::Invoice::STATUS_UNPAID
|
235
|
+
end
|
236
|
+
|
237
|
+
# actualiza el registro en la tabla invoice segun los parametros.
|
238
|
+
# en este caso la factura se genera antes del pago.
|
239
|
+
# genera el enlace de paypal.
|
240
|
+
def setup()
|
241
|
+
# busco el primer item de esta factura
|
242
|
+
item = self.items.sort_by {|obj| obj.create_time}.first
|
243
|
+
if item == nil
|
244
|
+
raise "Invoice has no items."
|
245
|
+
end
|
246
|
+
|
247
|
+
h = self.client.plans.select { |j| j[:item_number].to_s == item.item_number.to_s }.first
|
248
|
+
if h == nil
|
249
|
+
raise "Unknown item_number."
|
250
|
+
end
|
251
|
+
|
252
|
+
#
|
253
|
+
return_path = h[:return_path]
|
254
|
+
|
255
|
+
c = BlackStack::Client.where(:id=>id_client).first
|
256
|
+
if (c==nil)
|
257
|
+
raise "Client not found"
|
258
|
+
end
|
259
|
+
|
260
|
+
if (self.id == nil)
|
261
|
+
self.id = guid()
|
262
|
+
end
|
263
|
+
self.create_time = now()
|
264
|
+
self.id_client = c.id
|
265
|
+
self.id_buffer_paypal_notification = nil
|
266
|
+
self.paypal_url = self.paypal_link
|
267
|
+
|
268
|
+
#
|
269
|
+
self.save()
|
270
|
+
end
|
271
|
+
|
272
|
+
# cambia el estado de la factura de UNPAID a PAID
|
273
|
+
# verifica que el estado de la factura sea NULL o UNPAID
|
274
|
+
# crea los registros contables por el pago de esta factura
|
275
|
+
def getPaid()
|
276
|
+
if self.canBePaid? == false
|
277
|
+
raise "Method BlackStack::Invoice::getPaid requires the current status is nil or unpaid."
|
278
|
+
end
|
279
|
+
# marco la factura como pagada
|
280
|
+
self.status = BlackStack::Invoice::STATUS_PAID
|
281
|
+
self.save
|
282
|
+
# registro los asientos contables
|
283
|
+
InvoiceItem.where(:id_invoice=>self.id).all { |item|
|
284
|
+
BlackStack::Movement.new().parse(item, BlackStack::Movement::MOVEMENT_TYPE_ADD_PAYMENT, "Invoice Payment").save()
|
285
|
+
#
|
286
|
+
DB.disconnect
|
287
|
+
GC.start
|
288
|
+
}
|
289
|
+
end
|
290
|
+
|
291
|
+
# Verify if I can add this item_number to this invoice.
|
292
|
+
# Otherwise, it raise an exception.
|
293
|
+
#
|
294
|
+
# Si el atributo 'amount' ademas es distinto a nil, se filtran items por ese monto.
|
295
|
+
#
|
296
|
+
def check_create_item(item_number, validate_items_compatibility=true, amount=nil)
|
297
|
+
# busco el primer item de esta factura, si es que existe
|
298
|
+
item0 = self.items.sort_by {|obj| obj.create_time}.first
|
299
|
+
|
300
|
+
# encuentro el descriptor del plan
|
301
|
+
# el descriptor del plan tambien es necesario para la llamada a paypal_link
|
302
|
+
h = self.client.plans.select { |j| j[:item_number].to_s == item_number.to_s }.first
|
303
|
+
if h == nil
|
304
|
+
raise "Unknown item_number"
|
305
|
+
end
|
306
|
+
|
307
|
+
# returna true si el plan es una oferta one-time,
|
308
|
+
# => y ya existe una item de factura asignado a
|
309
|
+
# => este plan, con un importe igual al del trial1
|
310
|
+
# => o trial2.
|
311
|
+
if h[:one_time_offer] == true
|
312
|
+
if self.client.has_item(h[:item_number], amount) && h[:fee].to_f != amount.to_f
|
313
|
+
raise "The plan is a one-time offer and you already have it included in an existing invoice"
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# si la factura ya tiene un item
|
318
|
+
if !item0.nil?
|
319
|
+
plan_descriptor = self.client.plans.select { |j| j[:item_number].to_s == item0.item_number.to_s }.first
|
320
|
+
if plan_descriptor.nil?
|
321
|
+
raise "Plan '#{item0.item_number.to_s}' not found"
|
322
|
+
end
|
323
|
+
|
324
|
+
# valido que los items sean compatibles
|
325
|
+
if !BlackStack::Invoice.compatibility?(h, plan_descriptor) && validate_items_compatibility==true
|
326
|
+
raise "Incompatible Items"
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# configura la factura, segun el importe que pagara el cliente, la configuracion del plan en el descriptor h, y si el clietne merece un trial o no
|
332
|
+
def create_item(item_number, n=1, validate_items_compatibility=true)
|
333
|
+
#
|
334
|
+
deserve_trial = self.deserve_trial?
|
335
|
+
|
336
|
+
# busco el primer item de esta factura, si es que existe
|
337
|
+
item0 = self.items.sort_by {|obj| obj.create_time}.first
|
338
|
+
|
339
|
+
# numero de facturas previas a esta factura
|
340
|
+
subs = BlackStack::PayPalSubscription.where(:subscr_id=>self.subscr_id).first
|
341
|
+
nSubscriptionInvoices = 0 if subs.nil?
|
342
|
+
nSubscriptionInvoices = subs.invoices.count if !subs.nil?
|
343
|
+
|
344
|
+
# encuentro el descriptor del plan
|
345
|
+
# el descriptor del plan tambien es necesario para la llamada a paypal_link
|
346
|
+
h = self.client.plans.select { |j| j[:item_number].to_s == item_number.to_s }.first
|
347
|
+
|
348
|
+
# mapeo variables
|
349
|
+
c = self.client
|
350
|
+
amount = 0.to_f
|
351
|
+
unit_price = 0.to_f
|
352
|
+
units = 0.to_i
|
353
|
+
|
354
|
+
# decido si se trata de una suscripcion
|
355
|
+
isSubscription = false
|
356
|
+
isSubscription = true if h[:type] == "S"
|
357
|
+
|
358
|
+
# le seteo la fecha de hoy
|
359
|
+
self.billing_period_from = now()
|
360
|
+
|
361
|
+
# si es una suscripcion, y
|
362
|
+
# si el plan tiene un primer trial, y
|
363
|
+
# es primer pago de esta suscripcion, entonces:
|
364
|
+
# => se trata del primer pago por trial de esta suscripcion
|
365
|
+
if (isSubscription && h[:trial_fee] != nil && deserve_trial)
|
366
|
+
units = h[:trial_credits].to_i
|
367
|
+
unit_price = h[:trial_fee].to_f / h[:trial_credits].to_f
|
368
|
+
billing_period_to = DB["SELECT DATEADD(#{h[:trial_period].to_s}, +#{h[:trial_units].to_s}, '#{self.billing_period_from.to_s}') AS [now]"].map(:now)[0].to_s
|
369
|
+
|
370
|
+
# si es una suscripcion, y
|
371
|
+
# si el plan tiene un segundo trial, y
|
372
|
+
# es segundo pago de esta suscripcion, entonces:
|
373
|
+
# => se trata del segundo pago por trial de esta suscripcion
|
374
|
+
elsif (isSubscription && h[:trial2_fee] != nil && nSubscriptionInvoices == 1)
|
375
|
+
units = h[:trial2_credits].to_i
|
376
|
+
unit_price = h[:trial2_fee].to_f / h[:trial2_credits].to_f
|
377
|
+
billing_period_to = DB["SELECT DATEADD(#{h[:trial2_period].to_s}, +#{h[:trial2_units].to_s}, '#{self.billing_period_from.to_s}') AS [now]"].map(:now)[0].to_s
|
378
|
+
|
379
|
+
# si el plan tiene un fee, y
|
380
|
+
elsif (isSubscription && h[:fee].to_f != nil)
|
381
|
+
units = n.to_i * h[:credits].to_i
|
382
|
+
unit_price = h[:fee].to_f / h[:credits].to_f
|
383
|
+
billing_period_to = DB["SELECT DATEADD(#{h[:period].to_s}, +#{h[:units].to_s}, '#{self.billing_period_from.to_s}') AS [now]"].map(:now)[0].to_s
|
384
|
+
|
385
|
+
elsif (!isSubscription && h[:fee].to_f != nil)
|
386
|
+
units = n.to_i * h[:credits].to_i
|
387
|
+
unit_price = h[:fee].to_f / h[:credits].to_f
|
388
|
+
billing_period_to = billing_period_from
|
389
|
+
|
390
|
+
else # se hace un prorrateo
|
391
|
+
raise "Plan is specified wrong"
|
392
|
+
|
393
|
+
end
|
394
|
+
|
395
|
+
#
|
396
|
+
amount = units.to_f * unit_price.to_f
|
397
|
+
|
398
|
+
# valido si puedo agregar este item
|
399
|
+
self.check_create_item(item_number, validate_items_compatibility, amount)
|
400
|
+
|
401
|
+
# cuardo la factura en la base de datos
|
402
|
+
self.billing_period_to = billing_period_to
|
403
|
+
self.save()
|
404
|
+
|
405
|
+
# creo el item por el producto LGB2
|
406
|
+
item1 = BlackStack::InvoiceItem.new()
|
407
|
+
item1.id = guid()
|
408
|
+
item1.id_invoice = self.id
|
409
|
+
item1.product_code = h[:product_code]
|
410
|
+
item1.unit_price = unit_price.to_f
|
411
|
+
item1.units = units.to_i
|
412
|
+
item1.amount = amount.to_f
|
413
|
+
item1.item_number = h[:item_number]
|
414
|
+
item1.description = h[:name]
|
415
|
+
item1.detail = plan_payment_description(h)
|
416
|
+
|
417
|
+
#
|
418
|
+
return item1
|
419
|
+
end
|
420
|
+
|
421
|
+
def plan_payment_description(h)
|
422
|
+
ret = ""
|
423
|
+
ret += "$#{h[:trial_fee]} trial. " if self.deserve_trial? && !h[:trial_fee].nil?
|
424
|
+
ret += "$#{h[:trial2_fee]} one-time price. " if self.deserve_trial? && !h[:trial2_fee].nil?
|
425
|
+
ret += "$#{h[:fee]}/#{h[:period]}. " if h[:units].to_i <= 1
|
426
|
+
ret += "$#{h[:fee]}/#{h[:units]}#{h[:period]}. " if h[:units].to_i > 1
|
427
|
+
ret += "#{h[:credits]} credits. " if h[:credits].to_i > 1
|
428
|
+
ret
|
429
|
+
end
|
430
|
+
|
431
|
+
# configura la factura, segun el importe que pagara el cliente, la configuracion del plan en el descriptor h, y si el clietne merece un trial o no
|
432
|
+
def add_item(item_number, n=1)
|
433
|
+
# creo el item
|
434
|
+
item1 = self.create_item(item_number, n)
|
435
|
+
item1.save()
|
436
|
+
|
437
|
+
# agrega el item al array de la factura
|
438
|
+
self.items << item1
|
439
|
+
|
440
|
+
# reconfiguro la factura
|
441
|
+
self.setup()
|
442
|
+
end # add_item
|
443
|
+
|
444
|
+
def remove_item(item_id)
|
445
|
+
DB.execute("DELETE invoice_item WHERE id_invoice='#{self.id}' AND [id]='#{item_id}'")
|
446
|
+
self.setup
|
447
|
+
end # remove_item
|
448
|
+
|
449
|
+
# actualiza el registro en la tabla invoice como las siguiente factura.
|
450
|
+
# en este caso la factura se genera antes del pago.
|
451
|
+
# crea uno o mas registros en la tabla invoice_item.
|
452
|
+
def next(i)
|
453
|
+
b = i.buffer_paypal_notification
|
454
|
+
if b == nil
|
455
|
+
raise "Method BlackStack::Invoice::next requires the previous invoice (i) is linked to a record in the table buffer_paypal_notification."
|
456
|
+
end
|
457
|
+
|
458
|
+
id_client = i.id_client
|
459
|
+
c = BlackStack::Client.where(:id=>id_client).first
|
460
|
+
if c==nil
|
461
|
+
raise "Client not found"
|
462
|
+
end
|
463
|
+
|
464
|
+
h = BlackStack::InvoicingPaymentsProcessing::plans_descriptor.select { |obj| obj[:item_number].to_s == i.items.first.item_number.to_s }.first
|
465
|
+
if h==nil
|
466
|
+
raise "Plan not found"
|
467
|
+
end
|
468
|
+
|
469
|
+
return_path = h[:return_path]
|
470
|
+
|
471
|
+
if (self.id == nil)
|
472
|
+
self.id = guid()
|
473
|
+
end
|
474
|
+
self.create_time = now()
|
475
|
+
self.id_client = c.id
|
476
|
+
self.id_buffer_paypal_notification = nil
|
477
|
+
self.subscr_id = b.subscr_id
|
478
|
+
self.disabled_for_add_remove_items = true
|
479
|
+
|
480
|
+
i.items.each { |t|
|
481
|
+
self.add_item(t.item_number)
|
482
|
+
#
|
483
|
+
DB.disconnect
|
484
|
+
GC.start
|
485
|
+
}
|
486
|
+
|
487
|
+
|
488
|
+
self.billing_period_from = i.billing_period_to
|
489
|
+
self.billing_period_to = DB["SELECT DATEADD(#{h[:period].to_s}, +#{h[:units].to_s}, '#{self.billing_period_from.to_s}') AS [now]"].map(:now)[0].to_s
|
490
|
+
|
491
|
+
self.paypal_url = self.paypal_link
|
492
|
+
self.paypal_url = nil if h[:type] == "S" # si se trata de una suscripcion, entonces esta factura se pagara automaticamente
|
493
|
+
self.automatic_billing = 1 if h[:type] == "S" # si se trata de una suscripcion, entonces esta factura se pagara automaticamente
|
494
|
+
self.save
|
495
|
+
end
|
496
|
+
|
497
|
+
# actualiza el registro en la tabla invoice segun un registro en la tabla buffer_paypal_notification.
|
498
|
+
# en este caso la factura se genera despues del pago.
|
499
|
+
# crea uno o mas registros en la tabla invoice_item.
|
500
|
+
def parse(b)
|
501
|
+
item_number = b.item_number
|
502
|
+
payment_gross = b.payment_gross.to_f
|
503
|
+
billing_from = b.create_time
|
504
|
+
#isSubscription = b.isSubscription?
|
505
|
+
c = b.get_client
|
506
|
+
if (c==nil)
|
507
|
+
raise "Client not found"
|
508
|
+
end
|
509
|
+
self.setup(item_number, c.id, payment_gross, billing_from)
|
510
|
+
end # def parse(b)
|
511
|
+
|
512
|
+
# Genera lis items de la factura de reembolso.
|
513
|
+
# payment_gross: es el tital reembolsado
|
514
|
+
# id_invoice: es el ID de la factura que se está reembolsando
|
515
|
+
#
|
516
|
+
# Si el monto del reembolso es mayor al total de la factua, entocnes se levanta una excepcion
|
517
|
+
#
|
518
|
+
# Si existe un item, y solo uno, con importe igual al reembolso, entonces se aplica todo el reembolso a ese item. Y termina la funcion.
|
519
|
+
# Si existen mas de un item con igual importe que el reembolso, entonces se levanta una excepcion.
|
520
|
+
#
|
521
|
+
# Si el monto del reembolso es igual al total de la factura, se hace un reembolso total de todos los items. Y termina la funcion.
|
522
|
+
# Si la factura tiene un solo item, entonces se calcula un reembolso parcial.
|
523
|
+
# Sino, entonces se levanta una excepcion.
|
524
|
+
#
|
525
|
+
# TODO: Hacerlo transaccional
|
526
|
+
def setup_refund(payment_gross, id_invoice)
|
527
|
+
# cargo la factura
|
528
|
+
i = BlackStack::Invoice.where(:id=>id_invoice).first
|
529
|
+
raise "Invoice not found (#{id_invoice})" if i.nil?
|
530
|
+
|
531
|
+
# obtengo el total de la factura
|
532
|
+
total = i.total.to_f
|
533
|
+
|
534
|
+
# Si existe un item, y solo uno, con importe igual al reembolso, entonces se aplica todo el reembolso a ese item. Y termina la funcion.
|
535
|
+
# Si existen mas de un item con igual importe que el reembolso, entonces se levanta una excepcion.
|
536
|
+
matched_items = i.items.select { |o| o.amount.to_f == -payment_gross.to_f }
|
537
|
+
|
538
|
+
if total < -payment_gross
|
539
|
+
raise "The refund is higher than the invoice amount"
|
540
|
+
|
541
|
+
# Si el monto del reembolso es igual al total de la factura, se hace un reembolso total de todos los items. Y termina la funcion.
|
542
|
+
# Si el monto de la factura es distinto al moneto del reembolso, entonces se levanta una excepcion.
|
543
|
+
elsif total == -payment_gross
|
544
|
+
i.items.each { |u|
|
545
|
+
h = BlackStack::InvoicingPaymentsProcessing::plans_descriptor.select { |obj| obj[:item_number] == u.item_number }.first
|
546
|
+
raise "Plan not found" if h.nil?
|
547
|
+
item1 = BlackStack::InvoiceItem.new()
|
548
|
+
item1.id = guid()
|
549
|
+
item1.id_invoice = self.id
|
550
|
+
item1.unit_price = u.unit_price.to_f
|
551
|
+
item1.units = -u.units
|
552
|
+
item1.amount = -u.amount.to_f
|
553
|
+
item1.product_code = u.product_code.to_s
|
554
|
+
item1.item_number = u.item_number.to_s
|
555
|
+
item1.detail = u.detail.to_s
|
556
|
+
item1.description = u.description.to_s
|
557
|
+
item1.save()
|
558
|
+
BlackStack::Movement.new().parse(item1, BlackStack::Movement::MOVEMENT_TYPE_REFUND_BALANCE).save()
|
559
|
+
# release resources
|
560
|
+
DB.disconnect
|
561
|
+
GC.start
|
562
|
+
}
|
563
|
+
=begin
|
564
|
+
# si existe al menos un item que coincida el total con el monto del reembolso,
|
565
|
+
# entonces selecciono el primero
|
566
|
+
elsif matched_items.size > 0
|
567
|
+
t = matched_items.first
|
568
|
+
h = BlackStack::InvoicingPaymentsProcessing::plans_descriptor.select { |obj| obj[:item_number] == t.item_number }.first
|
569
|
+
raise "Plan not found" if h.nil?
|
570
|
+
item1 = BlackStack::InvoiceItem.new()
|
571
|
+
item1.id = guid()
|
572
|
+
item1.id_invoice = self.id
|
573
|
+
item1.unit_price = t.unit_price.to_f
|
574
|
+
item1.units = -t.units
|
575
|
+
item1.amount = -t.amount.to_f
|
576
|
+
item1.product_code = t.product_code.to_s
|
577
|
+
item1.item_number = t.item_number.to_s
|
578
|
+
item1.detail = t.detail.to_s
|
579
|
+
item1.description = t.description.to_s
|
580
|
+
item1.save()
|
581
|
+
BlackStack::Movement.new().parse(item1, BlackStack::Movement::MOVEMENT_TYPE_REFUND_BALANCE).save()
|
582
|
+
=end
|
583
|
+
# reembolso parcial de una factura con un unico item
|
584
|
+
elsif i.items.size == 1
|
585
|
+
t = i.items.first
|
586
|
+
|
587
|
+
amount = -payment_gross.to_f
|
588
|
+
unit_price = t.amount.to_f / t.units.to_f
|
589
|
+
units = (amount / unit_price.to_f).round.to_i
|
590
|
+
|
591
|
+
h = BlackStack::InvoicingPaymentsProcessing::plans_descriptor.select { |obj| obj[:item_number] == t.item_number }.first
|
592
|
+
raise "Plan not found" if h.nil?
|
593
|
+
item1 = BlackStack::InvoiceItem.new()
|
594
|
+
item1.id = guid()
|
595
|
+
item1.id_invoice = self.id
|
596
|
+
item1.unit_price = unit_price.to_f
|
597
|
+
item1.units = -units
|
598
|
+
item1.amount = -amount.to_f
|
599
|
+
item1.product_code = t.product_code.to_s
|
600
|
+
item1.item_number = t.item_number.to_s
|
601
|
+
item1.detail = t.detail.to_s
|
602
|
+
item1.description = t.description.to_s
|
603
|
+
item1.save()
|
604
|
+
BlackStack::Movement.new().parse(item1, BlackStack::Movement::MOVEMENT_TYPE_REFUND_BALANCE).save()
|
605
|
+
|
606
|
+
else
|
607
|
+
raise "Refund amount is not matching with the invoice total (#{total.to_s}) and the invoice has more than 1 item."
|
608
|
+
|
609
|
+
end
|
610
|
+
|
611
|
+
# release resources
|
612
|
+
DB.disconnect
|
613
|
+
GC.start
|
614
|
+
end # def setup_refund
|
615
|
+
|
616
|
+
end # class Invoice
|
617
|
+
end # module BlackStack
|