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.
@@ -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
@@ -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