| @@ -27,8 +27,15 @@ | ||
| 27 | 27 | * @package Yatra\Services |
| 28 | 28 | */ |
| 29 | 29 | class BookingService |
| 30 | 30 | { |
| 31 | + /** | |
| 32 | + * Accepted payment statuses — mirrors the `payment_status` ENUM on the | |
| 33 | + * bookings table. Anything outside this list is rejected before it reaches | |
| 34 | + * the database, where an unknown value would be silently coerced. | |
| 35 | + */ | |
| 36 | + public const PAYMENT_STATUSES = ['pending', 'partial', 'paid', 'refunded', 'failed']; | |
| 37 | + | |
| 31 | 38 | private BookingRepository $bookingRepository; |
| 32 | 39 | private PaymentRepository $paymentRepository; |
| 33 | 40 | private TravellerRepository $travellerRepository; |
| 34 | 41 | private CustomerRepository $customerRepository; |
| @@ -379,9 +386,14 @@ | ||
| 379 | 386 | 'yatra_calculate_amount_due', |
| 380 | 387 | $bs_total - $bs_paid, |
| 381 | 388 | $bs_total, |
| 382 | 389 | $bs_payment_method, |
| 383 | - ['trip_id' => (int) ($data['trip_id'] ?? 0)] | |
| 390 | + [ | |
| 391 | + 'trip_id' => (int) ($data['trip_id'] ?? 0), | |
| 392 | + // Tour start lets Pro enforce "pay in full when the tour is | |
| 393 | + // within the balance-due window" (tour-anchored payments). | |
| 394 | + 'travel_date' => (string) ($data['travel_date'] ?? ($data['start_date'] ?? '')), | |
| 395 | + ] | |
| 384 | 396 | ); |
| 385 | 397 | $data['amount_due'] = max(0.0, round($bs_due_now, 2)); |
| 386 | 398 | |
| 387 | 399 | // Create booking |
| @@ -521,8 +533,27 @@ | ||
| 521 | 533 | if (!$booking) { |
| 522 | 534 | return ['success' => false, 'message' => __('Booking not found.', 'yatra')]; |
| 523 | 535 | } |
| 524 | 536 | |
| 537 | + // Reject an unknown payment status instead of handing it to MySQL. The | |
| 538 | + // column is an ENUM, so an unrecognised value was silently coerced — | |
| 539 | + // resetting a fully-paid booking to "pending" while amount_paid kept the | |
| 540 | + // money that had actually been received, and still returning success. | |
| 541 | + if (array_key_exists('payment_status', $data)) { | |
| 542 | + $paymentStatus = (string) $data['payment_status']; | |
| 543 | + | |
| 544 | + if (!in_array($paymentStatus, self::PAYMENT_STATUSES, true)) { | |
| 545 | + return [ | |
| 546 | + 'success' => false, | |
| 547 | + 'message' => sprintf( | |
| 548 | + /* translators: %s: the list of accepted payment statuses. */ | |
| 549 | + __('Invalid payment status. Accepted values are: %s.', 'yatra'), | |
| 550 | + implode(', ', self::PAYMENT_STATUSES) | |
| 551 | + ), | |
| 552 | + ]; | |
| 553 | + } | |
| 554 | + } | |
| 555 | + | |
| 525 | 556 | // Check if date is being changed |
| 526 | 557 | $oldStartDate = $booking->start_date ?? $booking->travel_date ?? null; |
| 527 | 558 | $newStartDate = $data['start_date'] ?? $data['travel_date'] ?? null; |
| 528 | 559 | $dateChanged = false; |
| @@ -540,8 +571,9 @@ | ||
| 540 | 571 | } |
| 541 | 572 | |
| 542 | 573 | $oldStatus = (string) ($booking->status ?? ''); |
| 543 | 574 | $oldPaymentStatus = (string) ($booking->payment_status ?? ''); |
| 575 | + $oldTripId = (int) ($booking->trip_id ?? 0); | |
| 544 | 576 | |
| 545 | 577 | // Update booking |
| 546 | 578 | $updated = $this->bookingRepository->update($id, $data); |
| 547 | 579 | |
| @@ -553,15 +585,46 @@ | ||
| 553 | 585 | if ($newStatus !== null && $oldStatus === 'waitlist' && $newStatus !== 'waitlist') { |
| 554 | 586 | WaitlistService::releaseWaitlistHolding($booking); |
| 555 | 587 | } |
| 556 | 588 | |
| 557 | - // Handle departure date change if date was changed | |
| 558 | - if ($dateChanged && !empty($data['start_date']) && !empty($data['end_date'])) { | |
| 589 | + // Re-link the departure when the date changed OR the operator picked a | |
| 590 | + // different departure time. A trip running several departures a day needs the | |
| 591 | + // time as well — moving a booking from the 09:00 to the 14:00 slot is not a | |
| 592 | + // date change, and without this it silently stayed on the original slot. | |
| 593 | + $departureTimeForUpdate = null; | |
| 594 | + if (!empty($data['departure_time']) && is_string($data['departure_time'])) { | |
| 595 | + $departureTimeForUpdate = trim($data['departure_time']) !== '' ? trim($data['departure_time']) : null; | |
| 596 | + } | |
| 597 | + | |
| 598 | + // Changing the tour (trip_id) also has to move the departure: the booking | |
| 599 | + // now belongs to a different trip, so its seat has to be released from the | |
| 600 | + // old trip's departure and taken on the new trip's departure. Previously | |
| 601 | + // only a date/time change triggered the re-link, so switching Tour A -> Tour B | |
| 602 | + // left the booking counted against Tour A's departure (and absent from | |
| 603 | + // Tour B's) — over-selling A and under-selling B. | |
| 604 | + $tripChanged = isset($data['trip_id']) | |
| 605 | + && (int) $data['trip_id'] > 0 | |
| 606 | + && (int) $data['trip_id'] !== $oldTripId; | |
| 607 | + | |
| 608 | + // handleBookingDateChange() re-reads the booking (already saved with the new | |
| 609 | + // trip_id above) and needs the effective travel dates. When only the tour | |
| 610 | + // changed, the date fields aren't in $data, so fall back to the booking's | |
| 611 | + // current dates rather than skipping the re-link. | |
| 612 | + $effectiveStart = !empty($data['start_date']) | |
| 613 | + ? $data['start_date'] | |
| 614 | + : ($booking->start_date ?? $booking->travel_date ?? null); | |
| 615 | + $effectiveEnd = !empty($data['end_date']) | |
| 616 | + ? $data['end_date'] | |
| 617 | + : ($booking->end_date ?? $effectiveStart); | |
| 618 | + | |
| 619 | + if (($dateChanged || $tripChanged || $departureTimeForUpdate !== null) | |
| 620 | + && !empty($effectiveStart) && !empty($effectiveEnd)) { | |
| 559 | 621 | try { |
| 560 | 622 | $this->departureService->handleBookingDateChange( |
| 561 | 623 | $id, |
| 562 | - $data['start_date'], | |
| 563 | - $data['end_date'] | |
| 624 | + $effectiveStart, | |
| 625 | + $effectiveEnd, | |
| 626 | + $departureTimeForUpdate | |
| 564 | 627 | ); |
| 565 | 628 | } catch (\Exception $e) { |
| 566 | 629 | // Log error but don't fail the update |
| 567 | 630 | } |
| @@ -584,8 +647,43 @@ | ||
| 584 | 647 | if ($newPaymentStatus !== null && $newPaymentStatus !== $oldPaymentStatus) { |
| 585 | 648 | $this->handlePaymentStatusChange($id, $oldPaymentStatus, $newPaymentStatus); |
| 586 | 649 | } |
| 587 | 650 | |
| 651 | + // Changing the status here fired no event at all, so confirming a booking | |
| 652 | + // from the edit form saved the status and then went silent: no confirmation | |
| 653 | + // email, no Email Automation sequence (booking.confirmed / .cancelled / | |
| 654 | + // .completed), no seat release on cancel. Only the status action | |
| 655 | + // (updateStatus) ever emitted it, which is why the same change appeared to | |
| 656 | + // work from one screen and not the other. | |
| 657 | + // | |
| 658 | + // Fired last, once the travellers and related rows are saved, so listeners | |
| 659 | + // read the booking's final state — and only on a real transition, so | |
| 660 | + // re-saving the form without touching the status stays silent. | |
| 661 | + if ($newStatus !== null && $newStatus !== $oldStatus) { | |
| 662 | + // Same order as updateStatus(): the notification is sent inline (it is | |
| 663 | + // not a listener on the action below), then the event fans out. | |
| 664 | + $this->sendStatusChangeNotification($id, $oldStatus, $newStatus); | |
| 665 | + | |
| 666 | + /** | |
| 667 | + * Fires when a booking's status changes. | |
| 668 | + * | |
| 669 | + * @param int $id The booking ID | |
| 670 | + * @param string $oldStatus Previous status | |
| 671 | + * @param string $newStatus New status | |
| 672 | + */ | |
| 673 | + do_action('yatra_booking_status_changed', $id, $oldStatus, $newStatus); | |
| 674 | + | |
| 675 | + if ($newStatus === 'confirmed' && $oldStatus !== 'confirmed' | |
| 676 | + && function_exists('yatra_trigger_booking_confirmed')) { | |
| 677 | + yatra_trigger_booking_confirmed($id, $oldStatus); | |
| 678 | + } | |
| 679 | + | |
| 680 | + if ($newStatus === 'cancelled' && $oldStatus !== 'cancelled' | |
| 681 | + && function_exists('yatra_trigger_booking_cancelled')) { | |
| 682 | + yatra_trigger_booking_cancelled($id, $oldStatus); | |
| 683 | + } | |
| 684 | + } | |
| 685 | + | |
| 588 | 686 | // Return the fresh booking so the REST controller's `$result['data']` |
| 589 | 687 | // is defined (previously absent → "Undefined array key data" warning). |
| 590 | 688 | return [ |
| 591 | 689 | 'success' => true, |
| @@ -594,8 +692,45 @@ | ||
| 594 | 692 | ]; |
| 595 | 693 | } |
| 596 | 694 | |
| 597 | 695 | /** |
| 696 | + * Record an operator-confirmed payment against a booking. | |
| 697 | + * | |
| 698 | + * Used when a booking is marked paid by hand — typically an offline payment | |
| 699 | + * such as a bank transfer or cash, where no gateway callback ever arrives. | |
| 700 | + * Without this the booking claimed the money while the ledger showed | |
| 701 | + * nothing, and the Payments screen stayed empty. | |
| 702 | + * | |
| 703 | + * Written as `completed` because the operator is asserting the funds were | |
| 704 | + * received; `payment_type` reflects whether this settles a balance or is the | |
| 705 | + * only payment on the booking. | |
| 706 | + */ | |
| 707 | + private function recordManualPayment(object $booking, int $bookingId, float $amount, float $existingLedger): void | |
| 708 | + { | |
| 709 | + $gateway = (string) ($booking->payment_gateway ?? $booking->payment_method ?? ''); | |
| 710 | + | |
| 711 | + if (trim($gateway) === '') { | |
| 712 | + // `gateway` is NOT NULL on the payments table. | |
| 713 | + $gateway = 'manual'; | |
| 714 | + } | |
| 715 | + | |
| 716 | + $this->paymentRepository->create([ | |
| 717 | + 'booking_id' => $bookingId, | |
| 718 | + 'customer_id' => !empty($booking->customer_id) ? (int) $booking->customer_id : null, | |
| 719 | + 'gateway' => $gateway, | |
| 720 | + 'amount' => $amount, | |
| 721 | + 'currency' => (string) ($booking->currency ?? SettingsService::getCurrency()), | |
| 722 | + 'status' => 'completed', | |
| 723 | + 'payment_type' => $existingLedger > 0 ? 'final' : 'initial', | |
| 724 | + 'notes' => __('Recorded manually when the booking was marked as paid.', 'yatra'), | |
| 725 | + 'processed_at' => current_time('mysql'), | |
| 726 | + 'created_at' => current_time('mysql'), | |
| 727 | + ]); | |
| 728 | + | |
| 729 | + do_action('yatra_manual_payment_recorded', $bookingId, $amount, $gateway); | |
| 730 | + } | |
| 731 | + | |
| 732 | + /** | |
| 598 | 733 | * React to a manual payment-status change (admin edits, e.g. bank transfer |
| 599 | 734 | * marked Paid). Sends the customer + admin payment emails when money is |
| 600 | 735 | * (fully or partially) received, and fires `yatra_payment_status_changed` |
| 601 | 736 | * so integrations can react. Intentionally separate from |
| @@ -609,8 +744,57 @@ | ||
| 609 | 744 | } |
| 610 | 745 | |
| 611 | 746 | do_action('yatra_payment_status_changed', $bookingId, $oldStatus, $newStatus, $booking); |
| 612 | 747 | |
| 748 | + // Marking a booking paid has to settle its money fields too. An operator | |
| 749 | + // confirming an offline payment (bank transfer, cash) has no payment row | |
| 750 | + // to mark as completed — this status change is the only signal we get. | |
| 751 | + // Without reconciling here the booking read "paid" while amount_paid | |
| 752 | + // stayed 0 and amount_due kept the outstanding figure, so the invoice | |
| 753 | + // still reported "Payment Pending" with nothing paid and the full amount | |
| 754 | + // due. | |
| 755 | + // | |
| 756 | + // Only ever settles UP: a recorded amount_paid at or above the total is | |
| 757 | + // left alone, so this can never erase or reduce a real payment. The other | |
| 758 | + // statuses are deliberately untouched — "partial" carries no amount to | |
| 759 | + // apply, and zeroing on "pending"/"refunded" would destroy payment data. | |
| 760 | + if ($newStatus === 'paid') { | |
| 761 | + $total = (float) ($booking->total_amount ?? 0); | |
| 762 | + $recorded = (float) ($booking->amount_paid ?? 0); | |
| 763 | + | |
| 764 | + if ($total > 0) { | |
| 765 | + // The payments ledger is the source of truth: PaymentService | |
| 766 | + // recalculates amount_paid from it whenever a payment is added, | |
| 767 | + // so a booking marked paid without a matching ledger row would | |
| 768 | + // silently revert to "partial" the next time any payment was | |
| 769 | + // recorded. Write the outstanding balance as a real payment so | |
| 770 | + // the two agree and the Payments screen shows what was received. | |
| 771 | + $ledger = (float) $this->paymentRepository->getTotalPaidForBooking($bookingId); | |
| 772 | + | |
| 773 | + // Measure the gap against whichever figure is higher so an | |
| 774 | + // existing (pre-ledger) amount_paid is never double-counted. | |
| 775 | + $alreadyCovered = max($ledger, $recorded); | |
| 776 | + $outstanding = round($total - $alreadyCovered, 2); | |
| 777 | + | |
| 778 | + if ($outstanding > 0) { | |
| 779 | + $this->recordManualPayment($booking, $bookingId, $outstanding, $ledger); | |
| 780 | + $ledger = (float) $this->paymentRepository->getTotalPaidForBooking($bookingId); | |
| 781 | + } | |
| 782 | + | |
| 783 | + // Never reduce a recorded overpayment: settle up, never down. | |
| 784 | + $newAmountPaid = max($ledger, $recorded); | |
| 785 | + | |
| 786 | + if ($newAmountPaid > $recorded || $recorded < $total) { | |
| 787 | + // Canonical writer — also derives amount_due and keeps | |
| 788 | + // payment_status consistent with the amounts. | |
| 789 | + $this->bookingRepository->updateAmountPaid($bookingId, $newAmountPaid); | |
| 790 | + | |
| 791 | + $booking->amount_paid = $newAmountPaid; | |
| 792 | + $booking->amount_due = max(0.0, $total - $newAmountPaid); | |
| 793 | + } | |
| 794 | + } | |
| 795 | + } | |
| 796 | + | |
| 613 | 797 | if (in_array($newStatus, ['paid', 'partial'], true)) { |
| 614 | 798 | $paidAmount = (float) ($booking->amount_paid ?? 0); |
| 615 | 799 | if ($paidAmount <= 0) { |
| 616 | 800 | $paidAmount = (float) ($booking->total_amount ?? 0); |
| @@ -696,10 +880,17 @@ | ||
| 696 | 880 | $startDate = $booking->start_date ?? $booking->travel_date; |
| 697 | 881 | $endDate = $booking->end_date ?? $this->calculateEndDate($startDate, (int) $booking->trip_id); |
| 698 | 882 | |
| 699 | 883 | $trip = $this->tripRepository->find((int) $booking->trip_id); |
| 700 | - $maxCapacity = $trip ? ($trip->max_capacity ?? 9999) : 9999; | |
| 701 | - | |
| 884 | + // Resolve capacity from the trip's real column (`max_travelers`). | |
| 885 | + // The old `$trip->max_capacity` does not exist on the trips table, | |
| 886 | + // so this always fell back to 9999 — seeding a junk "unlimited" | |
| 887 | + // sentinel that then showed as a huge number on the Departures page | |
| 888 | + // but as 0 on the dashboard. Pass null when unset so | |
| 889 | + // findOrCreateForBooking resolves via Availability, then its own | |
| 890 | + // trip-default fallback, exactly like the primary creation path. | |
| 891 | + $maxCapacity = ($trip && !empty($trip->max_travelers)) ? (int) $trip->max_travelers : null; | |
| 892 | + | |
| 702 | 893 | $departure = $this->departureService->findOrCreateForBooking( |
| 703 | 894 | (int) $booking->trip_id, |
| 704 | 895 | $startDate, |
| 705 | 896 | $endDate, |
| @@ -731,8 +922,12 @@ | ||
| 731 | 922 | if ($status === 'confirmed' && $oldStatus !== 'confirmed') { |
| 732 | 923 | \yatra_trigger_booking_confirmed($id, $oldStatus); |
| 733 | 924 | } |
| 734 | 925 | |
| 926 | + if ($status === 'cancelled' && $oldStatus !== 'cancelled') { | |
| 927 | + \yatra_trigger_booking_cancelled($id, $oldStatus); | |
| 928 | + } | |
| 929 | + | |
| 735 | 930 | return [ |
| 736 | 931 | 'success' => true, |
| 737 | 932 | 'message' => sprintf( |
| 738 | 933 | /* translators: %s: new booking status. */ |
| @@ -1067,10 +1262,39 @@ | ||
| 1067 | 1262 | ); |
| 1068 | 1263 | } |
| 1069 | 1264 | |
| 1070 | 1265 | /** |
| 1266 | + * Send the "your booking has been confirmed" transactional email. | |
| 1267 | + * | |
| 1268 | + * Shared by the manual / programmatic status-change path ({@see updateStatus()} | |
| 1269 | + * → sendStatusChangeNotification()) and the asynchronous payment-completion | |
| 1270 | + * paths (via {@see yatra_trigger_booking_confirmed()}), which set a booking to | |
| 1271 | + * `confirmed` with a direct DB write and never pass through updateStatus(). | |
| 1272 | + * Gated by the template's enabled flag (`sendIfEnabled`). | |
| 1273 | + * | |
| 1274 | + * @param int $bookingId Booking ID. | |
| 1275 | + */ | |
| 1276 | + public function sendBookingConfirmedEmail(int $bookingId): void | |
| 1277 | + { | |
| 1278 | + $booking = $this->bookingRepository->findWithTrip($bookingId); | |
| 1279 | + | |
| 1280 | + if (!$booking || empty($booking->contact_email)) { | |
| 1281 | + return; | |
| 1282 | + } | |
| 1283 | + | |
| 1284 | + $vars = TransactionalEmailTemplateService::variablesFromBooking($booking); | |
| 1285 | + $vars['intro_paragraph'] = __('Your booking has been confirmed! Here are your details:', 'yatra'); | |
| 1286 | + $vars['transactional_context'] = 'status_confirmed'; | |
| 1287 | + TransactionalEmailTemplateService::sendIfEnabled( | |
| 1288 | + TransactionalEmailTemplateService::TYPE_BOOKING_CONFIRMATION, | |
| 1289 | + $booking->contact_email, | |
| 1290 | + $vars | |
| 1291 | + ); | |
| 1292 | + } | |
| 1293 | + | |
| 1294 | + /** | |
| 1071 | 1295 | * Send status change notification |
| 1072 | - * | |
| 1296 | + * | |
| 1073 | 1297 | * @param int $bookingId Booking ID |
| 1074 | 1298 | * @param string $oldStatus Previous status |
| 1075 | 1299 | * @param string $newStatus New status |
| 1076 | 1300 | */ |
| @@ -1101,16 +1325,9 @@ | ||
| 1101 | 1325 | return; |
| 1102 | 1326 | } |
| 1103 | 1327 | |
| 1104 | 1328 | if ($newStatus === 'confirmed') { |
| 1105 | - $vars = TransactionalEmailTemplateService::variablesFromBooking($booking); | |
| 1106 | - $vars['intro_paragraph'] = __('Your booking has been confirmed! Here are your details:', 'yatra'); | |
| 1107 | - $vars['transactional_context'] = 'status_confirmed'; | |
| 1108 | - TransactionalEmailTemplateService::sendIfEnabled( | |
| 1109 | - TransactionalEmailTemplateService::TYPE_BOOKING_CONFIRMATION, | |
| 1110 | - $booking->contact_email, | |
| 1111 | - $vars | |
| 1112 | - ); | |
| 1329 | + $this->sendBookingConfirmedEmail($bookingId); | |
| 1113 | 1330 | |
| 1114 | 1331 | return; |
| 1115 | 1332 | } |
| 1116 | 1333 | |
| @@ -1184,15 +1401,30 @@ | ||
| 1184 | 1401 | if (!$booking) { |
| 1185 | 1402 | return ['success' => false, 'message' => __('Booking not found.', 'yatra')]; |
| 1186 | 1403 | } |
| 1187 | 1404 | |
| 1188 | - if (empty($booking->contact_email)) { | |
| 1189 | - return ['success' => false, 'message' => __('No email address found.', 'yatra')]; | |
| 1405 | + // Customer-facing emails need a recipient; admin notifications go to the | |
| 1406 | + // store admin address, so they don't require the booking's contact email. | |
| 1407 | + $customerEmail = (string) ($booking->contact_email ?? ''); | |
| 1408 | + $customerTypes = ['confirmation', 'reminder', 'cancellation', 'completed', 'payment_confirmation']; | |
| 1409 | + if (in_array($emailType, $customerTypes, true) && $customerEmail === '') { | |
| 1410 | + return ['success' => false, 'message' => __('No customer email address on this booking.', 'yatra')]; | |
| 1190 | 1411 | } |
| 1191 | 1412 | |
| 1192 | 1413 | switch ($emailType) { |
| 1193 | 1414 | case 'confirmation': |
| 1194 | - $this->sendBookingConfirmationEmail($bookingId); | |
| 1415 | + // Resend the email that matches the booking's CURRENT state so it | |
| 1416 | + // is identical to the automated one. A confirmed booking (or a | |
| 1417 | + // completed one — it was confirmed before it travelled) gets the | |
| 1418 | + // "booking confirmed" email (status_confirmed context → the | |
| 1419 | + // `booking_confirmed` / booking.confirmed template); anything | |
| 1420 | + // still pending gets the initial "booking received" email | |
| 1421 | + // (booking_created context → the `booking_confirmation` template). | |
| 1422 | + if (in_array((string) ($booking->status ?? ''), ['confirmed', 'completed'], true)) { | |
| 1423 | + $this->sendBookingConfirmedEmail($bookingId); | |
| 1424 | + } else { | |
| 1425 | + $this->sendBookingConfirmationEmail($bookingId); | |
| 1426 | + } | |
| 1195 | 1427 | break; |
| 1196 | 1428 | |
| 1197 | 1429 | case 'reminder': |
| 1198 | 1430 | $this->sendBookingReminderEmail($booking); |
| @@ -1197,8 +1429,50 @@ | ||
| 1197 | 1429 | case 'reminder': |
| 1198 | 1430 | $this->sendBookingReminderEmail($booking); |
| 1199 | 1431 | break; |
| 1200 | 1432 | |
| 1433 | + case 'cancellation': | |
| 1434 | + // Mirrors sendStatusChangeNotification() so the resent email is | |
| 1435 | + // identical to the automated cancellation email. | |
| 1436 | + $vars = TransactionalEmailTemplateService::variablesFromBooking($booking); | |
| 1437 | + $vars['cancellation_reason'] = (string) ($booking->cancellation_reason ?? ''); | |
| 1438 | + TransactionalEmailTemplateService::sendIfEnabled( | |
| 1439 | + TransactionalEmailTemplateService::TYPE_BOOKING_CANCELLATION, | |
| 1440 | + $customerEmail, | |
| 1441 | + $vars | |
| 1442 | + ); | |
| 1443 | + break; | |
| 1444 | + | |
| 1445 | + case 'completed': | |
| 1446 | + $vars = TransactionalEmailTemplateService::variablesFromBooking($booking); | |
| 1447 | + $vars['completion_date'] = date_i18n(get_option('date_format')); | |
| 1448 | + TransactionalEmailTemplateService::sendIfEnabled( | |
| 1449 | + TransactionalEmailTemplateService::TYPE_BOOKING_COMPLETED, | |
| 1450 | + $customerEmail, | |
| 1451 | + $vars | |
| 1452 | + ); | |
| 1453 | + break; | |
| 1454 | + | |
| 1455 | + case 'payment_confirmation': | |
| 1456 | + $paymentData = $this->buildPaymentDataForResend($booking); | |
| 1457 | + if ($paymentData === null) { | |
| 1458 | + return ['success' => false, 'message' => __('No recorded payment to resend for this booking.', 'yatra')]; | |
| 1459 | + } | |
| 1460 | + \Yatra\Services\NotificationService::resendCustomerPaymentEmail($paymentData); | |
| 1461 | + break; | |
| 1462 | + | |
| 1463 | + case 'admin_new_booking': | |
| 1464 | + \Yatra\Services\NotificationService::sendBookingCreatedNotification($bookingId, (array) $booking); | |
| 1465 | + break; | |
| 1466 | + | |
| 1467 | + case 'admin_payment_received': | |
| 1468 | + $paymentData = $this->buildPaymentDataForResend($booking); | |
| 1469 | + if ($paymentData === null) { | |
| 1470 | + return ['success' => false, 'message' => __('No recorded payment to resend for this booking.', 'yatra')]; | |
| 1471 | + } | |
| 1472 | + \Yatra\Services\NotificationService::resendAdminPaymentEmail($paymentData); | |
| 1473 | + break; | |
| 1474 | + | |
| 1201 | 1475 | default: |
| 1202 | 1476 | return ['success' => false, 'message' => __('Unknown email type.', 'yatra')]; |
| 1203 | 1477 | } |
| 1204 | 1478 | |
| @@ -1204,8 +1478,32 @@ | ||
| 1204 | 1478 | |
| 1205 | 1479 | return [ |
| 1206 | 1480 | 'success' => true, |
| 1207 | 1481 | 'message' => __('Email sent successfully.', 'yatra'), |
| 1482 | + ]; | |
| 1483 | + } | |
| 1484 | + | |
| 1485 | + /** | |
| 1486 | + * Reconstruct the payment-notification payload for a resend from the latest | |
| 1487 | + * payment on the booking (falling back to the booking's own amount_paid / | |
| 1488 | + * gateway when no ledger row exists). Returns null when nothing has been | |
| 1489 | + * paid, so there is no payment to acknowledge. | |
| 1490 | + */ | |
| 1491 | + private function buildPaymentDataForResend(object $booking): ?array | |
| 1492 | + { | |
| 1493 | + $bookingId = (int) ($booking->id ?? 0); | |
| 1494 | + $payment = $this->paymentRepository->findLatestByBookingId($bookingId); | |
| 1495 | + | |
| 1496 | + $amount = (float) ($payment->amount ?? $booking->amount_paid ?? 0); | |
| 1497 | + if ($amount <= 0) { | |
| 1498 | + return null; | |
| 1499 | + } | |
| 1500 | + | |
| 1501 | + return [ | |
| 1502 | + 'booking_id' => $bookingId, | |
| 1503 | + 'amount' => $amount, | |
| 1504 | + 'payment_method' => (string) ($payment->gateway ?? $booking->payment_gateway ?? ''), | |
| 1505 | + 'transaction_id' => (string) ($payment->transaction_id ?? ''), | |
| 1208 | 1506 | ]; |
| 1209 | 1507 | } |
| 1210 | 1508 | |
| 1211 | 1509 | /** |