Passed
Push — develop-3.3.x ( 5a7b5f...c85e2c )
by Mario
02:35
created

transactions_controller::output_errors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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\controller\admin;
12
13
use phpbb\auth\auth;
14
use phpbb\config\config;
15
use phpbb\language\language;
16
use phpbb\log\log;
17
use phpbb\request\request;
18
use phpbb\template\template;
19
use phpbb\user;
20
use phpbb\user_loader;
21
use skouat\ppde\actions\core;
22
use skouat\ppde\actions\currency;
23
use skouat\ppde\exception\transaction_exception;
24
use skouat\ppde\operators\transactions;
25
use Symfony\Component\DependencyInjection\ContainerInterface;
26
27
/**
28
 * @property array              args               Array of args for hidden fields
29
 * @property config             config             Config object
30
 * @property ContainerInterface container          Service container interface
31
 * @property string             id_prefix_name     Prefix name for identifier in the URL
32
 * @property string             lang_key_prefix    Prefix for the messages thrown by exceptions
33
 * @property language           language           Language user object
34
 * @property log                log                The phpBB log system
35
 * @property string             module_name        Name of the module currently used
36
 * @property request            request            Request object
37
 * @property bool               submit             State of submit $_POST variable
38
 * @property template           template           Template object
39
 * @property string             u_action           Action URL
40
 * @property user               user               User object
41
 * @property user_loader        user_loader        User loader object
42
 */
43
class transactions_controller extends admin_main
44
{
45
	public $ppde_operator;
46
	protected $adm_relative_path;
47
	protected $auth;
48
	protected $user_loader;
49
	protected $entry_count;
50
	protected $last_page_offset;
51
	protected $php_ext;
52
	protected $phpbb_admin_path;
53
	protected $phpbb_root_path;
54
	protected $ppde_actions;
55
	protected $ppde_actions_currency;
56
	protected $ppde_entity;
57
	protected $table_prefix;
58
	protected $table_ppde_transactions;
59
60
	/**
61
	 * Constructor
62
	 *
63
	 * @param auth                             $auth                       Authentication object
64
	 * @param config                           $config                     Config object
65
	 * @param ContainerInterface               $container                  Service container interface
66
	 * @param language                         $language                   Language user object
67
	 * @param log                              $log                        The phpBB log system
68
	 * @param core                             $ppde_actions               PPDE actions object
69
	 * @param currency                         $ppde_actions_currency      PPDE currency actions object
70
	 * @param \skouat\ppde\entity\transactions $ppde_entity_transactions   Entity object
71
	 * @param transactions                     $ppde_operator_transactions Operator object
72
	 * @param request                          $request                    Request object
73
	 * @param template                         $template                   Template object
74
	 * @param user                             $user                       User object
75
	 * @param user_loader                      $user_loader                User loader object
76
	 * @param string                           $adm_relative_path          phpBB admin relative path
77
	 * @param string                           $phpbb_root_path            phpBB root path
78
	 * @param string                           $php_ext                    phpEx
79
	 * @param string                           $table_prefix               The table prefix
80
	 * @param string                           $table_ppde_transactions    Name of the table used to store data
81
	 */
82
	public function __construct(
83
		auth $auth,
84
		config $config,
85
		ContainerInterface $container,
86
		language $language,
87
		log $log,
88
		core $ppde_actions,
89
		currency $ppde_actions_currency,
90
		\skouat\ppde\entity\transactions $ppde_entity_transactions,
91
		transactions $ppde_operator_transactions,
92
		request $request,
93
		template $template,
94
		user $user,
95
		user_loader $user_loader,
96
		string $adm_relative_path,
97
		string $phpbb_root_path,
98
		string $php_ext,
99
		string $table_prefix,
100
		string $table_ppde_transactions
101
	)
102
	{
103
		$this->auth = $auth;
104
		$this->config = $config;
105
		$this->container = $container;
106
		$this->language = $language;
107
		$this->log = $log;
108
		$this->ppde_actions = $ppde_actions;
109
		$this->ppde_actions_currency = $ppde_actions_currency;
110
		$this->ppde_entity = $ppde_entity_transactions;
111
		$this->ppde_operator = $ppde_operator_transactions;
112
		$this->request = $request;
113
		$this->template = $template;
114
		$this->user = $user;
115
		$this->user_loader = $user_loader;
116
		$this->adm_relative_path = $adm_relative_path;
117
		$this->phpbb_admin_path = $phpbb_root_path . $adm_relative_path;
118
		$this->phpbb_root_path = $phpbb_root_path;
119
		$this->php_ext = $php_ext;
120
		$this->table_prefix = $table_prefix;
121
		$this->table_ppde_transactions = $table_ppde_transactions;
122
		parent::__construct(
123
			'transactions',
124
			'PPDE_DT',
125
			'transaction'
126
		);
127
	}
128
129
	/**
130
	 * {@inheritdoc}
131
	 */
132
	public function display(): void
133
	{
134
		// Sorting and pagination setup
135
		$sort_by_text = $this->get_sort_by_text_options();
136
		$sort_by_sql = $this->get_sort_options();
137
		$sort_key = $this->request->variable('sk', 't');
138
		$sort_dir = $this->request->variable('sd', 'd');
139
		$start = $this->request->variable('start', 0);
140
		$limit = (int) $this->config['topics_per_page'];
141
142
		// Filtering setup
143
		$limit_days = $this->get_limit_day_options();
144
		$selected_days = $this->request->variable('st', 0);
145
		$keywords = $this->request->variable('keywords', '', true);
146
147
		// Generate sorting and filtering selects
148
		$s_limit_days = $s_sort_key = $s_sort_dir = $u_sort_param = '';
149
		gen_sort_selects($limit_days, $sort_by_text, $selected_days, $sort_key, $sort_dir, $s_limit_days, $s_sort_key, $s_sort_dir, $u_sort_param);
150
151
		// Prepare SQL conditions
152
		$sql_sort = $sort_by_sql[$sort_key] . ' ' . (($sort_dir === 'd') ? 'DESC' : 'ASC');
153
154
		// Fetch log data
155
		$log_data = [];
156
		$log_count = 0;
157
		$log_time = $this->calculate_timestamp($selected_days);
158
		$this->view_txn_log($log_data, $log_count, $limit, $start, $log_time, $sql_sort, $keywords);
159
160
		// Generate pagination
161
		$this->generate_pagination($log_count, $limit, $start, $u_sort_param, $keywords);
162
163
		// Assign template variables
164
		$this->assign_template_vars($s_limit_days, $s_sort_key, $s_sort_dir, $u_sort_param, $keywords, $start);
165
166
		// Assign log entries to template
167
		$this->assign_log_entries_to_template($log_data);
168
	}
169
170
	/**
171
	 * Get sort by text options for transactions
172
	 *
173
	 * @return array An associative array of sort options and their corresponding language strings
174
	 */
175
	private function get_sort_by_text_options(): array
176
	{
177
		return [
178
			'txn'      => $this->language->lang('PPDE_DT_SORT_TXN_ID'),
179
			'u'        => $this->language->lang('PPDE_DT_SORT_DONORS'),
180
			'ipn'      => $this->language->lang('PPDE_DT_SORT_IPN_STATUS'),
181
			'ipn_test' => $this->language->lang('PPDE_DT_SORT_IPN_TYPE'),
182
			'ps'       => $this->language->lang('PPDE_DT_SORT_PAYMENT_STATUS'),
183
			't'        => $this->language->lang('SORT_DATE'),
184
		];
185
	}
186
187
	/**
188
	 * Get sort options for transactions
189
	 *
190
	 * @return array An associative array of sort keys and their corresponding SQL column names
191
	 */
192
	private function get_sort_options(): array
193
	{
194
		return [
195
			'txn'      => 'txn.txn_id',
196
			'u'        => 'u.username_clean',
197
			'ipn'      => 'txn.confirmed',
198
			'ipn_test' => 'txn.test_ipn',
199
			'ps'       => 'txn.payment_status',
200
			't'        => 'txn.payment_date',
201
		];
202
	}
203
204
	/**
205
	 * Get limit day options for filtering
206
	 *
207
	 * @return array An associative array of day limits and their corresponding language strings
208
	 */
209
	private function get_limit_day_options(): array
210
	{
211
		return [
212
			0   => $this->language->lang('ALL_ENTRIES'),
213
			1   => $this->language->lang('1_DAY'),
214
			7   => $this->language->lang('7_DAYS'),
215
			14  => $this->language->lang('2_WEEKS'),
216
			30  => $this->language->lang('1_MONTH'),
217
			90  => $this->language->lang('3_MONTHS'),
218
			180 => $this->language->lang('6_MONTHS'),
219
			365 => $this->language->lang('1_YEAR'),
220
		];
221
	}
222
223
	/**
224
	 * Calculate the timestamp for filtering transactions based on the selected number of days
225
	 *
226
	 * @param int $selected_days Number of days to look back for transactions
227
	 * @return int The calculated timestamp, or 0 if no day limit is set
228
	 */
229
	private function calculate_timestamp(int $selected_days): int
230
	{
231
		return $selected_days > 0 ? time() - ($selected_days * self::SECONDS_IN_A_DAY) : 0;
232
	}
233
234
	/**
235
	 * View transaction log
236
	 *
237
	 * @param array  &$log       The result array with the logs
238
	 * @param int    &$log_count If $log_count is set to false, we will skip counting all entries in the database
239
	 *                           Otherwise an integer with the number of total matching entries is returned
240
	 * @param int     $limit     Limit the number of entries that are returned
241
	 * @param int     $offset    Offset when fetching the log entries, e.g. when paginating
242
	 * @param int     $log_time  Timestamp to filter logs
243
	 * @param string  $sort_by   SQL order option, e.g. 'l.log_time DESC'
244
	 * @param string  $keywords  Will only return log entries that have the keywords in log_operation or log_data
245
	 * @return void
246
	 */
247
	private function view_txn_log(array &$log, &$log_count, int $limit = 0, int $offset = 0, int $log_time = 0, string $sort_by = 'txn.payment_date DESC', string $keywords = ''): void
248
	{
249
		$count_logs = ($log_count !== false);
250
251
		$log = $this->get_logs($count_logs, $limit, $offset, $log_time, $sort_by, $keywords);
252
		$log_count = $this->get_log_count();
253
	}
254
255
	/**
256
	 * Get logs based on specified parameters
257
	 *
258
	 * @param bool   $count_logs Whether to count the total number of logs
259
	 * @param int    $limit      Maximum number of logs to retrieve
260
	 * @param int    $offset     Starting point for retrieving logs
261
	 * @param int    $log_time   Timestamp to filter logs
262
	 * @param string $sort_by    SQL ORDER BY clause
263
	 * @param string $keywords   Keywords to filter logs
264
	 * @return array Array of log entries
265
	 */
266
	private function get_logs(bool $count_logs = true, int $limit = 0, int $offset = 0, int $log_time = 0, string $sort_by = 'txn.payment_date DESC', string $keywords = ''): array
267
	{
268
		$this->entry_count = 0;
269
		$this->last_page_offset = $offset;
270
		$url_ary = [];
271
272
		if ($this->phpbb_admin_path && $this->ppde_actions->is_in_admin())
273
		{
274
			$url_ary['profile_url'] = append_sid($this->phpbb_admin_path . 'index.' . $this->php_ext, 'i=users&amp;mode=overview');
275
			$url_ary['txn_url'] = append_sid($this->phpbb_admin_path . 'index.' . $this->php_ext, 'i=-skouat-ppde-acp-ppde_module&amp;mode=transactions');
276
		}
277
		else
278
		{
279
			$url_ary['profile_url'] = append_sid($this->phpbb_root_path . 'memberlist.' . $this->php_ext, 'mode=viewprofile');
280
			$url_ary['txn_url'] = '';
281
		}
282
283
		$get_logs_sql_ary = $this->ppde_operator->get_logs_sql_ary($keywords, $sort_by, $log_time);
284
285
		if ($count_logs)
286
		{
287
			$this->entry_count = $this->ppde_operator->query_sql_count($get_logs_sql_ary, 'txn.transaction_id');
288
289
			if ($this->entry_count === 0)
290
			{
291
				// Save the queries, because there are no logs to display
292
				$this->last_page_offset = 0;
293
294
				return [];
295
			}
296
297
			// Return the user to the last page that is valid
298
			while ($this->last_page_offset >= $this->entry_count)
299
			{
300
				$this->last_page_offset = max(0, $this->last_page_offset - $limit);
301
			}
302
		}
303
304
		return $this->ppde_operator->build_log_entries($get_logs_sql_ary, $url_ary, $limit, $this->last_page_offset);
305
	}
306
307
	/**
308
	 * Get the total count of log entries
309
	 *
310
	 * @return int The total number of log entries
311
	 */
312
	public function get_log_count(): int
313
	{
314
		return (int) $this->entry_count ?: 0;
315
	}
316
317
	/**
318
	 * Generate pagination for transaction list
319
	 *
320
	 * @param int    $log_count    Total number of log entries
321
	 * @param int    $limit        Number of entries per page
322
	 * @param int    $start        Starting offset for the current page
323
	 * @param string $u_sort_param URL parameters for sorting
324
	 * @param string $keywords     Search keywords
325
	 */
326
	private function generate_pagination(int $log_count, int $limit, int $start, string $u_sort_param, string $keywords): void
327
	{
328
		$pagination = $this->container->get('pagination');
329
		$base_url = $this->u_action . '&amp;' . $u_sort_param . $this->get_keywords_param($keywords);
330
		$pagination->generate_template_pagination($base_url, 'pagination', 'start', $log_count, $limit, $start);
331
	}
332
333
	/**
334
	 * Get keywords parameter for URL
335
	 *
336
	 * @param string $keywords Search keywords
337
	 * @return string URL-encoded keywords parameter
338
	 */
339
	private function get_keywords_param(string $keywords): string
340
	{
341
		return !empty($keywords) ? '&amp;keywords=' . urlencode(htmlspecialchars_decode($keywords)) : '';
342
	}
343
344
	/**
345
	 * Assign common template variables
346
	 *
347
	 * @param string $s_limit_days
348
	 * @param string $s_sort_key
349
	 * @param string $s_sort_dir
350
	 * @param string $u_sort_param
351
	 * @param string $keywords
352
	 * @param int    $start
353
	 */
354
	private function assign_template_vars(string $s_limit_days, string $s_sort_key, string $s_sort_dir, string $u_sort_param, string $keywords, int $start): void
355
	{
356
		$this->template->assign_vars([
357
			'S_CLEARLOGS'  => $this->auth->acl_get('a_ppde_manage'),
358
			'S_KEYWORDS'   => $keywords,
359
			'S_LIMIT_DAYS' => $s_limit_days,
360
			'S_SORT_KEY'   => $s_sort_key,
361
			'S_SORT_DIR'   => $s_sort_dir,
362
			'U_ACTION'     => $this->u_action . '&amp;' . $u_sort_param . $this->get_keywords_param($keywords) . '&amp;start=' . $start,
363
		]);
364
	}
365
366
	/**
367
	 * Assign log entries to template
368
	 *
369
	 * @param array $log_data Array of log entries
370
	 */
371
	private function assign_log_entries_to_template(array $log_data): void
372
	{
373
		foreach ($log_data as $row)
374
		{
375
			$this->template->assign_block_vars('log', [
376
				'CONFIRMED'        => ($row['confirmed']) ? $this->language->lang('PPDE_DT_VERIFIED') : $this->language->lang('PPDE_DT_UNVERIFIED'),
377
				'DATE'             => $this->user->format_date($row['payment_date']),
378
				'ID'               => $row['transaction_id'],
379
				'PAYMENT_STATUS'   => $this->language->lang(['PPDE_DT_PAYMENT_STATUS_VALUES', strtolower($row['payment_status'])]),
380
				'TXN_ID'           => $row['txn_id'],
381
				'USERNAME'         => $row['username_full'],
382
				'S_CONFIRMED'      => (bool) $row['confirmed'],
383
				'S_PAYMENT_STATUS' => strtolower($row['payment_status']) === 'completed',
384
				'S_TXN_ERRORS'     => !empty($row['txn_errors']),
385
				'S_TEST_IPN'       => (bool) $row['test_ipn'],
386
			]);
387
		}
388
	}
389
390
	/**
391
	 * Set hidden fields for the transaction form
392
	 *
393
	 * @param string $id     Module id
394
	 * @param string $mode   Module category
395
	 * @param string $action Action name
396
	 */
397
	public function set_hidden_fields($id, $mode, $action): void
398
	{
399
		$this->args['action'] = $action;
400
		$this->args['hidden_fields'] = [
401
			'start'     => $this->request->variable('start', 0),
402
			'delall'    => $this->request->variable('delall', false, false, \phpbb\request\request_interface::POST),
403
			'delmarked' => $this->request->variable('delmarked', false, false, \phpbb\request\request_interface::POST),
404
			'i'         => $id,
405
			'mark'      => $this->request->variable('mark', [0]),
406
			'mode'      => $mode,
407
			'st'        => $this->request->variable('st', 0),
408
			'sk'        => $this->request->variable('sk', 't'),
409
			'sd'        => $this->request->variable('sd', 'd'),
410
		];
411
412
		// Prepare args depending on actions
413
		if (($this->args['hidden_fields']['delall'] || ($this->args['hidden_fields']['delmarked'] && count($this->args['hidden_fields']['mark']))) && $this->auth->acl_get('a_ppde_manage'))
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($this->args['hidden_fie...cl_get('a_ppde_manage'), Probably Intended Meaning: $this->args['hidden_fiel...l_get('a_ppde_manage'))
Loading history...
414
		{
415
			$this->args['action'] = 'delete';
416
		}
417
		else if ($this->request->is_set('approve'))
418
		{
419
			$this->args['action'] = 'approve';
420
			$this->args['hidden_fields'] = array_merge($this->args['hidden_fields'], [
421
				'approve'             => true,
422
				'id'                  => $this->request->variable('id', 0),
423
				'txn_errors_approved' => $this->request->variable('txn_errors_approved', 0),
424
			]);
425
		}
426
		else if ($this->request->is_set('add'))
427
		{
428
			$this->args['action'] = 'add';
429
		}
430
		else if ($this->request->is_set_post('change'))
431
		{
432
			$this->args['action'] = 'change';
433
		}
434
	}
435
436
	/**
437
	 * Get hidden fields for the transaction form
438
	 *
439
	 * @return array
440
	 */
441
	public function get_hidden_fields(): array
442
	{
443
		return array_merge(
444
			['i'                           => $this->args['hidden_fields']['i'],
445
			 'mode'                        => $this->args['hidden_fields']['mode'],
446
			 'action'                      => $this->args['action'],
447
			 $this->id_prefix_name . '_id' => $this->args[$this->id_prefix_name . '_id']],
448
			$this->args['hidden_fields']);
449
	}
450
451
	/**
452
	 * {@inheritdoc}
453
	 */
454
	public function change(): void
455
	{
456
		$username = $this->request->variable('username', '', true);
457
		$donor_id = $this->request->variable('donor_id', 0);
458
		$transaction_id = $this->request->variable('id', 0);
459
460
		try
461
		{
462
			$user_id = $this->validate_user_id($username, $donor_id);
463
			$this->update_transaction($transaction_id, $user_id);
464
			$this->log_action('DT_UPDATED');
465
		}
466
		catch (transaction_exception $e)
467
		{
468
			$this->output_errors($e->get_errors());
469
		}
470
	}
471
472
	/**
473
	 * Validate and return the user ID
474
	 *
475
	 * @param string $username
476
	 * @param int    $donor_id
477
	 * @return int
478
	 * @throws transaction_exception
479
	 */
480
	private function validate_user_id(string $username, int $donor_id = 0): int
481
	{
482
		if (empty($username) && ($donor_id === ANONYMOUS || $this->request->is_set('u')))
483
		{
484
			return ANONYMOUS;
485
		}
486
487
		$user_id = ($username !== '') ? $this->user_loader->load_user_by_username($username) : $donor_id;
488
489
		if ($user_id <= ANONYMOUS)
490
		{
491
			throw new transaction_exception([$this->language->lang('PPDE_MT_DONOR_NOT_FOUND')]);
492
		}
493
494
		return $user_id;
495
	}
496
497
	/**
498
	 * Update transaction with new user ID
499
	 *
500
	 * @param int $transaction_id
501
	 * @param int $user_id
502
	 * @throws transaction_exception
503
	 */
504
	private function update_transaction($transaction_id, $user_id): void
505
	{
506
		$this->ppde_entity->load($transaction_id);
507
508
		if (!$this->ppde_entity->data_exists($this->ppde_entity->build_sql_data_exists()))
509
		{
510
			throw new transaction_exception([$this->language->lang('PPDE_DT_NO_TRANSACTION')]);
511
		}
512
513
		$this->ppde_entity->set_user_id($user_id)->add_edit_data();
514
	}
515
516
	/**
517
	 * Log the action in the admin log
518
	 *
519
	 * @param string $action_type
520
	 */
521
	private function log_action(string $action_type): void
522
	{
523
		$this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_PPDE_' . $action_type);
524
		trigger_error($this->language->lang('PPDE_' . $action_type) . adm_back_link($this->u_action));
525
	}
526
527
	/**
528
	 * {@inheritdoc}
529
	 */
530
	public function add(): void
531
	{
532
		$transaction_data = $this->request_transaction_vars();
533
534
		if ($this->is_form_submitted())
535
		{
536
			try
537
			{
538
				$this->process_transaction($transaction_data);
539
				$this->log_action('MT_ADDED');
540
			}
541
			catch (transaction_exception $e)
542
			{
543
				$this->prepare_add_template($e->get_errors(), $transaction_data);
544
				return;
545
			}
546
		}
547
548
		$this->prepare_add_template([], $transaction_data);
549
	}
550
551
	/**
552
	 * Request transaction variables from the form
553
	 *
554
	 * @return array
555
	 */
556
	private function request_transaction_vars(): array
557
	{
558
		return [
559
			'MT_ANONYMOUS'          => $this->request->is_set('u'),
560
			'MT_USERNAME'           => $this->request->variable('username', '', true),
561
			'MT_FIRST_NAME'         => $this->request->variable('first_name', '', true),
562
			'MT_LAST_NAME'          => $this->request->variable('last_name', '', true),
563
			'MT_PAYER_EMAIL'        => $this->request->variable('payer_email', '', true),
564
			'MT_RESIDENCE_COUNTRY'  => $this->request->variable('residence_country', ''),
565
			'MT_MC_GROSS'           => $this->request->variable('mc_gross', 0.0),
566
			'MT_MC_CURRENCY'        => $this->request->variable('mc_currency', ''),
567
			'MT_MC_FEE'             => $this->request->variable('mc_fee', 0.0),
568
			'MT_PAYMENT_DATE_YEAR'  => $this->request->variable('payment_date_year', (int) $this->user->format_date(time(), 'Y')),
569
			'MT_PAYMENT_DATE_MONTH' => $this->request->variable('payment_date_month', (int) $this->user->format_date(time(), 'n')),
570
			'MT_PAYMENT_DATE_DAY'   => $this->request->variable('payment_date_day', (int) $this->user->format_date(time(), 'j')),
571
			'MT_PAYMENT_TIME'       => $this->request->variable('payment_time', $this->user->format_date(time(), 'H:i:s')),
572
			'MT_MEMO'               => $this->request->variable('memo', '', true),
573
		];
574
	}
575
576
	/**
577
	 * Process a transaction with the given transaction data and handle any errors that occur
578
	 *
579
	 * @param array $transaction_data The data for the transaction
580
	 * @throws transaction_exception
581
	 */
582
	private function process_transaction(array $transaction_data): void
583
	{
584
		$data_ary = $this->build_data_ary($transaction_data);
585
		$this->ppde_actions->log_to_db($data_ary);
586
587
		$this->ppde_actions->set_transaction_data($transaction_data);
588
		$this->ppde_actions->is_donor_is_member();
589
590
		$this->do_transactions_actions(
591
			$this->ppde_actions->get_donor_is_member() && !$transaction_data['MT_ANONYMOUS']
592
		);
593
	}
594
595
	/**
596
	 * Prepare data array before sending it to $this->entity
597
	 *
598
	 * @param array $transaction_data
599
	 * @return array
600
	 * @throws transaction_exception
601
	 */
602
	private function build_data_ary(array $transaction_data): array
603
	{
604
		$errors = [];
605
606
		try
607
		{
608
			$user_id = $this->validate_user_id($transaction_data['MT_USERNAME']);
609
		}
610
		catch (transaction_exception $e)
611
		{
612
			$errors = array_merge($errors, $e->get_errors());
613
		}
614
615
		try
616
		{
617
			$payment_date_time = $this->validate_payment_date_time($transaction_data);
618
		}
619
		catch (transaction_exception $e)
620
		{
621
			$errors = array_merge($errors, $e->get_errors());
622
		}
623
624
		try
625
		{
626
			$this->validate_transaction_amounts($transaction_data);
627
		}
628
		catch (transaction_exception $e)
629
		{
630
			$errors = array_merge($errors, $e->get_errors());
631
		}
632
633
		if (!empty($errors))
634
		{
635
			throw new transaction_exception($errors);
636
		}
637
638
		return [
639
			'business'          => $this->config['ppde_account_id'],
640
			'confirmed'         => true,
641
			'custom'            => implode('_', ['uid', $user_id, time()]),
642
			'exchange_rate'     => '',
643
			'first_name'        => $transaction_data['MT_FIRST_NAME'],
644
			'item_name'         => '',
645
			'item_number'       => implode('_', ['uid', $user_id, time()]),
646
			'last_name'         => $transaction_data['MT_LAST_NAME'],
647
			'mc_currency'       => $transaction_data['MT_MC_CURRENCY'],
648
			'mc_gross'          => $transaction_data['MT_MC_GROSS'],
649
			'mc_fee'            => $transaction_data['MT_MC_FEE'],
650
			'net_amount'        => 0.0, // This value is calculated in core_actions:log_to_db()
651
			'parent_txn_id'     => '',
652
			'payer_email'       => $transaction_data['MT_PAYER_EMAIL'],
653
			'payer_id'          => '',
654
			'payer_status'      => '',
655
			'payment_date'      => $payment_date_time,
656
			'payment_status'    => 'Completed',
657
			'payment_type'      => '',
658
			'memo'              => $transaction_data['MT_MEMO'],
659
			'receiver_id'       => '',
660
			'receiver_email'    => '',
661
			'residence_country' => strtoupper($transaction_data['MT_RESIDENCE_COUNTRY']),
662
			'settle_amount'     => 0.0,
663
			'settle_currency'   => '',
664
			'test_ipn'          => false,
665
			'txn_errors'        => '',
666
			'txn_id'            => 'PPDE' . gen_rand_string(13),
667
			'txn_type'          => 'ppde_manual_donation',
668
			'user_id'           => $user_id,
669
		];
670
	}
671
672
	/**
673
	 * Validate the payment date and time
674
	 *
675
	 * @param array $transaction_data
676
	 * @return int
677
	 * @throws transaction_exception
678
	 */
679
	private function validate_payment_date_time(array $transaction_data)
680
	{
681
		$payment_date = implode('-', [
682
			$transaction_data['MT_PAYMENT_DATE_YEAR'],
683
			$transaction_data['MT_PAYMENT_DATE_MONTH'],
684
			$transaction_data['MT_PAYMENT_DATE_DAY'],
685
		]);
686
687
		$payment_time = $transaction_data['MT_PAYMENT_TIME'];
688
		$date_time_string = $payment_date . ' ' . $payment_time;
689
690
		$payment_date_time = $this->parse_date_time($date_time_string);
691
692
		if ($payment_date_time === false)
693
		{
694
			throw new transaction_exception([$this->language->lang('PPDE_MT_PAYMENT_DATE_ERROR', $date_time_string)]);
695
		}
696
697
		if ($payment_date_time > time())
698
		{
699
			throw new transaction_exception([$this->language->lang('PPDE_MT_PAYMENT_DATE_FUTURE', $this->user->format_date($payment_date_time))]);
700
		}
701
702
		// Validate time
703
		$time_parts = explode(':', $payment_time);
704
		if (count($time_parts) < 2 || count($time_parts) > 3)
705
		{
706
			throw new transaction_exception([$this->language->lang('PPDE_MT_PAYMENT_TIME_ERROR', $payment_time)]);
707
		}
708
709
		$hours = (int) $time_parts[0];
710
		$minutes = (int) $time_parts[1];
711
		$seconds = isset($time_parts[2]) ? (int) $time_parts[2] : 0;
712
713
		if ($hours >= 24 || $minutes >= 60 || $seconds >= 60)
714
		{
715
			throw new transaction_exception([$this->language->lang('PPDE_MT_PAYMENT_TIME_ERROR', $payment_time)]);
716
		}
717
718
		return $payment_date_time;
719
	}
720
721
	/**
722
	 * Parse date and time string
723
	 *
724
	 * @param string $date_time_string
725
	 * @return int|false
726
	 */
727
	private function parse_date_time($date_time_string)
728
	{
729
		$formats = ['Y-m-d H:i:s', 'Y-m-d G:i', 'Y-m-d h:i:s a', 'Y-m-d g:i A'];
730
731
		foreach ($formats as $format)
732
		{
733
			$parsed = \DateTime::createFromFormat($format, $date_time_string);
734
			if ($parsed !== false)
735
			{
736
				return $parsed->getTimestamp();
737
			}
738
		}
739
740
		return false;
741
	}
742
743
	/**
744
	 * Validate transaction amounts
745
	 *
746
	 * @param array $transaction_data
747
	 * @throws transaction_exception
748
	 */
749
	private function validate_transaction_amounts(array $transaction_data)
750
	{
751
		$errors = [];
752
753
		if ($transaction_data['MT_MC_GROSS'] <= 0)
754
		{
755
			$errors[] = $this->language->lang('PPDE_MT_MC_GROSS_TOO_LOW');
756
		}
757
758
		if ($transaction_data['MT_MC_FEE'] < 0)
759
		{
760
			$errors[] = $this->language->lang('PPDE_MT_MC_FEE_NEGATIVE');
761
		}
762
763
		if ($transaction_data['MT_MC_FEE'] >= $transaction_data['MT_MC_GROSS'])
764
		{
765
			$errors[] = $this->language->lang('PPDE_MT_MC_FEE_TOO_HIGH');
766
		}
767
768
		if (!empty($errors))
769
		{
770
			throw new transaction_exception($errors);
771
		}
772
	}
773
774
	/**
775
	 * Perform actions for validated transaction
776
	 *
777
	 * @param bool $is_member
778
	 */
779
	private function do_transactions_actions($is_member): void
780
	{
781
		$this->ppde_actions->update_overview_stats();
782
		$this->ppde_actions->update_raised_amount();
783
784
		if ($is_member)
785
		{
786
			$this->ppde_actions->update_donor_stats();
787
			$this->ppde_actions->donors_group_user_add();
788
			$this->ppde_actions->notification->notify_donor_donation_received();
789
		}
790
	}
791
792
	/**
793
	 * Prepare and assign template variables for adding a new transaction
794
	 *
795
	 * @param array $errors           Array of error messages
796
	 * @param array $transaction_data Transaction data to be displayed in the form
797
	 */
798
	private function prepare_add_template(array $errors, array $transaction_data): void
799
	{
800
		$this->ppde_actions_currency->build_currency_select_menu((int) $this->config['ppde_default_currency']);
801
		$this->s_error_assign_template_vars($errors);
802
		$this->template->assign_vars($transaction_data);
803
		$this->template->assign_vars([
804
			'U_ACTION'             => $this->u_action,
805
			'U_BACK'               => $this->u_action,
806
			'S_ADD'                => true,
807
			'ANONYMOUS_USER_ID'    => ANONYMOUS,
808
			'U_FIND_USERNAME'      => append_sid($this->phpbb_root_path . 'memberlist.' . $this->php_ext, 'mode=searchuser&amp;form=manual_transaction&amp;field=username&amp;select_single=true'),
809
			'PAYMENT_TIME_FORMATS' => $this->get_payment_time_examples(),
810
		]);
811
	}
812
813
	/**
814
	 * Returns a list of valid times that the user can provide in the manual transaction form
815
	 *
816
	 * @return array Array of strings representing the current time, each in a different format
817
	 */
818
	private function get_payment_time_examples(): array
819
	{
820
		$formats = [
821
			'H:i:s',
822
			'G:i',
823
			'h:i:s a',
824
			'g:i A',
825
		];
826
827
		$examples = [];
828
829
		foreach ($formats as $format)
830
		{
831
			$examples[] = $this->user->format_date(time(), $format);
832
		}
833
834
		return $examples;
835
	}
836
837
	/**
838
	 * Output errors
839
	 *
840
	 * @param array $errors
841
	 */
842
	private function output_errors(array $errors)
843
	{
844
		trigger_error(implode('<br>', $errors) . adm_back_link($this->u_action), E_USER_WARNING);
845
	}
846
847
	/**
848
	 * Approve a transaction
849
	 */
850
	public function approve(): void
851
	{
852
		$transaction_id = (int) $this->args['hidden_fields']['id'];
853
		$txn_approved = empty($this->args['hidden_fields']['txn_errors_approved']);
854
855
		// Update DB record
856
		$this->ppde_entity->load($transaction_id);
857
		$this->ppde_entity->set_txn_errors_approved($txn_approved);
858
		$this->ppde_entity->save(false);
859
860
		// Prepare transaction settings before doing actions
861
		$transaction_data = $this->ppde_entity->get_data($this->ppde_operator->build_sql_data($transaction_id));
862
		$this->ppde_actions->set_transaction_data($transaction_data[0]);
863
		$this->ppde_actions->set_ipn_test_properties($this->ppde_entity->get_test_ipn());
864
		$this->ppde_actions->is_donor_is_member();
865
866
		if ($txn_approved)
867
		{
868
			$this->do_transactions_actions(!$this->ppde_actions->get_ipn_test() && $this->ppde_actions->get_donor_is_member());
869
		}
870
871
		$this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_' . $this->lang_key_prefix . '_UPDATED', time());
872
	}
873
874
	/**
875
	 * View transaction details
876
	 */
877
	public function view(): void
878
	{
879
		// Request Identifier of the transaction
880
		$transaction_id = (int) $this->request->variable('id', 0);
881
882
		// Add additional fields to the table schema needed by entity->import()
883
		$additional_table_schema = [
884
			'item_username'    => ['name' => 'username', 'type' => 'string'],
885
			'item_user_colour' => ['name' => 'user_colour', 'type' => 'string'],
886
		];
887
888
		// Grab transaction data
889
		$data_ary = $this->ppde_entity->get_data($this->ppde_operator->build_sql_data($transaction_id), $additional_table_schema);
890
891
		array_map([$this, 'action_assign_template_vars'], $data_ary);
892
893
		$this->template->assign_vars([
894
			'U_FIND_USERNAME' => append_sid($this->phpbb_root_path . 'memberlist.' . $this->php_ext, 'mode=searchuser&amp;form=view_transactions&amp;field=username&amp;select_single=true'),
895
			'U_ACTION'        => $this->u_action,
896
			'U_BACK'          => $this->u_action,
897
			'S_VIEW'          => true,
898
		]);
899
	}
900
901
	/**
902
	 * Delete transaction(s)
903
	 */
904
	public function delete(): void
905
	{
906
		$where_sql = '';
907
908
		if ($this->args['hidden_fields']['delmarked'] && count($this->args['hidden_fields']['mark']))
909
		{
910
			$where_sql = $this->ppde_operator->build_marked_where_sql($this->args['hidden_fields']['mark']);
911
		}
912
913
		if ($where_sql || $this->args['hidden_fields']['delall'])
914
		{
915
			$this->ppde_entity->delete(0, '', $where_sql, $this->args['hidden_fields']['delall']);
916
			$this->ppde_actions->set_ipn_test_properties(true);
917
			$this->ppde_actions->update_overview_stats();
918
			$this->ppde_actions->set_ipn_test_properties(false);
919
			$this->ppde_actions->update_overview_stats();
920
			$this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_' . $this->lang_key_prefix . '_PURGED', time());
921
		}
922
	}
923
924
	/**
925
	 * Assign action template variables
926
	 *
927
	 * @param array $data Transaction data
928
	 */
929
	protected function action_assign_template_vars(array $data): void
930
	{
931
		$this->assign_hidden_fields($data);
932
		$this->assign_currency_data($data);
933
		$this->assign_user_data($data);
934
		$this->assign_transaction_details($data);
935
		$this->assign_payment_details($data);
936
		$this->assign_error_data($data);
937
	}
938
939
	/**
940
	 * Assign hidden fields
941
	 *
942
	 * @param array $data
943
	 */
944
	private function assign_hidden_fields(array $data): void
945
	{
946
		$s_hidden_fields = build_hidden_fields([
947
			'id'                  => $data['transaction_id'],
948
			'donor_id'            => $data['user_id'],
949
			'txn_errors_approved' => $data['txn_errors_approved'],
950
		]);
951
		$this->template->assign_var('S_HIDDEN_FIELDS', $s_hidden_fields);
952
	}
953
954
	/**
955
	 * Assign currency data to template variables
956
	 *
957
	 * @param array $data Transaction data
958
	 */
959
	private function assign_currency_data(array $data): void
960
	{
961
		$this->ppde_actions_currency->set_currency_data_from_iso_code($data['mc_currency']);
962
		$this->ppde_actions_currency->set_currency_data_from_iso_code($data['settle_currency']);
963
964
		$this->template->assign_vars([
965
			'EXCHANGE_RATE'                   => '1 ' . $data['mc_currency'] . ' = ' . $data['exchange_rate'] . ' ' . $data['settle_currency'],
966
			'MC_GROSS'                        => $this->ppde_actions_currency->format_currency($data['mc_gross']),
967
			'MC_FEE'                          => $this->ppde_actions_currency->format_currency($data['mc_fee']),
968
			'MC_NET'                          => $this->ppde_actions_currency->format_currency($data['net_amount']),
969
			'SETTLE_AMOUNT'                   => $this->ppde_actions_currency->format_currency($data['settle_amount']),
970
			'L_PPDE_DT_SETTLE_AMOUNT'         => $this->language->lang('PPDE_DT_SETTLE_AMOUNT', $data['settle_currency']),
971
			'L_PPDE_DT_EXCHANGE_RATE_EXPLAIN' => $this->language->lang('PPDE_DT_EXCHANGE_RATE_EXPLAIN', $this->user->format_date($data['payment_date'])),
972
			'S_CONVERT'                       => !((int) $data['settle_amount'] === 0 && empty($data['exchange_rate'])),
973
		]);
974
	}
975
976
	/**
977
	 * Assign user data to template variables
978
	 *
979
	 * @param array $data Transaction data
980
	 */
981
	private function assign_user_data(array $data): void
982
	{
983
		$this->template->assign_vars([
984
			'BOARD_USERNAME' => get_username_string('full', $data['user_id'], $data['username'], $data['user_colour'], $this->language->lang('GUEST'), append_sid($this->phpbb_admin_path . 'index.' . $this->php_ext, 'i=users&amp;mode=overview')),
985
			'NAME'           => $data['first_name'] . ' ' . $data['last_name'],
986
			'PAYER_EMAIL'    => $data['payer_email'],
987
			'PAYER_ID'       => $data['payer_id'],
988
			'PAYER_STATUS'   => $data['payer_status'] ? $this->language->lang('PPDE_DT_VERIFIED') : $this->language->lang('PPDE_DT_UNVERIFIED'),
989
		]);
990
	}
991
992
	/**
993
	 * Assign transaction details to template variables
994
	 *
995
	 * @param array $data Transaction data
996
	 */
997
	private function assign_transaction_details(array $data): void
998
	{
999
		$this->template->assign_vars([
1000
			'ITEM_NAME'      => $data['item_name'],
1001
			'ITEM_NUMBER'    => $data['item_number'],
1002
			'MEMO'           => $data['memo'],
1003
			'RECEIVER_EMAIL' => $data['receiver_email'],
1004
			'RECEIVER_ID'    => $data['receiver_id'],
1005
			'TXN_ID'         => $data['txn_id'],
1006
		]);
1007
	}
1008
1009
	/**
1010
	 * Assign payment details to template variables
1011
	 *
1012
	 * @param array $data Transaction data
1013
	 */
1014
	private function assign_payment_details(array $data): void
1015
	{
1016
		$this->template->assign_vars([
1017
			'PAYMENT_DATE'   => $this->user->format_date($data['payment_date']),
1018
			'PAYMENT_STATUS' => $this->language->lang(['PPDE_DT_PAYMENT_STATUS_VALUES', strtolower($data['payment_status'])]),
1019
		]);
1020
	}
1021
1022
	/**
1023
	 * Assign error data to template variables
1024
	 *
1025
	 * @param array $data Transaction data
1026
	 */
1027
	private function assign_error_data(array $data): void
1028
	{
1029
		$this->template->assign_vars([
1030
			'S_ERROR'          => !empty($data['txn_errors']),
1031
			'S_ERROR_APPROVED' => !empty($data['txn_errors_approved']),
1032
			'ERROR_MSG'        => (!empty($data['txn_errors'])) ? $data['txn_errors'] : '',
1033
		]);
1034
	}
1035
}
1036