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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c2495e0d2257ff3cec7862dad5c29aa0ef6f374e
4
+ data.tar.gz: 684caeb185db9734c8c0977ebceed61b88d3465a
5
+ SHA512:
6
+ metadata.gz: 89f4f7b34728ae6b95871ee58baf22dee4cf83827797cad575beab80ad46f4808f89be79b845006421e9c31ec51d29e388e8cb60efda0a4e403e67c606ca8559
7
+ data.tar.gz: b42b1696f7a099a713545ab2b2075dbf0f782336a3218a94f3c3312249e71c6cca4e7952eb6219737d809865be6740f723cef6c351d45be836b491a9637abcb8
@@ -0,0 +1,27 @@
1
+ module BlackStack
2
+ class Balance
3
+ attr_accessor :client, :product_code, :amount, :credits
4
+
5
+ def initialize(id_client, product_code)
6
+ self.client = BlackStack::Client.where(:id => id_client).first
7
+ self.product_code = product_code
8
+ self.calculate()
9
+ end
10
+
11
+ def calculate()
12
+ q =
13
+ "select cast(sum(cast(amount as numeric(18,12))) as numeric(18,6)) as amount, sum(credits) as credits " +
14
+ "from movement with (nolock index(IX_movement__id_client__product_code)) " +
15
+ "where id_client='#{self.client.id}' " +
16
+ "and product_code='#{self.product_code}' "
17
+ row = DB[q].first
18
+ self.amount = row[:amount].to_f
19
+ self.credits = row[:credits].to_f
20
+ # libero recursos
21
+ DB.disconnect
22
+ GC.start
23
+ end
24
+ end
25
+ end # module BlackStack
26
+
27
+
@@ -0,0 +1,391 @@
1
+ module BlackStack
2
+ class BufferPayPalNotification < Sequel::Model(:buffer_paypal_notification)
3
+ BlackStack::BufferPayPalNotification.dataset = BlackStack::BufferPayPalNotification.dataset.disable_insert_output
4
+ self.dataset = self.dataset.disable_insert_output
5
+
6
+ TXN_STATUS_CENCELED_REVERSAL = "Canceled_Reversal" # reembolso hecho por PayPal, luego de una disputa
7
+ TXN_STATUS_COMPLETED = "Completed"
8
+ TXN_STATUS_FAILED = "Failed"
9
+ TXN_STATUS_PENDING = "Pending"
10
+ TXN_STATUS_REFUNDED = "Refunded"
11
+ TXN_STATUS_REVERSED = "Reversed"
12
+
13
+ TXN_TYPE_SUBSCRIPTION_SIGNUP = "subscr_signup"
14
+ TXN_TYPE_SUBSCRIPTION_PAYMENT = "subscr_payment"
15
+ TXN_TYPE_SUBSCRIPTION_CANCEL = "subscr_cancel"
16
+ TXN_TYPE_SUBSCRIPTION_FAILED = "subscr_failed"
17
+ TXN_TYPE_SUBSCRIPTION_REFUND = ""
18
+ TXN_TYPE_SUBSCRIPTION_MODIFY = "subscr_modify"
19
+ TXN_TYPE_SUBSCRIPTION_SUSPEND = "recurring_payment_suspended"
20
+ TXN_TYPE_SUBSCRIPTION_SUSPEND_DUE_MAX_FAILURES = "recurring_payment_suspended_due_to_max_failed_payment"
21
+ TXN_TYPE_WEB_ACCEPT = "web_accept" # one time payment
22
+ # TXN_TYPE_SUBSCRIPTION_EOT = "subscr_eot" # no sabemos para que es esto
23
+
24
+ PAYMENT_STATUS_COMPLETED = "Completed"
25
+
26
+ LOCKING_FILENAME = "./accounting.bufferpaypalnotification.lock"
27
+ LOGGING_FILENAME = "./accounting.bufferpaypalnotification.log"
28
+ @@fd = File.open(LOCKING_FILENAME,"w")
29
+
30
+ def isSubscription?
31
+ self.txn_type =~ /^subscr_/
32
+ end
33
+
34
+ def self.revenue()
35
+ DB["SELECT ISNULL(SUM(CAST(payment_gross AS NUMERIC(18,4))),0) AS revenue FROM buffer_paypal_notification WITH (NOLOCK) WHERE txn_type='#{BlackStack::BufferPayPalNotification::TXN_TYPE_SUBSCRIPTION_PAYMENT}'"].first[:revenue]
36
+ end
37
+
38
+ # busca al cliente relacionado con este pago, de tres formas:
39
+ # 1) haciendo coincidir el campo client.paypal_email con el BlackStack::BufferPayPalNotification.payer_email
40
+ # 2) haciendo coincidir el campo user.email con el BlackStack::BufferPayPalNotification.payer_email
41
+ # 3) haciendo coincidir el campo payer_email de alguna suscripcion existente con el BlackStack::BufferPayPalNotification.payer_email
42
+ # 3) haciendo coincidir el primer guid en el codigo de invoice, con el id del cliente
43
+ def get_client()
44
+ # obtengo el cliente que machea con este perfil
45
+ c = nil
46
+ if (c == nil)
47
+ cid = self.invoice.split(".").first.to_s
48
+ if cid.guid?
49
+ c = BlackStack::Client.where(:id=>cid).first
50
+ end
51
+ end
52
+ if (c == nil)
53
+ c = BlackStack::Client.where(:paypal_email=>self.payer_email).first
54
+ if (c == nil)
55
+ u = User.where(:email=>self.payer_email).first
56
+ if (u!=nil)
57
+ c = u.client
58
+ end
59
+ end
60
+ end
61
+ if (c == nil)
62
+ s = BlackStack::PayPalSubscription.where(:payer_email=>self.payer_email).first
63
+ if (s!=nil)
64
+ c = s.client
65
+ end
66
+ end
67
+ if (c == nil)
68
+ # obtengo el cliente - poco elegante
69
+ q =
70
+ "SELECT TOP 1 i.id_client AS cid " +
71
+ "FROM buffer_paypal_notification b WITH (NOLOCK) " +
72
+ "JOIN invoice i WITH (NOLOCK) ON b.id=i.id_buffer_paypal_notification " +
73
+ "WHERE b.id<>'#{self.id}' AND b.payer_id = '#{self.payer_id}' "
74
+ row = DB[q].first
75
+ if (row!=nil)
76
+ # obtengo el cliente al que corresponde este IPN
77
+ c = BlackStack::Client.where(:id=>row[:cid]).first
78
+ end
79
+ end
80
+ c
81
+ end
82
+
83
+ def self.lock()
84
+ @@fd.flock(File::LOCK_EX)
85
+ end
86
+
87
+ def self.release()
88
+ @@fd.flock(File::LOCK_UN)
89
+ end
90
+
91
+
92
+ # -----------------------------------------------------------------------------------------
93
+ # Factory
94
+ # -----------------------------------------------------------------------------------------
95
+
96
+ def self.load(params)
97
+ BlackStack::BufferPayPalNotification.where(
98
+ :verify_sign=>params['verify_sign'],
99
+ :txn_type=>params['txn_type'],
100
+ :ipn_track_id=>params['ipn_track_id']
101
+ ).first
102
+ end
103
+
104
+ # crea un nuevo objeto BufferPayPalNotification, y le mapea los atributos en el hash params.
105
+ # no guarda el objeto en la base de datos.
106
+ # retorna el objeto creado.
107
+ def self.create(params)
108
+ b = BlackStack::BufferPayPalNotification.new()
109
+ b.txn_type = params['txn_type'].to_s
110
+ b.subscr_id = params['subscr_id'].to_s
111
+ b.last_name = params['last_name'].to_s
112
+ b.residence_country = params['residence_country'].to_s
113
+ b.mc_currency = params['mc_currency'].to_s
114
+ b.item_name = params['item_name'].to_s
115
+ b.amount1 = params['amount1'].to_s
116
+ b.business = params['business'].to_s
117
+ b.amount3 = params['amount3'].to_s
118
+ b.recurring = params['recurring'].to_s
119
+ b.verify_sign = params['verify_sign'].to_s
120
+ b.payer_status = params['payer_status'].to_s
121
+ b.test_ipn = params['test_ipn'].to_s
122
+ b.payer_email = params['payer_email'].to_s
123
+ b.first_name = params['first_name'].to_s
124
+ b.receiver_email = params['receiver_email'].to_s
125
+ b.payer_id = params['payer_id'].to_s
126
+ b.invoice = params['invoice'].to_s
127
+ b.reattempt = params['reattempt'].to_s
128
+ b.item_number = params['item_number'].to_s
129
+ b.subscr_date = params['subscr_date'].to_s
130
+ b.charset = params['charset'].to_s
131
+ b.notify_version = params['notify_version'].to_s
132
+ b.period1 = params['period1'].to_s
133
+ b.mc_amount1 = params['mc_amount1'].to_s
134
+ b.period3 = params['period3'].to_s
135
+ b.mc_amount3 = params['mc_amount3'].to_s
136
+ b.ipn_track_id = params['ipn_track_id'].to_s
137
+ b.transaction_subject = params['transaction_subject'].to_s
138
+ b.payment_date = params['payment_date'].to_s
139
+ b.payment_gross = params['payment_gross'].to_s
140
+ b.payment_type = params['payment_type'].to_s
141
+ b.txn_id = params['txn_id'].to_s
142
+ b.receiver_id = params['receiver_id'].to_s
143
+ b.payment_status = params['payment_status'].to_s
144
+ b.payment_fee = params['payment_fee'].to_s
145
+ b
146
+ end
147
+
148
+ # segun los atributos en el hash params, obtiene el objeto BufferPayPalNotification de a base de datos.
149
+ # si el objeto no existe, entonces crea un nuevo registro en la base de datos.
150
+ #
151
+ # este metodo es invocado desde el access point que recive las notificaciones de PayPal.
152
+ # por lo tanto, ejecuta mecanismos de bloqueo para manejar la concurrencia.
153
+ #
154
+ # retorna el objeto creado o cargado de la base de datos.
155
+ def self.parse(params)
156
+ begin
157
+ # Levantar el flag de reserva a mi favor
158
+ self.lock()
159
+
160
+ # escribo la notificacion cruda en un archivo de log, en caso que falle el mapeo a la base de datos por error del programador
161
+ File.open(LOGGING_FILENAME, 'a') { |file| file.puts(params.to_s) }
162
+
163
+ # si la notificacion no existe en la base de datos, la inserto
164
+ # si la notificacion ya existe entonces la actualizo, porque puede tratarse de un pago que no se habia podido completar (payment_status=='Completed')
165
+ b = BlackStack::BufferPayPalNotification.load(params)
166
+ if (b==nil)
167
+ b = self.create(params)
168
+ b.id = guid
169
+ b.create_time = now
170
+ b.save
171
+ end
172
+
173
+ # desbloquear
174
+ self.release()
175
+
176
+ return b
177
+ rescue => e
178
+ # ante cualquier falla, lo primero es desbloquear
179
+ self.release()
180
+ raise e
181
+ end
182
+ end # self.parse
183
+
184
+
185
+ def to_hash()
186
+ ret = {}
187
+ ret['id'] = self.id
188
+ ret['create_time'] = self.create_time.to_api
189
+ ret['txn_type'] = self.txn_type
190
+ ret['subscr_id'] = self.subscr_id
191
+ ret['last_name'] = self.last_name
192
+ ret['residence_country'] = self.residence_country
193
+ ret['mc_currency'] = self.mc_currency
194
+ ret['item_name'] = self.item_name
195
+ ret['amount1'] = self.amount1
196
+ ret['business'] = self.business
197
+ ret['amount3'] = self.amount3
198
+ ret['recurring'] = self.recurring
199
+ ret['verify_sign'] = self.verify_sign
200
+ ret['payer_status'] = self.payer_status
201
+ ret['test_ipn'] = self.test_ipn
202
+ ret['payer_email'] = self.payer_email
203
+ ret['first_name'] = self.first_name
204
+ ret['receiver_email'] = self.receiver_email
205
+ ret['payer_id'] = self.payer_id
206
+ ret['invoice'] = self.invoice
207
+ ret['reattempt'] = self.reattempt
208
+ ret['item_number'] = self.item_number
209
+ ret['subscr_date'] = self.subscr_date
210
+ ret['charset'] = self.charset
211
+ ret['notify_version'] = self.notify_version
212
+ ret['period1'] = self.period1
213
+ ret['mc_amount1'] = self.mc_amount1
214
+ ret['period3'] = self.period3
215
+ ret['mc_amount3'] = self.mc_amount3
216
+ ret['ipn_track_id'] = self.ipn_track_id
217
+ ret['transaction_subject'] = self.transaction_subject
218
+ ret['payment_date'] = self.payment_date
219
+ ret['payment_gross'] = self.payment_gross
220
+ ret['payment_type'] = self.payment_type
221
+ ret['txn_id'] = self.txn_id
222
+ ret['receiver_id'] = self.receiver_id
223
+ ret['payment_status'] = self.payment_status
224
+ ret['payment_fee'] = self.payment_fee
225
+ ret
226
+ end
227
+
228
+ # salda facturas
229
+ # genera nuevas facturas para pagos futuros
230
+ def self.process(params)
231
+ DB.transaction do
232
+ # verifico que no existe ya una notificacion
233
+ b = BlackStack::BufferPayPalNotification.where(:id=>params[:id]).first
234
+ if (b==nil)
235
+ # proceso la notificacion
236
+ b = BlackStack::BufferPayPalNotification.create(params)
237
+ # inserto la notificacion en la base de datos
238
+ b.id = params['id']
239
+ b.create_time = params['create_time'].api_to_sql_datetime
240
+ b.save
241
+ end
242
+
243
+ if !BlackStack::Invoice.where(:id_buffer_paypal_notification=>b.id).first.nil?
244
+ raise "IPN already linked to an invoice."
245
+ end
246
+
247
+ # varifico que el cliente exista
248
+ c = b.get_client
249
+ if (c == nil)
250
+ raise "Client not found (payer_email=#{b.payer_email.to_s})."
251
+ end
252
+
253
+ # parseo en nuemero de factura formado por id_client.id_invoice
254
+ cid = c.id.to_guid
255
+ iid = b.invoice.split(/\./).last # NOTA: el campo invoice tiene formato "cid.iid", pero originalmente solo tenia el formato "iid"
256
+
257
+ # si es un pago por un primer trial, sengundo trial o pago recurrente de suscripcion,
258
+ # entonces registro la factura, activo el rol de este cliente (deprecated), se agrega el cliente a la lista de emails (deprecated)
259
+ if ( ( b.txn_type == BlackStack::BufferPayPalNotification::TXN_TYPE_WEB_ACCEPT || b.txn_type == BlackStack::BufferPayPalNotification::TXN_TYPE_SUBSCRIPTION_PAYMENT || b.txn_type == BufferPayPalNotification::TXN_STATUS_COMPLETED ) && b.payment_status == 'Completed' )
260
+ # reviso si la factura ya estaba creada.
261
+ # la primer factura se por adelantado cuando el cliente hace signup, y antes de que se suscriba a paypal, y se le pone un ID igual a primer GUID del campo invoice del IPN
262
+ i = BlackStack::Invoice.where(:id=>iid, :status=>BlackStack::Invoice::STATUS_UNPAID).order(:billing_period_from).first
263
+ if (i == nil)
264
+ i = BlackStack::Invoice.where(:id=>iid, :status=>nil).order(:billing_period_from).first
265
+ end
266
+
267
+ # de la segunda factura en adelante, se generan con un ID diferente, pero se le guarda a subscr_id para identificar cuado llegue el pago de esa factura
268
+ if (i == nil)
269
+ # busco una factura en estado UNPAID que este vinculada a esta suscripcion
270
+ q =
271
+ "SELECT TOP 1 i.id AS iid " +
272
+ "FROM invoice i WITH (NOLOCK) " +
273
+ "WHERE i.id_client='#{cid}' " +
274
+ "AND ISNULL(i.status,#{BlackStack::Invoice::STATUS_UNPAID})=#{Invoice::STATUS_UNPAID} " +
275
+ "AND i.subscr_id = '#{b.subscr_id}' " +
276
+ "ORDER BY i.billing_period_from ASC "
277
+ row = DB[q].first
278
+ if row != nil
279
+ i = BlackStack::Invoice.where(:id=>row[:iid]).first
280
+ end
281
+ end
282
+
283
+ # valido haber encontrado la factura
284
+ raise "Invoice not found" if i.nil?
285
+
286
+ # valido que el importe de la factura sea igual al importe del IPN
287
+ raise "Invoice amount is not the equal to the amount of the IPN (#{i.total.to_s}!=#{b.payment_gross.to_s})" if i.total.to_f != b.payment_gross.to_f
288
+
289
+ # le asigno el id_buffer_paypal_notification
290
+ i.id_buffer_paypal_notification = b.id
291
+ i.save
292
+
293
+ # marco la factura como pagada
294
+ # registro contable - bookkeeping
295
+ i.getPaid() if i.canBePaid?
296
+
297
+ # crea una factura para el periodo siguiente (dia, semana, mes, anio)
298
+ j = BlackStack::Invoice.new()
299
+ j.id = guid()
300
+ j.id_client = c.id
301
+ j.create_time = now()
302
+ j.disabled_for_trial_ssm = c.disabled_for_trial_ssm
303
+ j.save()
304
+
305
+ # genero los datos de esta factura, como la siguiente factura a la que estoy pagando en este momento
306
+ j.next(i)
307
+
308
+ # creo el milestone con todo el credito pendiente que tiene esta subscripcion
309
+ buff_payment = i.buffer_paypal_notification
310
+ buff_signup = BlackStack::BufferPayPalNotification.where(:txn_type=>"subscr_signup", :subscr_id=>buff_payment.subscr_id).first
311
+ subs = buff_signup == nil ? nil : BlackStack::PayPalSubscription.where(:id_buffer_paypal_notification => buff_signup.id).first
312
+
313
+ elsif (b.txn_type == BlackStack::BufferPayPalNotification::TXN_TYPE_SUBSCRIPTION_SIGNUP)
314
+ # crear un registro en la tabla paypal_subscriptions
315
+ if BlackStack::PayPalSubscription.load(b.to_hash) != nil
316
+ # mensaje de error
317
+ raise 'Subscription Already Exists.'
318
+ else
319
+ # registro la suscripcion en la base de datos
320
+ s = BlackStack::PayPalSubscription.create(b.to_hash)
321
+ s.id = guid
322
+ s.id_buffer_paypal_notification = b.id
323
+ s.create_time = now
324
+ s.id_client = c.id
325
+ s.active = true
326
+ s.save
327
+ end
328
+
329
+ elsif (
330
+ b.txn_type == BlackStack::BufferPayPalNotification::TXN_TYPE_SUBSCRIPTION_CANCEL #||
331
+ #b.txn_type == BlackStack::BufferPayPalNotification::TXN_TYPE_SUBSCRIPTION_SUSPEND || # estos IPN traen el campo subscr_id en blanco
332
+ #b.txn_type == BlackStack::BufferPayPalNotification::TXN_TYPE_SUBSCRIPTION_SUSPEND_DUE_MAX_FAILURES # estos IPN traen el campo subscr_id en blanco
333
+ )
334
+ s = BlackStack::PayPalSubscription.load(b)
335
+ if s == nil
336
+ # mensaje de error
337
+ raise 'Subscription Not Found.'
338
+ else
339
+ # registro la suscripcion en la base de datos
340
+ s.active = false
341
+ s.save
342
+ end
343
+
344
+ elsif (b.txn_type == BlackStack::BufferPayPalNotification::TXN_TYPE_SUBSCRIPTION_FAILED)
345
+ # TODO: actualizar registro en la tabla paypal_subscriptions. notificar al usuario
346
+
347
+ elsif (b.txn_type == BlackStack::BufferPayPalNotification::TXN_TYPE_SUBSCRIPTION_MODIFY)
348
+ # TODO: ?
349
+
350
+ elsif (b.txn_type.to_s == BlackStack::BufferPayPalNotification::TXN_TYPE_SUBSCRIPTION_REFUND)
351
+ # PROBLEMA: Los IPN no dicen de a qué pago corresponde el reembolso
352
+
353
+ payment_gross = 0
354
+ if b.payment_status == BlackStack::BufferPayPalNotification::TXN_STATUS_CENCELED_REVERSAL
355
+ payment_gross = 0 - b.payment_gross.to_f - b.payment_fee.to_f
356
+ elsif b.payment_status == BlackStack::BufferPayPalNotification::TXN_STATUS_REFUNDED || b.payment_status == BlackStack::BufferPayPalNotification::TXN_STATUS_REVERSED
357
+ payment_gross = b.payment_gross.to_f # en negativo
358
+ end
359
+ if payment_gross < 0
360
+ # verifico que la factura por este IPN no exista
361
+ j = BlackStack::Invoice.where(:id_buffer_paypal_notification=>b.id).first
362
+ if (j!=nil)
363
+ raise 'Invoice already exists.'
364
+ end
365
+
366
+ # creo la factura por el reembolso
367
+ i = BlackStack::Invoice.new()
368
+ i.id = guid()
369
+ i.id_client = c.id
370
+ i.create_time = now()
371
+ i.disabled_for_trial_ssm = c.disabled_for_trial_ssm
372
+ i.id_buffer_paypal_notification = b.id
373
+ i.status = BlackStack::Invoice::STATUS_REFUNDED
374
+ i.billing_period_from = b.create_time
375
+ i.billing_period_to = b.create_time
376
+ i.paypal_url = nil
377
+ i.disabled_for_add_remove_items = true
378
+ i.save()
379
+
380
+ # parseo el reeembolso - creo el registro contable
381
+ i.setup_refund(payment_gross, iid)
382
+
383
+ end
384
+ else
385
+ # unknown
386
+
387
+ end
388
+ end # DB.transaction
389
+ end # def process
390
+ end # class
391
+ end # module BlackStack
@@ -0,0 +1,33 @@
1
+ module BlackStack
2
+ class CustomPlan < Sequel::Model(:custom_plan)
3
+ BlackStack::CustomPlan.dataset = BlackStack::CustomPlan.dataset.disable_insert_output
4
+ self.dataset = self.dataset.disable_insert_output
5
+
6
+ many_to_one :client, :class=>:'BlackStack::Client', :key=>:id_client
7
+
8
+ def to_hash
9
+ h = {}
10
+ h[:type] = self.type # not null
11
+ h[:item_number] = self.item_number # not null
12
+ h[:name] = self.name # not null
13
+ h[:credits] = self.credits # not null
14
+ h[:fee] = self.fee # not null
15
+ h[:period] = self.period # not null
16
+ h[:units] = self.units # not null
17
+
18
+ h[:trial_credits] = self.trial_credits if self.trial_credits != nil
19
+ h[:trial_fee] = self.trial_fee if self.trial_fee != nil
20
+ h[:trial_period] = self.trial_period if self.trial_period != nil
21
+ h[:trial_units] = self.trial_units if self.trial_units != nil
22
+
23
+ h[:trial2_credits] = self.trial2_credits if self.trial2_credits != nil
24
+ h[:trial2_fee] = self.trial2_fee if self.trial2_fee != nil
25
+ h[:trial2_period] = self.trial2_period if self.trial2_period != nil
26
+ h[:trial2_units] = self.trial2_units if self.trial2_units != nil
27
+
28
+ h[:description] = self.description # not null
29
+ h
30
+ end
31
+
32
+ end
33
+ end # module BlackStack