Class: BlackStack::Invoice

Inherits:
Object
  • Object
show all
Defined in:
lib/invoice.rb

Constant Summary collapse

STATUS_UNPAID =
0
STATUS_PAID =
1
STATUS_REFUNDED =
2
PARAMS_FLOAT_MULTIPLICATION_FACTOR =
10000

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.compatibility?(h, i) ⇒ Boolean

compara 2 planes, y retorna TRUE si ambos pueden coexistir en una misma facutra, con un mismo enlace de PayPal

Returns:

  • (Boolean)


17
18
19
20
21
22
23
24
25
26
# File 'lib/invoice.rb', line 17

def self.compatibility?(h, i)
  return false if h[:type]!=i[:type]
  return false if h[:type]=='S' && h[:type]==i[:type] && h[:period]!=i[:period] 
  return false if h[:type]=='S' && h[:type]==i[:type] && h[:units]!=i[:units] 
  return false if h[:type]=='S' && h[:type]==i[:type] && h[:trial_period]!=i[:trial_period] 
  return false if h[:type]=='S' && h[:type]==i[:type] && h[:trial_units]!=i[:trial_units] 
  return false if h[:type]=='S' && h[:type]==i[:type] && h[:trial2_period]!=i[:trial2_period] 
  return false if h[:type]=='S' && h[:type]==i[:type] && h[:trial2_units]!=i[:trial2_units] 
  true
end

.statusColor(status) ⇒ Object

retorna el valor del color HTML para un estado



47
48
49
50
51
52
53
54
55
56
57
# File 'lib/invoice.rb', line 47

def self.statusColor(status)
  if status == STATUS_UNPAID || status == nil
    return "red"
  elsif status == STATUS_PAID
    return "green"
  elsif status == STATUS_REFUNDED
    return "brown"
  else
    raise "Unknown Invoice Status (#{status.to_s})"
  end
end

.statusDescription(status) ⇒ Object

retorna un string con el nombre descriptivo del estado



34
35
36
37
38
39
40
41
42
43
44
# File 'lib/invoice.rb', line 34

def self.statusDescription(status)
  if status == STATUS_UNPAID || status == nil
    return "UNPAID"
  elsif status == STATUS_PAID
    return "PAID"
  elsif status == STATUS_REFUNDED
    return "REFUNDED"
  else
    raise "Unknown Invoice Status (#{status.to_s})"
  end
end

.statusesObject

retorna un array con la lista de estados posibles de una factura



29
30
31
# File 'lib/invoice.rb', line 29

def self.statuses()
  [STATUS_UNPAID, STATUS_PAID, STATUS_REFUNDED]
end

Instance Method Details

#add_item(item_number, n = 1) ⇒ Object

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
433
434
435
436
437
438
439
440
441
442
# File 'lib/invoice.rb', line 432

def add_item(item_number, n=1)
  # creo el item
  item1 = self.create_item(item_number, n)
  item1.save()
  
  # agrega el item al array de la factura
  self.items << item1
  
  # reconfiguro la factura
  self.setup()
end

#allowedToAddRemoveItems?Boolean

Returns:

  • (Boolean)


65
66
67
# File 'lib/invoice.rb', line 65

def allowedToAddRemoveItems?
  return (self.status == STATUS_UNPAID || self.status == nil) && (self.disabled_for_add_remove_items == false || self.disabled_for_add_remove_items == nil)
end

#billingPeriodFromDescObject



96
97
98
# File 'lib/invoice.rb', line 96

def billingPeriodFromDesc()
  Date.strptime(self.billing_period_from.to_s, '%Y-%m-%d').strftime('%b %d, %Y')
end

#billingPeriodToDescObject



101
102
103
# File 'lib/invoice.rb', line 101

def billingPeriodToDesc()
  Date.strptime(self.billing_period_to.to_s, '%Y-%m-%d').strftime('%b %d, %Y')
end

#canBePaid?Boolean

retorna true si el estado de la factura sea NULL o UNPAID

Returns:

  • (Boolean)


233
234
235
# File 'lib/invoice.rb', line 233

def canBePaid?
  self.status == nil || self.status == BlackStack::Invoice::STATUS_UNPAID
end

#check_create_item(item_number, validate_items_compatibility = true, amount = nil) ⇒ Object

Verify if I can add this item_number to this invoice. Otherwise, it raise an exception.

Si el atributo ‘amount’ ademas es distinto a nil, se filtran items por ese monto.



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/invoice.rb', line 296

def check_create_item(item_number, validate_items_compatibility=true, amount=nil)
  # busco el primer item de esta factura, si es que existe
  item0 = self.items.sort_by {|obj| obj.create_time}.first
  
  # encuentro el descriptor del plan
  # el descriptor del plan tambien es necesario para la llamada a paypal_link 
  h = self.client.plans.select { |j| j[:item_number].to_s == item_number.to_s }.first
  if h == nil
    raise "Unknown item_number"
  end 
  
  # returna true si el plan es una oferta one-time, 
  # => y ya existe una item de factura asignado a 
  # => este plan, con un importe igual al del trial1
  # => o trial2. 
  if h[:one_time_offer] == true
    if self.client.has_item(h[:item_number], amount) && h[:fee].to_f != amount.to_f
      raise "The plan is a one-time offer and you already have it included in an existing invoice"
    end
  end
      
  # si la factura ya tiene un item
  if !item0.nil?
    plan_descriptor = self.client.plans.select { |j| j[:item_number].to_s == item0.item_number.to_s }.first
    if plan_descriptor.nil?
      raise "Plan '#{item0.item_number.to_s}' not found"      
    end

    # valido que los items sean compatibles
    if !BlackStack::Invoice.compatibility?(h, plan_descriptor) && validate_items_compatibility==true
      raise "Incompatible Items"
    end
  end
end

#create_item(item_number, n = 1, validate_items_compatibility = true) ⇒ Object

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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/invoice.rb', line 332

def create_item(item_number, n=1, validate_items_compatibility=true)    
  #
  deserve_trial = self.deserve_trial?
  
  # busco el primer item de esta factura, si es que existe
  item0 = self.items.sort_by {|obj| obj.create_time}.first
  
  # numero de facturas previas a esta factura
  subs = BlackStack::PayPalSubscription.where(:subscr_id=>self.subscr_id).first
  nSubscriptionInvoices = 0 if subs.nil?
  nSubscriptionInvoices = subs.invoices.count if !subs.nil?
  
  # encuentro el descriptor del plan
  # el descriptor del plan tambien es necesario para la llamada a paypal_link 
  h = self.client.plans.select { |j| j[:item_number].to_s == item_number.to_s }.first
  
  # mapeo variables
  c = self.client
  amount = 0.to_f
  unit_price = 0.to_f
  units = 0.to_i
  
  # decido si se trata de una suscripcion    
  isSubscription = false
  isSubscription = true if h[:type] == "S"
  
  # le seteo la fecha de hoy
  self.billing_period_from = now()
  
  # si es una suscripcion, y
  # si el plan tiene un primer trial, y
  # es primer pago de esta suscripcion, entonces:
  # => se trata del primer pago por trial de esta suscripcion
  if (isSubscription && h[:trial_fee] != nil && deserve_trial)
    units = h[:trial_credits].to_i
    unit_price = h[:trial_fee].to_f / h[:trial_credits].to_f
    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
  
  # si es una suscripcion, y
  # si el plan tiene un segundo trial, y
  # es segundo pago de esta suscripcion, entonces:
  # => se trata del segundo pago por trial de esta suscripcion
  elsif (isSubscription && h[:trial2_fee] != nil && nSubscriptionInvoices == 1)
    units = h[:trial2_credits].to_i
    unit_price = h[:trial2_fee].to_f / h[:trial2_credits].to_f
    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
  
  # si el plan tiene un fee, y
  elsif (isSubscription && h[:fee].to_f != nil)
    units = n.to_i * h[:credits].to_i
    unit_price = h[:fee].to_f / h[:credits].to_f
    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
  
  elsif (!isSubscription && h[:fee].to_f != nil)
    units = n.to_i * h[:credits].to_i
    unit_price = h[:fee].to_f / h[:credits].to_f
    billing_period_to = billing_period_from
  
  else # se hace un prorrateo
    raise "Plan is specified wrong"
            
  end
  
  #
  amount = units.to_f * unit_price.to_f
  
  # valido si puedo agregar este item
  self.check_create_item(item_number, validate_items_compatibility, amount)
  
  # cuardo la factura en la base de datos
  self.billing_period_to = billing_period_to
  self.save()
  
  # creo el item por el producto LGB2
  item1 = BlackStack::InvoiceItem.new()
  item1.id = guid()
  item1.id_invoice = self.id
  item1.product_code = h[:product_code]
  item1.unit_price = unit_price.to_f
  item1.units = units.to_i
  item1.amount = amount.to_f
  item1.item_number = h[:item_number]
  item1.description = h[:name]   
  item1.detail = plan_payment_description(h)
  
  #
  return item1
end

#dateDescObject



86
87
88
# File 'lib/invoice.rb', line 86

def dateDesc()
  self.create_time.strftime('%b %d, %Y')
end

#deserve_trial?Boolean

Returns:

  • (Boolean)


60
61
62
# File 'lib/invoice.rb', line 60

def deserve_trial?
  return self.disabled_for_trial_ssm == false || self.disabled_for_trial_ssm == nil
end

#dueDateDescObject



91
92
93
# File 'lib/invoice.rb', line 91

def dueDateDesc()
  Date.strptime(self.billing_period_from.to_s, '%Y-%m-%d').strftime('%b %d, %Y')
end

#getPaidObject

cambia el estado de la factura de UNPAID a PAID verifica que el estado de la factura sea NULL o UNPAID crea los registros contables por el pago de esta factura



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/invoice.rb', line 275

def getPaid()
  if self.canBePaid? == false
    raise "Method BlackStack::Invoice::getPaid requires the current status is nil or unpaid."
  end
  # marco la factura como pagada
  self.status = BlackStack::Invoice::STATUS_PAID
  self.save
  # registro los asientos contables
  InvoiceItem.where(:id_invoice=>self.id).all { |item|      
    BlackStack::Movement.new().parse(item, BlackStack::Movement::MOVEMENT_TYPE_ADD_PAYMENT, "Invoice Payment").save()
    #
    DB.disconnect
    GC.start
  }
end

#next(i) ⇒ Object

actualiza el registro en la tabla invoice como las siguiente factura. en este caso la factura se genera antes del pago. crea uno o mas registros en la tabla invoice_item.



452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'lib/invoice.rb', line 452

def next(i)     
  b = i.buffer_paypal_notification
  if b == nil
    raise "Method BlackStack::Invoice::next requires the previous invoice (i) is linked to a record in the table buffer_paypal_notification."
  end
  
  id_client = i.id_client
  c = BlackStack::Client.where(:id=>id_client).first
  if c==nil
    raise "Client not found"
  end
  
  h = BlackStack::InvoicingPaymentsProcessing::plans_descriptor.select { |obj| obj[:item_number].to_s == i.items.first.item_number.to_s }.first
  if h==nil
    raise "Plan not found"
  end
  
  return_path = h[:return_path]
  
  if (self.id == nil)
    self.id = guid()
  end
  self.create_time = now()
  self.id_client = c.id
  self.id_buffer_paypal_notification = nil
  self.subscr_id = b.subscr_id
  self.disabled_for_add_remove_items = true
  
  i.items.each { |t| 
    self.add_item(t.item_number)
    #
    DB.disconnect
    GC.start 
  }
          
  
  self.billing_period_from = i.billing_period_to
  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
  
  self.paypal_url = self.paypal_link
  self.paypal_url = nil if h[:type] == "S" # si se trata de una suscripcion, entonces esta factura se pagara automaticamente
  self.automatic_billing = 1 if h[:type] == "S" # si se trata de una suscripcion, entonces esta factura se pagara automaticamente
  self.save
end

#notificationBodyObject



75
76
77
78
# File 'lib/invoice.rb', line 75

def notificationBody()
  "<p>We received your payment for a total of $#{("%.2f" % self.total()).to_s}.</p>" +
  "<p>You can find your invoice <a href='#{CS_HOME_PAGE}/member/invoice?iid=#{self.id.to_guid}'><b>here</b></a></p>"
end

#notificationSubjectObject

envia un email transaccional con informacion de facturacion, y pasos a seguir despues del pago



70
71
72
# File 'lib/invoice.rb', line 70

def notificationSubject()
  "We Received Your Payment"
end

#numberObject



81
82
83
# File 'lib/invoice.rb', line 81

def number()
  self.id.to_guid
end

#parse(b) ⇒ Object

actualiza el registro en la tabla invoice segun un registro en la tabla buffer_paypal_notification. en este caso la factura se genera despues del pago. crea uno o mas registros en la tabla invoice_item.



500
501
502
503
504
505
506
507
508
509
510
# File 'lib/invoice.rb', line 500

def parse(b)
  item_number = b.item_number
  payment_gross = b.payment_gross.to_f
  billing_from = b.create_time
  #isSubscription = b.isSubscription?
  c = b.get_client
  if (c==nil)
    raise "Client not found"
  end
  self.setup(item_number, c.id, payment_gross, billing_from)
end

TODO: refactor me



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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/invoice.rb', line 123

def paypal_link()
  item = self.items.first
  if item.nil?
    raise "Invoice has no items"      
  end
  
  plan_descriptor = self.client.plans.select { |j| j[:item_number].to_s == item.item_number.to_s }.first
  if plan_descriptor.nil?
    raise "Plan not found"      
  end
  
  product_descriptor = BlackStack::InvoicingPaymentsProcessing::products_descriptor.select { |j| j[:code].to_s == plan_descriptor[:product_code].to_s }.first
  if product_descriptor.nil?
    raise "Product not found"      
  end
  
  item_name = ""
  n = 0
  self.items.each { |t|
    h = BlackStack::InvoicingPaymentsProcessing::plans_descriptor.select { |obj| obj[:item_number].to_s == t.item_number.to_s }.first 
    item_name += '; ' if n>0
    item_name += h[:name]
    if item_name.size >= 65
      item_name += "..."
      break
    end
    n+=1
  }
  
  return_path = product_descriptor[:return_path]
  id_invoice = self.id
  id_client = self.client.id
  allow_trials = self.deserve_trial?
  
  bIsSubscription = false
  bIsSubscription = true if plan_descriptor[:type]=="S"
  
  # generating the invoice number
  invoice_id = "#{id_client.to_guid}.#{id_invoice.to_guid}"

  values = {}
  
  # common parameters for all the situations
  values[:business] = BlackStack::InvoicingPaymentsProcessing::paypal_business_email
  values[:lc] = "en_US"
  
  if bIsSubscription
    values[:cmd] = "_xclick-subscriptions"
  else
    values[:cmd] = "_xclick"
  end
  
  values[:upload] = 1
  values[:no_shipping] = 1
  values[:return] = BlackStack::Netting::add_param(return_path, "track_object_id", id_invoice.to_guid)
  values[:return_url] = BlackStack::Netting::add_param(return_path, "track_object_id", id_invoice.to_guid)
  values[:rm] = 1
  values[:notify_url] = BlackStack::InvoicingPaymentsProcessing::paypal_ipn_listener
  
  values[:invoice] = id_invoice
  values[:item_name] = item_name
  values[:item_number] = id_invoice.to_s
  values[:src] = '1'

  # si es una suscripcion
  if (bIsSubscription)

    trial1 = allow_trials && plan_descriptor[:trial_fee]!=nil && plan_descriptor[:trial_period]!=nil && plan_descriptor[:trial_units]!=nil
    trial2 = allow_trials && plan_descriptor[:trial2_fee]!=nil && plan_descriptor[:trial2_period]!=nil && plan_descriptor[:trial2_units]!=nil
    
    values[:a3] = 0
    self.items.each { |i|         
      if trial1 && i.units!=i.plan_descriptor[:trial_credits]
        raise 'Cannot order more than 1 package and trial in the same invoice'
      elsif trial1
        values[:a3] += i.plan_descriptor[:fee].to_f
      else # !trial1
        values[:a3] += ( i.units.to_f * ( i.plan_descriptor[:fee].to_f / i.plan_descriptor[:credits].to_f ).to_f )
      end 
    }
    values[:p3] = plan_descriptor[:units] # every 1
    values[:t3] = plan_descriptor[:period] # per month

    # si tiene un primer periodo de prueba
    if trial1
      values[:a1] = 0 # $1 fee
      self.items.each { |i| values[:a1] += i.plan_descriptor[:trial_fee].to_i }
      values[:p1] = plan_descriptor[:trial_units] # 15
      values[:t1] = plan_descriptor[:trial_period] # days

      # si tiene un segundo periodo de prueba
      if trial2
        values[:a2] = 0 # $50 fee
        self.items.each { |i| values[:a2] += i.plan_descriptor[:trial2_fee].to_i }
        values[:p2] = plan_descriptor[:trial2_units] # first 1
        values[:t2] = plan_descriptor[:trial2_period] # month
      end
    end

  # sino, entonces es un pago por unica vez
  else
    values[:amount] = 0
    self.items.each { |i| values[:amount] += i.amount.to_f }         
  end
    
  # return url
  "#{BlackStack::InvoicingPaymentsProcessing::PAYPAL_ORDERS_URL}/cgi-bin/webscr?" + URI.encode_www_form(values)
end

#plan_payment_description(h) ⇒ Object



421
422
423
424
425
426
427
428
429
# File 'lib/invoice.rb', line 421

def plan_payment_description(h)
  ret = ""
  ret += "$#{h[:trial_fee]} trial. " if self.deserve_trial? && !h[:trial_fee].nil?
  ret += "$#{h[:trial2_fee]} one-time price. " if self.deserve_trial? && !h[:trial2_fee].nil?
  ret += "$#{h[:fee]}/#{h[:period]}. " if h[:units].to_i <= 1 
  ret += "$#{h[:fee]}/#{h[:units]}#{h[:period]}. " if h[:units].to_i > 1 
  ret += "#{h[:credits]} credits. " if h[:credits].to_i > 1
  ret
end

#remove_item(item_id) ⇒ Object

add_item



444
445
446
447
# File 'lib/invoice.rb', line 444

def remove_item(item_id)
  DB.execute("DELETE invoice_item WHERE id_invoice='#{self.id}' AND [id]='#{item_id}'")
  self.setup
end

#setupObject

actualiza el registro en la tabla invoice segun los parametros. en este caso la factura se genera antes del pago. genera el enlace de paypal.



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/invoice.rb', line 240

def setup()
  # busco el primer item de esta factura
  item = self.items.sort_by {|obj| obj.create_time}.first
  if item == nil
    raise "Invoice has no items."      
  end
  
  h = self.client.plans.select { |j| j[:item_number].to_s == item.item_number.to_s }.first
  if h == nil
    raise "Unknown item_number."
  end 
  
  # 
  return_path = h[:return_path]
  
  c = BlackStack::Client.where(:id=>id_client).first
  if (c==nil)
    raise "Client not found"
  end
  
  if (self.id == nil)
    self.id = guid()
  end
  self.create_time = now()
  self.id_client = c.id
  self.id_buffer_paypal_notification = nil
  self.paypal_url = self.paypal_link
  
  #
  self.save()
end

#setup_refund(payment_gross, id_invoice) ⇒ Object

Genera lis items de la factura de reembolso. payment_gross: es el tital reembolsado id_invoice: es el ID de la factura que se está reembolsando

Si el monto del reembolso es mayor al total de la factua, entocnes se levanta una excepcion

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. Si existen mas de un item con igual importe que el reembolso, entonces se levanta una excepcion.

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. Si la factura tiene un solo item, entonces se calcula un reembolso parcial. Sino, entonces se levanta una excepcion.

TODO: Hacerlo transaccional



526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
# File 'lib/invoice.rb', line 526

def setup_refund(payment_gross, id_invoice)
  # cargo la factura
  i = BlackStack::Invoice.where(:id=>id_invoice).first    
  raise "Invoice not found (#{id_invoice})" if i.nil?
  
  # obtengo el total de la factura
  total = i.total.to_f
  
  # 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.
  # Si existen mas de un item con igual importe que el reembolso, entonces se levanta una excepcion.
  matched_items = i.items.select { |o| o.amount.to_f == -payment_gross.to_f }
  
  if total < -payment_gross
    raise "The refund is higher than the invoice amount (invoice #{id_invoice}, #{total.to_s}, #{payment_gross.to_s})"

  # 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.
  # Si el monto de la factura es distinto al moneto del reembolso, entonces se levanta una excepcion.
  elsif total == -payment_gross
    i.items.each { |u|
      h = BlackStack::InvoicingPaymentsProcessing::plans_descriptor.select { |obj| obj[:item_number] == u.item_number }.first
      raise "Plan not found" if h.nil?
      item1 = BlackStack::InvoiceItem.new()
      item1.id = guid()
      item1.id_invoice = self.id
      item1.unit_price = u.unit_price.to_f
      item1.units = -u.units
      item1.amount = -u.amount.to_f
      item1.product_code = u.product_code.to_s
      item1.item_number = u.item_number.to_s
      item1.detail = u.detail.to_s
      item1.description = u.description.to_s
      item1.save()
      BlackStack::Movement.new().parse(item1, BlackStack::Movement::MOVEMENT_TYPE_REFUND_BALANCE).save()        
      # release resources
      DB.disconnect
      GC.start
    }  
=begin
  # si existe al menos un item que coincida el total con el monto del reembolso, 
  # entonces selecciono el primero 
  elsif matched_items.size > 0
    t = matched_items.first
    h = BlackStack::InvoicingPaymentsProcessing::plans_descriptor.select { |obj| obj[:item_number] == t.item_number }.first
    raise "Plan not found" if h.nil?
    item1 = BlackStack::InvoiceItem.new()
    item1.id = guid()
    item1.id_invoice = self.id
    item1.unit_price = t.unit_price.to_f
    item1.units = -t.units
    item1.amount = -t.amount.to_f
    item1.product_code = t.product_code.to_s
    item1.item_number = t.item_number.to_s
    item1.detail = t.detail.to_s
    item1.description = t.description.to_s
    item1.save()
    BlackStack::Movement.new().parse(item1, BlackStack::Movement::MOVEMENT_TYPE_REFUND_BALANCE).save()
=end
  # reembolso parcial de una factura con un unico item
  elsif i.items.size == 1
    t = i.items.first
  
    amount = -payment_gross.to_f
    unit_price = t.amount.to_f / t.units.to_f
    units = (amount / unit_price.to_f).round.to_i
  
    h = BlackStack::InvoicingPaymentsProcessing::plans_descriptor.select { |obj| obj[:item_number] == t.item_number }.first
    raise "Plan not found" if h.nil?
    item1 = BlackStack::InvoiceItem.new()
    item1.id = guid()
    item1.id_invoice = self.id
    item1.unit_price = unit_price.to_f
    item1.units = -units
    item1.amount = -amount.to_f
    item1.product_code = t.product_code.to_s
    item1.item_number = t.item_number.to_s
    item1.detail = t.detail.to_s
    item1.description = t.description.to_s
    item1.save()
    BlackStack::Movement.new().parse(item1, BlackStack::Movement::MOVEMENT_TYPE_REFUND_BALANCE).save()
  
  else
    raise "Refund amount is not matching with the invoice total (#{total.to_s}) and the invoice has more than 1 item."

  end
  
  # release resources
  DB.disconnect
  GC.start
end

#totalObject



106
107
108
109
110
111
112
113
114
115
# File 'lib/invoice.rb', line 106

def total()
  ret = 0
  self.items.each { |item|
    ret += item.amount.to_f
    # libero recursos
    DB.disconnect
    GC.start
  }
  ret
end

#totalDescObject



118
119
120
# File 'lib/invoice.rb', line 118

def totalDesc()
  ("%.2f" % self.total.to_s)
end