284 lines
9.9 KiB
PHP
284 lines
9.9 KiB
PHP
<?php
|
|
|
|
namespace objects;
|
|
|
|
use classes\department_gate_config;
|
|
use classes\bird;
|
|
use classes\db;
|
|
use classes\object_property;
|
|
use classes\slack;
|
|
use Exception;
|
|
use traits\db_object_t;
|
|
|
|
class department_gates_o extends db
|
|
{
|
|
use db_object_t;
|
|
public object_property $department; // The department id the gate relates to
|
|
// The reason why there's two separate boolean properties for entrance and exit is to allow for departments where the same gate can be both an entrance and an exit (e.g. a turnstile that people both enter and exit through, where the gate criteria is based on a phone call triggered by passing through the turnstile, and the same criteria applies for both entering and exiting)
|
|
public object_property $is_entrance; // Whether the gate is an entrance gate
|
|
public object_property $is_exit; // Whether the gate is an exit gate
|
|
public object_property $name; // The name of the gate
|
|
public object_property $config; // A JSON with the gate config, including the type of gate and any relevant parameters (e.g. for a phone call triggered gate, the config would include the phone number to call and the call duration threshold)
|
|
public object_property $created_at;
|
|
public object_property $updated_at;
|
|
public object_property $deleted_at;
|
|
|
|
|
|
public function structure(): void
|
|
{
|
|
$this->setTable('department_gates');
|
|
}
|
|
|
|
/**
|
|
* Add a gate
|
|
* @param departments_o $department The department for the gate
|
|
* @param bool $is_entrance Whether the gate is an entrance gate (true) or an exit gate (false)
|
|
* @param bool $is_exit Whether the gate is an exit gate (true) or an entrance gate (false)
|
|
* @param string $name The name of the gate
|
|
* @param department_gate_config $config A JSON with the gate config, including the type of gate and any relevant parameters (e.g. for a phone call triggered gate, the config would include the phone number to call and the call duration threshold)
|
|
* @return department_gates_o
|
|
* @throws Exception If the object was not created successfully
|
|
*/
|
|
public function add(departments_o $department, bool $is_entrance, bool $is_exit, string $name, department_gate_config $config): department_gates_o
|
|
{
|
|
global /** @var db $db */ $db;
|
|
// Sanitize the input
|
|
$department->requireSelected();
|
|
$name = trim($db->escape_string($name));
|
|
if (empty($name)) {
|
|
throw new Exception('Gate name cannot be empty');
|
|
}
|
|
if (!$is_entrance && !$is_exit) {
|
|
throw new Exception('Gate must be either entrance, exit or both');
|
|
}
|
|
$config->validate();
|
|
// Add the object
|
|
$tmp_id = self::add_object([
|
|
'department' => (int)$department->id,
|
|
'is_entrance' => (bool)$is_entrance,
|
|
'is_exit' => (bool)$is_exit,
|
|
'name' => $name,
|
|
'config' => (array)$config->toArray(),
|
|
]);
|
|
$this->id = $tmp_id;
|
|
self::getObjectProperties();
|
|
self::objectChanged();
|
|
return $this;
|
|
}
|
|
|
|
|
|
|
|
public function getObjectProperties(): void
|
|
{
|
|
$this->department = new object_property($this->table, $this->id, 'department', 'int', false);
|
|
$this->is_entrance = new object_property($this->table, $this->id, 'is_entrance', 'bool', false);
|
|
$this->is_exit = new object_property($this->table, $this->id, 'is_exit', 'bool', false);
|
|
$this->name = new object_property($this->table, $this->id, 'name', 'string', false);
|
|
$this->config = new object_property($this->table, $this->id, 'config', 'json', false);
|
|
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'timestamp', false);
|
|
$this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'timestamp', false);
|
|
$this->deleted_at = new object_property($this->table, $this->id, 'deleted_at', 'timestamp', false);
|
|
}
|
|
|
|
public function objectChanged(): void
|
|
{
|
|
//TODO: Add cache invalidation
|
|
}
|
|
|
|
/**
|
|
* Get the gate as an array, including progress details if $include_progress is true
|
|
* @return array The gate as an array
|
|
* @throws Exception If the object is not valid or selected.
|
|
*/
|
|
public function asArray(): array
|
|
{
|
|
$this->requireSelected();
|
|
return [
|
|
'id' => (int)$this->id,
|
|
'department' => (int)$this->department->value(),
|
|
'is_entrance' => (bool)$this->is_entrance->value(),
|
|
'is_exit' => (bool)$this->is_exit->value(),
|
|
'name' => (string)$this->name->value(),
|
|
'config' => (array)$this->config->value(),
|
|
'created_at' => (string)$this->created_at->value(),
|
|
'updated_at' => (string)$this->updated_at->value(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
* @return department_gates_o[]
|
|
*/
|
|
public function getDepartmentGates(int $department_id): array
|
|
{
|
|
$gates = [];
|
|
$department_gates = self::getFieldsWhere(
|
|
[
|
|
'department' => $department_id,
|
|
'deleted_at' => null,
|
|
],
|
|
[
|
|
'id',
|
|
],
|
|
);
|
|
|
|
foreach ($department_gates as $department_gate) {
|
|
$gates[] = (new department_gates_o())->select((int)$department_gate['id']);
|
|
}
|
|
|
|
return $gates;
|
|
}
|
|
|
|
public function getEntranceGate(int $department_id): ?department_gates_o
|
|
{
|
|
$entrance_gate_data = self::getFieldsWhere(
|
|
[
|
|
'department' => $department_id,
|
|
'is_entrance' => true,
|
|
'deleted_at' => null,
|
|
],
|
|
[
|
|
'id',
|
|
],
|
|
);
|
|
|
|
if (empty($entrance_gate_data)) {
|
|
return null;
|
|
}
|
|
|
|
return (new department_gates_o())->select((int)$entrance_gate_data[0]['id']);
|
|
}
|
|
|
|
public function getExitGate(int $department_id): ?department_gates_o
|
|
{
|
|
$exit_gate_data = self::getFieldsWhere(
|
|
[
|
|
'department' => $department_id,
|
|
'is_exit' => true,
|
|
'deleted_at' => null,
|
|
],
|
|
[
|
|
'id',
|
|
],
|
|
);
|
|
|
|
if (empty($exit_gate_data)) {
|
|
return null;
|
|
}
|
|
|
|
return (new department_gates_o())->select((int)$exit_gate_data[0]['id']);
|
|
}
|
|
|
|
public static function normalizePhoneCandidate(mixed $candidate): ?array
|
|
{
|
|
if (is_array($candidate)) {
|
|
$phone = $candidate['phone_number'] ?? null;
|
|
if ($phone !== null) {
|
|
$country = self::extractCountryCodeFromPhoneNumber((string)$phone);
|
|
return self::normalizePhone((string)$phone, $country);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if (is_string($candidate) && trim($candidate) !== '') {
|
|
return self::normalizePhone($candidate);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static function normalizePhone(string $raw, ?int $defaultCountryCode = 45): ?array
|
|
{
|
|
$trimmed = trim($raw);
|
|
if ($trimmed === '') {
|
|
return null;
|
|
}
|
|
$digits = preg_replace('/\D+/', '', $trimmed);
|
|
if (!is_string($digits) || $digits === '') {
|
|
return null;
|
|
}
|
|
|
|
$country = $defaultCountryCode;
|
|
$phone = $digits;
|
|
|
|
if (str_starts_with($trimmed, '+')) {
|
|
$extractedCountry = self::extractCountryCodeFromPhoneNumber($trimmed);
|
|
if ($extractedCountry !== null) {
|
|
$country = $extractedCountry;
|
|
$phone = substr($digits, strlen((string)$extractedCountry));
|
|
} elseif (strlen($digits) > 8) {
|
|
$country = (int)substr($digits, 0, 2);
|
|
$phone = substr($digits, 2);
|
|
}
|
|
} elseif ($country !== null && str_starts_with($digits, (string)$country) && strlen($digits) > 8) {
|
|
$phone = substr($digits, strlen((string)$country));
|
|
}
|
|
|
|
if ($country === null || $country <= 0 || $phone === '') {
|
|
return null;
|
|
}
|
|
|
|
return [$country, (int)$phone];
|
|
}
|
|
|
|
public static function extractCountryCodeFromPhoneNumber(string $phone): ?int
|
|
{
|
|
if (str_starts_with($phone, '+45')) {
|
|
return 45;
|
|
}
|
|
if (str_starts_with($phone, '+46')) {
|
|
return 46;
|
|
}
|
|
if (str_starts_with($phone, '+47')) {
|
|
return 47;
|
|
}
|
|
if (str_starts_with($phone, '+358')) {
|
|
return 358;
|
|
}
|
|
if (str_starts_with($phone, '+49')) {
|
|
return 49;
|
|
}
|
|
if (str_starts_with($phone, '+44')) {
|
|
return 44;
|
|
}
|
|
if (str_starts_with($phone, '+1')) {
|
|
return 1;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function openGate(): void
|
|
{
|
|
$this->requireSelected();
|
|
$config = (array)$this->config->value();
|
|
|
|
if (!isset($config['type']) || $config['type'] !== 'PHONE_CALL') {
|
|
throw new Exception('Unsupported gate type: ' . (string)($config['type'] ?? ''));
|
|
}
|
|
if (!isset($config['phone_number'])) {
|
|
throw new Exception('Phone number is required for PHONE_CALL gate type');
|
|
}
|
|
|
|
$normalized = self::normalizePhoneCandidate($config['phone_number']);
|
|
if ($normalized === null) {
|
|
throw new Exception('Invalid phone number for PHONE_CALL gate type');
|
|
}
|
|
[$countryCode, $phone] = $normalized;
|
|
|
|
$timeout = (int)($config['call_duration_threshold'] ?? 10);
|
|
|
|
$client = new bird();
|
|
try {
|
|
$client->callGatePreferringFlashCall(
|
|
(int)$countryCode,
|
|
(int)$phone,
|
|
$timeout,
|
|
);
|
|
} catch (\Throwable $e) {
|
|
$slack = new slack();
|
|
$slack->send_message('Failed to call gate for phone ' . $countryCode . ' ' . $phone . ': ' . $e->getMessage(), 'Bird Voice Call Webhooks');
|
|
throw new Exception('Failed to open gate relay via phone call', 0, $e);
|
|
}
|
|
}
|
|
}
|