- Introduced `safety_seal` column in the `orders` table. - Updated order creation and completion logic to handle safety seal values. - Enhanced order and booking classes to manage safety seal attachment and retrieval. - Added tests to validate safety seal functionality in order processing.
573 lines
22 KiB
PHP
573 lines
22 KiB
PHP
<?php
|
|
|
|
namespace objects;
|
|
|
|
use attachments\helpers\attachment_content;
|
|
use classes\db;
|
|
use classes\email;
|
|
use classes\gatewayapi;
|
|
use classes\object_property;
|
|
use classes\pdf_generator;
|
|
use classes\slack;
|
|
use Exception;
|
|
use traits\db_object_t;
|
|
|
|
class order_bookings_o extends db
|
|
{
|
|
use db_object_t;
|
|
|
|
public object_property $customer_number;
|
|
public object_property $department;
|
|
public object_property $reg_1;
|
|
public object_property $reg_2;
|
|
public object_property $reg_3;
|
|
public object_property $datetime;
|
|
public object_property $note;
|
|
public object_property $reference;
|
|
public object_property $po;
|
|
public object_property $pickup;
|
|
public object_property $items;
|
|
public object_property $order_id;
|
|
public object_property $created_at;
|
|
public object_property $updated_at;
|
|
public object_property $deleted_at;
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function sendWashCertificateToCustomer(): void
|
|
{
|
|
self::requireSelected();
|
|
// Only send a wash certificate if the order has been created.
|
|
if (!$this->hasTransaction()) {
|
|
return;
|
|
}
|
|
// Check if a wash certificate is attached to the order
|
|
if (!self::getOrder()->hasWashCertificateAttached()) {
|
|
return;
|
|
}
|
|
// Get the customer from the booking
|
|
$customer = (new users_o())->getUserByCustomerNumber((int)$this->customer_number->value());
|
|
if (!$customer->exists()) {
|
|
return;
|
|
}
|
|
// Check if the customer wants email notifications
|
|
if (!$customer->wantsEmailNotifications()) {
|
|
return;
|
|
}
|
|
// Get the email
|
|
$customer_email = !empty($customer->wash_certificate_email->value())
|
|
? $customer->wash_certificate_email->value()
|
|
: $customer->email->value();
|
|
if (empty($customer_email)) {
|
|
return;
|
|
}
|
|
// Send wash certificate email
|
|
$email = new email();
|
|
$email->sendWashCertificateEmailToCustomer($this);
|
|
}
|
|
|
|
|
|
public function structure(): void
|
|
{
|
|
$this->setTable('order_bookings');
|
|
}
|
|
|
|
/**
|
|
* Add an object
|
|
* @param array $data The additional data of the object (e.g. ["key" => "value"])
|
|
* @return void
|
|
* @throws Exception If the object was not created successfully
|
|
*/
|
|
public function add(
|
|
array $data,
|
|
): void
|
|
{
|
|
global /** @var db $db */
|
|
$db;
|
|
// Add the object to the database
|
|
$new_id = self::add_object($data);
|
|
self::select($new_id);
|
|
$this->onAfterNewBooking();
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function onAfterNewBooking(): void
|
|
{
|
|
self::requireSelected();
|
|
$this->notifyNewBooking(); // Notify the department about the new booking
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
$department_notification = true;
|
|
$customer_notification = true;
|
|
/**
|
|
* Get the department, branding and customer
|
|
*/
|
|
// 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();
|
|
$message = "";
|
|
$message .= "Ny booking fra {$customer->getCustomerName($customer_array['customer_number'])} (Kundenr: {$customer_array['customer_number']}, Bookingnr: {$this->id})\n";
|
|
$message .= "Dato: " . date('d-m-Y H:i', strtotime($this->datetime->value())) . "\n";
|
|
$message .= "Køretøj: " . $this->reg_1->value() . (!empty($this->reg_2->value()) ? ", " . $this->reg_2->value() : "") . (
|
|
!empty($this->reg_3->value()) ? ", " . $this->reg_3->value() : ""
|
|
) ."\n";
|
|
// If the booking has a note, add it to the message
|
|
if (!empty($this->note->value())) $message .= "Note: " . $this->note->value() . "\n";
|
|
// If the booking has items, add them to the message
|
|
if (!empty($this->items->value())) {
|
|
$items = array_map(function ($item) {
|
|
// Get the product name
|
|
$product = (new products_o())->select((int)$item['id']);
|
|
if (!$product->exists()) {
|
|
return null;
|
|
}
|
|
$item['name'] = $product->name->value();
|
|
if (!isset($item['quantity'])) {
|
|
$item['quantity'] = 1;
|
|
}
|
|
return "- " . $item['quantity'] . " x " . $item['name'];
|
|
}, $this->items->value());
|
|
$message .= "Ydelser:\n" . implode("\n", array_filter($items)) . "\n";
|
|
}
|
|
// If the booking has a reference, add it to the message
|
|
if (!empty($this->reference->value())) $message .= "Reference: " . $this->reference->value() . "\n";
|
|
// If the booking has a PO, add it to the message
|
|
if (!empty($this->po->value())) $message .= "PO: " . $this->po->value() . "\n";
|
|
if ($this->pickup->value()) $message .= "Afhentning: Ja\n"; else $message .= "Afhentning: Nej\n";
|
|
// If the booking has a note, add it to the message
|
|
if (!empty($this->note->value())) $message .= "Note: " . $this->note->value() . "\n";
|
|
// Add the branding name
|
|
$message = "*" . $branding->name->value() . "*\n" . $message;
|
|
/**
|
|
* Department booking notification
|
|
*/
|
|
if ($department_notification) {
|
|
// Define delivery methods
|
|
$deliverSlack = (boolean)!empty($department->slack_webhook->value());
|
|
$deliverSMS = true;
|
|
// Check if the department has a slack webhook
|
|
if ($deliverSlack) {
|
|
// Construct email
|
|
$message = "*" . $branding->name->value() . "*\n";
|
|
$message .= "Ny booking fra *" . $customer->getCustomerName($customer_array['customer_number']) . "* (Kundenr: " . $customer_array['customer_number'] . ", Bookingnr: " . $this->id . ")\n";
|
|
$message .= "Dato: " . date('d-m-Y H:i', strtotime($this->datetime->value())) . "\n";
|
|
$message .= "Køretøj: " . $this->reg_1->value() . (!empty($this->reg_2->value()) ? ", " . $this->reg_2->value() : "") . (
|
|
!empty($this->reg_3->value()) ? ", " . $this->reg_3->value() : ""
|
|
) ."\n";
|
|
// If the booking has a note, add it to the message
|
|
if (!empty($this->note->value())) {
|
|
$message .= "Note: " . $this->note->value() . "\n";
|
|
}
|
|
// If the booking has items, add them to the message
|
|
$items = array_map(function ($item) {
|
|
// Get the product name
|
|
$product = (new products_o())->select((int)$item['id']);
|
|
if (!$product->exists()) {
|
|
return null;
|
|
}
|
|
$item['name'] = $product->name->value();
|
|
if (!isset($item['quantity'])) {
|
|
$item['quantity'] = 1;
|
|
}
|
|
return "- " . $item['quantity'] . " x " . $item['name'];
|
|
}, $this->items->value());
|
|
if (!empty($items)) {
|
|
$message .= "Ydelser:\n" . implode("\n", array_filter($items)) . "\n";
|
|
}
|
|
// Send a notification to the department
|
|
$slack = new slack();
|
|
$slack->send_department_booking_notification($department->id, $message);
|
|
}
|
|
|
|
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()
|
|
],
|
|
$message,
|
|
true
|
|
);
|
|
} catch (Exception $e) {
|
|
// Log the error
|
|
$logs = new logs_o();
|
|
$logs->add('gatewayapi', 0, 3, 0, 'SEND_DEPARTMENT_BOOKING_NOTIFICATION', $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Customer booking confirmation
|
|
*/
|
|
if ($customer_notification) {
|
|
// Check if the customer has a phone number
|
|
$country_code = $customer->phone_country_code->value();
|
|
$customer_phone = $customer->phone->value();
|
|
$customer_email = $customer->getWashCertificateEmail();
|
|
if (!$customer_email) {
|
|
$customer_email = $customer->email->value();
|
|
}
|
|
if (empty($customer_phone) && empty($customer_email)) {
|
|
return;
|
|
}
|
|
if (!empty($customer_phone) && $customer->wantsSmsNotifications()) {
|
|
// Send an SMS confirmation to the customer
|
|
$gatewayapi = new gatewayapi();
|
|
$message = "Tak for din booking hos " . $branding->name->value() . ". Dit bookingnummer er " . $this->id . ". "
|
|
. "\nDato: " . date('d-m-Y', strtotime($this->datetime->value())) . "."
|
|
. "\nKøretøj: " . $this->reg_1->value() . (!empty($this->reg_2->value()) ? ", " . $this->reg_2->value() : "") . (
|
|
!empty($this->reg_3->value()) ? ", " . $this->reg_3->value() : ""
|
|
) .". "
|
|
. "\nAdresse: " . $branding->address->value() . ". "
|
|
. "\nVi glæder os til at se dig!";
|
|
try {
|
|
$gatewayapi->send(
|
|
[
|
|
(string)$country_code . (string)$customer_phone,
|
|
],
|
|
$message,
|
|
true
|
|
);
|
|
} catch (Exception $e) {
|
|
// Log the error
|
|
$logs = new logs_o();
|
|
$logs->add('gatewayapi', 0, 3, 0, 'SEND_CUSTOMER_BOOKING_CONFIRMATION', $e->getMessage());
|
|
}
|
|
}
|
|
// Check if the customer has an email
|
|
if (!empty($customer_email) && $customer->wantsEmailNotifications()) {
|
|
// Send an email confirmation to the customer
|
|
$email = new email();
|
|
$email->sendOrderBookingConfirmationEmail($this);
|
|
}
|
|
// TODO: Send booking confirmation to customer
|
|
}
|
|
}
|
|
|
|
|
|
public function getObjectProperties(): void
|
|
{
|
|
$this->customer_number = new object_property($this->table, $this->id, 'customer_number', 'int', false);
|
|
$this->department = new object_property($this->table, $this->id, 'department', 'int', false);
|
|
$this->reg_1 = new object_property($this->table, $this->id, 'reg_1', 'string', false);
|
|
$this->reg_2 = new object_property($this->table, $this->id, 'reg_2', 'string', false);
|
|
$this->reg_3 = new object_property($this->table, $this->id, 'reg_3', 'string', false);
|
|
$this->datetime = new object_property($this->table, $this->id, 'datetime', 'timestamp', false);
|
|
$this->note = new object_property($this->table, $this->id, 'note', 'string', false);
|
|
$this->reference = new object_property($this->table, $this->id, 'reference', 'string', false);
|
|
$this->po = new object_property($this->table, $this->id, 'po', 'string', false);
|
|
$this->pickup = new object_property($this->table, $this->id, 'pickup', 'bool', false);
|
|
$this->items = new object_property($this->table, $this->id, 'items', 'json', false);
|
|
$this->order_id = new object_property($this->table, $this->id, 'order_id', 'int', false);
|
|
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'timestamp', false);
|
|
$this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'timestamp', false);
|
|
$this->deleted_at = new object_property($this->table, $this->id, 'deleted_at', 'timestamp', false);
|
|
}
|
|
|
|
public function objectChanged(): void
|
|
{
|
|
//TODO: Add cache invalidation
|
|
}
|
|
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function asArray(): array
|
|
{
|
|
$order_id = $this->order_id->value();
|
|
return [
|
|
'id' => (int)$this->id,
|
|
'customer_number' => (int)$this->customer_number->value(),
|
|
'department' => (int)$this->department->value(),
|
|
'reg_1' => $this->reg_1->value(),
|
|
'reg_2' => $this->reg_2->value(),
|
|
'reg_3' => $this->reg_3->value(),
|
|
'datetime' => $this->datetime->value(),
|
|
'note' => $this->note->value(),
|
|
'reference' => $this->reference->value(),
|
|
'po' => $this->po->value(),
|
|
'pickup' => (bool)$this->pickup->value(),
|
|
'items' => $this->parseProductNames($this->items->value()),
|
|
'order_id' => $order_id === null ? null : (int)$order_id,
|
|
'customer_name' => (new users_o())->getUserByCustomerNumber((int)$this->customer_number->value())->getCustomerName((int)$this->customer_number->value()),
|
|
'created_at' => $this->created_at->value(),
|
|
'updated_at' => $this->updated_at->value(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
private function parseProductNames(array $items): array
|
|
{
|
|
$parsedItems = [];
|
|
foreach ($items as $item) {
|
|
if (!isset($item['id']) || !isset($item['quantity'])) {
|
|
continue;
|
|
}
|
|
$product = (new products_o())->select((int)$item['id']);
|
|
if (!$product->exists()) {
|
|
continue;
|
|
}
|
|
$parsedItems[] = [
|
|
...$item,
|
|
'id' => (int)$product->id,
|
|
'name' => (string)$product->name->value(),
|
|
'quantity' => (int)$item['quantity'],
|
|
];
|
|
}
|
|
return $parsedItems;
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function completeBooking(int $user_id, string $safety_seal = null): void
|
|
{
|
|
self::requireSelected();
|
|
$orderWasCreatedDuringCompletion = false;
|
|
if (!$this->order_id->value()) {
|
|
// Create order, if not already created
|
|
$this->createOrderBy($user_id);
|
|
// Add order items, re-calculate the prices to be customer-specific
|
|
$this->createOrderItemsBy($user_id);
|
|
$orderWasCreatedDuringCompletion = true;
|
|
}
|
|
|
|
if (!$this->containsWashCertificateItem()) {
|
|
return;
|
|
}
|
|
|
|
$order = $this->getOrder();
|
|
$normalizedSafetySeal = orders_o::normalizeSafetySealValue($safety_seal);
|
|
if ($normalizedSafetySeal !== null) {
|
|
$order->setSafetySealValue($normalizedSafetySeal);
|
|
$order->objectChanged();
|
|
}
|
|
|
|
if (!$orderWasCreatedDuringCompletion) {
|
|
return;
|
|
}
|
|
|
|
$this->attachWashCertificate($user_id, $order->getSafetySealValue());
|
|
$this->sendWashCertificateToCustomer();
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function createOrderBy(int $user_id): void
|
|
{
|
|
self::requireSelected();
|
|
// Create order
|
|
$orders = new orders_o();
|
|
$orders->createOrderFromBooking($this->id, $user_id);
|
|
// Set the order id in the booking
|
|
$this->order_id->set((int)$orders->id);
|
|
self::objectChanged();
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function createOrderItemsBy(int $user_id): void
|
|
{
|
|
self::requireSelected();
|
|
// Get the order
|
|
$order = self::getOrder();
|
|
$order->requireSelected();
|
|
// Add order items
|
|
foreach ($this->items->value() as $item) {
|
|
if (!isset($item['id']) || !isset($item['quantity'])) {
|
|
continue;
|
|
}
|
|
$orderItems = new order_items_o();
|
|
$orderItems->addItemToOrder(
|
|
(int)$order->id,
|
|
(int)$item['id'],
|
|
(int)$user_id,
|
|
(int)$item['quantity'],
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function containsWashCertificateItem(): bool
|
|
{
|
|
self::requireSelected();
|
|
foreach ($this->items->value() as $item) {
|
|
$product_id = (int)($item['id'] ?? 0);
|
|
if ($product_id <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$product = (new products_o())->select($product_id);
|
|
if ($product->exists() && $product->isWashCertificate()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function getOrder(): orders_o
|
|
{
|
|
self::requireSelected();
|
|
if (!$this->order_id->value()) {
|
|
throw new \Exception('Order not found for this booking');
|
|
}
|
|
$order = new orders_o();
|
|
$order->select((int)$this->order_id->value());
|
|
if (!$order->exists()) {
|
|
throw new \Exception('Order not found for this booking');
|
|
}
|
|
return $order;
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
protected function attachWashCertificate(int $user_id, string $safety_seal = null): void
|
|
{
|
|
self::requireSelected();
|
|
// Check if the order already has a wash certificate attached
|
|
if ($this->getOrder()->hasWashCertificateAttached()) {
|
|
return;
|
|
}
|
|
// Get the operator name
|
|
$operator = (new users_o())->select($user_id);
|
|
if (!$operator->exists()) {
|
|
throw new Exception('Operator not found');
|
|
}
|
|
// Generate wash certificate
|
|
$this->generateWashCertificate($safety_seal, $operator->display_name->value());
|
|
}
|
|
|
|
/**
|
|
* 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(string|null $safety_seal = null, string|null $operator = null): void
|
|
{
|
|
self::requireSelected();
|
|
// 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' => orders_o::normalizeSafetySealValue($safety_seal),
|
|
'reg_1' => $booking_array['reg_1'],
|
|
'reg_2' => $booking_array['reg_2'],
|
|
'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 ?: '-',
|
|
'wash_type' => 'BOOK_WASH'
|
|
])
|
|
->getHtml()
|
|
);
|
|
$pdf_path = $pdf_generator->generate_pdf();
|
|
// Set the PDF in the order
|
|
$this->getOrder()->addAttachment(new attachment_content((object)['document' => $pdf_path, 'other' => 'wash_certificate']));
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function hasTransaction(): bool
|
|
{
|
|
self::requireSelected();
|
|
return !empty($this->order_id->value());
|
|
}
|
|
|
|
public function getDailyUnfulfilledBookingsCountForDepartment(int $department_id, string $date = null): int
|
|
{
|
|
global /** @var db $db */
|
|
$db;
|
|
if ($date === null) {
|
|
$date = date('Y-m-d');
|
|
}
|
|
// Sanitize date
|
|
$date = date('Y-m-d', strtotime($date));
|
|
$order_ids = self::getFieldsWhere([
|
|
'department' => (int)$department_id,
|
|
'DATE(datetime)' => $date,
|
|
'order_id' => null,
|
|
], ['id']);
|
|
return count($order_ids);
|
|
}
|
|
|
|
}
|