PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.14.2
Yatra – Travel Booking & Tour Operator Software v3.0.14.2
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / app / Repositories / PaymentRepository.php

PaymentRepository.php in Yatra – Travel Booking & Tour Operator Software 3.0.14.2, at app/Repositories/PaymentRepository.php

518 lines 15.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Repositories;
6
7 use Yatra\Database\Tables\BookingPaymentsTable;
8 use Yatra\Database\Tables\BookingsTable;
9 use Yatra\Database\Tables\TripsTable;
10
11 /**
12 * Payment Repository
13 *
14 * Handles all database operations for booking payments.
15 *
16 * @package Yatra\Repositories
17 */
18 class PaymentRepository extends BaseRepository
19 {
20 /**
21 * Table name without prefix
22 */
23 // private const TABLE_NAME = 'yatra_booking_payments';
24
25 /** @var bool|null */
26 private ?bool $customerColumnExists = null;
27
28 /**
29 * Get full table name with prefix
30 */
31 protected function getTableName(): string
32 {
33 return BookingPaymentsTable::getTableName();
34 }
35
36 private function hasCustomerColumn(): bool
37 {
38 if ($this->customerColumnExists !== null) {
39 return $this->customerColumnExists;
40 }
41
42 $table = esc_sql($this->getTableName());
43 $column = $this->wpdb->get_var(
44 $this->wpdb->prepare(
45 "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'customer_id'",
46 $table
47 )
48 );
49
50 $this->customerColumnExists = ((int) $column) > 0;
51 return $this->customerColumnExists;
52 }
53
54 /**
55 * Get bookings table name
56 */
57 protected function getBookingsTable(): string
58 {
59 return BookingsTable::getTableName();
60 }
61
62 /**
63 * Get trips table name
64 */
65 protected function getTripsTable(): string
66 {
67 return TripsTable::getTableName();
68 }
69
70 /**
71 * Get paginated payments with filters
72 *
73 * @param array $filters Filter options
74 * @return array {data: array, total: int, page: int, per_page: int, total_pages: int}
75 */
76 public function paginate(array $filters = []): array
77 {
78 $table = $this->getTableName();
79 $bookings_table = $this->getBookingsTable();
80 $trips_table = $this->getTripsTable();
81
82 // Pagination
83 $page = max(1, (int) ($filters['page'] ?? 1));
84 $per_page = max(1, min(100, (int) ($filters['per_page'] ?? 20)));
85 $offset = ($page - 1) * $per_page;
86
87 // Build WHERE clause
88 $where_clauses = ['1=1'];
89 $where_values = [];
90
91 if (!empty($filters['booking_id'])) {
92 $where_clauses[] = 'p.booking_id = %d';
93 $where_values[] = (int) $filters['booking_id'];
94 }
95
96 if (!empty($filters['status'])) {
97 $where_clauses[] = 'p.status = %s';
98 $where_values[] = sanitize_text_field($filters['status']);
99 }
100
101 if (!empty($filters['gateway'])) {
102 $where_clauses[] = 'p.gateway = %s';
103 $where_values[] = sanitize_text_field($filters['gateway']);
104 }
105
106 if (!empty($filters['search'])) {
107 $search_like = '%' . $this->wpdb->esc_like(sanitize_text_field($filters['search'])) . '%';
108 $where_clauses[] = '(p.transaction_id LIKE %s OR b.reference LIKE %s OR b.contact_email LIKE %s)';
109 $where_values = array_merge($where_values, [$search_like, $search_like, $search_like]);
110 }
111
112 if (!empty($filters['date_from'])) {
113 $where_clauses[] = 'p.created_at >= %s';
114 $where_values[] = sanitize_text_field($filters['date_from']);
115 }
116
117 if (!empty($filters['date_to'])) {
118 $where_clauses[] = 'p.created_at <= %s';
119 $where_values[] = sanitize_text_field($filters['date_to']);
120 }
121
122 $where_sql = implode(' AND ', $where_clauses);
123
124 // Get total count
125 $count_query = "SELECT COUNT(*)
126 FROM {$table} p
127 LEFT JOIN {$bookings_table} b ON p.booking_id = b.id
128 WHERE {$where_sql}";
129 if (!empty($where_values)) {
130 $count_query = $this->wpdb->prepare($count_query, ...$where_values);
131 }
132 $total = (int) $this->wpdb->get_var($count_query);
133
134 // Resolve the sort column from a strict whitelist. ORDER BY cannot be
135 // parameterised with $wpdb->prepare (that's for values), so the column
136 // MUST come from this map of known-safe expressions and the direction is
137 // constrained to ASC/DESC — user input never reaches the SQL directly.
138 $sortColumns = [
139 'payment' => 'p.id',
140 'customer' => 'b.contact_first_name',
141 'amount' => 'p.amount',
142 'method' => 'p.gateway',
143 'status' => 'p.status',
144 'date' => 'p.created_at',
145 'payment_date' => 'p.created_at',
146 'created_at' => 'p.created_at',
147 'transaction_id' => 'p.transaction_id',
148 ];
149 $orderColumn = $sortColumns[(string) ($filters['orderby'] ?? '')] ?? 'p.created_at';
150 $orderDir = strtoupper((string) ($filters['order'] ?? '')) === 'ASC' ? 'ASC' : 'DESC';
151 // Stable tie-breaker so equal values keep a deterministic order across pages.
152 $order_sql = $orderColumn . ' ' . $orderDir . ', p.id DESC';
153
154 // Get payments with booking and trip info
155 $query = "SELECT p.*,
156 b.reference as booking_reference,
157 b.contact_email,
158 b.contact_first_name,
159 b.contact_last_name,
160 t.title as trip_title
161 FROM {$table} p
162 LEFT JOIN {$bookings_table} b ON p.booking_id = b.id
163 LEFT JOIN {$trips_table} t ON b.trip_id = t.id
164 WHERE {$where_sql}
165 ORDER BY {$order_sql}
166 LIMIT %d OFFSET %d";
167
168 $query_values = array_merge($where_values, [$per_page, $offset]);
169 $payments = $this->wpdb->get_results($this->wpdb->prepare($query, ...$query_values));
170
171 return [
172 'data' => $payments ?: [],
173 'total' => $total,
174 'page' => $page,
175 'per_page' => $per_page,
176 'total_pages' => (int) ceil($total / $per_page),
177 ];
178 }
179
180 /**
181 * Find payment by ID with booking info
182 *
183 * @param int $id Payment ID
184 * @return object|null
185 */
186 public function findWithBooking(int $id): ?object
187 {
188 $table = $this->getTableName();
189 $bookings_table = $this->getBookingsTable();
190 $trips_table = $this->getTripsTable();
191
192 $query = $this->wpdb->prepare(
193 "SELECT p.*,
194 b.reference as booking_reference,
195 b.user_id as booking_user_id,
196 b.contact_email,
197 b.contact_first_name,
198 b.contact_last_name,
199 b.contact_data,
200 b.contact_country,
201 b.customer_id,
202 b.total_amount as booking_total_amount,
203 b.amount_paid as booking_amount_paid,
204 b.amount_due as booking_amount_due,
205 b.travel_date as travel_date,
206 b.end_date as booking_end_date,
207 b.trip_id as trip_id,
208 b.payment_method as payment_method,
209 t.title as trip_title,
210 t.duration_days as trip_duration_days,
211 t.duration_nights as trip_duration_nights
212 FROM {$table} p
213 LEFT JOIN {$bookings_table} b ON p.booking_id = b.id
214 LEFT JOIN {$trips_table} t ON b.trip_id = t.id
215 WHERE p.id = %d",
216 $id
217 );
218
219 return $this->wpdb->get_row($query) ?: null;
220 }
221
222 /**
223 * Find payments by booking ID
224 *
225 * @param int $bookingId Booking ID
226 * @return array
227 */
228 public function findByBookingId(int $bookingId): array
229 {
230 $table = $this->getTableName();
231
232 $query = $this->wpdb->prepare(
233 "SELECT * FROM {$table} WHERE booking_id = %d ORDER BY created_at DESC",
234 $bookingId
235 );
236
237 return $this->wpdb->get_results($query) ?: [];
238 }
239
240 /**
241 * Find the latest payment for a booking
242 *
243 * @param int $bookingId Booking ID
244 * @return object|null
245 */
246 public function findLatestByBookingId(int $bookingId): ?object
247 {
248 $table = $this->getTableName();
249
250 $query = $this->wpdb->prepare(
251 "SELECT * FROM {$table} WHERE booking_id = %d ORDER BY created_at DESC, id DESC LIMIT 1",
252 $bookingId
253 );
254
255 return $this->wpdb->get_row($query) ?: null;
256 }
257
258 /**
259 * Find payment by transaction ID
260 *
261 * @param string $transactionId Transaction ID
262 * @return object|null
263 */
264 public function findByTransactionId(string $transactionId): ?object
265 {
266 $table = $this->getTableName();
267
268 $query = $this->wpdb->prepare(
269 "SELECT * FROM {$table} WHERE transaction_id = %s",
270 sanitize_text_field($transactionId)
271 );
272
273 return $this->wpdb->get_row($query) ?: null;
274 }
275
276 /**
277 * Create a new payment
278 *
279 * @param array $data Payment data
280 * @return int Payment ID on success
281 * @throws \Exception on failure
282 */
283 public function create(array $data): int
284 {
285 $table = $this->getTableName();
286
287 $insertData = $this->preparePaymentData($data);
288 $insertData['created_at'] = current_time('mysql');
289
290 $result = $this->wpdb->insert($table, $insertData);
291
292 if ($result === false) {
293 throw new \Exception('Failed to create payment: ' . $this->wpdb->last_error);
294 }
295
296 return $this->wpdb->insert_id;
297 }
298
299 /**
300 * Update a payment
301 *
302 * @param int $id Payment ID
303 * @param array $data Payment data to update
304 * @return bool
305 */
306 public function update(int $id, array $data): bool
307 {
308 $table = $this->getTableName();
309
310 $updateData = $this->preparePaymentData($data);
311
312 $result = $this->wpdb->update(
313 $table,
314 $updateData,
315 ['id' => $id],
316 null,
317 ['%d']
318 );
319
320 return $result !== false;
321 }
322
323 /**
324 * Update payment status
325 *
326 * @param int $id Payment ID
327 * @param string $status New status
328 * @return bool
329 */
330 public function updateStatus(int $id, string $status): bool
331 {
332 $table = $this->getTableName();
333
334 $data = ['status' => sanitize_text_field($status)];
335
336 if ($status === 'completed') {
337 $data['processed_at'] = current_time('mysql');
338 }
339
340 $result = $this->wpdb->update($table, $data, ['id' => $id]);
341
342 return $result !== false;
343 }
344
345 /**
346 * Delete a payment
347 *
348 * @param int $id Payment ID
349 * @return bool
350 */
351 public function delete(int $id): bool
352 {
353 $table = $this->getTableName();
354
355 $result = $this->wpdb->delete($table, ['id' => $id], ['%d']);
356
357 return $result !== false;
358 }
359
360 /**
361 * Get total paid amount for a booking
362 *
363 * @param int $bookingId Booking ID
364 * @return float
365 */
366 public function getTotalPaidForBooking(int $bookingId): float
367 {
368 $table = $this->getTableName();
369
370 $result = $this->wpdb->get_var($this->wpdb->prepare(
371 "SELECT SUM(amount) FROM {$table} WHERE booking_id = %d AND status = 'completed'",
372 $bookingId
373 ));
374
375 return (float) ($result ?? 0);
376 }
377
378 /**
379 * Get payment statistics
380 *
381 * @return array
382 */
383 public function getStats(): array
384 {
385 $table = $this->getTableName();
386
387 // Total by status
388 $statusStats = $this->wpdb->get_results(
389 "SELECT status, COUNT(*) as count, SUM(amount) as total_amount
390 FROM {$table} GROUP BY status",
391 OBJECT_K
392 );
393
394 // By gateway
395 $gatewayStats = $this->wpdb->get_results(
396 "SELECT gateway, COUNT(*) as count, SUM(amount) as total_amount
397 FROM {$table} WHERE status = 'completed' GROUP BY gateway",
398 OBJECT_K
399 );
400
401 // This month
402 $thisMonth = $this->wpdb->get_row($this->wpdb->prepare(
403 "SELECT COUNT(*) as count, SUM(amount) as total_amount
404 FROM {$table} WHERE status = 'completed' AND created_at >= %s",
405 date('Y-m-01 00:00:00')
406 ));
407
408 return [
409 'by_status' => $statusStats,
410 'by_gateway' => $gatewayStats,
411 'this_month' => [
412 'count' => (int) ($thisMonth->count ?? 0),
413 'total_amount' => (float) ($thisMonth->total_amount ?? 0),
414 ],
415 ];
416 }
417
418 /**
419 * Flat counts for admin list toolbar (matches payment status filter keys).
420 *
421 * @return array{all:int,completed:int,pending:int,partial:int,failed:int,refunded:int,cancelled:int}
422 */
423 public function getAdminStatusCounts(): array
424 {
425 $table = $this->getTableName();
426 $rows = $this->wpdb->get_results(
427 "SELECT status, COUNT(*) AS c FROM {$table} GROUP BY status",
428 ARRAY_A
429 ) ?: [];
430
431 $out = [
432 'all' => 0,
433 'completed' => 0,
434 'pending' => 0,
435 'partial' => 0,
436 'failed' => 0,
437 'refunded' => 0,
438 'cancelled' => 0,
439 ];
440
441 foreach ($rows as $row) {
442 $status = isset($row['status']) ? (string) $row['status'] : '';
443 $c = isset($row['c']) ? (int) $row['c'] : 0;
444 $out['all'] += $c;
445 if ($status !== '' && array_key_exists($status, $out)) {
446 $out[$status] = $c;
447 }
448 }
449
450 return $out;
451 }
452
453 /**
454 * Prepare payment data for insert/update
455 *
456 * @param array $data Raw data
457 * @return array Sanitized data
458 */
459 private function preparePaymentData(array $data): array
460 {
461 $prepared = [];
462
463 if (array_key_exists('booking_id', $data)) {
464 $prepared['booking_id'] = (int) $data['booking_id'];
465 }
466
467 if (array_key_exists('customer_id', $data) && $this->hasCustomerColumn()) {
468 $prepared['customer_id'] = $data['customer_id'] !== null ? (int) $data['customer_id'] : null;
469 }
470
471 if (array_key_exists('transaction_id', $data)) {
472 $prepared['transaction_id'] = sanitize_text_field((string) $data['transaction_id']);
473 }
474
475 if (array_key_exists('gateway', $data)) {
476 $prepared['gateway'] = sanitize_text_field((string) $data['gateway']);
477 }
478
479 if (array_key_exists('amount', $data)) {
480 $prepared['amount'] = (float) $data['amount'];
481 }
482
483 if (array_key_exists('currency', $data)) {
484 $prepared['currency'] = sanitize_text_field((string) $data['currency']);
485 }
486
487 if (array_key_exists('status', $data)) {
488 $prepared['status'] = sanitize_text_field((string) $data['status']);
489 }
490
491 if (array_key_exists('payment_type', $data)) {
492 $prepared['payment_type'] = sanitize_text_field((string) $data['payment_type']);
493 }
494
495 if (array_key_exists('gateway_response', $data)) {
496 $prepared['gateway_response'] = is_string($data['gateway_response'])
497 ? $data['gateway_response']
498 : wp_json_encode($data['gateway_response']);
499 }
500
501 if (array_key_exists('notes', $data)) {
502 $prepared['notes'] = sanitize_textarea_field((string) $data['notes']);
503 }
504
505 if (array_key_exists('meta', $data)) {
506 $prepared['meta'] = is_string($data['meta'])
507 ? $data['meta']
508 : wp_json_encode($data['meta']);
509 }
510
511 if (array_key_exists('processed_at', $data) && $data['processed_at']) {
512 $prepared['processed_at'] = sanitize_text_field($data['processed_at']);
513 }
514
515 return $prepared;
516 }
517 }
518