diff --git a/services/nginx/app/classes/response.php b/services/nginx/app/classes/response.php index 4966b89d..c845f26d 100644 --- a/services/nginx/app/classes/response.php +++ b/services/nginx/app/classes/response.php @@ -40,7 +40,7 @@ class response implements response_i } } // If the debug mode is enabled, add the debug data to the response - if ($DEBUG) { + if ($DEBUG && false) { $this->add_include('debug', [ 'memory' => memory_get_usage(), 'time' => microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'], diff --git a/services/nginx/app/cli.php b/services/nginx/app/cli.php index 95571700..e6adefed 100644 --- a/services/nginx/app/cli.php +++ b/services/nginx/app/cli.php @@ -25,9 +25,9 @@ if (php_sapi_name() === 'cli') { // Set the max execution time to 0 // This is used to prevent the script from timing out // when running for a long time -set_time_limit(0); -// Set the memory limit to unlimited -ini_set('memory_limit', '-1'); +set_time_limit(10 * 60); // 10 minutes +// Set the memory limit to 16 GB +ini_set('memory_limit', '16G'); // If the first argument is 'run', switch to the second argument diff --git a/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ht_dysebom.php b/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ht_dysebom.php new file mode 100644 index 00000000..02664c3d --- /dev/null +++ b/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ht_dysebom.php @@ -0,0 +1,26 @@ +setup('ht_dysebom'); + } + + + /** + * @inheritDoc + * @throws Exception + */ + protected function parse(xlvask_wash_item $wash_item, xlvask_usage_log $xlvask_usage_log): products_o + { + return (new products_o())->select(64); // Other irrelevant product ID + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ingen_b_rster_foran_no_brush_wash_front.php b/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ingen_b_rster_foran_no_brush_wash_front.php new file mode 100644 index 00000000..e4b1bffd --- /dev/null +++ b/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ingen_b_rster_foran_no_brush_wash_front.php @@ -0,0 +1,26 @@ +setup('ingen_b_rster_foran_no_brush_wash_front'); + } + + + /** + * @inheritDoc + * @throws Exception + */ + protected function parse(xlvask_wash_item $wash_item, xlvask_usage_log $xlvask_usage_log): products_o + { + return (new products_o())->select(64); // Other irrelevant product ID + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/xlvask/helpers/xlvask_tasks.php b/services/nginx/app/modules/xlvask/helpers/xlvask_tasks.php index 32cac9ed..aa2b1b46 100644 --- a/services/nginx/app/modules/xlvask/helpers/xlvask_tasks.php +++ b/services/nginx/app/modules/xlvask/helpers/xlvask_tasks.php @@ -41,7 +41,6 @@ class xlvask_tasks }; } - /** * Run the synchronize users task * This task fetches the users from XL Vask, and synchronizes them with this system. @@ -187,8 +186,10 @@ class xlvask_tasks * @return array|null * @throws Exception */ - public function runSyncUsage(): array|null + public function runSyncUsage(string $dateFrom = null, string $dateTo = null): array|null { + // TODO: Remove this, this is just for testing purposes + return null; // Define the XL Vask object $xlvask = new \classes\xlvask(); // Require the module to be enabled @@ -197,10 +198,29 @@ class xlvask_tasks if (!$xlvask->config->synchronization_enabled->isTrue()) { throw new Exception('XL Vask synchronization is not enabled.'); } + // TODO: Remove this, this is just for testing purposes: + $dateFrom = "2025-06-01 00:00:00"; + $dateTo = "2025-07-01 00:00:00"; // Set the date from which to fetch the usage data // Since this is a cron task, we will fetch the usage data from the last 24 hours $minutes = 24 * 60; // 24 hours in minutes - $dateFrom = date('Y-m-d\TH:i:s.000', strtotime('-' . $minutes . ' minutes')); + // TODO: Remove this, this is just for testing purposes: + $minutes = $minutes * 20; + // If dateFrom is not set, set it to 24 hours ago + if (empty($dateFrom)) { + $dateFrom = date('Y-m-d\TH:i:s.000', strtotime('-' . $minutes . ' minutes')); + } else { + // If dateFrom is set, make sure it is in the correct format + $dateFrom = date('Y-m-d\TH:i:s.000', strtotime($dateFrom)); + } + // If dateTo is not set, set it to now + if (empty($dateTo)) { + $dateTo = date('Y-m-d\TH:i:s.000'); // Current time in ISO 8601 format + } else { + // If dateTo is set, make sure it is in the correct format + $dateTo = date('Y-m-d\TH:i:s.000', strtotime($dateTo)); + } + echo 'Fetching usage data from: ' . $dateFrom . ' to: ' . $dateTo . PHP_EOL; //echo $dateFrom; (E.g. 2025-06-11T12:13:16.000) @@ -210,6 +230,139 @@ class xlvask_tasks // Get the cached customers $customers = $xlvask->getCache()->getAllCachedCustomers(); + // Get all the usage logs from XL Vask + $debug_usage_logs = $xlvask->getUsageLog( + $dateFrom, + null, // regNr + null, // vehicleId + null // customerId + ); + + $debug_usage_logs = self::formatUsageLogs($debug_usage_logs); + + // Filter out the logs that are after the dateTo + $debug_usage_logs = array_filter($debug_usage_logs, function ($log) use ($dateTo) { + /** @var xlvask_usage_log $log */ + return strtotime($log->getFormattedDate()) <= strtotime($dateTo); + }); + echo 'Found ' . count($debug_usage_logs) . ' usage logs in the date range from ' . $dateFrom . ' to ' . $dateTo . PHP_EOL; + echo "Prepaid: " . count(array_filter($debug_usage_logs, function ($log) { + /** @var xlvask_usage_log $log */ + return $log->isPrepaid(); + })) . PHP_EOL; + echo "Finished: " . count(array_filter($debug_usage_logs, function ($log) { + /** @var xlvask_usage_log $log */ + return $log->isCompleted(); + })) . PHP_EOL; + echo 'Not finished (skipped): ' . count(array_filter($debug_usage_logs, function ($log) { + /** @var xlvask_usage_log $log */ + return !$log->isCompleted(); + })) . PHP_EOL; + echo 'Billed to default customer (skipped): ' . count(array_filter($debug_usage_logs, function ($log) { + /** @var xlvask_usage_log $log */ + return $log->hasDefaultCustomer(); + })) . PHP_EOL; + echo 'Unique customers: ' . count(array_unique(array_map(function ($log) { + /** @var xlvask_usage_log $log */ + return $log->CustomerId; + }, $debug_usage_logs))) . PHP_EOL; + $linked_orders = (new orders_o())->getFieldsWhere([ + 'wash_id' => array_map(function ($log) { + /** @var xlvask_usage_log $log */ + return $log->WashId; + }, $debug_usage_logs), + 'deleted_at' => null, + ], [ + 'id', + 'wash_id', + ]); + echo "Linked to orders: " . count($linked_orders) . PHP_EOL; + $eligible_for_automatic_continuance = array_filter($debug_usage_logs, function ($log) { + /** @var xlvask_usage_log $log */ + return $log->isEligibleForAutomaticContinuance(true); + }); + $eligible_for_automatic_continuance_without_prepaid = array_filter($debug_usage_logs, function ($log) { + /** @var xlvask_usage_log $log */ + return $log->isEligibleForAutomaticContinuance(false); + }); + echo "Eligible for automatic continuance: " . count($eligible_for_automatic_continuance) . " (" . count($eligible_for_automatic_continuance_without_prepaid) . " without prepaid)" . PHP_EOL; + // Print a list of customers that do not have an external ID + $customers_without_external_id = array_filter($debug_usage_logs, function ($log) { + /** @var xlvask_usage_log $log */ + return !$log->hasExternalId() && !$log->hasDefaultCustomer(); + }); + echo "Without billable customer: " . count(array_filter($debug_usage_logs, function ($log) { + /** @var xlvask_usage_log $log */ + return !$log->hasBillableCustomer(); + })) . " (" . count(array_filter($debug_usage_logs, function ($log) { + /** @var xlvask_usage_log $log */ + return !$log->hasBillableCustomer() && !$log->hasDefaultCustomer() && !$log->hasExternalId(); + })) . " without external ID)" . PHP_EOL; + + foreach ( self::sortLogsByDate($customers_without_external_id) as $customer ) { + /** @var xlvask_usage_log $customer */ + echo ' - ' . $customer->getFormattedDate() . ' - ' . $customer->Customer . ' - ' . $customer->getDepartment()->name->value() . ', ' . $customer->getLane() . ' - ' . $customer->getTotalPrice() . ' DKK' . ' ( ' . ($customer->isCompleted() ? 'Finished' : 'Not finished') . ', ' . ($customer->isPrepaid() ? 'Prepaid' : 'Not prepaid') . ' )' . PHP_EOL; + } + + // Get all unique external IDs from the customers + $unique_external_customer_ids = array_unique(array_map(function ($customer) { + /** @var xlvask_usage_log $customer */ + return $customer->CustomerId; + }, $debug_usage_logs)); + // Remove the external IDs that are not numeric or empty + $unique_external_customer_ids = array_filter($unique_external_customer_ids, function ($id) { + return !empty($id) && is_numeric($id); + }); + echo "Unique external customer IDs: " . count($unique_external_customer_ids) . PHP_EOL; + foreach ( $unique_external_customer_ids as $customer_id ) { + $doesCustomerExistInArray = array_filter($customers, function ($customer) use ($customer_id) { + /** @var xlvask_customer $customer */ + return $customer->externId === $customer_id; + }); + $tmp_does_customer_exist = count($doesCustomerExistInArray) > 0; + if (!$tmp_does_customer_exist) { + echo ' - ' . $customer_id . ' does not exist in the system (skipped)' . PHP_EOL; + continue; // Skip customers that do not exist in the system + } + $tmp_customer_object = reset($doesCustomerExistInArray); // Get the first customer object that matches the external ID + /** @var xlvask_customer $tmp_customer_object */ + echo ' - ' . $tmp_customer_object->name . ' (' . $tmp_customer_object->externId . ') - ' . $tmp_customer_object->customerId . PHP_EOL; + echo " - # Washes: " . count(array_filter($debug_usage_logs, function ($log) use ($tmp_customer_object) { + /** @var xlvask_usage_log $log */ + return $log->CustomerId === $tmp_customer_object->externId; + })) . PHP_EOL; + // Echo the eligible for automatic continuance logs + foreach ( self::sortLogsByDate(array_filter($debug_usage_logs, function ($log) use ($tmp_customer_object) { + /** @var xlvask_usage_log $log */ + return $log->CustomerId === $tmp_customer_object->externId && $log->isEligibleForAutomaticContinuance(false); + })) as $log ) { + /** @var xlvask_usage_log $log */ + $tmp_linked_to_order = array_filter($linked_orders, function ($order) use ($log) { + /** @var array $order */ + return $order['wash_id'] === $log->WashId; + }); + echo ' - - ' . $log->getFormattedDate() . ' - ' . $log->getDepartment()->name->value() . ', ' . $log->getLane() . ' - ' . $log->getTotalPrice() . ' DKK' . ' ( ' . ($log->isCompleted() ? 'Finished' : 'Not finished') . ', ' . ($log->isPrepaid() ? 'Prepaid' : 'Not prepaid') . ', ' . ($tmp_linked_to_order ? 'Linked to order' : 'Not linked to order') . ' )' . PHP_EOL; + } + // Echo the ineligible for automatic continuance logs + echo " - # Not eligible for automatic continuance: " . count(array_filter($debug_usage_logs, function ($log) use ($tmp_customer_object) { + /** @var xlvask_usage_log $log */ + return $log->CustomerId === $tmp_customer_object->externId && !$log->isEligibleForAutomaticContinuance(false); + })) . PHP_EOL; + foreach ( self::sortLogsByDate(array_filter($debug_usage_logs, function ($log) use ($tmp_customer_object) { + /** @var xlvask_usage_log $log */ + return $log->CustomerId === $tmp_customer_object->externId && !$log->isEligibleForAutomaticContinuance(false); + })) as $log ) { + /** @var xlvask_usage_log $log */ + echo ' - - ' . $log->getFormattedDate() . ' - ' . $log->getDepartment()->name->value() . ', ' . $log->getLane() . ' - ' . $log->getTotalPrice() . ' DKK' . ' ( ' . ($log->isCompleted() ? 'Finished' : 'Not finished') . ', ' . ($log->isPrepaid() ? 'Prepaid' : 'Not prepaid') . ' )' . PHP_EOL; + } + } + echo 'Customers:'; + print_r($customers); + echo 'Eligible for automatic continuance:'; + print_r($eligible_for_automatic_continuance); + exit; + + // Loop through the customers and check if there's any new usage data foreach ( $customers as $customer ) { /** @var xlvask_customer $customer */ @@ -330,6 +483,17 @@ class xlvask_tasks }, $getUsageLog); } + private static function sortLogsByDate(array $logs): array + { + // Sort the logs by date + usort($logs, function ($a, $b) { + /** @var xlvask_usage_log $a */ + /** @var xlvask_usage_log $b */ + return strtotime($a->getFormattedDate()) - strtotime($b->getFormattedDate()); + }); + return $logs; // Return the sorted logs + } + /** * @throws Exception If the log is not completed, or if the customer does not have an externId, or if the customer does not exist in this system. */ diff --git a/services/nginx/app/modules/xlvask/helpers/xlvask_usage_log.php b/services/nginx/app/modules/xlvask/helpers/xlvask_usage_log.php index 8d453dd7..0a17c79d 100644 --- a/services/nginx/app/modules/xlvask/helpers/xlvask_usage_log.php +++ b/services/nginx/app/modules/xlvask/helpers/xlvask_usage_log.php @@ -393,41 +393,6 @@ class xlvask_usage_log extends xlvask_helper return ''; } - /** - * Get the total price of the wash. - * This method calculates the total price of the wash by summing up the prices of all wash items. - * @return float The total price of the wash. - * @throws Exception If any of the wash items do not have a valid price. - */ - public function getTotalPrice(): float - { - // Calculate the total price of the wash by summing up the prices of all wash items - $totalPrice = 0.0; - foreach ( $this->WashItems as $item ) { - /** @var xlvask_wash_item $item */ - if (isset($item->PriceIncVat) && is_numeric($item->PriceIncVat)) { - // The reason why we use the PriceIncVat is that it is the final price, and the price without VAT is not always available / correct. - $totalPrice += ((float)$item->PriceIncVat - (float)$item->Vat); - } else { - //print_r($item); - throw new Exception("Invalid price for wash item: " . json_encode($item)); - } - } - return $totalPrice; - } - - /** - * Check if the wash is completed. - * @note This method checks the FinishStatus property to determine if the wash is completed. - * * A wash is considered completed if the FinishStatus is set to 1. - * @return bool - */ - public function isCompleted(): bool - { - // Check if the wash is completed based on the FinishStatus - return $this->FinishStatus === 1; // Assuming 1 indicates a completed wash - } - /** * Get the primary wash item from the usage log. * This method returns the first wash item in the WashItems array as the primary item. @@ -559,4 +524,84 @@ class xlvask_usage_log extends xlvask_helper // Return the start time as a timestamp return date('Y-m-d H:i:s', strtotime($this->StartTime)); } + + public function isEligibleForAutomaticContinuance(bool $allowPrepaid = false): bool + { + // Check if the wash is eligible for automatic continuance + // This can be based on various criteria, such as the finish status or other business rules + return + $this->isCompleted() && // Ensure the wash is completed + $this->hasBillableCustomer() && // Ensure the wash has a billable customer + ( + $this->getTotalPrice() > 0 || $allowPrepaid + // Allow washes with a total price greater than 0 or allow prepaid washes if $allowPrepaid is true + ) && + ( + !$this->isPrepaid() || $allowPrepaid + ); // Allow prepaid washes if $allowPrepaid is true) + } + + /** + * Check if the wash is completed. + * @note This method checks the FinishStatus property to determine if the wash is completed. + * * A wash is considered completed if the FinishStatus is set to 1. + * @return bool + */ + public function isCompleted(): bool + { + // Check if the wash is completed based on the FinishStatus + return $this->FinishStatus === 1; // Assuming 1 indicates a completed wash + } + + public function hasBillableCustomer(): bool + { + // Check if the wash has a non-default customer with a valid external ID + return !$this->hasDefaultCustomer() && $this->hasExternalId(); + } + + public function hasDefaultCustomer(): bool + { + // Check if the customer is a default customer (e.g., "Unknown" or similar) + $default_customers = [ + 'CTW', + 'Personalebiler Truckwash ', // The space at the end is intentional + 'XL Gønge Transport ApS', // This is a test customer + ]; + return in_array($this->Customer, $default_customers, true); + } + + public function hasExternalId(): bool + { + // Check if the wash has an external ID (e.g., CustomerId) + return !empty($this->CustomerId) && $this->CustomerId !== '0'; + } + + /** + * Get the total price of the wash. + * This method calculates the total price of the wash by summing up the prices of all wash items. + * @return float The total price of the wash. + * @throws Exception If any of the wash items do not have a valid price. + */ + public function getTotalPrice(): float + { + // Calculate the total price of the wash by summing up the prices of all wash items + $totalPrice = 0.0; + foreach ( $this->WashItems as $item ) { + /** @var xlvask_wash_item $item */ + if (isset($item->PriceIncVat) && is_numeric($item->PriceIncVat)) { + // The reason why we use the PriceIncVat is that it is the final price, and the price without VAT is not always available / correct. + $totalPrice += ((float)$item->PriceIncVat - (float)$item->Vat); + } else { + //print_r($item); + throw new Exception("Invalid price for wash item: " . json_encode($item)); + } + } + return $totalPrice; + } + + public function isPrepaid(): bool + { + // Check if the wash is prepaid + return !empty($this->Prepaid) && $this->Prepaid === 1; // Assuming 1 indicates a prepaid wash + } } \ No newline at end of file diff --git a/services/nginx/app/modules/xlvask/xlvask_helpers.php b/services/nginx/app/modules/xlvask/xlvask_helpers.php index 01300ab8..1cfabb1f 100644 --- a/services/nginx/app/modules/xlvask/xlvask_helpers.php +++ b/services/nginx/app/modules/xlvask/xlvask_helpers.php @@ -53,6 +53,8 @@ require_once WD . '/modules/xlvask/helpers/xlvask_parser_h_nger.php'; require_once WD . '/modules/xlvask/helpers/xlvask_parser_ikke_ht_dysebom_mellom_bil_trailer.php'; require_once WD . '/modules/xlvask/helpers/xlvask_parser_ikke_b_rster_mellom_bil_og_trailer.php'; require_once WD . '/modules/xlvask/helpers/xlvask_parser_s_be_dysebom_tak.php'; +require_once WD . '/modules/xlvask/helpers/xlvask_parser_ingen_b_rster_foran_no_brush_wash_front.php'; +require_once WD . '/modules/xlvask/helpers/xlvask_parser_ht_dysebom.php'; use helpers\xlvask_cache; use helpers\xlvask_create_customer; diff --git a/services/nginx/nginx.conf b/services/nginx/nginx.conf index 263c24e2..d9c62a6b 100644 --- a/services/nginx/nginx.conf +++ b/services/nginx/nginx.conf @@ -7,6 +7,9 @@ events { } http { + # Add rate limiting to prevent abuse + limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; + limit_req_status 429; # Include additional configuration files include /etc/nginx/mime.types; default_type application/octet-stream; @@ -71,7 +74,7 @@ http { include fastcgi_params; fastcgi_pass php:9000; fastcgi_index index.php; - fastcgi_read_timeout 600; + fastcgi_read_timeout 60; fastcgi_param SCRIPT_FILENAME index.php; } @@ -119,4 +122,6 @@ http { # Additional server blocks can be added here + # Restrict access to the server, if the + } \ No newline at end of file