Add safety seal support to orders and related logic for wash certificates
- 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.
This commit is contained in:
@@ -350,16 +350,32 @@ class order_bookings_o extends db
|
||||
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
|
||||
self::createOrderBy($user_id);
|
||||
$this->createOrderBy($user_id);
|
||||
// Add order items, re-calculate the prices to be customer-specific
|
||||
self::createOrderItemsBy($user_id);
|
||||
$this->createOrderItemsBy($user_id);
|
||||
$orderWasCreatedDuringCompletion = true;
|
||||
}
|
||||
// Create a wash certificate (If applicable)
|
||||
if (self::containsWashCertificateItem()) self::attachWashCertificate($user_id, $safety_seal);
|
||||
// Send wash certificate
|
||||
self::sendWashCertificateToCustomer();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,21 +423,18 @@ class order_bookings_o extends db
|
||||
public function containsWashCertificateItem(): bool
|
||||
{
|
||||
self::requireSelected();
|
||||
return self::containsProductId(41);
|
||||
}
|
||||
foreach ($this->items->value() as $item) {
|
||||
$product_id = (int)($item['id'] ?? 0);
|
||||
if ($product_id <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function containsProductId(int $productId): bool
|
||||
{
|
||||
self::requireSelected();
|
||||
$items = $this->items->value();
|
||||
foreach ($items as $item) {
|
||||
if (isset($item['id']) && (int)$item['id'] == $productId) {
|
||||
$product = (new products_o())->select($product_id);
|
||||
if ($product->exists() && $product->isWashCertificate()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -445,11 +458,11 @@ class order_bookings_o extends db
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function attachWashCertificate(int $user_id, string $safety_seal = null): void
|
||||
protected function attachWashCertificate(int $user_id, string $safety_seal = null): void
|
||||
{
|
||||
self::requireSelected();
|
||||
// Check if the order already has a wash certificate attached
|
||||
if (self::getOrder()->hasWashCertificateAttached()) {
|
||||
if ($this->getOrder()->hasWashCertificateAttached()) {
|
||||
return;
|
||||
}
|
||||
// Get the operator name
|
||||
@@ -458,7 +471,7 @@ class order_bookings_o extends db
|
||||
throw new Exception('Operator not found');
|
||||
}
|
||||
// Generate wash certificate
|
||||
self::generateWashCertificate($safety_seal, $operator->display_name->value());
|
||||
$this->generateWashCertificate($safety_seal, $operator->display_name->value());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -468,7 +481,7 @@ class order_bookings_o extends db
|
||||
* @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
|
||||
public function generateWashCertificate(string|null $safety_seal = null, string|null $operator = null): void
|
||||
{
|
||||
self::requireSelected();
|
||||
// Generate the wash certificate
|
||||
@@ -510,7 +523,7 @@ class order_bookings_o extends db
|
||||
])
|
||||
->addData([
|
||||
'booking_number' => $this->id,
|
||||
'seal_number' => ($safety_seal ?? null),
|
||||
'seal_number' => orders_o::normalizeSafetySealValue($safety_seal),
|
||||
'reg_1' => $booking_array['reg_1'],
|
||||
'reg_2' => $booking_array['reg_2'],
|
||||
'date' => date('d-m-Y'),
|
||||
@@ -556,4 +569,4 @@ class order_bookings_o extends db
|
||||
return count($order_ids);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ class orders_o extends db
|
||||
public object_property $wash_id; // The XL Vask Wash ID, if any
|
||||
public object_property $lane; // The lane used for the order, if any
|
||||
public object_property $po; // The (optional) PO number, filled by the customer.
|
||||
public object_property $safety_seal; // The optional safety seal value for wash certificates.
|
||||
public object_property $using_hand_held; // Whether the order is being processed using a handheld device
|
||||
|
||||
/**
|
||||
@@ -99,6 +100,7 @@ class orders_o extends db
|
||||
$this->wash_id = new object_property($this->table, $this->id, 'wash_id', 'string', false);
|
||||
$this->lane = new object_property($this->table, $this->id, 'lane', 'string', false);
|
||||
$this->po = new object_property($this->table, $this->id, 'po', 'string', false);
|
||||
$this->safety_seal = new object_property($this->table, $this->id, 'safety_seal', 'string', false);
|
||||
$this->using_hand_held = new object_property($this->table, $this->id, 'using_hand_held', 'bool', false);
|
||||
}
|
||||
|
||||
@@ -267,10 +269,8 @@ class orders_o extends db
|
||||
* @throws Exception If the order is not selected
|
||||
* @throws Exception If the order is already completed
|
||||
*/
|
||||
public function markAsCompleted(): void
|
||||
public function markAsCompleted(string|null $operator = null): void
|
||||
{
|
||||
global /** @var db $db */
|
||||
$db;
|
||||
self::requireSelected();
|
||||
// Check if the order is already completed
|
||||
if ($this->completed_at->value() !== null) {
|
||||
@@ -278,9 +278,15 @@ class orders_o extends db
|
||||
}
|
||||
// Set the completed_at property to the current timestamp
|
||||
$this->completed_at->set(date('Y-m-d H:i:s'));
|
||||
$sql = "UPDATE $this->table SET completed_at = '" . $this->completed_at->value() . "' WHERE id = " . $this->id;
|
||||
$db->query($sql);
|
||||
$washCertificateCreated = $this->completeWashCertificateIfNeeded(
|
||||
$operator,
|
||||
(string)$this->completed_at->value()
|
||||
);
|
||||
$this->objectChanged();
|
||||
|
||||
if ($washCertificateCreated && (int)$this->booking_id->value() > 0) {
|
||||
$this->getOrderBooking()?->sendWashCertificateToCustomer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -581,6 +587,7 @@ class orders_o extends db
|
||||
'wash_id' => $this->wash_id->value(),
|
||||
'lane' => $this->lane->value(),
|
||||
'po' => $this->po->value(),
|
||||
'safety_seal' => $this->getSafetySealValue(),
|
||||
'closed_at' => (int)$this->invoice_collection_id->value() ? (new collected_order_invoices_o())->select((int)$this->invoice_collection_id->value())->closed_at->value() : null,
|
||||
'pending_handheld' => $this->isPendingHandheld(),
|
||||
];
|
||||
@@ -1381,14 +1388,89 @@ class orders_o extends db
|
||||
return false; // No wash certificate product found in the order items
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function containsWashCertificateItem(): bool
|
||||
{
|
||||
self::requireSelected();
|
||||
$products = new products_o();
|
||||
|
||||
foreach ($this->getOrderItems((int)$this->id) as $item) {
|
||||
$product_id = (int)($item['product_id'] ?? 0);
|
||||
if ($product_id <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$product = $products->select($product_id);
|
||||
if ($product->exists() && $product->isWashCertificate()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function normalizeSafetySealValue(mixed $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$normalized = trim($value);
|
||||
return $normalized === '' ? null : $normalized;
|
||||
}
|
||||
|
||||
if (is_scalar($value)) {
|
||||
$normalized = trim((string)$value);
|
||||
return $normalized === '' ? null : $normalized;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getSafetySealValue(): ?string
|
||||
{
|
||||
self::requireSelected();
|
||||
return self::normalizeSafetySealValue($this->safety_seal->value());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setSafetySealValue(mixed $value): void
|
||||
{
|
||||
self::requireSelected();
|
||||
$normalized = self::normalizeSafetySealValue($value);
|
||||
$this->safety_seal->set($normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function completeWashCertificateIfNeeded(string|null $operator = null, $date = null): bool
|
||||
{
|
||||
self::requireSelected();
|
||||
if (!$this->containsWashCertificateItem() || $this->hasWashCertificateAttached()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->generateWashCertificate($this->getSafetySealValue(), $operator, $date);
|
||||
return $this->hasWashCertificateAttached();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and attach a wash certificate directly on an order (without a booking)
|
||||
* @param int|null $safety_seal Optional safety seal number
|
||||
* @param string|null $safety_seal Optional safety seal number
|
||||
* @param string|null $operator Optional operator/employee name who carried out the wash
|
||||
* @param string|DateTime|null $date Optional date of the wash (defaults to current date)
|
||||
* @throws Exception If the order is not selected or required related objects are missing
|
||||
*/
|
||||
public function generateWashCertificate(int|null $safety_seal = null, string|null $operator = null, $date = null): void
|
||||
public function generateWashCertificate(string|null $safety_seal = null, string|null $operator = null, $date = null): void
|
||||
{
|
||||
self::requireSelected();
|
||||
// Avoid generating duplicate certificates
|
||||
@@ -1437,7 +1519,7 @@ class orders_o extends db
|
||||
])
|
||||
->addData([
|
||||
'booking_number' => $this->id, // Used as document number on the template
|
||||
'seal_number' => ($safety_seal ?? null),
|
||||
'seal_number' => self::normalizeSafetySealValue($safety_seal),
|
||||
'reg_1' => $order_array['reg_1'],
|
||||
'reg_2' => $order_array['reg_2'],
|
||||
'date' => $date_formatted,
|
||||
|
||||
@@ -30,6 +30,26 @@ class subuser_grants_o extends db
|
||||
subusers_permission_node_key::BOOKINGS_DELETE,
|
||||
];
|
||||
|
||||
private static function normalizePermissionsValue(mixed $raw): array
|
||||
{
|
||||
if ($raw === null || $raw === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_array($raw)) {
|
||||
return array_values(array_filter($raw, static fn ($permission) => is_string($permission) && trim($permission) !== ''));
|
||||
}
|
||||
|
||||
if (is_string($raw)) {
|
||||
$decoded = json_decode($raw, true);
|
||||
if (is_array($decoded)) {
|
||||
return array_values(array_filter($decoded, static fn ($permission) => is_string($permission) && trim($permission) !== ''));
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
public function structure(): void
|
||||
{
|
||||
@@ -62,23 +82,7 @@ class subuser_grants_o extends db
|
||||
'subuser' => (int)$this->subuser->value(),
|
||||
'enabled' => (bool)$this->enabled->value(),
|
||||
'note' => $this->note->value(),
|
||||
'permissions' => (function ($raw) {
|
||||
// Handle different representations from object_property:
|
||||
// - When type is 'json', object_property::value() may already return an array
|
||||
// - In older behavior, it could return a JSON string
|
||||
// Normalize to an array for API output
|
||||
if ($raw === null || $raw === '') {
|
||||
return [];
|
||||
}
|
||||
if (is_array($raw)) {
|
||||
return $raw;
|
||||
}
|
||||
if (is_string($raw)) {
|
||||
$decoded = json_decode($raw, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
return [];
|
||||
})($this->permissions->value()),
|
||||
'permissions' => self::normalizePermissionsValue($this->permissions->value()),
|
||||
'created_at' => $this->created_at->value(),
|
||||
'updated_at' => $this->updated_at->value(),
|
||||
'deleted_at' => $this->deleted_at->value(),
|
||||
@@ -122,11 +126,33 @@ class subuser_grants_o extends db
|
||||
// Extract permissions from the grants
|
||||
$permissions = [];
|
||||
foreach ($grants as $grant) {
|
||||
$grant_permissions = json_decode($grant['permissions'], true);
|
||||
$grant_permissions = self::normalizePermissionsValue($grant['permissions'] ?? null);
|
||||
if (is_array($grant_permissions)) {
|
||||
$permissions = array_merge($permissions, $grant_permissions);
|
||||
}
|
||||
}
|
||||
return $permissions;
|
||||
return array_values(array_unique($permissions));
|
||||
}
|
||||
}
|
||||
|
||||
public function getGrantForSubuserAndCustomer(int $subuser_id, int $customer_number, bool $includeDisabled = true): ?subuser_grants_o
|
||||
{
|
||||
$grants = self::getFieldsWhere([
|
||||
'billing_customer_number' => $customer_number,
|
||||
'subuser' => $subuser_id,
|
||||
'deleted_at' => null,
|
||||
], ['id', 'enabled']);
|
||||
|
||||
if (!$includeDisabled) {
|
||||
$grants = array_values(array_filter($grants, static fn (array $grant): bool => (int)($grant['enabled'] ?? 0) === 1));
|
||||
}
|
||||
|
||||
if (count($grants) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
usort($grants, static fn (array $left, array $right): int => (int)$right['id'] <=> (int)$left['id']);
|
||||
$grant = (new subuser_grants_o())->select((int)$grants[0]['id']);
|
||||
$grant->getObjectProperties();
|
||||
return $grant;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +261,34 @@ class subusers_o extends db
|
||||
return $subuser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getSubuserByEmail(string $email): ?subusers_o
|
||||
{
|
||||
global $db;
|
||||
$email = $db->escape_string($email);
|
||||
$tmp = self::getFieldsWhere([
|
||||
'email' => $email,
|
||||
], ['id']);
|
||||
if (count($tmp) === 0) {
|
||||
return null;
|
||||
}
|
||||
$subuser = (new subusers_o())->select((int)$tmp[0]['id']);
|
||||
$subuser->getObjectProperties();
|
||||
return $subuser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function requiresSetup(): bool
|
||||
{
|
||||
self::requireSelected();
|
||||
$password = $this->password->value();
|
||||
return !is_string($password) || trim($password) === '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
* @throws Exception
|
||||
@@ -277,6 +305,11 @@ class subusers_o extends db
|
||||
return $session_token;
|
||||
}
|
||||
|
||||
public function invalidateSessionToken(string $token): void
|
||||
{
|
||||
$this->deleteCached('session_token:' . $token, 'subuser_sessions');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $token The session token
|
||||
* @return subusers_o|null The subuser object or null if the token is invalid or expired
|
||||
@@ -332,4 +365,4 @@ class subusers_o extends db
|
||||
$grant = new subuser_user_grant((int)$this->id, (int)$customer_number);
|
||||
return $grant->hasNode($permission_node_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user