-
Notifications
You must be signed in to change notification settings - Fork 87
/
EEM_Transaction.model.php
495 lines (451 loc) · 18.6 KB
/
EEM_Transaction.model.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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
231
232
233
234
235
236
237
238
239
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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
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
330
331
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
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
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
<?php
use EventEspresso\core\exceptions\InvalidDataTypeException;
use EventEspresso\core\exceptions\InvalidInterfaceException;
use EventEspresso\core\services\loaders\LoaderFactory;
use EventEspresso\core\services\request\RequestInterface;
/**
* Transaction Model
*
* @package Event Espresso
* @subpackage includes/models/
* @author Brent Christensen
* @method EE_Transaction get_one(array $query_params)
* @method EE_Transaction ensure_is_obj($base_class_obj_or_id, $ensure_is_in_db = false)
* @method EE_Transaction[] get_all(array $query_params)
* @method EE_Transaction[] $wpdb::get_results($query, $output)
*/
class EEM_Transaction extends EEM_Base
{
protected static ?EEM_Transaction $_instance = null;
/**
* Status ID(STS_ID on esp_status table) to indicate the transaction is complete,
* but payment is pending. This is the state for transactions where payment is promised
* from an offline gateway.
*/
// const open_status_code = 'TPN';
/**
* Status ID(STS_ID on esp_status table) to indicate the transaction failed,
* either due to a technical reason (server or computer crash during registration),
* or some other reason that prevent the collection of any useful contact information from any of the registrants
*/
const failed_status_code = 'TFL';
/**
* Status ID(STS_ID on esp_status table) to indicate the transaction was abandoned,
* either due to a technical reason (server or computer crash during registration),
* or due to an abandoned cart after registrant chose not to complete the registration process
* HOWEVER...
* an abandoned TXN differs from a failed TXN in that it was able to capture contact information for at least one
* registrant
*/
const abandoned_status_code = 'TAB';
/**
* Status ID(STS_ID on esp_status table) to indicate an incomplete transaction,
* meaning that monies are still owing: TXN_paid < TXN_total
*/
const incomplete_status_code = 'TIN';
/**
* Status ID (STS_ID on esp_status table) to indicate a complete transaction.
* meaning that NO monies are owing: TXN_paid == TXN_total
*/
const complete_status_code = 'TCM';
/**
* Status ID(STS_ID on esp_status table) to indicate the transaction is overpaid.
* This is the same as complete, but site admins actually owe clients the moneys! TXN_paid > TXN_total
*/
const overpaid_status_code = 'TOP';
/**
* private constructor to prevent direct creation
*
* @param string|null $timezone string representing the timezone we want to set for returned Date Time Strings (and
* any incoming timezone data that gets saved). Note this just sends the timezone info
* to the date time model field objects. Default is NULL (and will be assumed using
* the set timezone in the 'timezone_string' wp option)
* @throws EE_Error
* @throws ReflectionException
*/
protected function __construct(?string $timezone = '')
{
$this->singular_item = esc_html__('Transaction', 'event_espresso');
$this->plural_item = esc_html__('Transactions', 'event_espresso');
$this->_tables = [
'TransactionTable' => new EE_Primary_Table('esp_transaction', 'TXN_ID'),
];
$this->_fields = [
'TransactionTable' => [
'TXN_ID' => new EE_Primary_Key_Int_Field(
'TXN_ID', esc_html__('Transaction ID', 'event_espresso')
),
'TXN_timestamp' => new EE_Datetime_Field(
'TXN_timestamp',
esc_html__('date when transaction was created', 'event_espresso'),
false,
EE_Datetime_Field::now,
$timezone
),
'TXN_total' => new EE_Money_Field(
'TXN_total',
esc_html__('Total value of Transaction', 'event_espresso'),
false,
0
),
'TXN_paid' => new EE_Money_Field(
'TXN_paid',
esc_html__('Amount paid towards transaction to date', 'event_espresso'),
false,
0
),
'STS_ID' => new EE_Foreign_Key_String_Field(
'STS_ID',
esc_html__('Status ID', 'event_espresso'),
false,
EEM_Transaction::failed_status_code,
'Status'
),
'TXN_session_data' => new EE_Serialized_Text_Field(
'TXN_session_data',
esc_html__('Serialized session data', 'event_espresso'),
true,
''
),
'TXN_hash_salt' => new EE_Plain_Text_Field(
'TXN_hash_salt',
esc_html__('Transaction Hash Salt', 'event_espresso'),
true,
''
),
'PMD_ID' => new EE_Foreign_Key_Int_Field(
'PMD_ID',
esc_html__("Last Used Payment Method", 'event_espresso'),
true,
null,
'Payment_Method'
),
'TXN_reg_steps' => new EE_Serialized_Text_Field(
'TXN_reg_steps',
esc_html__('Registration Steps', 'event_espresso'),
false,
[]
),
],
];
$this->_model_relations = [
'Registration' => new EE_Has_Many_Relation(),
'Payment' => new EE_Has_Many_Relation(),
'Status' => new EE_Belongs_To_Relation(),
'Line_Item' => new EE_Has_Many_Relation(false),
// you can delete a transaction without needing to delete its line items
'Payment_Method' => new EE_Belongs_To_Relation(),
'Message' => new EE_Has_Many_Relation(),
];
$this->_model_chain_to_wp_user = 'Registration.Event';
parent::__construct($timezone);
}
/**
* txn_status_array
* get list of transaction statuses
*
* @access public
* @return array
*/
public static function txn_status_array()
{
return apply_filters(
'FHEE__EEM_Transaction__txn_status_array',
[
EEM_Transaction::overpaid_status_code,
EEM_Transaction::complete_status_code,
EEM_Transaction::incomplete_status_code,
EEM_Transaction::abandoned_status_code,
EEM_Transaction::failed_status_code,
]
);
}
/**
* get the revenue per day for the Transaction Admin page Reports Tab
*
* @access public
* @param string $period
* @return stdClass[]
* @throws EE_Error
* @throws EE_Error
*/
public function get_revenue_per_day_report($period = '-1 month')
{
$sql_date = $this->convert_datetime_for_query(
'TXN_timestamp',
date('Y-m-d H:i:s', strtotime($period)),
'Y-m-d H:i:s',
'UTC'
);
$query_interval = EEH_DTT_Helper::get_sql_query_interval_for_offset($this->get_timezone(), 'TXN_timestamp');
return $this->_get_all_wpdb_results(
[
[
'TXN_timestamp' => ['>=', $sql_date],
],
'group_by' => 'txnDate',
'order_by' => ['TXN_timestamp' => 'ASC'],
],
OBJECT,
[
'txnDate' => ['DATE(' . $query_interval . ')', '%s'],
'revenue' => ['SUM(TransactionTable.TXN_paid)', '%d'],
]
);
}
/**
* get the revenue per event for the Transaction Admin page Reports Tab
*
* @access public
* @param string $period
* @return EE_Transaction[]
*/
public function get_revenue_per_event_report($period = '-1 month')
{
global $wpdb;
$transaction_table = $wpdb->prefix . 'esp_transaction';
$registration_table = $wpdb->prefix . 'esp_registration';
$registration_payment_table = $wpdb->prefix . 'esp_registration_payment';
$event_table = $wpdb->posts;
$payment_table = $wpdb->prefix . 'esp_payment';
$sql_date = date('Y-m-d H:i:s', strtotime($period));
$approved_payment_status = EEM_Payment::status_id_approved;
$extra_event_on_join = '';
// exclude events not authored by user if permissions in effect
if (! EE_Registry::instance()->CAP->current_user_can('ee_read_others_registrations', 'reg_per_event_report')) {
$extra_event_on_join = ' AND Event.post_author = ' . get_current_user_id();
}
return $wpdb->get_results(
"SELECT
Transaction_Event.event_name AS event_name,
SUM(Transaction_Event.paid) AS revenue
FROM
(
SELECT
DISTINCT(Registration.REG_ID),
Event.post_title AS event_name,
Registration_Payment.RPY_amount AS paid
FROM
$registration_payment_table as Registration_Payment
JOIN
$registration_table as Registration
ON Registration.REG_ID = Registration_Payment.REG_ID
JOIN
$transaction_table as TransactionTable
ON Registration.TXN_ID = TransactionTable.TXN_ID
JOIN
$payment_table as Payment
ON Payment.TXN_ID = Registration.TXN_ID
AND Payment.PAY_timestamp > '$sql_date'
AND Payment.STS_ID = '$approved_payment_status'
JOIN
$event_table AS Event
ON Registration.EVT_ID = Event.ID
$extra_event_on_join
) AS Transaction_Event
GROUP BY event_name"
);
}
/**
* Gets the current transaction given the reg_url_link, or assumes the reg_url_link is in the
* request global variable. Either way, tries to find the current transaction (through
* the registration pointed to by reg_url_link), if not returns null
*
* @param string $reg_url_link
* @return EE_Transaction
* @throws EE_Error
*/
public function get_transaction_from_reg_url_link($reg_url_link = '')
{
if (empty($reg_url_link)) {
$request = LoaderFactory::getLoader()->getShared(RequestInterface::class);
$reg_url_link = $request->getRequestParam('e_reg_url_link');
}
return $this->get_one(
[
[
'Registration.REG_url_link' => $reg_url_link,
],
]
);
}
/**
* Updates the provided EE_Transaction with all the applicable payments
* (or fetch the EE_Transaction from its ID)
*
* @param EE_Transaction|int $transaction_obj_or_id
* @param boolean $save_txn whether or not to save the transaction during this function call
* @return array
* @throws EE_Error
* @throws ReflectionException
* @deprecated
*/
public function update_based_on_payments($transaction_obj_or_id, $save_txn = true)
{
EE_Error::doing_it_wrong(
__CLASS__ . '::' . __FUNCTION__,
sprintf(
esc_html__('This method is deprecated. Please use "%s" instead', 'event_espresso'),
'EE_Transaction_Processor::update_transaction_and_registrations_after_checkout_or_payment()'
),
'4.6.0'
);
/** @type EE_Transaction_Processor $transaction_processor */
$transaction_processor = EE_Registry::instance()->load_class('Transaction_Processor');
return $transaction_processor->update_transaction_and_registrations_after_checkout_or_payment(
$this->ensure_is_obj($transaction_obj_or_id)
);
}
/**
* Deletes "junk" transactions that were probably added by bots. There might be TONS
* of these, so we are very careful to NOT select (which the models do even when deleting),
* and so we only use wpdb directly and only do minimal joins.
* Transactions are considered "junk" if they're failed for longer than a week.
* Also, there is an extra check for payments related to the transaction, because if a transaction has a payment on
* it, it's probably not junk (regardless of what status it has).
* The downside to this approach is that is addons are listening for object deletions
* on EEM_Base::delete() they won't be notified of this. However, there is an action that plugins can hook into
* to catch these types of deletions.
*
* @return int
* @throws EE_Error
* @throws EE_Error
* @global WPDB $wpdb
*/
public function delete_junk_transactions()
{
global $wpdb;
$deleted = false;
$time_to_leave_alone = (int) apply_filters(
'FHEE__EEM_Transaction__delete_junk_transactions__time_to_leave_alone',
WEEK_IN_SECONDS
);
/**
* This allows code to filter the query arguments used for retrieving the transaction IDs to delete.
* Useful for plugins that want to exclude transactions matching certain query parameters.
* The query parameters should be in the format accepted by the EEM_Base model queries.
*/
$ids_query = apply_filters(
'FHEE__EEM_Transaction__delete_junk_transactions__initial_query_args',
[
0 => [
'STS_ID' => EEM_Transaction::failed_status_code,
'Payment.PAY_ID' => ['IS NULL'],
'TXN_timestamp' => ['<', time() - $time_to_leave_alone],
],
'order_by' => ['TXN_timestamp' => 'ASC'],
'limit' => 1000,
],
$time_to_leave_alone
);
/**
* This filter is for when code needs to filter the list of transaction ids that represent transactions
* about to be deleted based on some other criteria that isn't easily done via the query args filter.
*/
$txn_ids = apply_filters(
'FHEE__EEM_Transaction__delete_junk_transactions__transaction_ids_to_delete',
EEM_Transaction::instance()->get_col($ids_query, 'TXN_ID'),
$time_to_leave_alone
);
// now that we have the ids to delete
if (! empty($txn_ids) && is_array($txn_ids)) {
// first, make sure these TXN's are removed the "ee_locked_transactions" array
EEM_Transaction::unset_locked_transactions($txn_ids);
// Create IDs placeholder.
$placeholders = array_fill(0, count($txn_ids), '%d');
// Glue it together to use inside $wpdb->prepare.
$format = implode(', ', $placeholders);
// let's get deleting...
// We got the ids from the original query to get them FROM
// the db (which is sanitized) so no need to prepare them again.
$query = $wpdb->prepare("DELETE FROM " . $this->table() . " WHERE TXN_ID IN ( $format )", $txn_ids);
$deleted = $wpdb->query($query);
}
if ($deleted) {
/**
* Allows code to do something after the transactions have been deleted.
*/
do_action('AHEE__EEM_Transaction__delete_junk_transactions__successful_deletion', $txn_ids);
}
return $deleted;
}
/**
* @param array $transaction_IDs
* @return bool
*/
public static function unset_locked_transactions(array $transaction_IDs)
{
$locked_transactions = get_option('ee_locked_transactions', []);
$update = false;
foreach ($transaction_IDs as $TXN_ID) {
if (isset($locked_transactions[ $TXN_ID ])) {
unset($locked_transactions[ $TXN_ID ]);
$update = true;
}
}
if ($update) {
update_option('ee_locked_transactions', $locked_transactions);
}
return $update;
}
/**
* returns an array of EE_Transaction objects whose timestamp is greater than
* the current time minus the session lifespan, which defaults to 60 minutes
*
* @return EE_Base_Class[]|EE_Transaction[]
* @throws EE_Error
* @throws InvalidArgumentException
* @throws InvalidDataTypeException
* @throws InvalidInterfaceException
*/
public function get_transactions_in_progress()
{
return $this->_get_transactions_in_progress();
}
/**
* returns an array of EE_Transaction objects whose timestamp is less than
* the current time minus the session lifespan, which defaults to 60 minutes
*
* @return EE_Base_Class[]|EE_Transaction[]
* @throws EE_Error
* @throws InvalidArgumentException
* @throws InvalidDataTypeException
* @throws InvalidInterfaceException
*/
public function get_transactions_not_in_progress()
{
return $this->_get_transactions_in_progress('<=');
}
/**
* @param string $comparison
* @return EE_Transaction[]
* @throws EE_Error
* @throws InvalidArgumentException
* @throws InvalidDataTypeException
* @throws InvalidInterfaceException
*/
private function _get_transactions_in_progress($comparison = '>=')
{
$comparison = $comparison === '>=' || $comparison === '<='
? $comparison
: '>=';
/** @var EventEspresso\core\domain\values\session\SessionLifespan $session_lifespan */
$session_lifespan = LoaderFactory::getLoader()->getShared(
'EventEspresso\core\domain\values\session\SessionLifespan'
);
return $this->get_all(
[
[
'TXN_timestamp' => [
$comparison,
$session_lifespan->expiration(),
],
'STS_ID' => [
'!=',
EEM_Transaction::complete_status_code,
],
],
]
);
}
}