invoicing_payments_processing 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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