2068 lines
76 KiB
PHP
2068 lines
76 KiB
PHP
<?php
|
|
|
|
namespace objects;
|
|
|
|
use classes\db;
|
|
use classes\customer_rule_product_restriction_schema_bootstrap;
|
|
use classes\customer_name_cache_payload_builder;
|
|
use classes\object_property;
|
|
use classes\redis;
|
|
use classes\response;
|
|
use classes\system_search_economic_customer_index;
|
|
use classes\xlvask;
|
|
use customers\economic_customer_mo;
|
|
use customers\economicCustomers;
|
|
use Exception;
|
|
use traits\db_object_t;
|
|
|
|
class users_o extends db
|
|
{
|
|
use db_object_t;
|
|
|
|
public const KEY_SUPERUSER_NEW_CUSTOMER_EMAIL_NOTIFICATIONS = 'superuser_new_customer_email_notifications_enabled';
|
|
|
|
public object_property $customer_number;
|
|
public object_property $display_name;
|
|
public object_property $group_id;
|
|
public economic_customer_mo $economic_customer;
|
|
public object_property $created_at;
|
|
public object_property $updated_at;
|
|
public array $permissions;
|
|
public array $attributes;
|
|
public array $discounts;
|
|
public array $orders_not_invoiced;
|
|
public array $all_keys;
|
|
public customer_codes_o $customer_codes;
|
|
public user_key_value_pairs_o $keys;
|
|
public user_price_overrides_o $price_overrides;
|
|
public object_property $xlvask_customer_id;
|
|
protected object_property $password;
|
|
public object_property $phone_country_code;
|
|
public object_property $phone;
|
|
public object_property $email;
|
|
public object_property $sms_notifications_enabled;
|
|
public object_property $email_notifications_enabled;
|
|
public object_property $wash_certificate_email; // Optional
|
|
protected array $wash_subscription_transactions;
|
|
public object_property $two_factor_secret;
|
|
public object_property $two_factor_enabled;
|
|
|
|
public function structure(): void
|
|
{
|
|
$this->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;
|
|
customer_rule_product_restriction_schema_bootstrap::ensureSchema();
|
|
if ($user_id === null) {
|
|
self::requireSelected();
|
|
$user_id = $this->id;
|
|
}
|
|
$attribute = $db->escape_string($attribute);
|
|
$userIds = $this->attributeSiblingUserIds((int)$user_id);
|
|
foreach ($userIds as $targetUserId) {
|
|
$db->query(
|
|
"INSERT IGNORE INTO customer_attributes (user_id, attribute)
|
|
VALUES ({$targetUserId}, '{$attribute}')"
|
|
);
|
|
}
|
|
}
|
|
|
|
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);
|
|
foreach ($this->attributeSiblingUserIds((int)$user_id) as $targetUserId) {
|
|
$db->query("DELETE FROM customer_attributes WHERE user_id = {$targetUserId} AND attribute = '{$attribute}'");
|
|
}
|
|
}
|
|
|
|
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);
|
|
$userIds = $this->attributeSiblingUserIds((int)$user_id);
|
|
$sql = "SELECT id FROM customer_attributes WHERE user_id IN (" . implode(',', $userIds) . ") 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;
|
|
}
|
|
$userIds = $this->attributeSiblingUserIds((int)$user_id);
|
|
$sql = "SELECT MIN(id) AS id, MIN(user_id) AS user_id, attribute, MIN(created_at) AS created_at
|
|
FROM customer_attributes
|
|
WHERE user_id IN (" . implode(',', $userIds) . ")
|
|
GROUP BY attribute
|
|
ORDER BY attribute";
|
|
$result = $db->query($sql);
|
|
$array = $db->fetch_all($result);
|
|
$this->attributes = $array;
|
|
return $this->attributes;
|
|
}
|
|
|
|
/** @return list<int> */
|
|
private function attributeSiblingUserIds(int $userId): array
|
|
{
|
|
global $db;
|
|
if ($userId < 1) {
|
|
return [$userId];
|
|
}
|
|
$result = $db->query("SELECT customer_number FROM users WHERE id = {$userId} LIMIT 1");
|
|
if (!$result || $result->num_rows < 1) {
|
|
return [$userId];
|
|
}
|
|
$row = $result->fetch_assoc();
|
|
$customerNumber = (int)($row['customer_number'] ?? 0);
|
|
if ($customerNumber < 1) {
|
|
return [$userId];
|
|
}
|
|
$siblings = $db->query("SELECT id FROM users WHERE customer_number = {$customerNumber}");
|
|
$ids = [];
|
|
if ($siblings) {
|
|
while ($sibling = $siblings->fetch_assoc()) {
|
|
$ids[] = (int)$sibling['id'];
|
|
}
|
|
}
|
|
return $ids !== [] ? array_values(array_unique($ids)) : [$userId];
|
|
}
|
|
|
|
/**
|
|
* 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<int, array{id:int, customer_number:int, display_name:string|null, email:string}>
|
|
*/
|
|
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 $department_id = null): int|null
|
|
{
|
|
self::requireSelected();
|
|
if ($department_id !== null && (new departments_o())->isCustomPricingOnly((int)$department_id)) {
|
|
$row = (new department_customer_price_overrides_o())->getDirectPriceRow(
|
|
(int)$department_id,
|
|
(int)$this->id,
|
|
$is_category,
|
|
$object_id
|
|
);
|
|
return $row === null ? 0 : (int)$row['percentage'];
|
|
}
|
|
// 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 $department_id = null): int|null
|
|
{
|
|
self::requireSelected();
|
|
if ($department_id !== null && (new departments_o())->isCustomPricingOnly((int)$department_id)) {
|
|
return $this->getDepartmentScopedProductDiscountPercentage($product_id, (int)$department_id);
|
|
}
|
|
// 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;
|
|
}
|
|
|
|
public function getProductFixedPrice(int $product_id, ?int $department_id = null): ?int
|
|
{
|
|
self::requireSelected();
|
|
if ($department_id !== null && (new departments_o())->isCustomPricingOnly((int)$department_id)) {
|
|
$row = (new department_customer_price_overrides_o())->getDirectPriceRow(
|
|
(int)$department_id,
|
|
(int)$this->id,
|
|
false,
|
|
(int)$product_id
|
|
);
|
|
if ($row === null || $row['fixed_price'] === null) {
|
|
return null;
|
|
}
|
|
return (int)$row['fixed_price'];
|
|
}
|
|
return $this->price_overrides->setUser($this->id)->getFixedPrice(false, $product_id);
|
|
}
|
|
|
|
public function applyProductCustomerPricing(int $product_id, int $base_price, bool $use_final_price_discount_calculation = true, ?int $department_id = null): int
|
|
{
|
|
self::requireSelected();
|
|
|
|
$fixed_price = $this->getProductFixedPrice($product_id, $department_id);
|
|
if ($fixed_price !== null) {
|
|
return $fixed_price;
|
|
}
|
|
|
|
$discount_percentage = $use_final_price_discount_calculation
|
|
? (int)$this->getProductDiscountPercentage($product_id, $department_id)
|
|
: (int)$this->getCustomPrice($product_id, false, $department_id);
|
|
if ($discount_percentage <= 0) {
|
|
return $base_price;
|
|
}
|
|
|
|
return (int)round($base_price * (1 - ($discount_percentage / 100)));
|
|
}
|
|
|
|
private function getDepartmentScopedProductDiscountPercentage(int $product_id, int $department_id): int
|
|
{
|
|
self::requireSelected();
|
|
|
|
$overrides = new department_customer_price_overrides_o();
|
|
$product = (new products_o())->select((int)$product_id);
|
|
$discounts = [];
|
|
|
|
$productRow = $overrides->getDirectPriceRow($department_id, (int)$this->id, false, (int)$product_id);
|
|
if ($productRow !== null) {
|
|
$discounts[] = (int)$productRow['percentage'];
|
|
}
|
|
|
|
if ((bool)$product->apply_category_discount->value()) {
|
|
$categoryRow = $overrides->getDirectPriceRow($department_id, (int)$this->id, true, (string)$product->category->value());
|
|
if ($categoryRow !== null) {
|
|
$discounts[] = (int)$categoryRow['percentage'];
|
|
}
|
|
|
|
$globalRow = $overrides->getDirectPriceRow($department_id, (int)$this->id, true, 'global');
|
|
if ($globalRow !== null) {
|
|
$discounts[] = (int)$globalRow['percentage'];
|
|
}
|
|
}
|
|
|
|
return max([0, ...$discounts]);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $users
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function markLimitedBackofficeManagedUsers(array $users): array
|
|
{
|
|
$userIds = [];
|
|
foreach ($users as $user) {
|
|
$userId = (int)($user['id'] ?? 0);
|
|
if ($userId > 0) {
|
|
$userIds[$userId] = true;
|
|
}
|
|
}
|
|
|
|
if ($userIds === []) {
|
|
return $users;
|
|
}
|
|
|
|
global $db;
|
|
$rows = $db->fetch_all($db->query(
|
|
'SELECT `user_id` FROM `limited_backoffice_employees` WHERE `user_id` IN (' .
|
|
implode(',', array_map('intval', array_keys($userIds))) .
|
|
')'
|
|
));
|
|
|
|
$managedUserIds = [];
|
|
foreach ($rows as $row) {
|
|
$managedUserIds[(int)$row['user_id']] = true;
|
|
}
|
|
|
|
foreach ($users as $key => $user) {
|
|
$users[$key]['limited_backoffice_managed'] = isset($managedUserIds[(int)($user['id'] ?? 0)]);
|
|
}
|
|
|
|
return $users;
|
|
}
|
|
|
|
public function isLimitedBackofficeManagedUser(int $userId): bool
|
|
{
|
|
if ($userId <= 0) {
|
|
return false;
|
|
}
|
|
|
|
global $db;
|
|
$result = $db->query(
|
|
'SELECT `user_id` FROM `limited_backoffice_employees` WHERE `user_id` = ' . (int)$userId . ' LIMIT 1'
|
|
);
|
|
|
|
return $result !== false && $result->num_rows > 0;
|
|
}
|
|
|
|
public function parseCustomerNumbers(array $listObjectsWithPaginationIfSet): array
|
|
{
|
|
foreach ( $listObjectsWithPaginationIfSet as $key => $value ) {
|
|
if ((bool)($value['limited_backoffice_managed'] ?? false) || (int)($value['customer_number'] ?? -1) === 0) {
|
|
$listObjectsWithPaginationIfSet[$key]['customer_name'] = $value['display_name'] ?? null;
|
|
continue;
|
|
}
|
|
|
|
$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
|
|
* @param int|null $fixed_price The fixed product price, when set
|
|
* @return void
|
|
*/
|
|
public function setCustomPrice(int $user_id, int|string $object_id, int $discount_percentage, bool $is_category = false, ?int $fixed_price = null): 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, $fixed_price);
|
|
}
|
|
|
|
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<string, int> 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<int,string>
|
|
*/
|
|
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<int,string>
|
|
*/
|
|
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<int,string>
|
|
*/
|
|
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<int, string> 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;
|
|
}
|
|
|
|
}
|