Refactor input value handling and improve validation in ObjectsGlobal component

This commit is contained in:
Jeppe Bundgaard
2026-06-29 11:08:50 +02:00
parent 148b575767
commit 42f8ae0c47
13 changed files with 469 additions and 12 deletions
+112 -2
View File
@@ -11665,6 +11665,53 @@ paths:
schema:
$ref: '#/components/schemas/ModuleConfigUpdateResponse'
/slack/config/test:
post:
tags: [Config]
summary: Test Slack customer registration webhook
operationId: testSlackCustomerRegistrationWebhook
responses:
'200':
description: Slack customer registration webhook test completed successfully
content:
application/json:
schema:
$ref: '#/components/schemas/SlackConfigTestResponse'
'400':
description: Slack customer registration webhook URL is not configured
'502':
description: Slack customer registration webhook test failed
/slack/config/internal-department-goal-progress:
get:
tags: [Config]
summary: Get Slack internal department goal progress config
operationId: getSlackInternalDepartmentGoalProgressConfig
responses:
'200':
description: Slack internal department goal progress configuration retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigResponse'
post:
tags: [Config]
summary: Update Slack internal department goal progress config
operationId: updateSlackInternalDepartmentGoalProgressConfig
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigUpdate'
responses:
'200':
description: Slack internal department goal progress configuration updated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigResponse'
/backups/config:
get:
tags: [Config]
@@ -15183,12 +15230,59 @@ components:
type: object
properties:
module: { type: string, enum: [Slack] }
variable: { type: string, enum: [customer_registration_webhook_url] }
variable: { type: string, enum: [customer_registration_webhook_url, internal_department_goal_progress_webhook_url, internal_department_ids] }
type: { type: string, enum: [string] }
value:
oneOf:
- type: string
example: https://hooks.slack.com/services/...
- type: string
example: '[1,2,3]'
required: [module, variable, type, value]
SlackConfigTestResult:
type: object
properties:
configured: { type: boolean }
sent: { type: boolean }
message: { type: string }
required: [configured, sent, message]
SlackInternalDepartmentGoalProgressDepartment:
type: object
properties:
id: { type: integer }
name: { type: string }
order_priority: { type: integer }
required: [id, name, order_priority]
SlackInternalDepartmentGoalProgressConfig:
type: object
properties:
internal_department_goal_progress_webhook_url:
type: string
example: https://hooks.slack.com/services/...
required: [module, variable, type, value]
internal_department_ids:
type: array
items: { type: integer }
example: [1, 2, 3]
departments:
type: array
items:
$ref: '#/components/schemas/SlackInternalDepartmentGoalProgressDepartment'
required: [internal_department_goal_progress_webhook_url, internal_department_ids, departments]
SlackInternalDepartmentGoalProgressConfigUpdate:
type: object
properties:
internal_department_goal_progress_webhook_url:
type: string
example: https://hooks.slack.com/services/...
internal_department_ids:
type: array
items: { type: integer }
example: [1, 2, 3]
required: [internal_department_goal_progress_webhook_url, internal_department_ids]
BackupsConfigEntry:
type: object
@@ -15466,6 +15560,22 @@ components:
data: { type: array, items: { $ref: '#/components/schemas/SlackConfigEntry' } }
required: [data]
SlackConfigTestResponse:
allOf:
- $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
- type: object
properties:
data: { $ref: '#/components/schemas/SlackConfigTestResult' }
required: [data]
SlackInternalDepartmentGoalProgressConfigResponse:
allOf:
- $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
- type: object
properties:
data: { $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfig' }
required: [data]
BackupsConfigListResponse:
allOf:
- $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
File diff suppressed because one or more lines are too long
+57
View File
@@ -201,6 +201,63 @@ class slack implements notification_i
return trim((string)$this->getConfig()->customer_registration_webhook_url->getVariableValue());
}
public function get_internal_department_goal_progress_webhook_url(): string
{
return trim((string)$this->getConfig()->internal_department_goal_progress_webhook_url->getVariableValue());
}
/**
* @return int[]
*/
public function get_internal_department_ids(): array
{
return $this->getConfig()->internal_department_ids->getDepartmentIds();
}
/**
* @param int[] $department_ids
* @throws \Exception
*/
public function set_internal_department_goal_progress_config(string $webhook_url, array $department_ids): array
{
$this->getConfig()->internal_department_goal_progress_webhook_url->setVariableValue(trim($webhook_url));
$this->getConfig()->internal_department_ids->setVariableValue($department_ids);
return $this->get_internal_department_goal_progress_config();
}
public function get_internal_department_goal_progress_config(): array
{
$departments = (new departments_o())->getFieldsWhere(
[
'visible' => 1,
'archived' => 0,
],
[
'id',
'name',
'order_priority',
]
);
usort($departments, static function (array $a, array $b): int {
return (int)($a['order_priority'] ?? 0) <=> (int)($b['order_priority'] ?? 0)
?: (int)($a['id'] ?? 0) <=> (int)($b['id'] ?? 0);
});
return [
'internal_department_goal_progress_webhook_url' => $this->get_internal_department_goal_progress_webhook_url(),
'internal_department_ids' => $this->get_internal_department_ids(),
'departments' => array_map(static function (array $department): array {
return [
'id' => (int)$department['id'],
'name' => (string)$department['name'],
'order_priority' => (int)$department['order_priority'],
];
}, $departments),
];
}
public function is_webhook_send_successful(string $result): bool
{
return !str_starts_with($result, 'Failed to send message:');
@@ -0,0 +1,29 @@
<?php
namespace slack\config;
use Exception;
use traits\module_config_variable;
class slack_internal_department_goal_progress_webhook_url_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'Slack',
'internal_department_goal_progress_webhook_url',
'string',
false,
null,
'Slack webhook URL used for internal department goal progress notifications',
'https://hooks.slack.com/services/...',
true,
''
);
}
}
@@ -0,0 +1,76 @@
<?php
namespace slack\config;
use Exception;
use traits\module_config_variable;
class slack_internal_department_ids_c
{
use module_config_variable {
setVariableValue as private traitSetVariableValue;
}
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'Slack',
'internal_department_ids',
'string',
false,
null,
'JSON encoded department IDs considered internal departments for Slack goal progress notifications',
'[1,2,3]',
false,
'[]'
);
}
public function getDepartmentIds(): array
{
return self::normalizeDepartmentIds($this->getVariableValue());
}
/**
* @throws Exception
*/
public function setVariableValue(mixed $value): void
{
$this->traitSetVariableValue(json_encode(
self::normalizeDepartmentIds($value),
JSON_THROW_ON_ERROR
));
}
public static function normalizeDepartmentIds(mixed $value): array
{
if (is_string($value)) {
$decoded = json_decode($value, true);
if (is_array($decoded)) {
$value = $decoded;
} else {
$value = array_filter(array_map('trim', explode(',', $value)));
}
}
if (!is_array($value)) {
return [];
}
$departmentIds = [];
foreach ($value as $departmentId) {
$departmentId = (int)$departmentId;
if ($departmentId > 0) {
$departmentIds[] = $departmentId;
}
}
$departmentIds = array_values(array_unique($departmentIds));
sort($departmentIds);
return $departmentIds;
}
}
@@ -3,8 +3,12 @@
namespace slack;
require_once WD . '/modules/slack/config/slack_customer_registration_webhook_url_c.php';
require_once WD . '/modules/slack/config/slack_internal_department_goal_progress_webhook_url_c.php';
require_once WD . '/modules/slack/config/slack_internal_department_ids_c.php';
use slack\config\slack_customer_registration_webhook_url_c;
use slack\config\slack_internal_department_goal_progress_webhook_url_c;
use slack\config\slack_internal_department_ids_c;
use traits\module_config_t;
class slack_c
@@ -12,14 +16,20 @@ class slack_c
use module_config_t;
public slack_customer_registration_webhook_url_c $customer_registration_webhook_url;
public slack_internal_department_goal_progress_webhook_url_c $internal_department_goal_progress_webhook_url;
public slack_internal_department_ids_c $internal_department_ids;
public function __construct()
{
$this->setupConfig('Slack');
$this->allowUpdate([
slack_customer_registration_webhook_url_c::class,
slack_internal_department_goal_progress_webhook_url_c::class,
slack_internal_department_ids_c::class,
]);
$this->customer_registration_webhook_url = new slack_customer_registration_webhook_url_c();
$this->internal_department_goal_progress_webhook_url = new slack_internal_department_goal_progress_webhook_url_c();
$this->internal_department_ids = new slack_internal_department_ids_c();
}
}
+10 -3
View File
@@ -718,7 +718,7 @@ class departments_o extends db
* @return null|array
* @throws Exception
*/
public function sendSlackInternalStatisticNotification(string $date_start, string $date_end, array $department_ids, array $product_ids = [
public function sendSlackInternalStatisticNotification(string $date_start, string $date_end, array $department_ids = [], array $product_ids = [
25,
[23, 24], // Used to merge two products into one percentage (Spot Free)
22,
@@ -727,6 +727,11 @@ class departments_o extends db
26
], bool $return_as_array = false): null|array
{
$slack = new slack();
if (empty($department_ids)) {
$department_ids = $slack->get_internal_department_ids();
}
if (empty($department_ids)) {
throw new Exception('No department ids provided for the Slack internal statistic notification.');
}
@@ -847,9 +852,11 @@ class departments_o extends db
$array_of_results['daily_management'][] = $tmp;
// Send the message to the internal Slack webhook
$slack = new slack();
if (!$return_as_array) {
$slack->send_webhook_message($tmp, (new departments_o())->select(10)->slack_webhook->value());
$webhook = $slack->get_internal_department_goal_progress_webhook_url();
if ($webhook !== '') {
$slack->send_webhook_message($tmp, $webhook);
}
}
// Send a department-specific message for each internal department with the percentage of addons sold
foreach ( $department_ids as $department_id ) {
+80 -3
View File
@@ -11682,6 +11682,36 @@ paths:
'502':
description: Slack customer registration webhook test failed
/slack/config/internal-department-goal-progress:
get:
tags: [Config]
summary: Get Slack internal department goal progress config
operationId: getSlackInternalDepartmentGoalProgressConfig
responses:
'200':
description: Slack internal department goal progress configuration retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigResponse'
post:
tags: [Config]
summary: Update Slack internal department goal progress config
operationId: updateSlackInternalDepartmentGoalProgressConfig
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigUpdate'
responses:
'200':
description: Slack internal department goal progress configuration updated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigResponse'
/backups/config:
get:
tags: [Config]
@@ -15200,11 +15230,14 @@ components:
type: object
properties:
module: { type: string, enum: [Slack] }
variable: { type: string, enum: [customer_registration_webhook_url] }
variable: { type: string, enum: [customer_registration_webhook_url, internal_department_goal_progress_webhook_url, internal_department_ids] }
type: { type: string, enum: [string] }
value:
type: string
example: https://hooks.slack.com/services/...
oneOf:
- type: string
example: https://hooks.slack.com/services/...
- type: string
example: '[1,2,3]'
required: [module, variable, type, value]
SlackConfigTestResult:
@@ -15215,6 +15248,42 @@ components:
message: { type: string }
required: [configured, sent, message]
SlackInternalDepartmentGoalProgressDepartment:
type: object
properties:
id: { type: integer }
name: { type: string }
order_priority: { type: integer }
required: [id, name, order_priority]
SlackInternalDepartmentGoalProgressConfig:
type: object
properties:
internal_department_goal_progress_webhook_url:
type: string
example: https://hooks.slack.com/services/...
internal_department_ids:
type: array
items: { type: integer }
example: [1, 2, 3]
departments:
type: array
items:
$ref: '#/components/schemas/SlackInternalDepartmentGoalProgressDepartment'
required: [internal_department_goal_progress_webhook_url, internal_department_ids, departments]
SlackInternalDepartmentGoalProgressConfigUpdate:
type: object
properties:
internal_department_goal_progress_webhook_url:
type: string
example: https://hooks.slack.com/services/...
internal_department_ids:
type: array
items: { type: integer }
example: [1, 2, 3]
required: [internal_department_goal_progress_webhook_url, internal_department_ids]
BackupsConfigEntry:
type: object
properties:
@@ -15499,6 +15568,14 @@ components:
data: { $ref: '#/components/schemas/SlackConfigTestResult' }
required: [data]
SlackInternalDepartmentGoalProgressConfigResponse:
allOf:
- $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
- type: object
properties:
data: { $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfig' }
required: [data]
BackupsConfigListResponse:
allOf:
- $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
@@ -526,7 +526,7 @@ class departmentsRoute
'results' => $departments_o->sendSlackInternalStatisticNotification(
$week_monday,
$week_sunday,
[1, 2, 3, 4, 5, 6, 7],
[],
// Default value
[
25,
@@ -250,6 +250,60 @@ class moduleConfigRoute
]
);
/** Slack internal department goal progress config > GET */
$this->get('/slack/config/internal-department-goal-progress', function () {
global $response;
$this->requirePermission('slack_config');
$user = (new authentication())->get_user();
if (!$user) {
(new logs_o())->add('slack_config', 'global', 1, 0, 'SLACK_INTERNAL_DEPARTMENT_GOAL_PROGRESS_CONFIG', 'No user found, or invalid session');
$response->error('Invalid session', 400);
}
(new logs_o())->add('slack_config', 'global', 1, $user->id, 'SLACK_INTERNAL_DEPARTMENT_GOAL_PROGRESS_CONFIG', 'Successfully fetched Slack internal department goal progress config');
$response->success(
(new slack())->get_internal_department_goal_progress_config()
);
},
[
'slack_config' => 'Get Slack internal department goal progress config'
]
);
/** Slack internal department goal progress config > POST */
$this->post('/slack/config/internal-department-goal-progress', function () {
global $response;
$this->requirePermission('slack_config');
$user = (new authentication())->get_user();
if (!$user) {
(new logs_o())->add('slack_config', 'global', 1, 0, 'SLACK_INTERNAL_DEPARTMENT_GOAL_PROGRESS_CONFIG', 'No user found, or invalid session');
$response->error('Invalid session', 400);
}
$webhook_url = $response->getRequestParameter('internal_department_goal_progress_webhook_url')
?? $response->getRequestParameter('webhook_url')
?? '';
$department_ids = $response->getRequestParameter('internal_department_ids')
?? $response->getRequestParameter('department_ids')
?? [];
if (!is_array($department_ids)) {
$department_ids = array_filter(array_map('trim', explode(',', (string)$department_ids)));
}
(new logs_o())->add('slack_config', 'global', 1, $user->id, 'SLACK_INTERNAL_DEPARTMENT_GOAL_PROGRESS_CONFIG', 'Successfully updated Slack internal department goal progress config');
$response->success(
(new slack())->set_internal_department_goal_progress_config(
(string)$webhook_url,
$department_ids
)
);
},
[
'slack_config' => 'Update Slack internal department goal progress config'
]
);
$this->get('/backups/config', function () {
global $response;
$this->requirePermission('backups_config');
+1 -1
View File
@@ -48,7 +48,7 @@ class workerRoute
// The time should be from 00:00:00 of the start date to 23:59:59 of the end date
$date_end = date('Y-m-d 23:59:59', strtotime("-1 days")); // Yesterday (Sunday) at 23:59:59
$date_start = date('Y-m-d 00:00:00', strtotime("-$days days")); // $days ago at 00:00:00
$department->sendSlackInternalStatisticNotification($date_start, $date_end, [1,2,3,4,5,6,7]);
$department->sendSlackInternalStatisticNotification($date_start, $date_end);
$response->success(['message' => 'Test message sent to Slack (not really, this is a placeholder).' ]);
exit;
// Configuration
@@ -7,34 +7,55 @@ it('registers Slack module config endpoints and customer registration webhook co
expect($routeContent)->not->toBeFalse()
->and($routeContent)->toContain('/slack/config')
->and($routeContent)->toContain('/slack/config/test')
->and($routeContent)->toContain('/slack/config/internal-department-goal-progress')
->and($routeContent)->toContain("requirePermission('slack_config')")
->and($routeContent)->toContain("(new slack())->getConfig()->getConfigRequest()")
->and($routeContent)->toContain("(new slack())->getConfig()->postConfigRequest()")
->and($routeContent)->toContain("(new slack())->test_customer_registration_webhook()");
->and($routeContent)->toContain("(new slack())->test_customer_registration_webhook()")
->and($routeContent)->toContain("(new slack())->get_internal_department_goal_progress_config()")
->and($routeContent)->toContain("set_internal_department_goal_progress_config");
$moduleContent = file_get_contents(app_path('modules/slack/slack_c.php'));
$variableContent = file_get_contents(app_path('modules/slack/config/slack_customer_registration_webhook_url_c.php'));
$internalWebhookContent = file_get_contents(app_path('modules/slack/config/slack_internal_department_goal_progress_webhook_url_c.php'));
$internalDepartmentsContent = file_get_contents(app_path('modules/slack/config/slack_internal_department_ids_c.php'));
$slackClassContent = file_get_contents(app_path('classes/slack.php'));
$authRouteContent = file_get_contents(app_path('routes/authRoute.php'));
$departmentsContent = file_get_contents(app_path('objects/departments_o.php'));
$openApiContent = file_get_contents(app_path('openapi.yaml'));
expect($moduleContent)->not->toBeFalse()
->and($moduleContent)->toContain("setupConfig('Slack')")
->and($moduleContent)->toContain('slack_customer_registration_webhook_url_c::class')
->and($moduleContent)->toContain('slack_internal_department_goal_progress_webhook_url_c::class')
->and($moduleContent)->toContain('slack_internal_department_ids_c::class')
->and($variableContent)->not->toBeFalse()
->and($variableContent)->toContain("'customer_registration_webhook_url'")
->and($variableContent)->toContain('Slack webhook URL used for successful customer registration notifications')
->and($internalWebhookContent)->not->toBeFalse()
->and($internalWebhookContent)->toContain("'internal_department_goal_progress_webhook_url'")
->and($internalDepartmentsContent)->not->toBeFalse()
->and($internalDepartmentsContent)->toContain("'internal_department_ids'")
->and($internalDepartmentsContent)->toContain('normalizeDepartmentIds')
->and($slackClassContent)->not->toBeFalse()
->and($slackClassContent)->toContain('send_customer_registration_notification')
->and($slackClassContent)->toContain('test_customer_registration_webhook')
->and($slackClassContent)->toContain('get_internal_department_goal_progress_config')
->and($slackClassContent)->toContain('get_internal_department_goal_progress_webhook_url')
->and($slackClassContent)->toContain('format_customer_registration_test')
->and($slackClassContent)->toContain('format_customer_registration')
->and($authRouteContent)->not->toBeFalse()
->and($authRouteContent)->toContain("AUTH_REGISTER_CVR_SLACK_NOTIFICATION_FAILED")
->and($departmentsContent)->not->toBeFalse()
->and($departmentsContent)->toContain('$slack->get_internal_department_ids()')
->and($departmentsContent)->toContain('$slack->get_internal_department_goal_progress_webhook_url()')
->and($departmentsContent)->not->toContain('select(10)->slack_webhook')
->and($openApiContent)->not->toBeFalse()
->and($openApiContent)->toContain('/slack/config')
->and($openApiContent)->toContain('/slack/config/test')
->and($openApiContent)->toContain('/slack/config/internal-department-goal-progress')
->and($openApiContent)->toContain('SlackConfigListResponse')
->and($openApiContent)->toContain('SlackInternalDepartmentGoalProgressConfigResponse')
->and($openApiContent)->toContain('SlackConfigTestResponse')
->and($openApiContent)->toContain('SlackConfigEntry');
});
@@ -0,0 +1,16 @@
<?php
app_require('modules/slack/config/slack_internal_department_ids_c.php');
use slack\config\slack_internal_department_ids_c;
it('normalizes configured Slack internal department ids', function (): void {
expect(slack_internal_department_ids_c::normalizeDepartmentIds([3, '2', 2, 0, -1, 'abc', 1]))
->toBe([1, 2, 3])
->and(slack_internal_department_ids_c::normalizeDepartmentIds('[7, "5", 5]'))
->toBe([5, 7])
->and(slack_internal_department_ids_c::normalizeDepartmentIds('4, 2, invalid'))
->toBe([2, 4])
->and(slack_internal_department_ids_c::normalizeDepartmentIds('not-json'))
->toBe([]);
});