[Back] <?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;
}
}
}