transactions::build_donor_where_clause()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 12
rs 9.9666
1
<?php
2
/**
3
 *
4
 * PayPal Donation extension for the phpBB Forum Software package.
5
 *
6
 * @copyright (c) 2015-2024 Skouat
7
 * @license GNU General Public License, version 2 (GPL-2.0)
8
 *
9
 */
10
11
namespace skouat\ppde\operators;
12
13
use phpbb\db\driver\driver_interface;
14
15
class transactions
16
{
17
	protected $db;
18
	protected $ppde_transactions_log_table;
19
20
	/**
21
	 * Constructor
22
	 *
23
	 * @param driver_interface $db                          Database connection
24
	 * @param string           $ppde_transactions_log_table Table name
25
	 */
26
	public function __construct(driver_interface $db, string $ppde_transactions_log_table)
27
	{
28
		$this->db = $db;
29
		$this->ppde_transactions_log_table = $ppde_transactions_log_table;
30
	}
31
32
	/**
33
	 * Builds SQL Query to return Transaction log data
34
	 *
35
	 * @param int $transaction_id ID of the transaction to fetch (0 for all transactions)
36
	 * @return string SQL query string
37
	 */
38
	public function build_sql_data(int $transaction_id = 0): string
39
	{
40
		$sql_ary = [
41
			'SELECT'    => 'txn.*, u.username, u.user_colour',
42
			'FROM'      => [$this->ppde_transactions_log_table => 'txn'],
43
			'LEFT_JOIN' => [
44
				[
45
					'FROM' => [USERS_TABLE => 'u'],
46
					'ON'   => 'u.user_id = txn.user_id',
47
				],
48
			],
49
			'WHERE'     => $transaction_id ? 'txn.transaction_id = ' . $transaction_id : '',
50
			'ORDER_BY'  => 'txn.transaction_id',
51
		];
52
53
		// Return all transactions entities
54
		return $this->db->sql_build_query('SELECT', $sql_ary);
55
	}
56
57
	/**
58
	 * Builds SQL Query array for donor list
59
	 *
60
	 * @param bool   $detailed Whether to include detailed information
61
	 * @param string $order_by SQL ORDER BY clause
62
	 * @return array SQL query array
63
	 */
64
	public function sql_donorlist_ary(bool $detailed = false, string $order_by = ''): array
65
	{
66
		// Build sql request
67
		$sql_donorslist_ary = [
68
			'SELECT'   => 'txn.user_id, txn.mc_currency',
69
			'FROM'     => [$this->ppde_transactions_log_table => 'txn'],
70
			'WHERE'    => 'txn.user_id <> ' . ANONYMOUS . "
71
							AND txn.payment_status = 'Completed'
72
							AND txn.test_ipn = 0",
73
			'GROUP_BY' => 'txn.user_id, txn.mc_currency',
74
			'ORDER_BY' => $order_by,
75
		];
76
77
		if ($detailed)
78
		{
79
			$sql_donorslist_ary['SELECT'] = 'txn.user_id, txn.mc_currency, MAX(txn.transaction_id) AS max_txn_id, SUM(txn.mc_gross) AS amount, MAX(u.username)';
80
			$sql_donorslist_ary['LEFT_JOIN'] = [
81
				[
82
					'FROM' => [USERS_TABLE => 'u'],
83
					'ON'   => 'u.user_id = txn.user_id',
84
				]];
85
		}
86
87
		return $sql_donorslist_ary;
88
	}
89
90
	/**
91
	 * Builds SQL Query array for the last donation of a donor
92
	 *
93
	 * @param int $transaction_id ID of the transaction
94
	 * @return array SQL query array
95
	 */
96
	public function sql_last_donation_ary(int $transaction_id): array
97
	{
98
		return [
99
			'SELECT' => 'txn.payment_date, txn.mc_gross, txn.mc_currency',
100
			'FROM'   => [$this->ppde_transactions_log_table => 'txn'],
101
			'WHERE'  => 'txn.transaction_id = ' . $transaction_id,
102
		];
103
	}
104
105
	/**
106
	 * Builds SQL Query to return the donors list
107
	 *
108
	 * @param array $sql_donorlist_ary SQL query array
109
	 * @return string SQL query string
110
	 */
111
	public function build_sql_donorlist_data(array $sql_donorlist_ary): string
112
	{
113
		return $this->db->sql_build_query('SELECT', $sql_donorlist_ary);
114
	}
115
116
	/**
117
	 * Executes a COUNT query and returns the result
118
	 *
119
	 * @param array  $count_sql_ary  SQL query array
120
	 * @param string $selected_field Field to count
121
	 * @return int Count result
122
	 */
123
	public function query_sql_count(array $count_sql_ary, string $selected_field): int
124
	{
125
		$count_sql_ary['SELECT'] = 'COUNT(' . $selected_field . ') AS total_entries';
126
127
		if (array_key_exists('GROUP_BY', $count_sql_ary))
128
		{
129
			$count_sql_ary['SELECT'] = 'COUNT(DISTINCT ' . $count_sql_ary['GROUP_BY'] . ') AS total_entries';
130
		}
131
		unset($count_sql_ary['ORDER_BY'], $count_sql_ary['GROUP_BY']);
132
133
		$sql = $this->db->sql_build_query('SELECT', $count_sql_ary);
134
		$result = $this->db->sql_query($sql);
135
		$count = (int) $this->db->sql_fetchfield('total_entries');
136
		$this->db->sql_freeresult($result);
137
138
		return $count;
139
	}
140
141
	/**
142
	 * Builds SQL Query array for displaying simple transactions details
143
	 *
144
	 * @param string $keywords Search keywords
145
	 * @param string $sort_by  SQL ORDER BY clause
146
	 * @param int    $log_time Timestamp to filter logs
147
	 * @return array SQL query array
148
	 */
149
	public function get_logs_sql_ary(string $keywords, string $sort_by, int $log_time): array
150
	{
151
		$sql_keywords = $this->generate_sql_keyword($keywords);
152
153
		$sql_ary = [
154
			'SELECT'   => 'txn.transaction_id, txn.txn_id, txn.test_ipn, txn.confirmed, txn.txn_errors, txn.payment_date, txn.payment_status, txn.user_id, u.username, u.user_colour',
155
			'FROM'     => [
156
				$this->ppde_transactions_log_table => 'txn',
157
				USERS_TABLE                        => 'u',
158
			],
159
			'WHERE'    => 'txn.user_id = u.user_id ' . $sql_keywords,
160
			'ORDER_BY' => $sort_by,
161
		];
162
163
		if ($log_time)
164
		{
165
			$sql_ary['WHERE'] = 'txn.payment_date >= ' . (int) $log_time . ' AND ' . $sql_ary['WHERE'];
166
		}
167
168
		return $sql_ary;
169
	}
170
171
	/**
172
	 * Generates SQL condition for the specified keywords
173
	 *
174
	 * @param string $keywords           The keywords the user specified to search for
175
	 * @param string $statement_operator SQL operator to use ('AND' by default)
176
	 * @return string SQL condition string
177
	 */
178
	private function generate_sql_keyword(string $keywords, string $statement_operator = 'AND'): string
179
	{
180
		// Use no preg_quote for $keywords because this would lead to sole
181
		// backslashes being added. We also use an OR connection here for
182
		// spaces and the | string. Currently, regex is not supported for
183
		// searching (but may come later).
184
		$keywords = preg_split('#[\s|]+#u', utf8_strtolower($keywords), 0, PREG_SPLIT_NO_EMPTY);
0 ignored issues
show
Bug introduced by
The function utf8_strtolower was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

184
		$keywords = preg_split('#[\s|]+#u', /** @scrutinizer ignore-call */ utf8_strtolower($keywords), 0, PREG_SPLIT_NO_EMPTY);
Loading history...
185
		if (empty($keywords))
186
		{
187
			return '';
188
		}
189
190
		// Build pattern and keywords...
191
		$keywords = array_map(function ($keyword) {
192
			return $this->db->sql_like_expression($this->db->get_any_char() . $keyword . $this->db->get_any_char());
193
		}, $keywords);
194
195
		$sql_keywords = ' ' . $statement_operator . ' (';
196
		$columns = ['txn.txn_id', 'u.username'];
197
198
		$sql_clauses = [];
199
		foreach ($columns as $column_name)
200
		{
201
			$sql_lower = $this->db->sql_lower_text($column_name);
202
			$sql_clauses[] = $sql_lower . ' ' . implode(' OR ' . $sql_lower . ' ', $keywords);
203
		}
204
205
		$sql_keywords .= implode(' OR ', $sql_clauses) . ')';
206
207
		return $sql_keywords;
208
	}
209
210
	/**
211
	 * Retrieves user information based on the donor ID or email
212
	 *
213
	 * @param string     $type Type of identifier ('user', 'username', or 'email')
214
	 * @param int|string $arg  Identifier value
215
	 * @return array User data
216
	 */
217
	public function query_donor_user_data(string $type = 'user', $arg = 1): array
218
	{
219
		$sql_where = $this->build_donor_where_clause($type, $arg);
220
		return $this->fetch_donor_data($sql_where);
221
	}
222
223
	/**
224
	 * Builds SQL WHERE clause for donor query
225
	 *
226
	 * @param string $type Type of identifier
227
	 * @param mixed  $arg  Identifier value
228
	 * @return array SQL WHERE clause
229
	 */
230
	private function build_donor_where_clause(string $type, $arg): array
231
	{
232
		switch ($type)
233
		{
234
			case 'user':
235
				return ['WHERE' => 'u.user_id = ' . (int) $arg];
236
			case 'username':
237
				return ['WHERE' => "u.username_clean = '" . $this->db->sql_escape(utf8_clean_string($arg)) . "'"];
238
			case 'email':
239
				return ['WHERE' => "u.user_email = '" . $this->db->sql_escape(strtolower($arg)) . "'"];
240
			default:
241
				return [];
242
		}
243
	}
244
245
	/**
246
	 * Fetches donor data from the database
247
	 *
248
	 * @param array $sql_where SQL WHERE clause
249
	 * @return array Donor data
250
	 */
251
	private function fetch_donor_data(array $sql_where): array
252
	{
253
		$sql_ary = [
254
			'SELECT'   => 'u.user_id, u.username, u.user_ppde_donated_amount',
255
			'FROM'     => [USERS_TABLE => 'u'],
256
		];
257
		$sql_ary = array_merge($sql_where, $sql_ary);
258
259
		$sql = $this->db->sql_build_query('SELECT', $sql_ary);
260
		$result = $this->db->sql_query($sql);
261
		$row = $this->db->sql_fetchrow($result);
262
		$this->db->sql_freeresult($result);
263
264
		return $row ?: [];
265
	}
266
267
	/**
268
	 * Builds log entries for PayPal transactions
269
	 *
270
	 * @param array $get_logs_sql_ary SQL query array
271
	 * @param array $url_ary          Array of URLs for building links
272
	 * @param int   $limit            Maximum number of entries to return
273
	 * @param int   $last_page_offset Offset for pagination
274
	 * @return array Log entries
275
	 */
276
	public function build_log_entries(array $get_logs_sql_ary, array $url_ary, int $limit = 0, int $last_page_offset = 0): array
277
	{
278
		$sql = $this->db->sql_build_query('SELECT', $get_logs_sql_ary);
279
		$result = $this->db->sql_query_limit($sql, $limit, $last_page_offset);
280
281
		$log_entries = [];
282
283
		while ($row = $this->db->sql_fetchrow($result))
284
		{
285
			$log_entries[] = $this->build_log_entry($row, $url_ary);
286
		}
287
		$this->db->sql_freeresult($result);
288
289
		return $log_entries;
290
	}
291
292
	/**
293
	 * Builds a single log entry
294
	 *
295
	 * @param array $row     Database row data
296
	 * @param array $url_ary Array of URLs for building links
297
	 * @return array Formatted log entry
298
	 */
299
	private function build_log_entry(array $row, array $url_ary): array
300
	{
301
		return [
302
			'confirmed'      => $row['confirmed'],
303
			'payment_date'   => $row['payment_date'],
304
			'payment_status' => $row['payment_status'],
305
			'test_ipn'       => $row['test_ipn'],
306
			'transaction_id' => $row['transaction_id'],
307
			'txn_errors'     => $row['txn_errors'],
308
			'txn_id'         => $this->build_transaction_url($row['transaction_id'], $row['txn_id'], $url_ary['txn_url'], $row['confirmed']),
309
			'username_full'  => get_username_string('full', $row['user_id'], $row['username'], $row['user_colour'], false, $url_ary['profile_url']),
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $guest_username of get_username_string(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

309
			'username_full'  => get_username_string('full', $row['user_id'], $row['username'], $row['user_colour'], /** @scrutinizer ignore-type */ false, $url_ary['profile_url']),
Loading history...
310
		];
311
	}
312
313
	/**
314
	 * Builds transaction URL for templates
315
	 *
316
	 * @param int    $id         Transaction ID
317
	 * @param string $txn_id     PayPal transaction ID
318
	 * @param string $custom_url Custom URL (optional)
319
	 * @param bool   $colour     Whether to apply color to the URL
320
	 * @return string Formatted transaction URL or plain transaction ID
321
	 */
322
	private function build_transaction_url(int $id, string $txn_id, string $custom_url = '', bool $colour = false): string
323
	{
324
		if (empty($custom_url))
325
		{
326
			return $txn_id;
327
		}
328
329
		$txn_url = $custom_url . '&amp;action=view&amp;id=' . $id;
330
		return $this->format_transaction_link($txn_url, $txn_id, $colour);
331
	}
332
333
	/**
334
	 * Formats the transaction link
335
	 *
336
	 * @param string $txn_url Transaction URL
337
	 * @param string $txn_id  PayPal transaction ID
338
	 * @param bool   $colour  Whether to apply color to the URL
339
	 * @return string Formatted transaction link
340
	 */
341
	private function format_transaction_link(string $txn_url, string $txn_id, bool $colour): string
342
	{
343
		$style = $colour ? '' : ' style="color: #ff0000;"';
344
		return sprintf('<a href="%s"%s>%s</a>', $txn_url, $style, $txn_id);
345
	}
346
347
	/**
348
	 * Builds SQL WHERE clause for marked transactions
349
	 *
350
	 * @param array $marked Array of marked transaction IDs
351
	 * @return string SQL WHERE clause
352
	 */
353
	public function build_marked_where_sql(array $marked): string
354
	{
355
		if (empty($marked))
356
		{
357
			return '';
358
		}
359
360
		return ' WHERE ' . $this->db->sql_in_set('transaction_id', array_map('intval', $marked));
361
	}
362
363
	/**
364
	 * Executes a query to count results for updating stats
365
	 *
366
	 * @param string $type     Type of count query
367
	 * @param bool   $test_ipn Whether to include test IPNs
368
	 * @return int Count result
369
	 */
370
	public function sql_query_count_result(string $type, bool $test_ipn): int
371
	{
372
		$field_name = strpos($type, 'transactions_count') !== false ? 'txn_id' : 'payer_id';
373
		$sql_ary = $this->sql_select_stats_main($field_name);
374
		$test_ipn_str = (int) $test_ipn;
375
376
		$this->add_where_clause($sql_ary, $type, $test_ipn_str);
377
378
		$sql = $this->db->sql_build_query('SELECT', $sql_ary);
379
		$result = $this->db->sql_query($sql);
380
		$count = (int) $this->db->sql_fetchfield('count_result');
381
		$this->db->sql_freeresult($result);
382
383
		return $count;
384
	}
385
386
	/**
387
	 * Builds base SQL query array for stats calculation
388
	 *
389
	 * @param string $field_name Name of the field to count
390
	 * @return array SQL query array
391
	 */
392
	private function sql_select_stats_main(string $field_name): array
393
	{
394
		return [
395
			'SELECT' => 'COUNT(DISTINCT txn.' . $field_name . ') AS count_result',
396
			'FROM'   => [$this->ppde_transactions_log_table => 'txn'],
397
		];
398
	}
399
400
	/**
401
	 * Adds WHERE clause to the SQL query array for stats calculation
402
	 *
403
	 * @param array  &$sql_ary      SQL query array (passed by reference)
404
	 * @param string  $type         Type of count query
405
	 * @param int     $test_ipn_str Test IPN flag (as integer)
406
	 */
407
	private function add_where_clause(array &$sql_ary, string $type, int $test_ipn_str): void
408
	{
409
		if (strpos($type, 'transactions_count') !== false)
410
		{
411
			$sql_ary['WHERE'] = "confirmed = 1 AND payment_status = 'Completed' AND txn.test_ipn = " . $test_ipn_str;
412
		}
413
		else if (strpos($type, 'known_donors_count') !== false)
414
		{
415
			$sql_ary['LEFT_JOIN'] = [
416
				[
417
					'FROM' => [USERS_TABLE => 'u'],
418
					'ON'   => 'txn.user_id = u.user_id',
419
				],
420
			];
421
			$sql_ary['WHERE'] = '(u.user_type = ' . USER_NORMAL . ' OR u.user_type = ' . USER_FOUNDER . ') AND txn.test_ipn = ' . $test_ipn_str;
422
		}
423
		else if (strpos($type, 'anonymous_donors_count') !== false)
424
		{
425
			$sql_ary['WHERE'] = 'txn.user_id = ' . ANONYMOUS . ' AND txn.test_ipn = ' . $test_ipn_str;
426
		}
427
	}
428
429
	/**
430
	 * Updates the user's donated amount
431
	 *
432
	 * @param int   $user_id User ID
433
	 * @param float $value   New donated amount
434
	 */
435
	public function sql_update_user_stats(int $user_id, float $value): void
436
	{
437
		$sql = 'UPDATE ' . USERS_TABLE . '
438
			SET user_ppde_donated_amount = ' . $value . '
439
			WHERE user_id = ' . $user_id;
440
		$this->db->sql_query($sql);
441
	}
442
443
	/**
444
	 * Get sort options for transactions
445
	 *
446
	 * @return array An associative array of sort keys and their corresponding SQL column names
447
	 */
448
	public function get_sort_options(): array
449
	{
450
		return [
451
			'txn'      => 'txn.txn_id',
452
			'u'        => 'u.username_clean',
453
			'ipn'      => 'txn.confirmed',
454
			'ipn_test' => 'txn.test_ipn',
455
			'ps'       => 'txn.payment_status',
456
			't'        => 'txn.payment_date',
457
		];
458
	}
459
460
	/**
461
	 * Prepares transaction data array for entity
462
	 *
463
	 * @param array $data Raw transaction data
464
	 * @return array Formatted transaction data
465
	 */
466
	public function build_transaction_data_ary(array $data): array
467
	{
468
		return [
469
			'business'          => $data['business'],
470
			'confirmed'         => (bool) $data['confirmed'],
471
			'custom'            => $data['custom'],
472
			'exchange_rate'     => $data['exchange_rate'],
473
			'first_name'        => $data['first_name'],
474
			'item_name'         => $data['item_name'],
475
			'item_number'       => $data['item_number'],
476
			'last_name'         => $data['last_name'],
477
			'mc_currency'       => $data['mc_currency'],
478
			'mc_gross'          => (float) $data['mc_gross'],
479
			'mc_fee'            => (float) $data['mc_fee'],
480
			'net_amount'        => (float) $data['net_amount'],
481
			'parent_txn_id'     => $data['parent_txn_id'],
482
			'payer_email'       => $data['payer_email'],
483
			'payer_id'          => $data['payer_id'],
484
			'payer_status'      => $data['payer_status'],
485
			'payment_date'      => $data['payment_date'],
486
			'payment_status'    => $data['payment_status'],
487
			'payment_type'      => $data['payment_type'],
488
			'memo'              => $data['memo'],
489
			'receiver_id'       => $data['receiver_id'],
490
			'receiver_email'    => $data['receiver_email'],
491
			'residence_country' => $data['residence_country'],
492
			'settle_amount'     => (float) $data['settle_amount'],
493
			'settle_currency'   => $data['settle_currency'],
494
			'test_ipn'          => (bool) $data['test_ipn'],
495
			'txn_errors'        => $data['txn_errors'],
496
			'txn_id'            => $data['txn_id'],
497
			'txn_type'          => $data['txn_type'],
498
			'user_id'           => (int) $data['user_id'],
499
		];
500
	}
501
}
502