## Summary SENERE 14 / **TRU-106**: only PICKUP bookings should post a new-booking notification to the department Slack channel. Drop-off bookings (pickup_bool = 0) are now silently filtered out. SMS and email delivery paths are unaffected. ## Change Minimal, non-refactor: - New `classes\slack::send_new_booking_notification(...)` that wraps `format_new_booking` + `send_webhook_message` and short-circuits when `pickup_bool === false`. Returns bool (sent vs. filtered). - Two call sites in `objects/bookings_o.php` (`addOrUpdate` + `notifyNewBooking`) updated to use the new wrapper. Same arguments, no other behavior changes. - Other Slack notification types (customer registration, internal department goal progress, unfulfilled bookings) are deliberately untouched. ## Tests New Pest test `tests/Unit/Slack/SlackNewBookingPickupFilterTest.php`: - pickup -> notification sent (one webhook call, message contains the booking id) - drop-off -> no notification, no log entry - no webhook configured -> no notification - webhook URL never appears in log payload PHP isn't installed in this sandbox; the test was code-reviewed against the existing `SlackCustomerRegistrationWebhookTest` pattern (subclass + it()/expect()). Please run `./vendor/bin/phpunit tests/Unit/Slack/SlackNewBookingPickupFilterTest.php` on CI / locally to confirm. ## Risk Low. Adds an early-return filter inside a new method; existing call sites already pass pickup_bool as a boolean. No DB schema change, no new dependency, no config file change. Closes TRU-106 --------- Co-authored-by: backend-subagent <agent@openclaw.local> Co-authored-by: Jeppe B <jeppe@copenhagentruckwash.io> Co-authored-by: OpenClaw Bugfix <bugfix@openclaw.local> Co-authored-by: Truck Wash Bugfix Bot <bugfix@truckwash.local>
395 lines
14 KiB
PHP
395 lines
14 KiB
PHP
<?php
|
|
|
|
namespace classes;
|
|
|
|
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
|
|
* @throws \Exception
|
|
*/
|
|
public function send_department_booking_notification(int $department_id, $message): self
|
|
{
|
|
// Get the departments webhook
|
|
$webhook = static::get_department_webhook($department_id);
|
|
// Check if the webhook is empty
|
|
if (empty($webhook)) {
|
|
throw new \Exception('Department webhook is empty');
|
|
}
|
|
// Send the notification to the department
|
|
self::add_log(static::send_webhook_message($message, $webhook));
|
|
return $this;
|
|
}
|
|
|
|
protected function get_department_webhook(int $department_id): string|null
|
|
{
|
|
// Check if the department webhook is cached
|
|
$webhook = redis->get_department_webhook($department_id);
|
|
// If the department webhook is not cached, get it from the database
|
|
if ($webhook === null) {
|
|
$department = (new departments_o())->select((int)$department_id);
|
|
$webhook = $department->slack_webhook->value();
|
|
// Cache the department webhook (If it is not empty or null)
|
|
if (!empty($webhook)) {
|
|
redis->cache_department_webhook($department_id, $webhook);
|
|
}
|
|
}
|
|
return redis->get_department_webhook($department_id);
|
|
}
|
|
|
|
/**
|
|
* Send a message to a slack webhook
|
|
* @param string $message
|
|
* @param string $webhook
|
|
* @return string The response from the webhook, unparsed
|
|
*/
|
|
public function send_webhook_message(string $message, string $webhook): string
|
|
{
|
|
global $DEBUG;
|
|
try {
|
|
// Initialize Guzzle client
|
|
$client = new Client();
|
|
|
|
// Prepare the payload for the webhook
|
|
$payload = [
|
|
'text' => $message
|
|
];
|
|
|
|
// Send POST request to the webhook
|
|
$response = $client->post($webhook, [
|
|
'headers' => ['Content-Type' => 'application/json'],
|
|
'body' => json_encode($payload),
|
|
'verify' => ($DEBUG === false) // Disable SSL verification (Only in development)
|
|
]);
|
|
|
|
// Return success message
|
|
return 'Message sent successfully. Response: ' . $response->getBody();
|
|
} catch (\Exception $e) {
|
|
// Handle any errors that occur
|
|
return 'Failed to send message: ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
public function format_new_booking($id, $customer_number, string $wash_type, string $contact_email, string $reference_number, string $regNrTraekker, string $regNrTrailer, string $washCertificateEmail, string $date, int $department, $pickup_bool, string $notes, string $washCertificateStatus, string $washCertificateUrl, string $status): string
|
|
{
|
|
// Get the department name
|
|
$department_name = (new departments_o())->getDepartmentName($department);
|
|
// Get the customer name
|
|
$customer_name = (new users_o())->getCustomerName($customer_number);
|
|
// Format the message
|
|
return "*Ny booking oprettet* ( ID: " . $id . " )\n"
|
|
. "Kunde: $customer_name ($customer_number)\n"
|
|
. "Type: $wash_type\n"
|
|
. "Reference nummer: $reference_number\n"
|
|
. "RegNr Traekker: $regNrTraekker\n"
|
|
. "RegNr Trailer: $regNrTrailer\n"
|
|
. "Dato: $date\n"
|
|
. "Hentning: $pickup_bool\n"
|
|
. "Noter: $notes";
|
|
}
|
|
|
|
public function format_unfulfilled_booking($id, $customer_number, string $wash_type, string $contact_email, string $reference_number, string $regNrTraekker, string $regNrTrailer, string $washCertificateEmail, string $date, int $department, $pickup_bool, string $notes, string $washCertificateStatus, string $washCertificateUrl, string $status): string
|
|
{
|
|
// Get the department name
|
|
$department_name = (new departments_o())->getDepartmentName($department);
|
|
// Get the customer name
|
|
$customer_name = (new users_o())->getCustomerName($customer_number);
|
|
// Format the message
|
|
return "Unfulfilled booking from " . $date . "\n"
|
|
. "ID: $id\n"
|
|
. "Customer: $customer_name\n"
|
|
. "Customer number: $customer_number\n"
|
|
. "Wash type: $wash_type\n"
|
|
. "Contact email: $contact_email\n"
|
|
. "Reference number: $reference_number\n"
|
|
. "RegNr Traekker: $regNrTraekker\n"
|
|
. "RegNr Trailer: $regNrTrailer\n"
|
|
. "Wash certificate email: $washCertificateEmail\n"
|
|
. "Date: $date\n"
|
|
. "Department: $department_name\n"
|
|
. "Pickup: $pickup_bool\n"
|
|
. "Notes: $notes\n"
|
|
. "Status: $status";
|
|
}
|
|
|
|
/**
|
|
* Send a new-booking notification to the department's Slack webhook.
|
|
*
|
|
* Filter: only PICKUP bookings trigger a Slack notification. Drop-off
|
|
* bookings (pickup_bool === false) are intentionally silenced per
|
|
* Mikkel's SENERE 14 / TRU-106 request — drop-offs are noise in the
|
|
* channel. Other delivery channels (SMS, email) are unaffected.
|
|
*
|
|
* Returns true if a Slack message was sent, false if it was filtered
|
|
* out (drop-off) or the department has no Slack webhook configured.
|
|
*
|
|
* @throws \Exception If the department lookup or webhook send fails.
|
|
*/
|
|
public function send_new_booking_notification(
|
|
$id,
|
|
$customer_number,
|
|
string $wash_type,
|
|
string $contact_email,
|
|
string $reference_number,
|
|
string $regNrTraekker,
|
|
string $regNrTrailer,
|
|
string $washCertificateEmail,
|
|
string $date,
|
|
int $department,
|
|
bool $pickup_bool,
|
|
string $notes,
|
|
string $washCertificateStatus,
|
|
string $washCertificateUrl,
|
|
string $status
|
|
): bool {
|
|
// TRU-106: drop-off bookings must not post to Slack.
|
|
if (!$pickup_bool) {
|
|
return false;
|
|
}
|
|
|
|
$webhook = static::get_department_webhook($department);
|
|
if (empty($webhook)) {
|
|
return false;
|
|
}
|
|
|
|
$message = static::format_new_booking(
|
|
$id,
|
|
$customer_number,
|
|
$wash_type,
|
|
$contact_email,
|
|
$reference_number,
|
|
$regNrTraekker,
|
|
$regNrTrailer,
|
|
$washCertificateEmail,
|
|
$date,
|
|
$department,
|
|
$pickup_bool,
|
|
$notes,
|
|
$washCertificateStatus,
|
|
$washCertificateUrl,
|
|
$status
|
|
);
|
|
|
|
self::add_log(static::send_webhook_message($message, $webhook));
|
|
return true;
|
|
}
|
|
|
|
public function send_message(string $string, ?string $module = null): void
|
|
{
|
|
global $SLACK_DEFAULT_WEBHOOK;
|
|
// Format the message if a module is provided
|
|
if ($module !== null) {
|
|
$string = "*$module*\n" . $string;
|
|
}
|
|
// 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 = $this->get_customer_registration_webhook_url();
|
|
if ($webhook === '') {
|
|
return $this;
|
|
}
|
|
|
|
self::add_log(self::send_webhook_message(
|
|
$this->format_customer_registration($customer_number),
|
|
$webhook
|
|
));
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Send a sanitized customer-registration test notification to the saved Slack webhook.
|
|
*
|
|
* @return array{configured:bool,sent:bool,message:string}
|
|
*/
|
|
public function test_customer_registration_webhook(): array
|
|
{
|
|
$webhook = $this->get_customer_registration_webhook_url();
|
|
if ($webhook === '') {
|
|
return [
|
|
'configured' => false,
|
|
'sent' => false,
|
|
'message' => 'Slack customer registration webhook URL is not configured.',
|
|
];
|
|
}
|
|
|
|
$result = $this->send_webhook_message(
|
|
$this->format_customer_registration_test(),
|
|
$webhook
|
|
);
|
|
$sent = $this->is_webhook_send_successful($result);
|
|
|
|
self::add_log($sent
|
|
? 'Slack customer registration test webhook sent successfully.'
|
|
: 'Slack customer registration test webhook failed.'
|
|
);
|
|
|
|
return [
|
|
'configured' => true,
|
|
'sent' => $sent,
|
|
'message' => $sent
|
|
? 'Slack test message sent successfully.'
|
|
: 'Slack test message failed.',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Send a sanitized internal department goal progress test notification to the saved Slack webhook.
|
|
*
|
|
* @return array{configured:bool,sent:bool,message:string}
|
|
*/
|
|
public function test_internal_department_goal_progress_webhook(): array
|
|
{
|
|
$webhook = $this->get_internal_department_goal_progress_webhook_url();
|
|
if ($webhook === '') {
|
|
return [
|
|
'configured' => false,
|
|
'sent' => false,
|
|
'message' => 'Slack internal department goal progress webhook URL is not configured.',
|
|
];
|
|
}
|
|
|
|
$result = $this->send_webhook_message(
|
|
$this->format_internal_department_goal_progress_test(),
|
|
$webhook
|
|
);
|
|
$sent = $this->is_webhook_send_successful($result);
|
|
|
|
self::add_log($sent
|
|
? 'Slack internal department goal progress test webhook sent successfully.'
|
|
: 'Slack internal department goal progress test webhook failed.'
|
|
);
|
|
|
|
return [
|
|
'configured' => true,
|
|
'sent' => $sent,
|
|
'message' => $sent
|
|
? 'Slack test message sent successfully.'
|
|
: 'Slack test message failed.',
|
|
];
|
|
}
|
|
|
|
protected function get_customer_registration_webhook_url(): string
|
|
{
|
|
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:');
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
public function format_customer_registration_test(): string
|
|
{
|
|
return "*Truck Wash Slack test*\n"
|
|
. "Customer registration notifications are configured correctly.";
|
|
}
|
|
|
|
public function format_internal_department_goal_progress_test(): string
|
|
{
|
|
return "*Truck Wash Slack test*\n"
|
|
. "Internal department goal progress notifications are configured correctly.";
|
|
}
|
|
}
|