setTable('bookings'); } public function objectChanged(): void { // Clear the cache redis->clear_department_booking_count($this->department->value()); } public function getCustomerBookingsPaginated(int $customer_number, int $page = 1, int $limit = 10, array $order = ['id' => 'DESC'], string $search = null, array $filters = null): array { global /** @var response $response */ $db, $response; // Add the customer number to the filters $filters['customer_number'] = $customer_number; // List the objects with pagination $array = $this->listObjectsWithPagination($page, $limit, $search, $filters, $order); // Get the total number of objects $total = $this->getTotalObjects($search, $filters); $response->paginate($page, $limit, $total); return $array; } public function getDepartmentBookingsUnfulfilledCount(int $department_id): int { global /** @var response $response */ $db, $response; // Check if the count is cached if (redis->get_department_booking_count($department_id)) { $response->add_meta('cached', true); return redis->get_department_booking_count($department_id); } $sql = "SELECT COUNT(*) as count FROM $this->table WHERE department = $department_id AND status = 'pending'"; $result = $db->query($sql); $row = $db->fetch_assoc($result); // Cache the count redis->cache_department_booking_count($department_id, $row['count']); return $row['count']; } public function delete(int $id): void { $this->id = $id; $this->getObjectProperties(); // Set the status to cancelled $this->status->set('cancelled'); // Remove the cache redis->clear_department_booking_count($this->department->value()); } public function getObjectProperties(): void { $this->customer_number = new object_property($this->table, $this->id, 'customer_number', 'int', true); $this->wash_type = new object_property($this->table, $this->id, 'wash_type', 'string', true); $this->contact_email = new object_property($this->table, $this->id, 'contact_email', 'string', true); $this->reference_number = new object_property($this->table, $this->id, 'reference_number', 'string', true); $this->regNrTraekker = new object_property($this->table, $this->id, 'regNrTraekker', 'string', true); $this->regNrTrailer = new object_property($this->table, $this->id, 'regNrTrailer', 'string', true); $this->washCertificateEmail = new object_property($this->table, $this->id, 'washCertificateEmail', 'string', true); $this->date = new object_property($this->table, $this->id, 'date', 'string', true); $this->department = new object_property($this->table, $this->id, 'department', 'int', true); $this->pickup_bool = new object_property($this->table, $this->id, 'pickup_bool', 'int', true); $this->notes = new object_property($this->table, $this->id, 'notes', 'string', true); $this->washCertificateStatus = new object_property($this->table, $this->id, 'washCertificateStatus', 'string', true); $this->washCertificateUrl = new object_property($this->table, $this->id, 'washCertificateUrl', 'string', true); $this->wash_certificate_pdf = new object_property($this->table, $this->id, 'wash_certificate_pdf', 'string', true); $this->status = new object_property($this->table, $this->id, 'status', 'string', true); $this->data = new object_property($this->table, $this->id, 'data', 'string', true); $this->virtual_cart = new object_property($this->table, $this->id, 'virtual_cart', 'int', false); } public function parseBookings(array $listObjectsWithPaginationIfSet): array { // Parse the customer numbers $bookings = $this->parseCustomerNumbers($listObjectsWithPaginationIfSet); return $bookings; } public function parseCustomerNumbers(array $listObjectsWithPaginationIfSet): array { // Parse the customer numbers foreach ( $listObjectsWithPaginationIfSet as $key => $value ) { $listObjectsWithPaginationIfSet[$key]['customer_name'] = (new users_o())->getCustomerName($value['customer_number']); } return $listObjectsWithPaginationIfSet; } public function syncBookings(): array { global /** @var response $response */ $db, $response; $wordpress_bookings_remote = new wordpress_bookings_remote(); // Get all the bookings from the remote API $bookings = $wordpress_bookings_remote->get_all_bookings()['data']['all_wash_bookings']; // Parse the bookings $parsed_bookings = $wordpress_bookings_remote->parse_bookings($bookings); // Remove the cancelled bookings from the $parsed_bookings array $cancelled_bookings = $this->getAllCancelledOrCompletedBookings(); foreach ( $cancelled_bookings as $cancelled_booking ) { // Remove the cancelled booking from the parsed bookings foreach ( $parsed_bookings as $key => $parsed_booking ) { if ((int)$parsed_booking['id'] === (int)$cancelled_booking['id']) { unset($parsed_bookings[$key]); } } } // Sync the bookings foreach ( $parsed_bookings as $booking ) { // Add or update the booking $this->addOrUpdate( $booking['id'], $booking['customer_number'], $booking['wash_type'], $booking['contact_email'], $booking['reference_number'], $booking['regNrTraekker'], $booking['regNrTrailer'], $booking['washCertificateEmail'], $booking['date'], $booking['department'], $booking['pickup_bool'] ? 1 : 0, $booking['notes'], $booking['washCertificateStatus'], $booking['washCertificateUrl'], $booking['status'] ); } return [ "bookings" => count($bookings), "cancelled" => count($cancelled_bookings), "parsed" => count($parsed_bookings), ]; } public function getAllCancelledOrCompletedBookings(): array { global $db; // Get all the cancelled bookings $sql = "SELECT * FROM $this->table WHERE status = 'cancelled' OR status = 'completed'"; $result = $db->query($sql); return $db->fetch_all($result); } public function addOrUpdate(int $id, $customer_number, $wash_type, $contact_email, $reference_number, $regNrTraekker, $regNrTrailer, $washCertificateEmail, $date, $department, $pickup_bool, $notes, $washCertificateStatus, $washCertificateUrl, $status): void { global $db; // Avoid SQL injection $wash_type = $db->escape_string($wash_type); $contact_email = $db->escape_string($contact_email); $reference_number = $db->escape_string($reference_number); $regNrTraekker = $db->escape_string($regNrTraekker); $regNrTrailer = $db->escape_string($regNrTrailer); $washCertificateEmail = $db->escape_string($washCertificateEmail); $date = $db->escape_string($date); // Parse the department name to id $department = $this->getDepartmentIdByLegacyName($department); $notes = $db->escape_string($notes); $washCertificateStatus = $db->escape_string($washCertificateStatus); $washCertificateUrl = $db->escape_string($washCertificateUrl); $status = $db->escape_string($status); $pickup_bool = (int)$pickup_bool; // Check if the entry already exists $sql = "SELECT * FROM $this->table WHERE id = $id"; $result = $db->query($sql); if ($db->num_rows($result) === 0) { // Send a department webhook if the booking is new. // TRU-106: send_new_booking_notification() filters out drop-offs // (pickup_bool = 0) so only pickup bookings post to Slack. $slack = new slack(); try { $slack->send_new_booking_notification( $id, $customer_number, $wash_type, $contact_email, $reference_number, $regNrTraekker, $regNrTrailer, $washCertificateEmail, $date, $department, (bool)$pickup_bool, $notes, $washCertificateStatus, $washCertificateUrl, $status ); } catch (Exception $e) { // Log the error $logs = new logs_o(); $logs->add('slack', 0, 3, 0, 'SEND_DEPARTMENT_BOOKING_NOTIFICATION', $e->getMessage()); } } // Create a new record in the database ( Replace the code, if an entry already exists ) $sql = "INSERT INTO $this->table (id, customer_number, wash_type, contact_email, reference_number, regNrTraekker, regNrTrailer, washCertificateEmail, date, department, pickup_bool, notes, washCertificateStatus, washCertificateUrl, status) VALUES ($id, $customer_number, '$wash_type', '$contact_email', '$reference_number', '$regNrTraekker', '$regNrTrailer', '$washCertificateEmail', '$date', '$department', $pickup_bool, '$notes', '$washCertificateStatus', '$washCertificateUrl', '$status') ON DUPLICATE KEY UPDATE customer_number = $customer_number, wash_type = '$wash_type', contact_email = '$contact_email', reference_number = '$reference_number', regNrTraekker = '$regNrTraekker', regNrTrailer = '$regNrTrailer', washCertificateEmail = '$washCertificateEmail', date = '$date', department = '$department', pickup_bool = $pickup_bool, notes = '$notes', washCertificateStatus = '$washCertificateStatus', washCertificateUrl = '$washCertificateUrl', status = '$status'"; $db->query($sql); // Clear the cache redis->clear_department_booking_count($department); } public function getDepartmentIdByLegacyName(string $departmentName): int { // Get the department id by the legacy name $departmentLegacyNames = [ 'køge' => 4, 'taastrup' => 2, 'aarhusc' => 5, 'roskilde' => 6, 'hvidovre' => 1, 'glostrup' => 3, 'taulov' => 7, ]; return $departmentLegacyNames[strtolower($departmentName)] ?? 0; } /** * Add a new booking * @throws Exception If the object could not be created */ public function add(int $customer_number, string $wash_type, string $contact_email, string $reference_number, string $regNrTraekker, string $regNrTrailer, string $washCertificateEmail, string $date, int $department, int $pickup_bool, string $notes, string $washCertificateStatus, string $washCertificateUrl, string $status, array $data = []): void { global $db; $tmp_id = self::add_object([ 'customer_number' => (int)$db->escape_string($customer_number), 'wash_type' => (string)$db->escape_string($wash_type), 'contact_email' => (string)$db->escape_string($contact_email), 'reference_number' => (string)$db->escape_string($reference_number), 'regNrTraekker' => (string)$db->escape_string($regNrTraekker), 'regNrTrailer' => (string)$db->escape_string($regNrTrailer), 'washCertificateEmail' => (string)$washCertificateEmail ? $db->escape_string($washCertificateEmail) : '', 'date' => (string)$db->escape_string($date), 'department' => (int)$db->escape_string($department), 'pickup_bool' => (int)$db->escape_string($pickup_bool), 'notes' => (string)$db->escape_string($notes), 'washCertificateStatus' => (string)$db->escape_string($washCertificateStatus), 'washCertificateUrl' => (string)$db->escape_string($washCertificateUrl), 'status' => (string)$db->escape_string($status), 'data' => json_encode($data), ]); // Check if the booking was created successfully self::select((int)$tmp_id); self::requireSelected(); self::notifyNewBooking(); } /** * Notify the department about a new booking * @throws Exception If the object is not selected * @throws ClientExceptionInterface If the request to the API fails */ public function notifyNewBooking(): void { self::requireSelected(); // Get the department from the booking $department = (new departments_o())->select((int)$this->department->value()); if (!$department->exists()) { throw new Exception('Department not found'); } // Get the branding from the department $branding = (new branding_o())->select((int)$department->branding->value()); if (!$branding->exists()) { throw new Exception('Branding not found'); } // Get the customer from the booking $customer = (new users_o())->getUserByCustomerNumber((int)$this->customer_number->value()); $department_array = $department->asArray(); $customer_array = $customer->asArray(); // Define delivery methods $deliverSlack = (boolean)!empty($department->slack_webhook->value()); $deliverSMS = true; $deliverEmail = ( !empty($this->contact_email->value()) && !$deliverSlack // Only send email if slack is not available ); // Check if the department has a slack webhook // TRU-106: send_new_booking_notification() filters out drop-offs // (pickup_bool = false) so only pickup bookings post to Slack. if ($deliverSlack) { // Send a notification to the department $slack = new slack(); try { $slack->send_new_booking_notification( $this->id, $customer_array['customer_number'], self::formatWashTypeFromServices(json_decode($this->data->value(), true)), $this->contact_email->value(), $this->reference_number->value() ?: '-', $this->regNrTraekker->value(), $this->regNrTrailer->value(), $this->washCertificateEmail->value(), $this->date->value(), $department->id, (bool)$this->pickup_bool->value(), $this->notes->value(), $this->washCertificateStatus->value(), $this->washCertificateUrl->value(), $this->status->value() ); } catch (Exception $e) { // Previously this bare call would crash the entire // notifyNewBooking() flow if Slack returned non-2xx, so // bookings_o::add() rolled back the whole add. The same // call in addOrUpdate() (around line 219) is already wrapped // in try/catch; mirror that handling here so a transient // Slack outage cannot prevent the booking from being saved. $logs = new logs_o(); $logs->add('slack', 0, 3, 0, 'SEND_DEPARTMENT_BOOKING_NOTIFICATION', $e->getMessage()); } } if ($deliverSMS) { // Send an SMS notification to the department $gatewayapi = new gatewayapi(); try { $gatewayapi->send( [ // Add all the phone numbers from the department ...$department->notificationSmsPhoneNumbers() ], 'New booking from ' . $customer->getCustomerName($customer_array['customer_number']) . ' (' . $this->id . ')', ); } catch (Exception $e) { // Log the error $logs = new logs_o(); $logs->add('gatewayapi', 0, 3, 0, 'SEND_DEPARTMENT_BOOKING_NOTIFICATION', $e->getMessage()); } } if ($deliverEmail) { // Send an email notification to the department $email = new email(); try { $email->sendBookingNotification($this->id); } catch (Exception $e) { // Previously this bare call would crash notifyNewBooking() // (and therefore bookings_o::add()) if MailerSend returned // non-2xx, even though the SMS path below is guarded. // Wrap it so a transient email-provider outage cannot // reject the booking. $logs = new logs_o(); $logs->add('email', 0, 3, 0, 'SEND_DEPARTMENT_BOOKING_NOTIFICATION', $e->getMessage()); } } } /** * Convert the object to an array * @options include_customer_name Include the customer name * @options include_department_name Include the department name * @options include_parsed_services Include the parsed services * @param array $options [@options] The options to include in the array * @return array The object as an array * @throws Exception If the object is not selected */ public function asArray(array $options = []): array { self::requireSelected(); $options = array_merge([ 'include_customer_name' => false, 'include_department_name' => false, 'include_parsed_services' => false, ], $options); $washCertificatePDF = $this->wash_certificate_pdf->value(); $washCertificateStatus = $this->washCertificateStatus->value(); $status = $this->status->value(); // If the wash certificate PDF is not empty, but the status is not 'completed', set the status to 'completed' if (!empty($washCertificatePDF) && $status !== 'completed') { $this->status->set('completed'); $status = 'completed'; } if ($washCertificateStatus === 'pending' && $status === 'completed') { $this->washCertificateStatus->set('completed'); $washCertificateStatus = 'completed'; } $tmp_array = [ 'id' => (int)$this->id, 'customer_number' => (int)$this->customer_number->value(), 'wash_type' => $this->wash_type->value(), 'contact_email' => $this->contact_email->value(), 'reference_number' => $this->reference_number->value(), 'regNrTraekker' => $this->regNrTraekker->value(), 'regNrTrailer' => $this->regNrTrailer->value(), 'washCertificateEmail' => $this->washCertificateEmail->value(), 'date' => $this->date->value(), 'department' => (int)$this->department->value(), 'pickup_bool' => $this->pickup_bool->value() ? true : false, 'notes' => $this->notes->value(), 'washCertificateStatus' => $this->washCertificateStatus->value(), 'washCertificateUrl' => $this->washCertificateUrl->value(), 'wash_certificate_pdf' => $washCertificatePDF, 'status' => $status, 'data' => $this->data->value() ? json_decode($this->data->value(), true) : null, ]; // Include the customer name if ($options['include_customer_name']) { $customer = (new users_o())->getUserByCustomerNumber((int)$this->customer_number->value()); $tmp_array['customer_name'] = $customer->getCustomerName($this->customer_number->value()); } // Include the department name if ($options['include_department_name']) { $department = (new departments_o())->select((int)$this->department->value()); if ($department->exists()) { $tmp_array['department_name'] = $department->name->value(); } } // Include the parsed services if ($options['include_parsed_services']) { // Format the wash type from the services array as followed: // 1 => 'Udvendig sættevognstræk ( Trækker / trailer )', $tmp_array['parsed_services']['string'] = $this->formatWashTypeFromServices( $this->data->value() ? json_decode($this->data->value(), true) : [] ); $tmp_array['parsed_services']['array'] = $this->data->value() ? json_decode($this->data->value(), true) : []; } return $tmp_array; } /** * Format the wash type from the services array * @param array $services The services array * @return string The formatted wash type */ private function formatWashTypeFromServices(array $services): string { // Format the wash type from the services array $wash_services = self::getWashServicesString(self::getWashServices()); $string = ''; foreach ( $services as $service ) { if (isset($wash_services[$service])) { $string .= $wash_services[$service] . ', '; } } // Remove the last comma and space return rtrim($string, ', '); } private static function getWashServicesString(array $services): array { // Format the wash services as a string (product1, product2, product3) $string = []; foreach ( $services as $key => $service ) { if (isset($service['overrides']['name'])) { $string[$key] = $service['overrides']['name']; } else { $string[$key] = $service['name']; } } return $string; } /** * @throws Exception */ private function getWashServices(): array { // Define the product function // Get the wash services from the database return [ // Exterior wash "Sættevognstræk" 1 => self::defineProduct(1, 3, [ 'name' => 'Udvendig sættevognstræk ( Trækker / trailer )', 'description' => 'Udvendig vask af sættevognstræk', ]), // Exterior trailer wash "Udvendig trailer vask" 2 => self::defineProduct(2, 2, [ 'name' => 'Udvendig trailer vask', 'description' => 'Udvendig vask af trailer', ]), 3 => self::defineProduct(3, 10, [ 'name' => 'Indvendig trailer vask', 'description' => 'Indvendig vask af trailer', ]), 4 => self::defineProduct(41, 41), ]; } /** * Define a product for the booking * @param int $id * @param int $product_id * @param array $overrides * @param array $options * @param array $addons * @return array * @throws Exception * @see getWashServices() * @see getWashServicesString() */ private function defineProduct(int $id, int $product_id, array $overrides = [], array $options = [], array $addons = []): array { // Check if the product exists if (!empty($product_id)) { $product_obj = (new products_o())->select((int)$product_id); if ($product_obj->exists()) { $default_options = $product_obj->asArray(); } } // Return the product return [ 'id' => (int)$id, 'product_id' => (int)$product_id, 'addons' => [...$addons], ...($default_options ?? []), 'overrides' => [ 'name' => null, 'quantity' => null, 'description' => null, 'notes' => null, 'reference' => null, ...$overrides, ], ...$options, ]; } public function checkUnfulfilledBookings(): void { $unfulfilled_bookings = []; function addUnfulfilledBooking(int $department, array $arr): array { // Check if the department has a count in the unfulfilled bookings array if (!isset($arr[$department])) { $arr[$department] = 0; } // Add the unfulfilled booking to the department count $arr[$department]++; return $arr; } global $db; // Get all the unfulfilled bookings $bookings = $this->listObjectsWithPagination(1, 100000, null, ['status' => 'pending']); // Check if the booking has been fulfilled foreach ( $bookings as $booking ) { if ($this->isCancelled($booking['id'])) { echo "Booking with ID $booking[id] has been cancelled\n"; continue; } // Check if the booking is scheduled for the future if (strtotime($booking['date']) > time()) { continue; } // Check if the booking has been fulfilled $fulfilled = $this->checkBookingFulfilled($booking['id']); if (!$fulfilled) { $unfulfilled_bookings = addUnfulfilledBooking($booking['department'], $unfulfilled_bookings); echo "Booking with ID $booking[id] has not been fulfilled\n"; } } // Send a department webhook if the booking has not been fulfilled foreach ( $unfulfilled_bookings as $department => $count ) { $slack = new slack(); try { $slack->send_department_booking_notification($department, 'There are ' . $count . ' unfulfilled bookings'); } catch (Exception $e) { // Log the error $logs = new logs_o(); $logs->add('slack', 0, 3, 0, 'SEND_DEPARTMENT_UNFULFILLED_BOOKINGS_NOTIFICATION', $e->getMessage()); } } } private static function isCancelled(int $id): bool { global $db; // Check if the booking has been cancelled $sql = "SELECT status FROM bookings WHERE id = $id"; $result = $db->query($sql); $row = $db->fetch_assoc($result); return $row['status'] === 'cancelled'; } public function checkBookingFulfilled(int $booking_id): bool { // Check if the booking has a wash certificate $wash_certificate_store = new wash_certificate_store(); return $wash_certificate_store->washCertificateExists($booking_id); } public function completeWashWithoutWashCertificate(int $id): void { global $db; // Set the status to completed $sql = "UPDATE $this->table SET status = 'completed', washCertificateStatus = 'cancelled' WHERE id = $id"; $db->query($sql); } /** * Generate a wash certificate * @param int|null $safety_seal The safety seal number * @param string|null $operator The operator name * @throws Exception If the object is not selected * @throws Exception If the booking already has a wash certificate */ public function generateWashCertificate(int|null $safety_seal = null, string|null $operator = null): void { self::requireSelected(); // Check if the booking already has a wash certificate if ($this->hasWashCertificate()) { throw new Exception('Booking already has a wash certificate'); } // Generate the wash certificate // Get the department from the booking $department = (new departments_o())->select((int)$this->department->value()); if (!$department->exists()) { throw new Exception('Department not found'); } // Get the branding from the department $branding = (new branding_o())->select((int)$department->branding->value()); if (!$branding->exists()) { throw new Exception('Branding not found'); } // Get the customer from the booking $customer = (new users_o())->getUserByCustomerNumber((int)$this->customer_number->value()); $department_array = $department->asArray(); $customer_array = $customer->asArray(); $booking_array = self::asArray(); $department_array['branding'] = $branding->asArray(); //print_r($department_array); //print_r($customer_array); //print_r($booking_array); $pdf_generator = new pdf_generator(); $pdf_generator->add_html($pdf_generator->templates->getTemplate('wash_certificate') ->setCompany([ 'name' => 'Truck Wash', 'address' => 'Letland Allé 2', 'zip' => 2630, 'city' => 'Taastrup', 'phone_prefix' => 45, 'phone' => 43717886, 'email' => 'cph@truckwash.dk', 'website' => 'www.truckwash.dk', 'images' => [ 'logo' => '/truckwash-banner-png.png', 'banner' => '/truckwash-banner-png.png', 'signature' => '/truckwash-underskrift.png', ], ]) ->addData([ 'booking_number' => $this->id, 'seal_number' => ($safety_seal ?? null), 'reg_1' => $booking_array['regNrTraekker'], 'reg_2' => $booking_array['regNrTrailer'], 'date' => date('d-m-Y'), 'time' => date('H:i'), 'carried_out_by' => ($operator ?? null), 'department_id' => $department->id, 'department_name' => $department_array['branding']['name'] ?: $department_array['name'], 'department_address' => $department_array['branding']['address'] ?: $department_array['description'], 'customer_name' => $customer->getCustomerName($customer_array['customer_number']), 'customer_address' => $customer->getCustomerEcocomicData($customer_array['customer_number'])->economic_customer->address ?: '-', 'type' => $booking_array['wash_type'], ]) ->getHtml() ); $pdf_path = $pdf_generator->generate_pdf(); // Set the PDF in the booking $this->wash_certificate_pdf->set($pdf_path); // Add the wash certificate to the bookings data (If it does not exist) if (!self::hasServiceInBookingData(4)) { self::addServiceToBookingData(4); } } /** * Check if the booking has a wash certificate * @throws Exception If the object is not selected */ public function hasWashCertificate(): bool { self::requireSelected(); // Check if the booking has a wash certificate if ($this->wash_certificate_pdf->value() !== null) { return true; } return false; } /** * Check if the booking data has a service * @param int $service_id The service id * @return bool True if the service exists, false otherwise * @throws Exception If the object is not selected */ public function hasServiceInBookingData(int $service_id): bool { self::requireSelected(); // Get the current data $data = $this->data->value() ? json_decode($this->data->value(), true) : []; // Check if the service exists return in_array($service_id, $data); } /** * Add a service to the booking data * @param int $service_id The service id * @throws Exception If the object is not selected */ public function addServiceToBookingData(int $service_id): void { self::requireSelected(); // Get the current data $data = $this->data->value() ? json_decode($this->data->value(), true) : []; // Add the service to the data $data[] = (int)$service_id; // Set the data $this->data->set(json_encode($data)); } /** * Send the wash certificate to the customer * @throws Exception If the object is not selected * @throws ClientExceptionInterface */ public function sendWashCertificateToCustomer(): void { self::requireSelected(); // Check if the booking has a wash certificate if (!$this->hasWashCertificate()) { throw new Exception('Booking does not have a wash certificate'); } // Note: unlike order_bookings_o::sendWashCertificateToCustomer(), the // legacy bookings_o path does NOT check wantsEmailNotifications() and // instead relies on $this->washCertificateEmail being non-empty. We // emit a structured "skip" event when washCertificateEmail is empty // so mis-routed legacy sends (e.g. a customer whose wash_certificate // email field was never populated) become visible without changing // existing behaviour. if (empty($this->washCertificateEmail->value())) { try { (new \objects\logs_o())->add( 'email', 'global', 3, 0, 'WASH_CERT_SKIP', json_encode([ 'reason' => 'legacy_no_wash_certificate_email', 'booking_id' => (int)$this->id, 'customer_number' => (int)$this->customer_number->value(), 'contact_email' => $this->contact_email->value(), ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ); } catch (\Throwable) { // Logging must never block the booking flow. } error_log('[wash-cert-skip] legacy_no_wash_certificate_email booking_id=' . $this->id); } // Send the wash certificate to the customer $email = new email(); $email->sendWashCertificateEmail( $this->id, (new users_o())->getCustomerName((int)$this->customer_number->value()), $this->contact_email->value(), $this->reference_number->value() ?: '-', $this->wash_certificate_pdf->value(), $this->washCertificateEmail->value(), ); } /** * @throws Exception */ public function createTransaction(): orders_o { global $response; self::requireSelected(); // Get the services from the booking $services = $this->data->value() ? json_decode($this->data->value(), true) : []; // Get the department from the booking $department = (new departments_o())->select((int)$this->department->value()); if (!$department->exists()) { throw new Exception('Department not found'); } // Create a new order $order = new orders_o(); $order->add( (int)$this->customer_number->value(), (int)$this->id, (string)$this->reference_number->value(), (string)$this->notes->value(), (int)$this->department->value(), (string)$this->regNrTraekker->value(), (string)$this->regNrTrailer->value(), ); // Set the orders booking id $order->booking_id->set((int)$this->id); // Define the relation order item ids, when applicable. $relations = [ // This is the order item id for the primary product, the wash certificate applies to. // This can be left null, if the product does not exist. 4 => null, ]; // Sort the services, so the primary product is first, and the services with relations are last. $services_sorted = []; $services_not_relation = []; $services_relation = []; foreach ( $services as $service ) { // Get the service id $service_id = (int)$service; // Check if the service has the posibility to have a relation (null values are allowed) if (in_array($service_id, array_keys($relations))) { $services_relation[] = $service_id; } else { $services_not_relation[] = $service_id; } } // Sort the services, so the primary product is first, and the services with (potential) relations are last. $services_sorted = [ ...$services_not_relation, ...$services_relation, ]; // Add the services to the order foreach ( $services_sorted as $service_id ) { $service = self::getWashServiceProduct((int)$service_id); // Add the service to the order $tmp_order_item = new order_items_o(); $tmp_order_item->addItemToOrder( $order->id, (int)$service->id, (int)$response->get_user()->id, (int)1, $relations[(int)$service_id] ?? null, ); // Check if the service allows for a wash certificate if ((int)$service_id === 3) { // Set the relation id for the wash certificate $relations[4] = (int)$tmp_order_item->id; } } // Return the order return $order; } /** * @throws Exception */ private function getWashServiceProduct(int $service_id): products_o { // Get the product id from the service id $services = self::getWashServices(); if (!isset($services[$service_id])) { throw new Exception('Service not found'); } $product_id = $services[$service_id]['product_id']; // Get the wash service product from the database $product = (new products_o())->select($product_id); if (!$product->exists()) { throw new Exception('Product not found'); } return $product; } }