Add Slack customer registration notification functionality
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -6,13 +6,25 @@ use GuzzleHttp\Client;
|
||||
use interfaces\notification_i;
|
||||
use objects\departments_o;
|
||||
use objects\users_o;
|
||||
use slack\slack_c;
|
||||
use traits\notification_t;
|
||||
|
||||
require_once WD . '/modules/slack/slack_c.php';
|
||||
|
||||
class slack implements notification_i
|
||||
{
|
||||
use notification_t;
|
||||
|
||||
private ?slack_c $config = null;
|
||||
|
||||
public function getConfig(): slack_c
|
||||
{
|
||||
if ($this->config === null) {
|
||||
$this->config = new slack_c();
|
||||
}
|
||||
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
@@ -132,4 +144,38 @@ class slack implements notification_i
|
||||
// Send the message to the slack webhook
|
||||
self::add_log(self::send_webhook_message($string, $SLACK_DEFAULT_WEBHOOK));
|
||||
}
|
||||
}
|
||||
|
||||
public function send_customer_registration_notification(int $customer_number): self
|
||||
{
|
||||
$webhook = trim((string)$this->getConfig()->customer_registration_webhook_url->getVariableValue());
|
||||
if ($webhook === '') {
|
||||
return $this;
|
||||
}
|
||||
|
||||
self::add_log(self::send_webhook_message(
|
||||
$this->format_customer_registration($customer_number),
|
||||
$webhook
|
||||
));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function format_customer_registration(int $customer_number): string
|
||||
{
|
||||
$customer = (new users_o())->getUserByCustomerNumber($customer_number);
|
||||
$customerName = $customer->exists()
|
||||
? $customer->getCustomerName((int)$customer->customer_number->value())
|
||||
: '';
|
||||
$customerName = trim((string)$customerName);
|
||||
if ($customerName === '') {
|
||||
$customerName = 'Unknown customer';
|
||||
}
|
||||
|
||||
$safeCustomerNumber = (int)$customer_number;
|
||||
$customerUrl = 'https://truckwash.io/superuser/users?search=' . $safeCustomerNumber;
|
||||
|
||||
return "*New customer registered on Truck Wash*\n"
|
||||
. "Customer: $customerName ($safeCustomerNumber)\n"
|
||||
. "Open in Superuser: $customerUrl";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace slack\config;
|
||||
|
||||
use Exception;
|
||||
use traits\module_config_variable;
|
||||
|
||||
class slack_customer_registration_webhook_url_c
|
||||
{
|
||||
use module_config_variable;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
self::setupConfigVariable(
|
||||
'Slack',
|
||||
'customer_registration_webhook_url',
|
||||
'string',
|
||||
false,
|
||||
null,
|
||||
'Slack webhook URL used for successful customer registration notifications',
|
||||
'https://hooks.slack.com/services/...',
|
||||
true,
|
||||
''
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace slack;
|
||||
|
||||
require_once WD . '/modules/slack/config/slack_customer_registration_webhook_url_c.php';
|
||||
|
||||
use slack\config\slack_customer_registration_webhook_url_c;
|
||||
use traits\module_config_t;
|
||||
|
||||
class slack_c
|
||||
{
|
||||
use module_config_t;
|
||||
|
||||
public slack_customer_registration_webhook_url_c $customer_registration_webhook_url;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setupConfig('Slack');
|
||||
$this->allowUpdate([
|
||||
slack_customer_registration_webhook_url_c::class,
|
||||
]);
|
||||
|
||||
$this->customer_registration_webhook_url = new slack_customer_registration_webhook_url_c();
|
||||
}
|
||||
}
|
||||
@@ -11601,6 +11601,35 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ModuleConfigTestResponse'
|
||||
|
||||
/slack/config:
|
||||
get:
|
||||
tags: [Config]
|
||||
summary: Get Slack config
|
||||
operationId: getSlackConfig
|
||||
responses:
|
||||
'200':
|
||||
description: Slack configuration retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SlackConfigListResponse'
|
||||
post:
|
||||
tags: [Config]
|
||||
summary: Update Slack config
|
||||
operationId: updateSlackConfig
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
responses:
|
||||
'200':
|
||||
description: Slack configuration updated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ModuleConfigUpdateResponse'
|
||||
|
||||
/backups/config:
|
||||
get:
|
||||
tags: [Config]
|
||||
@@ -15115,6 +15144,17 @@ components:
|
||||
- type: integer
|
||||
required: [module, variable, type, value]
|
||||
|
||||
SlackConfigEntry:
|
||||
type: object
|
||||
properties:
|
||||
module: { type: string, enum: [Slack] }
|
||||
variable: { type: string, enum: [customer_registration_webhook_url] }
|
||||
type: { type: string, enum: [string] }
|
||||
value:
|
||||
type: string
|
||||
example: https://hooks.slack.com/services/...
|
||||
required: [module, variable, type, value]
|
||||
|
||||
BackupsConfigEntry:
|
||||
type: object
|
||||
properties:
|
||||
@@ -15383,6 +15423,14 @@ components:
|
||||
data: { type: array, items: { $ref: '#/components/schemas/EmailConfigEntry' } }
|
||||
required: [data]
|
||||
|
||||
SlackConfigListResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
|
||||
- type: object
|
||||
properties:
|
||||
data: { type: array, items: { $ref: '#/components/schemas/SlackConfigEntry' } }
|
||||
required: [data]
|
||||
|
||||
BackupsConfigListResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
|
||||
|
||||
@@ -7,6 +7,7 @@ use classes\economic;
|
||||
use classes\email;
|
||||
use classes\release_manager;
|
||||
use classes\recaptcha;
|
||||
use classes\slack;
|
||||
use classes\totp;
|
||||
use classes\virkdata;
|
||||
use classes\webauthn;
|
||||
@@ -819,6 +820,15 @@ class authRoute
|
||||
'message' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
(new slack())->send_customer_registration_notification($customerNumber);
|
||||
} catch (Exception $exception) {
|
||||
$this->logRegisterCvrIssue('AUTH_REGISTER_CVR_SLACK_NOTIFICATION_FAILED', [
|
||||
'customerNumber' => $customerNumber,
|
||||
'message' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function logRegisterCvrIssue(string $action, array $context): void
|
||||
|
||||
@@ -12,6 +12,7 @@ use classes\n8n;
|
||||
use classes\recaptcha;
|
||||
use classes\response;
|
||||
use classes\router;
|
||||
use classes\slack;
|
||||
use classes\stripe;
|
||||
use classes\weatherapi;
|
||||
use classes\workfeed;
|
||||
@@ -180,6 +181,46 @@ class moduleConfigRoute
|
||||
]
|
||||
);
|
||||
|
||||
/** Slack config > GET */
|
||||
$this->get('/slack/config', function () {
|
||||
global $response;
|
||||
$this->requirePermission('slack_config');
|
||||
$user = (new authentication())->get_user();
|
||||
if ($user) {
|
||||
(new logs_o())->add('slack_config', 'global', 1, $user->id, 'SLACK_CONFIG', 'Successfully fetched Slack config');
|
||||
$response->success(
|
||||
(new slack())->getConfig()->getConfigRequest()
|
||||
);
|
||||
} else {
|
||||
(new logs_o())->add('slack_config', 'global', 1, 0, 'SLACK_CONFIG', 'No user found, or invalid session');
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
},
|
||||
[
|
||||
'slack_config' => 'Get Slack config'
|
||||
]
|
||||
);
|
||||
|
||||
/** Slack config > POST */
|
||||
$this->post('/slack/config', function () {
|
||||
global $response;
|
||||
$this->requirePermission('slack_config');
|
||||
$user = (new authentication())->get_user();
|
||||
if ($user) {
|
||||
(new logs_o())->add('slack_config', 'global', 1, $user->id, 'SLACK_CONFIG', 'Successfully updated Slack config');
|
||||
$response->success(
|
||||
(new slack())->getConfig()->postConfigRequest()
|
||||
);
|
||||
} else {
|
||||
(new logs_o())->add('slack_config', 'global', 1, 0, 'SLACK_CONFIG', 'No user found, or invalid session');
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
},
|
||||
[
|
||||
'slack_config' => 'Update Slack config'
|
||||
]
|
||||
);
|
||||
|
||||
$this->get('/backups/config', function () {
|
||||
global $response;
|
||||
$this->requirePermission('backups_config');
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
it('registers Slack module config endpoints and customer registration webhook config', function (): void {
|
||||
$routeFile = app_path('routes/moduleConfigRoute.php');
|
||||
$routeContent = file_get_contents($routeFile);
|
||||
|
||||
expect($routeContent)->not->toBeFalse()
|
||||
->and($routeContent)->toContain('/slack/config')
|
||||
->and($routeContent)->toContain("requirePermission('slack_config')")
|
||||
->and($routeContent)->toContain("(new slack())->getConfig()->getConfigRequest()")
|
||||
->and($routeContent)->toContain("(new slack())->getConfig()->postConfigRequest()");
|
||||
|
||||
$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'));
|
||||
$slackClassContent = file_get_contents(app_path('classes/slack.php'));
|
||||
$authRouteContent = file_get_contents(app_path('routes/authRoute.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($variableContent)->not->toBeFalse()
|
||||
->and($variableContent)->toContain("'customer_registration_webhook_url'")
|
||||
->and($variableContent)->toContain('Slack webhook URL used for successful customer registration notifications')
|
||||
->and($slackClassContent)->not->toBeFalse()
|
||||
->and($slackClassContent)->toContain('send_customer_registration_notification')
|
||||
->and($slackClassContent)->toContain('format_customer_registration')
|
||||
->and($authRouteContent)->not->toBeFalse()
|
||||
->and($authRouteContent)->toContain("AUTH_REGISTER_CVR_SLACK_NOTIFICATION_FAILED")
|
||||
->and($openApiContent)->not->toBeFalse()
|
||||
->and($openApiContent)->toContain('/slack/config')
|
||||
->and($openApiContent)->toContain('SlackConfigListResponse')
|
||||
->and($openApiContent)->toContain('SlackConfigEntry');
|
||||
});
|
||||
@@ -180,6 +180,26 @@ namespace classes {
|
||||
}
|
||||
}
|
||||
|
||||
class slack
|
||||
{
|
||||
public static array $customer_registration_notifications = [];
|
||||
|
||||
public static function reset(): void
|
||||
{
|
||||
self::$customer_registration_notifications = [];
|
||||
}
|
||||
|
||||
public function send_customer_registration_notification($customer_number): self
|
||||
{
|
||||
self::$customer_registration_notifications[] = [
|
||||
'customer_number' => (int)$customer_number,
|
||||
];
|
||||
\objects\users_o::$interaction_log[] = 'slack-customer-registration:' . (int)$customer_number;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
class authentication
|
||||
{
|
||||
public function get_plate_scanner(): bool
|
||||
@@ -383,6 +403,7 @@ namespace {
|
||||
assert_true(count(\classes\economic::$create_calls) === 0, 'Fresh create must not run for local duplicates.');
|
||||
assert_true(count(\classes\email::$sent) === 0, 'Duplicate registrations must not send welcome emails.');
|
||||
assert_true(count(\classes\email::$superuser_notifications) === 0, 'Duplicate registrations must not send superuser notifications.');
|
||||
assert_true(count(\classes\slack::$customer_registration_notifications) === 0, 'Duplicate registrations must not send Slack customer registration notifications.');
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -410,6 +431,8 @@ namespace {
|
||||
assert_true(\classes\email::$sent[1]['email'] === 'test@test.com', 'Recovery should notify the invoice email.');
|
||||
assert_true(count(\classes\email::$superuser_notifications) === 1, 'Recovery must notify opted-in superusers once.');
|
||||
assert_true(\classes\email::$superuser_notifications[0]['customer_number'] === 12345678, 'Recovery superuser notification must use the recovered customer number.');
|
||||
assert_true(count(\classes\slack::$customer_registration_notifications) === 1, 'Recovery must notify Slack once.');
|
||||
assert_true(\classes\slack::$customer_registration_notifications[0]['customer_number'] === 12345678, 'Recovery Slack notification must use the recovered customer number.');
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -429,6 +452,7 @@ namespace {
|
||||
'assert' => static function (): void {
|
||||
assert_true(count(\classes\email::$sent) === 0, 'Duplicate recovery attempts must not send welcome emails.');
|
||||
assert_true(count(\classes\email::$superuser_notifications) === 0, 'Duplicate recovery attempts must not send superuser notifications.');
|
||||
assert_true(count(\classes\slack::$customer_registration_notifications) === 0, 'Duplicate recovery attempts must not send Slack customer registration notifications.');
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -448,6 +472,7 @@ namespace {
|
||||
assert_true(count(\classes\economic::$create_calls) === 0, 'Conflict on existing mismatched customer must not trigger create.');
|
||||
assert_true(count(\classes\email::$sent) === 0, 'Conflict on existing mismatched customer must not send welcome emails.');
|
||||
assert_true(count(\classes\email::$superuser_notifications) === 0, 'Conflict on existing mismatched customer must not send superuser notifications.');
|
||||
assert_true(count(\classes\slack::$customer_registration_notifications) === 0, 'Conflict on existing mismatched customer must not send Slack customer registration notifications.');
|
||||
assert_true(count(\objects\logs_o::$entries) === 1, 'Conflict should be logged for manual cleanup.');
|
||||
},
|
||||
],
|
||||
@@ -480,6 +505,8 @@ namespace {
|
||||
assert_true(\classes\email::$sent[0]['email'] === 'jm@truckwash.dk', 'Fresh registration should notify the internal welcome recipient first.');
|
||||
assert_true(count(\classes\email::$superuser_notifications) === 1, 'Fresh registration must notify opted-in superusers once.');
|
||||
assert_true(\classes\email::$superuser_notifications[0]['customer_number'] === 12345678, 'Fresh registration superuser notification must use the created customer number.');
|
||||
assert_true(count(\classes\slack::$customer_registration_notifications) === 1, 'Fresh registration must notify Slack once.');
|
||||
assert_true(\classes\slack::$customer_registration_notifications[0]['customer_number'] === 12345678, 'Fresh registration Slack notification must use the created customer number.');
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -499,6 +526,7 @@ namespace {
|
||||
assert_true(count(\objects\users_o::$interaction_log) === 0, 'Create mismatch must not bootstrap the wrong local customer.');
|
||||
assert_true(count(\classes\email::$sent) === 0, 'Create mismatch must not send welcome emails.');
|
||||
assert_true(count(\classes\email::$superuser_notifications) === 0, 'Create mismatch must not send superuser notifications.');
|
||||
assert_true(count(\classes\slack::$customer_registration_notifications) === 0, 'Create mismatch must not send Slack customer registration notifications.');
|
||||
assert_true(count(\objects\logs_o::$entries) === 1, 'Create mismatch should be logged for manual cleanup.');
|
||||
},
|
||||
],
|
||||
@@ -510,6 +538,7 @@ namespace {
|
||||
\classes\recaptcha::$mock_valid = true;
|
||||
\classes\economic::reset();
|
||||
\classes\email::reset();
|
||||
\classes\slack::reset();
|
||||
\classes\virkdata::$mock_name = 'Mock Company';
|
||||
\classes\virkdata::$mock_address = 'Demo Street 1';
|
||||
\classes\virkdata::$mock_zipcode = 2630;
|
||||
|
||||
Reference in New Issue
Block a user