PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.8
Yatra – Travel Booking & Tour Operator Software v3.0.8
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 / Services / PaymentService.php

PaymentService.php in Yatra – Travel Booking & Tour Operator Software 3.0.8, at app/Services/PaymentService.php

357 lines 11.6 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\Services;
6
7 use Yatra\Repositories\PaymentRepository;
8 use Yatra\Repositories\BookingRepository;
9
10 /**
11 * Payment Service
12 *
13 * Contains business logic for payments.
14 *
15 * @package Yatra\Services
16 */
17 class PaymentService
18 {
19 private PaymentRepository $paymentRepository;
20 private BookingRepository $bookingRepository;
21
22 public function __construct()
23 {
24 $this->paymentRepository = new PaymentRepository();
25 $this->bookingRepository = new BookingRepository();
26 }
27
28 /**
29 * Get paginated payments
30 *
31 * @param array $filters Filters
32 * @return array
33 */
34 public function getPayments(array $filters = []): array
35 {
36 $result = $this->paymentRepository->paginate($filters);
37
38 $result['data'] = array_map([$this, 'formatPayment'], $result['data']);
39
40 return $result;
41 }
42
43 /**
44 * Get single payment
45 *
46 * @param int $id Payment ID
47 * @return array|null
48 */
49 public function getPayment(int $id): ?array
50 {
51 $payment = $this->paymentRepository->findWithBooking($id);
52
53 if (!$payment) {
54 return null;
55 }
56
57 return $this->formatPayment($payment);
58 }
59
60 /**
61 * Get payments for a booking
62 *
63 * @param int $bookingId Booking ID
64 * @return array
65 */
66 public function getBookingPayments(int $bookingId): array
67 {
68 $payments = $this->paymentRepository->findByBookingId($bookingId);
69
70 return array_map([$this, 'formatPayment'], $payments);
71 }
72
73 /**
74 * Create a new payment
75 *
76 * @param array $data Payment data
77 * @return array {success: bool, payment_id?: int, message: string}
78 */
79 public function createPayment(array $data): array
80 {
81 // Validate booking exists
82 $booking = $this->bookingRepository->find((int) $data['booking_id']);
83
84 if (!$booking) {
85 return ['success' => false, 'message' => __('Booking not found.', 'yatra')];
86 }
87
88 // Create payment
89 $paymentId = $this->paymentRepository->create($data);
90
91 if (!$paymentId) {
92 return ['success' => false, 'message' => __('Failed to create payment.', 'yatra')];
93 }
94
95 // Update booking amount paid
96 $totalPaid = $this->paymentRepository->getTotalPaidForBooking((int) $data['booking_id']);
97 $this->bookingRepository->updateAmountPaid((int) $data['booking_id'], $totalPaid);
98
99 return [
100 'success' => true,
101 'payment_id' => $paymentId,
102 'message' => __('Payment recorded successfully.', 'yatra'),
103 ];
104 }
105
106 /**
107 * Update a payment
108 *
109 * @param int $id Payment ID
110 * @param array $data Payment data
111 * @return array {success: bool, message: string}
112 */
113 public function updatePayment(int $id, array $data): array
114 {
115 $payment = $this->paymentRepository->find($id);
116
117 if (!$payment) {
118 return ['success' => false, 'message' => __('Payment not found.', 'yatra')];
119 }
120
121 $updated = $this->paymentRepository->update($id, $data);
122
123 if (!$updated) {
124 return ['success' => false, 'message' => __('Failed to update payment.', 'yatra')];
125 }
126
127 // Recalculate booking amount paid
128 $totalPaid = $this->paymentRepository->getTotalPaidForBooking((int) $payment->booking_id);
129 $this->bookingRepository->updateAmountPaid((int) $payment->booking_id, $totalPaid);
130
131 return [
132 'success' => true,
133 'message' => __('Payment updated successfully.', 'yatra'),
134 ];
135 }
136
137 /**
138 * Update payment status
139 *
140 * @param int $id Payment ID
141 * @param string $status New status
142 * @return array {success: bool, message: string}
143 */
144 public function updateStatus(int $id, string $status): array
145 {
146 $validStatuses = ['pending', 'completed', 'failed', 'refunded', 'cancelled'];
147
148 if (!in_array($status, $validStatuses, true)) {
149 return ['success' => false, 'message' => __('Invalid status.', 'yatra')];
150 }
151
152 $payment = $this->paymentRepository->find($id);
153
154 if (!$payment) {
155 return ['success' => false, 'message' => __('Payment not found.', 'yatra')];
156 }
157
158 $updated = $this->paymentRepository->updateStatus($id, $status);
159
160 if (!$updated) {
161 return ['success' => false, 'message' => __('Failed to update status.', 'yatra')];
162 }
163
164 // Recalculate booking amount paid
165 $totalPaid = $this->paymentRepository->getTotalPaidForBooking((int) $payment->booking_id);
166 $this->bookingRepository->updateAmountPaid((int) $payment->booking_id, $totalPaid);
167
168 return [
169 'success' => true,
170 'message' => sprintf(
171 /* translators: %s: new payment status. */
172 __('Payment status updated to %s.', 'yatra'),
173 $status
174 ),
175 ];
176 }
177
178 /**
179 * Delete a payment
180 *
181 * @param int $id Payment ID
182 * @return array {success: bool, message: string}
183 */
184 public function deletePayment(int $id): array
185 {
186 $payment = $this->paymentRepository->find($id);
187
188 if (!$payment) {
189 return ['success' => false, 'message' => __('Payment not found.', 'yatra')];
190 }
191
192 $bookingId = (int) $payment->booking_id;
193
194 $deleted = $this->paymentRepository->delete($id);
195
196 if (!$deleted) {
197 return ['success' => false, 'message' => __('Failed to delete payment.', 'yatra')];
198 }
199
200 // Recalculate booking amount paid
201 $totalPaid = $this->paymentRepository->getTotalPaidForBooking($bookingId);
202 $this->bookingRepository->updateAmountPaid($bookingId, $totalPaid);
203
204 return [
205 'success' => true,
206 'message' => __('Payment deleted successfully.', 'yatra'),
207 ];
208 }
209
210 /**
211 * Get payment statistics
212 *
213 * @return array
214 */
215 public function getStats(): array
216 {
217 return $this->paymentRepository->getStats();
218 }
219
220 /**
221 * Admin toolbar: counts per payment status.
222 */
223 public function getAdminStatusCounts(): array
224 {
225 return $this->paymentRepository->getAdminStatusCounts();
226 }
227
228 /**
229 * Process refund
230 *
231 * @param int $paymentId Payment ID to refund
232 * @param float $amount Refund amount (optional, full refund if not provided)
233 * @param string $reason Refund reason
234 * @return array {success: bool, refund_id?: int, message: string}
235 */
236 public function processRefund(int $paymentId, ?float $amount = null, string $reason = ''): array
237 {
238 $payment = $this->paymentRepository->find($paymentId);
239
240 if (!$payment) {
241 return ['success' => false, 'message' => __('Payment not found.', 'yatra')];
242 }
243
244 if ($payment->status !== 'completed') {
245 return ['success' => false, 'message' => __('Only completed payments can be refunded.', 'yatra')];
246 }
247
248 $refundAmount = $amount ?? (float) $payment->amount;
249
250 if ($refundAmount > (float) $payment->amount) {
251 return ['success' => false, 'message' => __('Refund amount exceeds payment amount.', 'yatra')];
252 }
253
254 // Create refund record
255 $refundId = $this->paymentRepository->create([
256 'booking_id' => $payment->booking_id,
257 'gateway' => $payment->gateway,
258 'amount' => -$refundAmount, // Negative for refund
259 'currency' => $payment->currency,
260 'status' => 'completed',
261 'payment_type' => 'refund',
262 'notes' => $reason,
263 ]);
264
265 if (!$refundId) {
266 return ['success' => false, 'message' => __('Failed to process refund.', 'yatra')];
267 }
268
269 // Update original payment status
270 $this->paymentRepository->updateStatus($paymentId, 'refunded');
271
272 // Recalculate booking amount paid
273 $totalPaid = $this->paymentRepository->getTotalPaidForBooking((int) $payment->booking_id);
274 $this->bookingRepository->updateAmountPaid((int) $payment->booking_id, $totalPaid);
275
276 return [
277 'success' => true,
278 'refund_id' => $refundId,
279 'message' => __('Refund processed successfully.', 'yatra'),
280 ];
281 }
282
283 /**
284 * Format payment for API response
285 *
286 * @param object $payment Raw payment data
287 * @return array
288 */
289 /**
290 * Format a raw payment row for the REST API.
291 *
292 * `public` so other services (notably {@see \Yatra\Services\CustomerService::getPaymentsForBookingIds()})
293 * can share the same formatter and we don't end up with two competing
294 * shapes — that's how the Account → Payments tab used to render
295 * blank/N/A for `date`, `method`, `reference`, and `type` even after the
296 * formatter here was updated.
297 */
298 public function formatPayment(object $payment): array
299 {
300 $contactName = isset($payment->contact_first_name)
301 ? trim($payment->contact_first_name . ' ' . ($payment->contact_last_name ?? ''))
302 : null;
303
304 $status = (string) ($payment->status ?? 'pending');
305 $gateway = (string) ($payment->gateway ?? '');
306 $bookingRef = $payment->booking_reference ?? null;
307 $processedAt = $payment->processed_at ?? null;
308 $createdAt = $payment->created_at ?? null;
309 $paymentDate = ($processedAt !== null && $processedAt !== '') ? $processedAt : ($createdAt ?? '');
310
311 // Build a human-readable payment reference once, then expose it under
312 // both `payment_number` (canonical) and `reference` (what the React
313 // Payment type at resources/js/pages/account/types.ts expects).
314 $reference = sprintf('PAY-%06d', (int) $payment->id);
315
316 return [
317 'id' => (int) $payment->id,
318 'booking_id' => (int) $payment->booking_id,
319 'booking_reference' => $bookingRef,
320 'booking_number' => ($bookingRef !== null && $bookingRef !== '')
321 ? (string) $bookingRef
322 : '#' . (int) ($payment->booking_id ?? 0),
323 'contact_email' => $payment->contact_email ?? null,
324 'contact_name' => $contactName,
325 'customer_name' => $contactName,
326 'customer_email' => $payment->contact_email ?? null,
327 'trip_title' => $payment->trip_title ?? null,
328 'transaction_id' => $payment->transaction_id,
329 'gateway' => $payment->gateway,
330 'payment_method' => $gateway,
331 'amount' => (float) $payment->amount,
332 'currency' => $payment->currency,
333 'status' => $payment->status,
334 'payment_status' => $status,
335 'payment_type' => $payment->payment_type,
336 'notes' => $payment->notes,
337 'processed_at' => $payment->processed_at,
338 'created_at' => $payment->created_at,
339 'payment_date' => $paymentDate,
340 'payment_number' => $reference,
341 // Aliases for the React Payment interface (account page).
342 // Without these, the payments tab rendered:
343 // - reference: undefined → blank line above "Booking:" label
344 // - method: undefined → blank under "Payment Method"
345 // - date: undefined → formatDate(undefined) → "N/A"
346 // - type: undefined → paymentTypeLabel(undefined) → empty
347 // Keeping the existing payment_* fields preserves any other
348 // consumer that reads them.
349 'reference' => $reference,
350 'method' => $gateway,
351 'date' => $paymentDate,
352 'type' => $payment->payment_type,
353 ];
354 }
355 }
356
357