PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 2.0.11 All 82 releases
yatra / app / Services / BookingCronService.php

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

334 lines 11.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yatra\Services;
4
5 use Yatra\Repositories\BookingRepository;
6 use Yatra\Repositories\TripRepository;
7
8 /**
9 * Handles scheduled booking tasks:
10 * - Sending reminder emails before departure
11 * - Auto-cancelling expired pending bookings
12 */
13 class BookingCronService
14 {
15 /**
16 * Get BookingRepository instance
17 *
18 * @return BookingRepository
19 */
20 private static function getBookingRepository(): BookingRepository
21 {
22 static $repository = null;
23 if ($repository === null) {
24 $repository = new BookingRepository();
25 }
26 return $repository;
27 }
28
29 /**
30 * Get TripRepository instance
31 *
32 * @return TripRepository
33 */
34 private static function getTripRepository(): TripRepository
35 {
36 static $repository = null;
37 if ($repository === null) {
38 $repository = new TripRepository();
39 }
40 return $repository;
41 }
42
43 /**
44 * Register cron hooks
45 */
46 public static function register(): void
47 {
48 // Register cron hooks
49 add_action('yatra_booking_reminder', [self::class, 'sendBookingReminders']);
50 add_action('yatra_booking_expiry', [self::class, 'expirePendingBookings']);
51
52 // Schedule events if not already scheduled
53 self::scheduleEvents();
54 }
55
56 /**
57 * Schedule cron events
58 */
59 public static function scheduleEvents(): void
60 {
61 // Schedule reminder emails - run daily
62 if (!wp_next_scheduled('yatra_booking_reminder')) {
63 wp_schedule_event(time(), 'daily', 'yatra_booking_reminder');
64 }
65
66 // Schedule expiry check - run hourly
67 if (!wp_next_scheduled('yatra_booking_expiry')) {
68 wp_schedule_event(time(), 'hourly', 'yatra_booking_expiry');
69 }
70 }
71
72 /**
73 * Unschedule cron events (on plugin deactivation)
74 */
75 public static function unscheduleEvents(): void
76 {
77 $timestamp = wp_next_scheduled('yatra_booking_reminder');
78 if ($timestamp) {
79 wp_unschedule_event($timestamp, 'yatra_booking_reminder');
80 }
81
82 $timestamp = wp_next_scheduled('yatra_booking_expiry');
83 if ($timestamp) {
84 wp_unschedule_event($timestamp, 'yatra_booking_expiry');
85 }
86 }
87
88 /**
89 * Send reminder emails for upcoming trips
90 */
91 public static function sendBookingReminders(): void
92 {
93 $reminder_days = (int) SettingsService::get('booking_reminder_days', 3);
94
95 if ($reminder_days <= 0) {
96 return; // Reminders disabled
97 }
98
99 $bookingRepository = self::getBookingRepository();
100
101 // Calculate the target date (X days from now)
102 $target_date = date('Y-m-d', strtotime("+{$reminder_days} days"));
103
104 // Get confirmed bookings with travel date matching the target
105 $bookings = $bookingRepository->getBookingsForReminder($target_date);
106
107 if (empty($bookings)) {
108 return;
109 }
110
111 foreach ($bookings as $booking) {
112 if (self::sendReminderEmail($booking)) {
113 $bookingRepository->markReminderSent($booking->id);
114 }
115 }
116
117 // Log the operation
118 if (defined('WP_DEBUG') && WP_DEBUG) {
119 }
120 }
121
122 /**
123 * Ensure the daily booking-completion sweep is scheduled.
124 *
125 * Wired from CronHooks (the plugin's live cron bootstrap) rather than the
126 * legacy register()/scheduleEvents() path above, which is not invoked. Only
127 * the completion event is scheduled here — the reminder/expiry events are
128 * intentionally left as-is to avoid changing their (separate) behavior.
129 */
130 public static function registerCompletionCron(): void
131 {
132 if (!wp_next_scheduled('yatra_booking_completion')) {
133 wp_schedule_event(time(), 'daily', 'yatra_booking_completion');
134 }
135 }
136
137 /**
138 * Unschedule the booking-completion sweep (plugin deactivation).
139 */
140 public static function unregisterCompletionCron(): void
141 {
142 $timestamp = wp_next_scheduled('yatra_booking_completion');
143 if ($timestamp) {
144 wp_unschedule_event($timestamp, 'yatra_booking_completion');
145 }
146 }
147
148 /**
149 * Mark confirmed bookings 'completed' once their tour has taken place.
150 *
151 * Nothing previously transitioned a booking to 'completed' automatically —
152 * the status (and therefore the booking.completed email / Email Automation
153 * sequence) only changed when an operator edited each booking by hand. So
154 * the post-tour email was effectively never sent. This daily sweep does
155 * what an operator would: for every confirmed booking whose tour date has
156 * passed, it calls the same updateStatus() path the admin UI uses, which
157 * fires the notification, the yatra_booking_status_changed action (Pro
158 * sequences), and schedules the review reminder.
159 *
160 * Backward-compat: an activation floor (yatra_booking_autocomplete_since) is
161 * stamped on the first run so we never retroactively complete — and email
162 * the customers of — tours that ended before this automation shipped. Only
163 * tours finishing from activation onward are auto-completed. Operators can
164 * disable the sweep entirely via the yatra_auto_complete_bookings filter,
165 * and the email itself still respects its own template on/off setting.
166 */
167 public static function completeFinishedBookings(): void
168 {
169 /**
170 * Allow disabling automatic booking completion entirely.
171 *
172 * @param bool $enabled Default true.
173 */
174 if (!apply_filters('yatra_auto_complete_bookings', true)) {
175 return;
176 }
177
178 $floorOption = 'yatra_booking_autocomplete_since';
179 $today = current_time('Y-m-d');
180
181 $floor = (string) get_option($floorOption, '');
182 if ($floor === '') {
183 // First run on this site: establish the floor at today so historical
184 // bookings are never retroactively completed/emailed. Tours finishing
185 // from now on are picked up on subsequent runs.
186 update_option($floorOption, $today);
187
188 return;
189 }
190
191 $bookingRepository = self::getBookingRepository();
192 $ids = $bookingRepository->getConfirmedBookingIdsPastTour($today, $floor, 500);
193
194 if (empty($ids)) {
195 return;
196 }
197
198 $bookingService = new BookingService();
199
200 foreach ($ids as $id) {
201 // Same entry point the admin "change status" action uses, so all
202 // side effects (notification, status-changed hook, review reminder,
203 // departure booked_count handling) stay identical to a manual mark.
204 $bookingService->updateStatus((int) $id, 'completed');
205 }
206 }
207
208 /**
209 * Send a reminder email to the customer
210 */
211 private static function sendReminderEmail(object $booking): bool
212 {
213 $customer_email = $booking->contact_email;
214
215 if (empty($customer_email)) {
216 return false;
217 }
218
219 $reminder_days = (int) SettingsService::get('booking_reminder_days', 3);
220 $vars = TransactionalEmailTemplateService::variablesFromBooking($booking);
221 $vars['reminder_days'] = (string) $reminder_days;
222 $vars['days_until_trip'] = (string) $reminder_days;
223
224 $amount_due = (float) $booking->amount_due;
225 $extra = '';
226 if ($amount_due > 0) {
227 $extra = '<p><strong>' . esc_html__('Payment reminder', 'yatra') . '</strong></p>'
228 . '<p>' . esc_html(sprintf(
229 /* translators: %s: formatted outstanding balance amount. */
230 __('Outstanding balance: %s — please pay before travel.', 'yatra'),
231 yatra_format_price($amount_due)
232 )) . '</p>';
233 }
234 $extra .= '<p><strong>' . esc_html__('Preparation checklist', 'yatra') . '</strong></p><ul>'
235 . '<li>' . esc_html__('Valid government-issued ID', 'yatra') . '</li>'
236 . '<li>' . esc_html__('Travel insurance', 'yatra') . '</li>'
237 . '<li>' . esc_html__('Emergency contacts', 'yatra') . '</li>'
238 . '</ul>';
239 $vars['reminder_extra_html'] = $extra;
240
241 return TransactionalEmailTemplateService::sendIfEnabled(
242 TransactionalEmailTemplateService::TYPE_BOOKING_REMINDER,
243 $customer_email,
244 $vars
245 );
246 }
247
248 /**
249 * Expire pending bookings that have passed the expiry time
250 */
251 public static function expirePendingBookings(): void
252 {
253 $expiry_hours = (int) SettingsService::get('booking_expiry_hours', 24);
254
255 if ($expiry_hours <= 0) {
256 return; // Expiry disabled
257 }
258
259 $bookingRepository = self::getBookingRepository();
260 $tripRepository = self::getTripRepository();
261
262 // Calculate the expiry threshold
263 $expiry_threshold = date('Y-m-d H:i:s', strtotime("-{$expiry_hours} hours"));
264
265 // Get pending bookings that are older than the expiry threshold
266 $expired_bookings = $bookingRepository->getExpiredPendingBookings($expiry_threshold);
267
268 if (empty($expired_bookings)) {
269 return;
270 }
271
272 foreach ($expired_bookings as $booking) {
273 // Update booking status to expired/cancelled
274 $bookingRepository->expireBooking(
275 $booking->id,
276 __('Booking expired due to non-payment', 'yatra')
277 );
278
279 do_action('yatra_booking_status_changed', (int) $booking->id, 'pending', 'cancelled');
280
281 // Get trip title for email
282 $trip = $tripRepository->find($booking->trip_id);
283
284 // Send expiry notification email
285 self::sendExpiryEmail($booking, $trip);
286 }
287
288 // Log the operation
289 if (defined('WP_DEBUG') && WP_DEBUG) {
290 }
291 }
292
293 /**
294 * Send expiry notification email
295 */
296 private static function sendExpiryEmail(object $booking, ?object $trip): void
297 {
298 $customer_email = $booking->contact_email;
299
300 if (empty($customer_email)) {
301 return;
302 }
303
304 $expiry_hours = (int) SettingsService::get('booking_expiry_hours', 24);
305 $full = self::getBookingRepository()->findWithTrip((int) $booking->id) ?: $booking;
306
307 $vars = TransactionalEmailTemplateService::variablesFromBooking($full);
308 if ($trip && !empty($trip->title)) {
309 $vars['trip_name'] = (string) $trip->title;
310 }
311 $vars['expiry_policy_note'] = sprintf(
312 /* translators: %d: hours until unpaid booking expires */
313 __('Unpaid bookings are released after %d hours.', 'yatra'),
314 $expiry_hours
315 );
316
317 TransactionalEmailTemplateService::sendIfEnabled(
318 TransactionalEmailTemplateService::TYPE_BOOKING_EXPIRED_CUSTOMER,
319 (string) $customer_email,
320 $vars
321 );
322
323 $admin_email = sanitize_email((string) SettingsService::getString('admin_email', (string) get_option('admin_email', '')));
324 if ($admin_email !== '' && is_email($admin_email)) {
325 TransactionalEmailTemplateService::sendIfEnabled(
326 TransactionalEmailTemplateService::TYPE_ADMIN_BOOKING_EXPIRED,
327 $admin_email,
328 $vars
329 );
330 }
331 }
332 }
333
334