invoicing_payments_processing 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 34bca7e013f48b4303f62310d10af93adb0150d5
4
+ data.tar.gz: d5be57777768cf864a273d34c87350fc7b48e124
5
+ SHA512:
6
+ metadata.gz: dc65a5b539e09a60dbdcc71ebf4cc1ff4c718f8235bbd6c7fe989b34fbd3daf4e32bd5ef9b053cf842e4e75855295d8185c5da1b8e86052c0a18a7fd915d84fd
7
+ data.tar.gz: d71ff8365ca5a3142faee45b8da1de7a11b6c0a42b1169e7c493b9b38cd21e6b167676dc99aa2a499670aa03bfa615fd1c9b98af0b583218076ff9c720ec04d2
@@ -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.to_hash)
335
+ if s.nil?
336
+ # mensaje de error
337
+ raise "Subscription (#{b.subscr_id}) 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