<?php /* * Copyright (c) 2022 LatePoint LLC. All rights reserved. */ class OsProcessJobsHelper { public static function create_jobs_for_process(OsProcessModel $process, array $objects){ if(!$process->check_if_objects_satisfy_trigger_conditions($objects)) return; $job = new OsProcessJobModel(); $job->process_id = $process->id; $job->object_id = $objects[0]['model_ready']->id; // check if job exists already $existing_job = new OsProcessJobModel(); $exists = $existing_job->select('id')->where(['process_id' => $job->process_id, 'object_id' => $job->object_id, 'status' => LATEPOINT_JOB_STATUS_SCHEDULED])->get_results(); if($exists) return; $job_settings = []; foreach($process->actions as $action){ if($action->status != LATEPOINT_STATUS_ACTIVE) continue; $action->selected_data_objects = $objects; $action->prepare_data_for_run(); $job_settings['action_info'][$action->id] = ['type' => $action->type]; $job_settings['action_data'][$action->id] = $action->prepared_data_for_run; } $event_time_utc = null; $object_model_type = null; switch($process->event_type){ case 'booking_updated'; case 'booking_created'; case 'booking_start'; case 'booking_end'; $object_model_type = 'booking'; break; case 'customer_created': case 'customer_updated': $object_model_type = 'customer'; break; case 'order_created': case 'order_updated': $object_model_type = 'order'; break; case 'service_created': case 'service_updated': $object_model_type = 'service'; break; case 'agent_created': case 'agent_updated': $object_model_type = 'agent'; break; case 'transaction_created': case 'transaction_updated': $object_model_type = 'transaction'; break; case 'payment_request_created': $object_model_type = 'payment_request'; break; } /** * Determine a type of a model based on a process * * @since 5.1.0 * @hook latepoint_get_object_model_type_for_process * * @param {string} $object_model_type Type of model * @param {OsProcessModel} $process Process object * @param {array} $objects Array of objects used for a process * * @returns {string} Filtered type of model */ $object_model_type = apply_filters('latepoint_get_object_model_type_for_process', $object_model_type, $process, $objects); try{ switch($process->event_type){ case 'booking_updated': case 'order_updated': $event_time_utc = new OsWpDateTime($objects[0]['model_ready']->updated_at, new DateTimeZone('UTC')); break; case 'order_created': case 'booking_created': case 'transaction_created': case 'customer_created': case 'payment_request_created': $event_time_utc = new OsWpDateTime($objects[0]['model_ready']->created_at, new DateTimeZone('UTC')); break; case 'booking_start': $event_time_utc = new OsWpDateTime($objects[0]['model_ready']->start_datetime_utc, new DateTimeZone('UTC')); break; case 'booking_end': $event_time_utc = new OsWpDateTime($objects[0]['model_ready']->end_datetime_utc, new DateTimeZone('UTC')); break; } /** * Determine UTC event time based on a process * * @since 5.1.0 * @hook latepoint_get_event_time_utc_for_process * * @param {string} $event_time_utc Event time in UTC * @param {OsProcessModel} $process Process object * @param {array} $objects Array of objects used for a process * * @returns {string} Filtered event time in UTC */ $event_time_utc = apply_filters('latepoint_get_event_time_utc_for_process', $event_time_utc, $process, $objects); }catch(Exception $e){ OsDebugHelper::log('Error creating jobs for workflow', 'process_jobs_error', print_r($process->id, true).' '.print_r($objects,true).' '.$e->getMessage()); return; } if(empty($event_time_utc)) return; $job->settings = wp_json_encode($job_settings); $job->process_info = wp_json_encode($process->get_info()); // apply time offset if exists in process $modify_by = self::should_modify_event_time($process); if(!empty($modify_by)) $event_time_utc->modify($modify_by); $now_utc = new OsWpDateTime('now', new DateTimeZone('UTC')); // we need to make sure we are not creating jobs that are already past their relevance (e.g. booking updated but // 2 day before booking_start notification was already sent before, so scheduling a new job for that doesn't // make sense). Problem is that is we have booking_udpated notification - then the time of "booking_updated" event // is technically in the past, since the DB update happened couple of milliseconds ago. So solution is to create a // buffer time to allow for these discrepancies in which we can resend the notification if it's within that buffer $buffer = '+5 minutes'; $event_time_utc_buffered = clone $event_time_utc; $event_time_utc_buffered->modify($buffer); // event time with buffer is already long passed (cron already ran it probably, or after changing booking times, this job is not relevant anymore) if($event_time_utc_buffered < $now_utc) return; $is_in_the_future = $event_time_utc > $now_utc; $job->object_model_type = $object_model_type ?? 'n/a'; $job->to_run_after_utc =$event_time_utc->format(LATEPOINT_DATETIME_DB_FORMAT); $job->status = LATEPOINT_JOB_STATUS_SCHEDULED; $job->save(); // execute immediately, if there is no delay(time offset) specified // todo add ability toggling setting to allow delayed execution even on instant events (to speed up frontend experience for customers) if(!$is_in_the_future){ $job->run(); } } /** * @param string $event_type * @param array $objects example format: ['model' => 'booking', 'id' => $booking->id, 'model_ready' => OsModel $booking] * @return void */ public static function create_jobs_for_event(string $event_type, array $objects){ $processes = new OsProcessModel(); // find all processes that match this event type $processes = $processes->where(['event_type' => $event_type])->should_be_active()->get_results_as_models(); if($processes){ foreach($processes as $process){ $process->build_from_json(); self::create_jobs_for_process($process, $objects); } } } /** * * Searches existing records that match this process conditions and schedules a job, for example if you created a new * process that sends a notification 15 minute before the booking start - this method will find those bookings and * schedule jobs to send notification * * @param OsProcessModel $process * @return bool|void */ public static function recreate_jobs_for_existing_records(OsProcessModel $process){ // don't create jobs for booking_updated event, since we don't capture the exact information of what was updated in // the booking and can't check if event conditions are satisfied if(!in_array($process->event_type, ['booking_start', 'booking_end', 'booking_created', 'customer_created', 'transaction_created'])) return false; // calculate the cutoff date to search records that could be affected by this event $cutoff_datetime_utc = OsTimeHelper::now_datetime_utc(); $modify_by = self::should_modify_event_time($process, true); if($modify_by){ $cutoff_datetime_utc->modify($modify_by); }else{ // if there is no time offset for this process and event type is not "start" or "end" of the booking - we don't need to // create any jobs, since other events have already past if(!in_array($process->event_type, ['booking_start', 'booking_end'])) return true; } $formatted_cutoff_utc = $cutoff_datetime_utc->format(LATEPOINT_DATETIME_DB_FORMAT); $args = []; switch($process->event_type){ case 'booking_start': $args['start_datetime_utc >='] = $formatted_cutoff_utc; break; case 'booking_end': $args['end_datetime_utc >='] = $formatted_cutoff_utc; break; case 'booking_created': $args['created_at >='] = $formatted_cutoff_utc; break; case 'booking_updated': $args['updated_at >='] = $formatted_cutoff_utc; break; case 'transaction_created': $args['created_at >='] = $formatted_cutoff_utc; break; case 'customer_created': $args['created_at >='] = $formatted_cutoff_utc; break; } if($process->event_type == 'customer_created'){ $models = new OsCustomerModel(); $model_name = 'customer'; }else{ $models = new OsBookingModel(); $model_name = 'booking'; } if($args) $models->where($args); $models = $models->get_results_as_models(); foreach($models as $model){ $objects = []; $objects[] = ['model' => $model_name, 'id' => $model->id, 'model_ready' => $model]; self::create_jobs_for_process($process, $objects); } } public static function process_scheduled_jobs(){ $jobs = new OsProcessJobModel(); // find jobs that are scheduled to run in a period from [24 hour ago to NOW] - so that we don't run old irrelevant jobs $jobs = $jobs->where(['status' => LATEPOINT_JOB_STATUS_SCHEDULED, 'to_run_after_utc <=' => OsTimeHelper::now_datetime_utc_in_db_format(), 'to_run_after_utc >=' => OsTimeHelper::custom_datetime_utc_in_db_format('-24 hours')])->get_results_as_models(); foreach($jobs as $job){ $job->run(); $result = json_decode($job->run_result, true); echo '<div>'.esc_html($job->id).':'.esc_html($result['status']).', '.esc_html($result['message']).'</div>'; } } public static function init_hooks(){ add_action('latepoint_customer_created', 'OsProcessJobsHelper::handle_customer_created', 12); add_action('latepoint_transaction_created', 'OsProcessJobsHelper::handle_transaction_created', 12); add_action('latepoint_booking_created', 'OsProcessJobsHelper::handle_booking_created', 12); add_action('latepoint_booking_updated', 'OsProcessJobsHelper::handle_booking_updated', 12, 2); add_action('latepoint_order_created', 'OsProcessJobsHelper::handle_order_created', 12); add_action('latepoint_order_updated', 'OsProcessJobsHelper::handle_order_updated', 12, 2); add_action('latepoint_payment_request_created', 'OsProcessJobsHelper::handle_payment_request_created', 12); } public static function handle_customer_created(OsCustomerModel $customer){ $objects = []; $objects[] = ['model' => 'customer', 'id' => $customer->id, 'model_ready' => $customer]; self::create_jobs_for_event('customer_created', $objects); } public static function handle_transaction_created(OsTransactionModel $transaction){ $objects = []; $objects[] = ['model' => 'transaction', 'id' => $transaction->id, 'model_ready' => $transaction]; self::create_jobs_for_event('transaction_created', $objects); } public static function get_nice_job_status_name($status){ $names = [ LATEPOINT_JOB_STATUS_COMPLETED => __('Completed', 'latepoint'), LATEPOINT_JOB_STATUS_SCHEDULED => __('Scheduled', 'latepoint'), LATEPOINT_JOB_STATUS_CANCELLED => __('Cancelled', 'latepoint'), LATEPOINT_JOB_STATUS_ERROR => __('Error', 'latepoint'), ]; return $names[$status] ?? __('n/a', 'latepoint'); } public static function handle_booking_created(OsBookingModel $booking){ $objects = []; $objects[] = ['model' => 'booking', 'id' => $booking->id, 'model_ready' => $booking]; self::create_jobs_for_event('booking_created', $objects); self::create_jobs_for_event('booking_start', $objects); self::create_jobs_for_event('booking_end', $objects); } public static function handle_booking_updated(OsBookingModel $new_booking, OsBookingModel $old_booking){ // remove previously scheduled jobs for this booking because it's changed and might not need them anymore // remove only those that are in "scheduled" status, those that were already sent or errored should stay $jobs = new OsProcessJobModel(); $jobs->delete_where(['status' => LATEPOINT_JOB_STATUS_SCHEDULED, 'object_id' => $new_booking->id, 'object_model_type' => 'booking']); $objects = []; $objects[] = ['model' => 'booking', 'id' => $new_booking->id, 'model_ready' => $new_booking]; $objects[] = ['model' => 'old_booking', 'id' => $old_booking->id, 'model_ready' => $old_booking]; self::create_jobs_for_event('booking_updated', $objects); // some changes might have triggered other webhooks (e.g. service changed, so now it could be required to be reminded of booking start/end) self::create_jobs_for_event('booking_start', $objects); self::create_jobs_for_event('booking_end', $objects); } public static function handle_order_created(OsOrderModel $order){ $objects = []; $objects[] = ['model' => 'order', 'id' => $order->id, 'model_ready' => $order]; self::create_jobs_for_event('order_created', $objects); } public static function handle_payment_request_created(OsPaymentRequestModel $payment_request){ $objects = []; $objects[] = ['model' => 'payment_request', 'id' => $payment_request->id, 'model_ready' => $payment_request]; self::create_jobs_for_event('payment_request_created', $objects); } public static function handle_order_updated(OsOrderModel $new_order, OsOrderModel $old_order){ // remove previously scheduled jobs for this order because it's changed and might not need them anymore // remove only those that are in "scheduled" status, those that were already sent or errored should stay $jobs = new OsProcessJobModel(); $jobs->delete_where(['status' => LATEPOINT_JOB_STATUS_SCHEDULED, 'object_id' => $new_order->id, 'object_model_type' => 'order']); $objects = []; $objects[] = ['model' => 'order', 'id' => $new_order->id, 'model_ready' => $new_order]; $objects[] = ['model' => 'old_order', 'id' => $old_order->id, 'model_ready' => $old_order]; self::create_jobs_for_event('order_updated', $objects); } /** * @param OsProcessModel $process * @param $opposite determines if apply time offset in opposite direction (to search events eligible for cutoff) * @return string */ public static function should_modify_event_time(OsProcessModel $process, $opposite = false): string{ if(empty($process->time_offset)) { // no time offset return ''; }else{ $time_offset_settings = $process->time_offset; // offset, calculate how much to modify by $sign = ($opposite) ? (($time_offset_settings['before_after'] == 'after') ? '-' : '+') : (($time_offset_settings['before_after'] == 'after') ? '+' : '-'); $modify_by = $sign.$time_offset_settings['value'].' '.$time_offset_settings['unit']; return $modify_by; } } }