setTable('users'); } private static function redisCache(): ?redis { return defined('redis') ? constant('redis') : null; } public function edit(int $id, string $customer_number, string|null $role, string|null $password, string|null $display_name): void { global $db; $this->id = $id; // Clear old cache mapping if customer number changed $old_res = $db->query("SELECT customer_number FROM $this->table WHERE id = $this->id"); if ($old_res && $old_res->num_rows > 0) { $old_cn = (int)$old_res->fetch_assoc()['customer_number']; if ($old_cn !== 0 && $old_cn !== (int)$customer_number) { self::redisCache()?->clear_user_id_from_customer_number($old_cn); } } // Cache the mapping from customer_number to user_id (new value) self::redisCache()?->cache_user_id_from_customer_number((int)$customer_number, $this->id); self::redisCache()?->cache_customer_number_from_user_id($this->id, (int)$customer_number); // Avoid SQL injection $customer_number = $db->escape_string($customer_number); if ($password !== null) { // Hash the password $password = password_hash($password, PASSWORD_DEFAULT); $password = $db->escape_string($password); } if ($role !== null) { $role = $db->escape_string($role); } if ($display_name !== null) { $display_name = $db->escape_string($display_name); } // Update the record in the database $sql = "UPDATE $this->table SET customer_number = '$customer_number'"; if ($password !== null) { $sql .= ", password = '$password'"; } if ($role !== null) { $sql .= ", group_id = '$role'"; } if ($display_name !== null) { $sql .= ", display_name = '$display_name'"; } $sql .= " WHERE id = $this->id"; $db->query($sql); // Set the values of the object properties $this->getObjectProperties(); } public function getObjectProperties(): void { $this->customer_number = new object_property($this->table, $this->id, 'customer_number', 'string', true); $this->display_name = new object_property($this->table, $this->id, 'display_name', 'string', false); $this->password = new object_property($this->table, $this->id, 'password', 'string', true); $this->group_id = new object_property($this->table, $this->id, 'group_id', 'int', true); $this->phone_country_code = new object_property($this->table, $this->id, 'phone_country_code', 'int', false); $this->phone = new object_property($this->table, $this->id, 'phone', 'int', false); $this->email = new object_property($this->table, $this->id, 'email', 'string', false); $this->xlvask_customer_id = new object_property($this->table, $this->id, 'xlvask_customer_id', 'string', false); $this->created_at = new object_property($this->table, $this->id, 'created_at', 'string', false); $this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'string', false); $this->keys = (new user_key_value_pairs_o())->setUser($this->id); $this->price_overrides = (new user_price_overrides_o())->setUser($this->id); $this->sms_notifications_enabled = new object_property($this->table, $this->id, 'sms_notifications_enabled', 'bool', false); $this->email_notifications_enabled = new object_property($this->table, $this->id, 'email_notifications_enabled', 'bool', false); $this->wash_certificate_email = new object_property($this->table, $this->id, 'wash_certificate_email', 'string', false); $this->two_factor_secret = new object_property($this->table, $this->id, 'two_factor_secret', 'string', false); $this->two_factor_enabled = new object_property($this->table, $this->id, 'two_factor_enabled', 'bool', false); } private function importCustomerFromExternalSource(int $customer_number): object|bool { // Get the customer data from the external source $economic = new economicCustomers(); $customer_data = $economic->getCustomerId($customer_number); if ($customer_data) { return $this->importCustomerFromEconomicCustomerData($customer_data); } return false; } public function importCustomerFromEconomicCustomerData(object $customer_data): users_o|bool { global $db; if (!isset($customer_data->customerNumber) || !is_numeric($customer_data->customerNumber)) { return false; } $customer_number = $db->escape_string((string)$customer_data->customerNumber); $sql = "SELECT * FROM $this->table WHERE customer_number = '$customer_number'"; $result = $db->query($sql); if ($result->num_rows > 0) { $this->id = (int)$result->fetch_assoc()['id']; $this->getObjectProperties(); return $this; } $this->add($customer_number, '', 0); $this->password->nullify(); if (isset($customer_data->email)) { $this->email->set($customer_data->email); } if (isset($customer_data->name)) { $this->display_name->set($customer_data->name); } return $this; } /** * @throws Exception */ public function wantsSmsNotifications(): bool { self::requireSelected(); return (bool)$this->sms_notifications_enabled->value(); } /** * @throws Exception */ public function wantsEmailNotifications(): bool { self::requireSelected(); return (bool)$this->email_notifications_enabled->value(); } /** * @throws Exception */ public function getWashCertificateEmail(): string|null { self::requireSelected(); return $this->wash_certificate_email->value(); } /** * @throws Exception */ public function isTwoFactorEnabled(): bool { self::requireSelected(); return (bool)$this->two_factor_enabled->value(); } /** * @throws Exception */ public function getTwoFactorSecret(): string|null { self::requireSelected(); return $this->two_factor_secret->value(); } /** * @throws Exception */ public function setTwoFactorSecret(string|null $secret): void { self::requireSelected(); $this->two_factor_secret->set($secret); } /** * @throws Exception */ public function setTwoFactorEnabled(bool $enabled): void { self::requireSelected(); $this->two_factor_enabled->set($enabled); } public function add(string $customer_number, mixed $password, int $role = 0): void { global $db; // Avoid SQL injection $customer_number = $db->escape_string($customer_number); $role = $db->escape_string($role); // Hash the password $password = password_hash($password, PASSWORD_DEFAULT); $password = $db->escape_string($password); // Create a new record in the database $sql = "INSERT INTO $this->table (customer_number, password, group_id) VALUES ('$customer_number', '$password', $role)"; $db->query($sql); // Get the id of the new record $this->id = (int)$db->insert_id(); // Cache the mapping from customer_number to user_id self::redisCache()?->cache_user_id_from_customer_number((int)$customer_number, $this->id); self::redisCache()?->cache_customer_number_from_user_id($this->id, (int)$customer_number); // Set the values of the object properties $this->getObjectProperties(); // Set the default attributes //$this->addAttribute('invoiceAllOrdersIndividually'); $this->addAttribute('restrictTankCleaning'); } /** * Add an attribute to the user * @param string $attribute The attribute to add * @param int|null $user_id The user id to add the attribute to * @throws Exception If the user is not selected, and the user_id is null */ public function addAttribute(string $attribute, ?int $user_id = null): void { global $db; if ($user_id === null) { self::requireSelected(); $user_id = $this->id; } $attribute = $db->escape_string($attribute); // To prevent two attributes with the same name for the same user, we will delete the old one if it exists $this->deleteAttribute($attribute, $user_id); $sql = "INSERT INTO customer_attributes (user_id, attribute) VALUES ($user_id, '$attribute')"; $db->query($sql); } public function deleteAttribute(string $attribute, ?int $user_id = null): void { global $db; if ($user_id === null) { $user_id = $this->id; } $attribute = $db->escape_string($attribute); // If the user has a customer number, we will delete the attribute from all associated customers $customer_number = $this->customer_number->value(); if (!empty($customer_number)) { $sql = "SELECT * FROM users WHERE customer_number = $customer_number"; $result = $db->query($sql); $user_ids = []; while ($row = $result->fetch_assoc()) { $user_ids[] = (int)$row['id']; } // Delete the attribute from all users foreach ($user_ids as $user_id) { $sql = "DELETE FROM customer_attributes WHERE user_id = $user_id AND attribute = '$attribute' LIMIT 1"; $db->query($sql); } return; } // Make sure the attribute exists if (!$this->doesUserHaveAttribute((string)$attribute, (int)$user_id)) { return; } // If the attribute exists, delete it, if it does not exist, nothing will happen $sql = "DELETE FROM customer_attributes WHERE user_id = $user_id AND attribute = '$attribute' LIMIT 1"; $db->query($sql); } public function doesUserHaveAttribute(string $attribute, ?int $user_id = null): bool { global $db; if ($user_id === null) { $user_id = $this->id; } $attribute = $db->escape_string($attribute); $sql = "SELECT * FROM customer_attributes WHERE user_id = $user_id AND attribute = '$attribute' LIMIT 1"; $result = $db->query($sql); if ($result->num_rows > 0) { return true; } return false; } public function automaticGetTargetUserFromRequest(): users_o { // Get the data from the request if ($_SERVER['REQUEST_METHOD'] === 'GET' || $_SERVER['REQUEST_METHOD'] === 'DELETE') { $data = $_GET; } else { $data = json_decode(file_get_contents('php://input'), true); } if (!is_array($data)) { return $this; } // Check if the user id is set in the request $user_id = $this->parsePositiveIntFromRequest($data['user_id'] ?? null); if ($user_id !== null) { return $this->getUserById($user_id); } $customer_number = $this->parsePositiveIntFromRequest($data['customer_number'] ?? null); if ($customer_number !== null) { return $this->getUserByCustomerNumber($customer_number); } return $this; } private function parsePositiveIntFromRequest(mixed $value): ?int { if (is_int($value)) { return $value > 0 ? $value : null; } if (!is_string($value)) { return null; } $value = trim($value); if ($value === '' || !ctype_digit($value)) { return null; } $parsed = (int)$value; return $parsed > 0 ? $parsed : null; } public function getUserById(int $id): users_o { global $db; // Check Redis for existence (by checking if we have the customer number) $customer_number = self::redisCache()?->get_customer_number_from_user_id($id); if ($customer_number !== null) { $this->id = $id; $this->getObjectProperties(); return $this; } // Get the record from the database $sql = "SELECT customer_number FROM $this->table WHERE id = $id"; $result = $db->query($sql); if ($result->num_rows > 0) { $this->id = $id; $customer_number = (int)$result->fetch_assoc()['customer_number']; // Cache the result self::redisCache()?->cache_customer_number_from_user_id($id, $customer_number); $this->getObjectProperties(); } return $this; } public function getUserByCustomerNumber(int $customer_number): users_o { global $db; // Check Redis first $user_id = self::redisCache()?->get_user_id_from_customer_number($customer_number); if ($user_id !== null) { $this->id = (int)$user_id; $this->getObjectProperties(); return $this; } // Get the record from the database $sql = "SELECT id FROM $this->table WHERE customer_number = '$customer_number'"; $result = $db->query($sql); if ($result->num_rows > 0) { $this->id = (int)$result->fetch_assoc()['id']; // Cache the result self::redisCache()?->cache_user_id_from_customer_number($customer_number, $this->id); $this->getObjectProperties(); } else { // Import the customer $this->importCustomerFromExternalSource($customer_number); } return $this; } public function asArray(): array { $phone_country_code = $this->phone_country_code->value(); $phone_country_code = $phone_country_code === null ? null : (int)$phone_country_code; $phone = $this->phone->value(); $phone = $phone === null ? null : (int)$phone; $display_name = $this->display_name->value(); $customer_number = (int)$this->customer_number->value(); $customer_name = $this->getCustomerName($customer_number); // Create the array with the object properties $array = [ 'id' => (int)$this->id, 'customer_number' => $customer_number, 'customer_name' => $customer_name, 'display_name' => $display_name === 'Unnamed' ? $customer_name : $display_name, 'group_id' => (int)$this->group_id->value(), 'phone' => [ 'country_code' => $phone_country_code, 'number' => $phone, ], 'email' => $this->email->value(), 'notifications' => [ 'sms_notifications_enabled' => (bool)$this->sms_notifications_enabled->value(), 'email_notifications_enabled' => (bool)$this->email_notifications_enabled->value(), 'wash_certificate_email' => $this->wash_certificate_email->value(), self::KEY_SUPERUSER_NEW_CUSTOMER_EMAIL_NOTIFICATIONS => $this->isSuperuserNewCustomerEmailNotificationsEnabled(), ], 'created_at' => $this->created_at->value(), 'updated_at' => $this->updated_at->value(), ]; // If the economic customer data is set, add it to the array if (isset($this->economic_customer)) { $array['economic_customer'] = $this->economic_customer->asArray(); } // If the permissions are set, add them to the array if (isset($this->permissions)) { $array['permissions'] = $this->permissions; } // If the attributes are set, add them to the array if (isset($this->attributes)) { $array['attributes'] = $this->attributes; } // If the discounts are set, add them to the array if (isset($this->discounts)) { $array['discounts'] = $this->discounts; } // If the orders not invoiced are set, add them to the array if (isset($this->orders_not_invoiced)) { $array['orders_not_invoiced'] = $this->orders_not_invoiced; } // If the keys are set, add them to the array if (isset($this->all_keys)) { $array['keys'] = $this->all_keys; } // If the wash subscription transactions are set, add them to the array if (isset($this->wash_subscription_transactions)) { $array['wash_subscription_transactions'] = $this->wash_subscription_transactions; } return $array; } /** * @throws Exception */ public function getCustomerName(int $customer_number): string|null { // Get the customer from the customer object if ($customer_number === 0) { return null; } // Create a temporary user object $tmp_user = new users_o(); $tmp_user->getUserByCustomerNumber($customer_number); $fallbackName = null; if ($tmp_user->exists()) { $displayName = $tmp_user->display_name->value(); if (is_string($displayName) && trim($displayName) !== '') { $fallbackName = $displayName; } } // Get the customer name (Check cache first) $cached = $tmp_user->getCached('economic_customer'); if (!$cached) { try { $tmp_user->getCustomerEcocomicData($customer_number); } catch (Exception) { return $fallbackName; } $cached = $tmp_user->getCached('economic_customer'); } $cachePayload = self::buildCustomerNameCachePayload($cached, $fallbackName); if ($cachePayload !== null) { return $cachePayload['name']; } return $fallbackName; } /** * @return array{name:string}|null */ private static function buildCustomerNameCachePayload(mixed $cached_name, ?string $fallback_name): ?array { return customer_name_cache_payload_builder::build($cached_name, $fallback_name); } public function getCustomerEcocomicData(?int $customer_number = null): users_o { if ($customer_number !== null && !isset($this->id)) { $this->getUserByCustomerNumber($customer_number); } // Check if the customer number is set if (!isset($this->customer_number) && $customer_number === null) { return $this; } $customer_number = (int)($customer_number ?? $this->customer_number->value()); if ($customer_number <= 0) { $this->economic_customer = new economic_customer_mo(); return $this; } $cachedCustomer = isset($this->id) && $this->id > 0 ? $this->getCached('economic_customer') : null; if (is_object($cachedCustomer)) { $cachedCustomerNumber = (int)($cachedCustomer->customerNumber ?? $cachedCustomer->customer_number ?? 0); if ($cachedCustomerNumber === $customer_number) { $this->economic_customer = (new economic_customer_mo())->parseCustomer($cachedCustomer); return $this; } } try { $this->economic_customer = (new economic_customer_mo())->getCustomerByCustomerNumber($customer_number); } catch (Exception) { $this->economic_customer = new economic_customer_mo(); } return $this; } public function getNotes(): array { global $db; // Create the customer notes object $customer_notes = new customer_notes_o(); // Get the customer notes return $customer_notes->getCustomerNotesAsArray($this->id); } public function addNote($customer_id, $note, $cashier_id): customer_notes_o { global $db; // Create the customer notes object $customer_notes = new customer_notes_o(); // Add the note $customer_notes->add($customer_id, $note, $cashier_id); return $customer_notes; } public function deleteNote(int $note_id): void { global $db; // Create the customer notes object $customer_notes = new customer_notes_o(); // Delete the note $customer_notes->delete($note_id); } public function getOrImportCustomerByCustomerNumber(int $customer_number): object|bool { global $db; // Check if the customer exists $sql = "SELECT * FROM $this->table WHERE customer_number = $customer_number"; $result = $db->query($sql); if ($result->num_rows > 0) { $this->id = $result->fetch_assoc()['id']; $this->getObjectProperties(); } else { // Import the customer return $this->importCustomerFromExternalSource($customer_number); } return $this; } /** * If the customer requires a reference number for each order * @return bool */ public function requiresReference(): bool { return $this->doesUserHaveAttribute('requiresReferenceNumber'); } /** * If the customer is NOT allowed to purchase spot free washes * @return bool */ public function restrictSpotFree(): bool { return $this->doesUserHaveAttribute('restrictSpotFree'); } /** * If the customer is NOT allowed to purchase interior cleaning * @return bool */ public function restrictInteriorCleaning(): bool { return $this->doesUserHaveAttribute('restrictInteriorCleaning'); } /** * If the customer is NOT allowed to purchase tank cleaning * @return bool */ public function restrictTankCleaning(): bool { return $this->doesUserHaveAttribute('restrictTankCleaning'); } /** * If the customer is shown prices on the booking page * @return bool */ public function showPricesOnBookingPage(): bool { return $this->doesUserHaveAttribute('showPricesOnBookingPage'); } /** * If the customer uses PO numbers * @return bool */ public function usePONumbers(): bool { return $this->doesUserHaveAttribute('usePONumbers'); } public function includeIncludes(array $includes = []): users_o { global /** @var response $response */ $response; $includeEverything = $response->getRequestParameter('include_all') === 'true' || in_array('all', $includes); /** * economicCustomer */ if ($includeEverything || $response->getRequestParameter('includeEconomicCustomer') === 'true' || in_array('economicCustomer', $includes)) { $this->getCustomerEcocomicData(); } /** * permissions */ if ($includeEverything || $response->getRequestParameter('includePermissions') === 'true' || in_array('permissions', $includes)) { $this->getPermissions(); } /** * Attributes */ if ($includeEverything || $response->getRequestParameter('includeAttributes') === 'true' || in_array('attributes', $includes)) { $this->getUserAttributes(); } /** * Discounts */ if ($includeEverything || $response->getRequestParameter('includeDiscounts') === 'true' || in_array('discounts', $includes)) { $this->getAllDiscounts(); } /** * OrdersNotInvoiced */ if ($includeEverything || $response->getRequestParameter('includeOrdersNotInvoiced') === 'true' || in_array('ordersNotInvoiced', $includes)) { $this->getUserOrdersNotInvoiced(); } /** * Key value pairs */ if ($includeEverything || $response->getRequestParameter('includeKeys') === 'true' || in_array('keys', $includes)) { $this->getAllKeys(); } /** * Wash subscription transactions */ if ($includeEverything || $response->getRequestParameter('includeWashSubscriptionTransactions') === 'true' || in_array('washSubscriptionTransactions', $includes)) { $this->getWashSubscriptionTransactions(); } return $this; } private function getPermissions(): void { global $db; $sql = "SELECT permission FROM groups_permissions WHERE group_id = " . $this->group_id->value(); $result = $db->query($sql); $perms = []; while ($row = $result->fetch_assoc()) { $perms[] = $row['permission']; } // Add the attributes as `has_attribute_key_{attribute_key}` $attributes = array_map(function ($attribute_arr) { return $attribute_arr['attribute']; }, (new users_o())->getUserAttributes($this->id)); // Remove all duplicates $attributes = array_unique($attributes, SORT_REGULAR); foreach ($attributes as $attribute) { $perms[] = 'has_attribute_' . $attribute; } $this->permissions = $perms; } public function getUserAttributes(?int $user_id = null): array { global $db; if ($user_id === null) { $user_id = $this->id; } $sql = "SELECT * FROM customer_attributes WHERE user_id = $user_id"; $result = $db->query($sql); $array = $db->fetch_all($result); $this->attributes = $array; return $this->attributes; } /** * Get all discounts for the user (Include) * @return void */ public function getAllDiscounts(): void { // Get all discounts (key = 'custom_price') $this->discounts = $this->price_overrides->setUser($this->id)->getAllPrices(); } public function getUserOrdersNotInvoiced(): void { global $db; // $sql = "SELECT * FROM $this->table WHERE deleted_at IS NULL AND id NOT IN (SELECT id FROM economic_module_orders WHERE invoice_draft_id IS NOT NULL OR invoice_id IS NOT NULL)"; $sql = "SELECT id FROM orders WHERE customer_id = " . $this->customer_number->value() . " AND deleted_at IS NULL AND id NOT IN (SELECT id FROM economic_module_orders WHERE invoice_draft_id IS NOT NULL OR invoice_id IS NOT NULL)"; $result = $db->query($sql); $this->orders_not_invoiced = $db->fetch_all($result); } public function getAllKeys(): void { // Get all keys (key = 'key_value_pair') $this->all_keys = $this->keys->setUser($this->id)->getAllKeys(); } /** * Get the wash subscription transactions for the user * @throws Exception If the user is not selected */ private function getWashSubscriptionTransactions(): void { self::requireSelected(); $orders_o = new orders_o(); $wash_subscription_transactions = $orders_o->getWashSubscriptionTransactions((int)$this->customer_number->value()); $this->wash_subscription_transactions = $wash_subscription_transactions; } /** * Get the group of the user * @throws Exception If the user is not selected */ public function getGroup(): groups_o { self::requireSelected(); // Get the group of the user $group = new groups_o(); $group->select((int)$this->group_id->value()); return $group; } public function getCode(): string|null { // Get the customer code $this->customer_codes = new customer_codes_o(); $this->customer_codes->getCode($this->id); return $this->customer_codes->code->value(); } public function setCode(mixed $code): customer_codes_o { // Set the customer code $this->customer_codes = new customer_codes_o(); return $this->customer_codes->setCode($this->id, $code); } /** * Check if the user has access to the order * @param int $order_id * @return bool */ public function hasAccessToOrder(int $order_id): bool { global $db; // Check if the user has access to all orders if ($this->hasPermission('fetch_all_orders')) { return true; } // Get the record from the database $sql = "SELECT * FROM orders WHERE id = $order_id AND customer_id = " . $this->customer_number->value(); $result = $db->query($sql); if ($result->num_rows > 0) { return true; } return false; } public function hasPermission(string $permission): bool { global $db; // Get the user's group id $group_id = $this->group_id->value(); // If the users is an admin, they have all permissions if ((int)$group_id === 1) { return true; } // Get the record from the database $sql = "SELECT * FROM groups_permissions WHERE group_id = $group_id AND permission = '$permission'"; $result = $db->query($sql); if ($result->num_rows > 0) { return true; } return false; } public function isSuperuserNewCustomerEmailNotificationsEnabled(): bool { self::requireSelected(); $value = $this->keys->setUser($this->id)->getValue(self::KEY_SUPERUSER_NEW_CUSTOMER_EMAIL_NOTIFICATIONS); return in_array(strtolower((string)$value), ['1', 'true', 'yes', 'on'], true); } public function setSuperuserNewCustomerEmailNotificationsEnabled(bool $enabled): void { self::requireSelected(); $this->keys->setUser($this->id)->setValue( self::KEY_SUPERUSER_NEW_CUSTOMER_EMAIL_NOTIFICATIONS, $enabled ? '1' : '0' ); } /** * @return array */ public function getSuperuserNewCustomerEmailNotificationRecipients(): array { global $db; $key = $db->escape_string(self::KEY_SUPERUSER_NEW_CUSTOMER_EMAIL_NOTIFICATIONS); $sql = " SELECT DISTINCT u.id, u.customer_number, u.display_name, u.email FROM users u INNER JOIN user_key_value_pairs kv ON kv.user_id = u.id AND kv.var = '$key' AND LOWER(kv.val) IN ('1', 'true', 'yes', 'on') LEFT JOIN groups_permissions gp ON gp.group_id = u.group_id AND gp.permission = 'superuser' WHERE u.deleted_at IS NULL AND u.email IS NOT NULL AND u.email <> '' AND (u.group_id = 1 OR gp.id IS NOT NULL) "; return $db->fetch_all($db->query($sql)); } public function setOpenInvoiceDraft(int $draftInvoiceNumber): void { // Set the open invoice draft (key = 'open_invoice_draft') $this->keys->setUser($this->id)->setValue('open_invoice_draft', $draftInvoiceNumber); } public function deleteOpenInvoiceDraft(): void { // Check if the user has an open invoice draft if ($this->hasOpenInvoiceDraft()) { // Remove all links to the draft invoice $order = new economic_module_orders(); $order->unlinkAllOrdersFromDraft($this->getOpenInvoiceDraft()); // Unset the open invoice draft $this->unsetOpenInvoiceDraft(); } } public function hasOpenInvoiceDraft(): bool { // Check if the user has an open invoice draft (key = 'open_invoice_draft') return $this->keys->setUser($this->id)->getValue('open_invoice_draft') !== null; } public function getOpenInvoiceDraft(): int|null { // Get the open invoice draft (key = 'open_invoice_draft') return (int)$this->keys->setUser($this->id)->getValue('open_invoice_draft'); } public function unsetOpenInvoiceDraft(): void { // Unset the open invoice draft (key = 'open_invoice_draft') $this->keys->setUser($this->id)->deleteValue('open_invoice_draft'); } /** * Get the custom price (DISCOUNT) for the user * @param int $object_id The ID of the object * @param bool $is_category If the object is a category * @return int|null The discount percentage */ public function getCustomPrice(int $object_id, bool $is_category = false): int|null { self::requireSelected(); // Get the custom price for the product $discount = $this->price_overrides->setUser($this->id)->getPrice($is_category, $object_id); // If the is_category is false, check if there is a custom price for the category that the product belongs to if ($discount === 0 && !$is_category) { $product = new products_o(); // Get the product by ID $product->getProductById($object_id); // Check if the product allows category inheritance of discounts if ($product->apply_category_discount->value()) { // Get the custom price for the category $discount = $this->price_overrides->setUser($this->id)->getPrice(true, $product->category->value()); } } return $discount; } /** * Get the final price of the product for the user * This includes: * - Global discount (If the product allows category inheritance of discounts) * - Category discount (If the product allows category inheritance of discounts) * - Product discount (If the product has a custom price) */ public function getProductDiscountPercentage(int $product_id): int|null { self::requireSelected(); // Get the product by ID $product = (new products_o())->select((int)$product_id); $doesProductAllowCategoryDiscount = (bool)$product->apply_category_discount->value(); $economic_discount_percentage = $this->getEconomicCustomerDiscountPercentage(); $discount_percentage = $this->getCustomPrice($product_id); // If the product allows category inheritance of discounts, get the category discounts if ($doesProductAllowCategoryDiscount) { // If the category discount is higher than the product discount, use the category discount if ($economic_discount_percentage > $discount_percentage) { $discount_percentage = $economic_discount_percentage; } } // If the discount percentage is null, return 0 return $discount_percentage === null ? 0 : (int)$discount_percentage; } /** * Get all custom prices (DISCOUNT) for the user * @return array The custom prices */ public function getCustomPrices(): array { // Get the custom prices (key = 'custom_price') return $this->price_overrides->setUser($this->id)->getAllPrices(); } /** * Get all users in a group * @param int $group_id * @return array */ public function getUsersInGroup(int $group_id): array { global $db; $sql = "SELECT id FROM $this->table WHERE group_id = $group_id"; $result = $db->query($sql); // Create an array of user objects $users = []; while ($row = $result->fetch_assoc()) { $user = new users_o(); $user->id = $row['id']; $user->getObjectProperties(); $users[] = $user; } return $users; } /** * Get the employee data, that's public to the customers * @return array */ public function listPublicEmployeeData(): array { return [ 'id' => (int)$this->id, 'display_name' => $this->display_name->value(), ]; } public function getUsersWithPermission(string $permission): array { global $db; $sql = "SELECT id FROM $this->table WHERE group_id IN (SELECT group_id FROM groups_permissions WHERE permission = '$permission')"; $result = $db->query($sql); // Create an array of user objects $users = []; while ($row = $result->fetch_assoc()) { $user = new users_o(); $user->id = $row['id']; $user->getObjectProperties(); $users[] = $user; } return $users; } /** * Does the user have access to the booking? * @param int $id * @return bool */ public function hasAccessToBooking(int $id): bool { global $db; // Check if the user has access to all bookings if ($this->hasPermission('list_bookings')) { return true; } // Get the record from the database $sql = "SELECT * FROM bookings WHERE id = $id AND customer_number = " . $this->customer_number->value(); $result = $db->query($sql); if ($result->num_rows > 0) { return true; } return false; } public function parseUsers(array $listObjectsWithPaginationIfSet, $variables = []): array { $tmp = self::parseCustomerNumbers($listObjectsWithPaginationIfSet); // Check if the user has a password, and change it to a boolean instead of a string foreach ( $tmp as $key => $value ) { if ($value['password'] === '' || $value['password'] === null) { $tmp[$key]['password'] = false; } else { $tmp[$key]['password'] = true; } } return $tmp; } public function parseCustomerNumbers(array $listObjectsWithPaginationIfSet): array { foreach ( $listObjectsWithPaginationIfSet as $key => $value ) { $listObjectsWithPaginationIfSet[$key]['customer_name'] = $this->getCustomerNameById($value['id']); } return $listObjectsWithPaginationIfSet; } private function getCustomerNameById(int $id): ?string { // Check if the name is cached $cached_name = $this->getCached('economic_customer', $id); if (!$cached_name) { // No cached name, get the name from the external source // Get the name from the external source $tmp_user = new users_o(); $tmp_user->getUserById($id); $tmp_user->getCustomerEcocomicData(); $cached_name = $tmp_user->getCached('economic_customer'); if (!$cached_name) { // Cache the NULL value $this->cache('economic_customer', 'NULL_OR_EMPTY', $id); return null; } } return $cached_name->name ?? null; } public function hasPassword(): bool { return $this->password->value() !== null; } public function getPassword(): string|null { return $this->password->value(); } /** * Set the password for the user * @throws Exception If the user is not selected */ public function setPassword(?string $password = null): void { self::requireSelected(); global $db; // Hash the password $password = password_hash($password, PASSWORD_DEFAULT); $password = $db->escape_string($password); // Update the password in the database $this->password->set( $password, ); // The object has changed, so we need to update the database self::objectChanged(); } public function objectChanged(): void { // Since the user object is not cached, there is no need to invalidate the cache } public function clearAllUsersEconomicCustomerDiscountsFromCache(): void { // Get all the cached results matching the pattern 'users_*_economic_customer_discount_percentage' $cached_results = self::redisCache()?->get_keys('users_*_economic_customer_discount_percentage') ?? []; // Loop through the cached results foreach ( $cached_results as $key ) { // Clear the cached discount percentage self::redisCache()?->delete($key); } } public function clearAllUsersEconomicCustomerDetailsFromCache(): void { // Get all the cached results matching the pattern 'users_*_economic_customer' $cached_results = self::redisCache()?->get_keys('users_*_economic_customer') ?? []; // Loop through the cached results foreach ( $cached_results as $key ) { // Clear the cached economic customer details self::redisCache()?->delete($key); } } public function getEconomicCustomerDiscountPercentage(): int { self::requireSelected(); // Check if the discount percentage is cached $cached_discount_percentage = self::redisCache()?->get_economic_customer_discount_percentage($this->id); if ($cached_discount_percentage !== null) { return $cached_discount_percentage; } // Get the discount percentage from the external source $economic = new economicCustomers(); $discount_percentage = $economic->getCustomerDiscountPercentage($this->customer_number->value()); // Cache the discount percentage self::redisCache()?->cache_economic_customer_discount_percentage($this->id, $discount_percentage); return $discount_percentage; } /** * Set a custom price (DISCOUNT) for the user * @param int $user_id * @param int $object_id The ID of the object * @param int $discount_percentage The discount percentage * @param bool $is_category If the object is a category * @return void */ public function setCustomPrice(int $user_id, int|string $object_id, int $discount_percentage, bool $is_category = false): void { $this->id = $user_id; // Get the user object properties $this->getObjectProperties(); // Set the custom price (key = 'custom_price') $this->price_overrides->setUser($this->id)->setPrice($is_category, $object_id, $discount_percentage); } public function syncAllUsersEconomicCustomerDetails(): void { // Get all users $users = $this->getFields(['id', 'customer_number']); // Loop through the users foreach ( $users as $user ) { // Check if the customer number is set (Or if it is 0) if ($user['customer_number'] === 0) { continue; } // Get the customer data from the external source $this->getCustomerEcocomicData($user['customer_number']); } } public function isImportedFromEconomic($customerNumber): bool { // Check Redis first $user_id = self::redisCache()?->get_user_id_from_customer_number((int)$customerNumber); if ($user_id !== null) { return true; } // Check if the user is imported from the external source if (self::countRowsWhere(['customer_number' => $customerNumber]) > 0) { return true; } return false; } public function getUserIdFromEconomic($customerNumber): int { // Check Redis first $user_id = self::redisCache()?->get_user_id_from_customer_number((int)$customerNumber); if ($user_id !== null) { return (int)$user_id; } // Get the user id from the external source $user = self::getFieldsWhere(['customer_number' => $customerNumber], ['id']); $id = (int)$user[0]['id']; // Cache the result self::redisCache()?->cache_user_id_from_customer_number((int)$customerNumber, $id); return $id; } /** * Get the invoice collection ID, where the new order should be added to. * This is based on the user's settings. (If the user has the attribute 'invoiceAllOrdersIndividually', the new order should be added to a new invoice collection) * @return int The ID of the order invoice collection the new order should be added to. * @throws Exception */ public function getNewOrderInvoiceCollectionId(): int { // Check if the user has the attribute 'invoiceAllOrdersIndividually' $invoice_collection = new collected_order_invoices_o(); if ($this->invoicePerOrder() || !self::hasOpenInvoiceCollection()) { // Require the user to have a customer number above 0 if (is_null($this->customer_number->value()) || $this->customer_number->value() === 0) { throw new Exception('The user does not have a customer number'); } // The user has the attribute 'invoiceAllOrdersIndividually', create a new invoice collection $invoice_collection->add((int)$this->customer_number->value()); return $invoice_collection->id; } // The user does not have the attribute 'invoiceAllOrdersIndividually', get the ID of the last invoice collection return self::getOpenInvoiceCollection()->id; } /** * If the customer should have an invoice per order (Otherwise, they will have a monthly invoice for all orders) * @return bool */ public function invoicePerOrder(): bool { return $this->doesUserHaveAttribute('invoiceAllOrdersIndividually'); } /** * Check if the user has an open invoice collection * @return bool */ public function hasOpenInvoiceCollection(): bool { // Check if the user has an open invoice collection $invoice_collection = new collected_order_invoices_o(); return $invoice_collection->hasOpenInvoiceCollection($this->customer_number->value()); } /** * Get the open invoice collection of the user * @return collected_order_invoices_o * @throws Exception If the user does not have an open invoice collection * @throws Exception If the user does not have a customer number */ public function getOpenInvoiceCollection(): collected_order_invoices_o { // Require the user to have a customer number above 0 if (is_null($this->customer_number->value()) || $this->customer_number->value() === 0) { throw new Exception('The user does not have a customer number'); } self::requireSelected(); // Get the open invoice collection of the user $invoice_collection = new collected_order_invoices_o(); return $invoice_collection->getLatestOpenInvoiceCollection($this->customer_number->value()); } /** * Get all customers with the given attributes * @notation Retrieve all customers with ALL the given attributes * @param array $attributes The attributes to search for (e.g. ['attribute1', 'attribute2']) * @return array The e-conomic customer numbers with the given attributes (e.g. [123456, 654321]) */ public function getCustomerNumbersWithAttributes(array $attributes): array { global $db; $safeAttributes = []; foreach ($attributes as $attribute) { if (!is_scalar($attribute)) { continue; } $attribute = trim((string)$attribute); if ($attribute === '') { continue; } $safeAttributes[] = "'" . $db->escape_string($attribute) . "'"; } $safeAttributes = array_values(array_unique($safeAttributes)); if (empty($safeAttributes)) { return []; } $userIds = []; $sql = "SELECT DISTINCT user_id FROM customer_attributes WHERE attribute IN (" . implode(',', $safeAttributes) . ")"; $result = $db->query($sql); while ($row = $result->fetch_assoc()) { $userIds[] = (int)$row['user_id']; } $userIds = array_values(array_unique($userIds)); if (empty($userIds)) { return []; } $customerNumbers = []; $sql = "SELECT customer_number FROM $this->table WHERE id IN (" . implode(',', array_map('intval', $userIds)) . ")"; $result = $db->query($sql); while ($row = $result->fetch_assoc()) { $customerNumbers[] = (int)$row['customer_number']; } return array_values(array_unique($customerNumbers)); } /** * Check if the password matches the hashed password * @param string $password_to_check_against The password (plain text) to check against the hashed password * @return bool True if the password matches, false otherwise * @throws Exception If the user is not selected * @throws Exception If the user does not have a password */ public function passwordMatches(string $password_to_check_against): bool { // Require the user to be selected self::requireSelected(); // Check if the user has a password if (empty($this->password->value())) { throw new Exception('The user does not have a password, unable to check if the password matches'); } // Check if the password matches return password_verify( (string)$password_to_check_against, (string)$this->password->value() ); } /** * Set the email for the user * @param string $email The email address to set * @throws Exception If the user is not selected * @throws Exception If the email address is invalid * @throws Exception If the email address is already in use */ public function setEmail(string $email): void { self::requireSelected(); // Check if the email is valid if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new Exception('Invalid email address'); } // Check if the email is already in use if (self::countRowsWhere(['email' => $email]) > 0) { throw new Exception('Email address already in use'); } // Set the email $this->email->set($email); } public function isCustomerBarred(int $customer_number): bool { if ($customer_number === 0) { return false; } // Create a temporary user object $tmp_user = new users_o(); $tmp_user->getUserByCustomerNumber($customer_number); // Get the customer name (Check cache first) $cached = $tmp_user->getCached('economic_customer'); if (!$cached) { $tmp_user->getCustomerEcocomicData($customer_number); $cached = $tmp_user->getCached('economic_customer'); } if ($cached) { return (isset($cached->barred) && $cached->barred); } return false; } /** * @throws Exception */ public function doesUserHaveDefaultDepartment(): bool { // Check if the user has a default department set self::requireSelected(); return (new customer_default_department_o())->doesUserHaveDefaultDepartment((int)$this->customer_number->value()); } /** * @throws Exception */ public function getDefaultDepartment(): int|null { // Get the default department for the user self::requireSelected(); return (new customer_default_department_o())->getDefaultDepartment((int)$this->customer_number->value()); } public function getCustomersWithVehicleSubscriptions(): array { global $db; // Get all customers with vehicle subscriptions and not fixed pricing $sql = "SELECT DISTINCT customer_id FROM customer_vehicles WHERE wash_subscription = 1 AND customer_id NOT IN (SELECT customer_number FROM customer_fixed_pricing)"; $result = $db->query($sql); $customer_numbers = []; while ($row = $result->fetch_assoc()) { $customer_numbers[] = (int)$row['customer_id']; } return $customer_numbers; } public function getCustomersWithFixedPricing(): array { global $db; // Get all customers with fixed pricing $sql = "SELECT DISTINCT customer_number FROM customer_fixed_pricing"; $result = $db->query($sql); $customer_numbers = []; while ($row = $result->fetch_assoc()) { $customer_numbers[] = (int)$row['customer_number']; } return $customer_numbers; } public function getCustomersWithTankCleaning(): array { global $db; // Get all customers with tank cleaning $sql = "SELECT DISTINCT user_id FROM customer_attributes WHERE attribute = 'onlyTankCleaning'"; $result = $db->query($sql); $customer_numbers = []; while ($row = $result->fetch_assoc()) { $customer_numbers[] = (int)$row['user_id']; } // Get the customer numbers from the user IDs $customer_numbers = array_unique($customer_numbers); // Convert user IDs to customer numbers return self::getCustomerNumbersFromUserIds($customer_numbers); } private static function getCustomerNumbersFromUserIds(array $user_ids): array { global $db; $customer_numbers = (new users_o)->getFieldsWhere( ['id' => $user_ids], ['customer_number'] ); return array_map(function ($user) { return (int)$user['customer_number']; }, $customer_numbers); } /** * @throws Exception */ public function getUsersByCustomerNumbers(array $customer_numbers, bool $customerNumberAndUserArray = false): array { global $db; // Get the users by customer numbers $customer_numbers = array_map('intval', $customer_numbers); $customer_numbers = implode(',', $customer_numbers); $sql = "SELECT id, customer_number FROM $this->table WHERE customer_number IN ($customer_numbers)"; $result = $db->query($sql); // Create an array of user objects $ids = []; while ($row = $result->fetch_assoc()) { $ids[] = (int)$row['id']; } // Only get unique IDs $ids = array_unique($ids); $users = []; foreach ( $ids as $id ) { $user = new users_o(); $user->id = $id; $user->getObjectProperties(); if ($customerNumberAndUserArray) { // If the user should be returned with the customer number $users[$user->customer_number->value()] = $user; } else { // If the user should be returned as an object $users[] = $user; } } return $users; } /** * Check if the user has an XLVask customer account * @return bool True if the user has an XLVask customer account, false otherwise * @throws Exception if the user is not selected */ public function hasXLVaskCustomerAccount(): bool { self::requireSelected(); $customer_number = $this->customer_number->value(); if (empty($customer_number)) { // If the customer number is not set, the user cannot have an XLVask customer account return false; } if ((new xlvask())->getCache()->isCustomerCached((int)$this->customer_number->value())) { // If the customer is cached, it means the user has an XLVask customer account return true; } // Check if the user exists in the XLVask system if (count((new xlvask())->getCustomers(null, null, $customer_number)) > 0) { // If the user exists in the XLVask system, it means the user has an XLVask customer account return true; } // Check if the user has an XLVask customer account associated internally return !empty($this->xlvask_customer_id->value()); } public function getCustomersWithSpecialArrangements(): array { global $db; $sql = "SELECT DISTINCT c.customer_number FROM users c INNER JOIN user_key_value_pairs kv ON c.id = kv.user_id WHERE kv.var = 'OtherSpecialArrangement' AND kv.val IS NOT NULL AND kv.val != ''"; $result = $db->query($sql); $customer_numbers = []; while ($row = $result->fetch_assoc()) { $customer_numbers[] = (int)$row['customer_number']; } return $customer_numbers; } /** * @throws Exception */ public function setPhoneNumber(int $phone_number, int $country_code = 45): void { self::requireSelected(); // Set the phone number $this->phone->set((int)$phone_number); $this->phone_country_code->set((int)$country_code); $this->objectChanged(); } public function setSMSNotificationsEnabled(bool $enabled): void { self::requireSelected(); // Set SMS notifications enabled/disabled $this->sms_notifications_enabled->set($enabled ? 1 : 0); $this->objectChanged(); } public function setEmailNotificationsEnabled(bool $enabled): void { self::requireSelected(); // Set email notifications enabled/disabled $this->email_notifications_enabled->set($enabled ? 1 : 0); $this->objectChanged(); } /** * @param int[] $customer_numbers * @return array Map of customer number to customer name */ public function getCustomerNames(array $customer_numbers, bool $allowExternalFetch = true): array { global $db; $customer_numbers = array_map('intval', $customer_numbers); if (empty($customer_numbers)) { return []; } // Look in the cache first $customer_numbers_to_fetch = []; $customer_names_cached = self::getCachedForMultipleObjects('economic_customer_name', $customer_numbers); // Loop through the customer numbers and check if they are cached $customer_names = array_map(function ($cached_name) { $cache_payload = self::buildCustomerNameCachePayload($cached_name, null); return $cache_payload['name'] ?? null; }, array_values($customer_names_cached)); // Set the names for the cached customer numbers [ "customer_number" => "customer_name" ] $customer_names = array_combine( array_map('strval', $customer_numbers), $customer_names ); // Find the customer numbers that are not cached foreach ( $customer_names as $customer_number => $customer_name ) { if ($customer_name === null) { $customer_numbers_to_fetch[] = (int)$customer_number; } } $fallback_names = $this->getLocalDisplayNamesByCustomerNumber($customer_numbers_to_fetch); $local_cached_names = $this->getCachedEconomicCustomerNamesByCustomerNumber($customer_numbers_to_fetch); if (!$allowExternalFetch) { foreach ($customer_numbers_to_fetch as $customer_number) { $customer_names[(string)$customer_number] = $local_cached_names[$customer_number] ?? $fallback_names[$customer_number] ?? 'Unknown Customer'; } return $customer_names; } // Fetch the remaining customer names from E-conomic if (count($customer_numbers_to_fetch) > 0) { foreach ( $customer_numbers_to_fetch as $customer_number ) { if (isset($local_cached_names[$customer_number])) { $customer_names[(string)$customer_number] = $local_cached_names[$customer_number]; continue; } // Get the customer name from the external source $fallback_name = $fallback_names[$customer_number] ?? null; try { // Try to get the economic customer data cached in the user $tmp_user = new users_o(); $tmp_user->getUserByCustomerNumber($customer_number); if ($tmp_user->exists()) { $display_name = $tmp_user->display_name->value(); if (is_string($display_name) && trim($display_name) !== '') { $fallback_name = $display_name; } } $cached_name = $tmp_user->getCached('economic_customer'); // If not cached, fetch from E-conomic if (!$cached_name) { $tmp_user->getCustomerEcocomicData($customer_number); $cached_name = $tmp_user->getCached('economic_customer'); } $cache_payload = self::buildCustomerNameCachePayload($cached_name, $fallback_name); if ($cache_payload !== null) { $customer_names[(string)$customer_number] = $cache_payload['name']; $this->cache('economic_customer_name', $cache_payload, $customer_number); $this->setCachedExpiration('economic_customer_name', self::$economicCustomerNameCacheExpiration, $customer_number); } } catch ( Exception $e ) { // Ignore exceptions $customer_names[(string)$customer_number] = $fallback_name ?? 'Unable to fetch name'; } } } // Return the customer names return $customer_names; } /** * @param int[] $customer_numbers * @return array */ private function getLocalDisplayNamesByCustomerNumber(array $customer_numbers): array { global $db; $customer_numbers = array_values(array_unique(array_filter(array_map('intval', $customer_numbers), static fn(int $customer_number): bool => $customer_number > 0))); if (empty($customer_numbers)) { return []; } $sql = "SELECT customer_number, display_name FROM $this->table WHERE customer_number IN (" . implode(',', $customer_numbers) . ")"; $result = $db->query($sql); if (!$result) { return []; } $names = []; while ($row = $result->fetch_assoc()) { $customer_number = (int)($row['customer_number'] ?? 0); $display_name = trim((string)($row['display_name'] ?? '')); if ($customer_number > 0 && $display_name !== '') { $names[$customer_number] = $display_name; } } return $names; } /** * Resolve names from local e-conomic snapshots only. This keeps period/listing * requests fast while still avoiding "Unnamed" fallbacks when a richer cached * e-conomic customer payload already exists. * * @param int[] $customer_numbers * @return array */ private function getCachedEconomicCustomerNamesByCustomerNumber(array $customer_numbers): array { global $db; $customer_numbers = array_values(array_unique(array_filter(array_map('intval', $customer_numbers), static fn(int $customer_number): bool => $customer_number > 0))); if (empty($customer_numbers)) { return []; } $sql = "SELECT id, customer_number FROM $this->table WHERE customer_number IN (" . implode(',', $customer_numbers) . ")"; $result = $db->query($sql); if (!$result) { return $this->getIndexedEconomicCustomerNamesByCustomerNumber($customer_numbers); } $user_ids_by_customer_number = []; while ($row = $result->fetch_assoc()) { $customer_number = (int)($row['customer_number'] ?? 0); $user_id = (int)($row['id'] ?? 0); if ($customer_number <= 0 || $user_id <= 0) { continue; } $user_ids_by_customer_number[$customer_number] = $user_id; } $names = []; $customer_numbers_by_index = array_keys($user_ids_by_customer_number); $cached_names = $this->getCachedForMultipleObjects('economic_customer', array_values($user_ids_by_customer_number)); foreach ($customer_numbers_by_index as $index => $customer_number) { $cached_name = $cached_names[$index] ?? null; $cache_payload = self::buildCustomerNameCachePayload($cached_name, null); if ($cache_payload === null) { continue; } $names[$customer_number] = $cache_payload['name']; $this->cache('economic_customer_name', $cache_payload, $customer_number); $this->setCachedExpiration('economic_customer_name', self::$economicCustomerNameCacheExpiration, $customer_number); } $missing_customer_numbers = array_values(array_diff($customer_numbers, array_keys($names))); if (!empty($missing_customer_numbers)) { foreach ($this->getIndexedEconomicCustomerNamesByCustomerNumber($missing_customer_numbers) as $customer_number => $name) { $cache_payload = self::buildCustomerNameCachePayload((object)['name' => $name], null); if ($cache_payload === null) { continue; } $names[$customer_number] = $cache_payload['name']; $this->cache('economic_customer_name', $cache_payload, $customer_number); $this->setCachedExpiration('economic_customer_name', self::$economicCustomerNameCacheExpiration, $customer_number); } } return $names; } /** * @param int[] $customer_numbers * @return array */ private function getIndexedEconomicCustomerNamesByCustomerNumber(array $customer_numbers): array { global $db; $customer_numbers = array_values(array_unique(array_filter(array_map('intval', $customer_numbers), static fn(int $customer_number): bool => $customer_number > 0))); if (empty($customer_numbers)) { return []; } try { system_search_economic_customer_index::ensureTable(); } catch (\Throwable) { return []; } $result = $db->query( "SELECT customer_number, economic_name FROM `" . system_search_economic_customer_index::TABLE . "`" . " WHERE customer_number IN (" . implode(',', $customer_numbers) . ")" ); if (!$result) { return []; } $names = []; while ($row = $result->fetch_assoc()) { $customer_number = (int)($row['customer_number'] ?? 0); $cache_payload = self::buildCustomerNameCachePayload((object)['name' => $row['economic_name'] ?? null], null); if ($customer_number > 0 && $cache_payload !== null) { $names[$customer_number] = $cache_payload['name']; } } return $names; } /** * @param int[] $cashier_ids * @return array Map of cashier id => display name */ public function getCashierNames(array $cashier_ids): array { $cashier_ids = array_values(array_unique(array_filter(array_map('intval', $cashier_ids), static fn(int $id): bool => $id > 0))); if (empty($cashier_ids)) { return []; } $cache_key = 'cashier_name'; $virtual_cache_ids = array_map(static fn(int $id): string => "cashier_$id", $cashier_ids); $cached_values = $this->getCachedForMultipleObjects($cache_key, $virtual_cache_ids); $names = []; $missing_ids = []; foreach ($cashier_ids as $index => $cashier_id) { $cached_name = $cached_values[$index] ?? null; if ($cached_name !== null && $cached_name !== '') { $names[$cashier_id] = (string)$cached_name; continue; } $missing_ids[] = $cashier_id; } if (!empty($missing_ids)) { $rows = $this->getFieldsWhereIn( ['id' => $missing_ids], ['id', 'display_name'] ); $fetched_names = []; foreach ($rows as $row) { $cashier_id = (int)($row['id'] ?? 0); if ($cashier_id <= 0) { continue; } $display_name = trim((string)($row['display_name'] ?? '')); $fetched_names[$cashier_id] = ($display_name !== '') ? $display_name : 'Unknown Cashier'; } foreach ($missing_ids as $cashier_id) { $resolved_name = $fetched_names[$cashier_id] ?? 'Unknown Cashier'; $names[$cashier_id] = $resolved_name; $virtual_cache_object_id = "cashier_$cashier_id"; $this->cache($cache_key, $resolved_name, $virtual_cache_object_id); $this->setCachedExpiration($cache_key, self::$cashierNameCacheExpiration, $virtual_cache_object_id); } } return $names; } public function getCashierName(int $cashier_id): string { $virtualCacheObjectID = "cashier_$cashier_id"; $cache_key = 'cashier_name'; // Check if the name is cached $cached_name = $this->getCached($cache_key, $virtualCacheObjectID); if ($cached_name) { return (string)$cached_name; } else { // Not cached, get the name from the database $cashier = new users_o(); $cashier->select($cashier_id); $name = $cashier->display_name->value(); // If the name is empty, set it to 'Unknown Cashier' if (empty($name)) { $name = 'Unknown Cashier'; } // Cache the name $this->cache($cache_key, $name, $virtualCacheObjectID); $this->setCachedExpiration($cache_key, self::$cashierNameCacheExpiration, $virtualCacheObjectID); return $name; } } /** * Generate password reset link for the user * @return string The password reset link * @throws Exception If the user is not selected * @throws Exception If the user does not have an email address */ public function generatePasswordResetLink(): string { self::requireSelected(); // Generate token $token = customer_password_reset_keys_o::generateToken(); // Save token $reset_key_o = new customer_password_reset_keys_o(); $reset_key_o->add([ 'customer_id' => (int)$this->customer_number->value(), 'token' => (string)$token, 'note' => 'Requested via API' ]); // Generate reset link return "https://truckwash.io/auth/password-reset/" . $token; } }