Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76dddad410 |
@@ -1,98 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace classes;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class customer_order_product_policy
|
||||
{
|
||||
public const ONLY_TANKCLEANING_ATTRIBUTE = 'onlyTankCleaning';
|
||||
public const ONLY_TANKCLEANING_MESSAGE = 'Only tankcleaning customers can only have tankcleaning products in their orders.';
|
||||
|
||||
public static function assertOrderAllowsProduct(int $orderId, int $productId): void
|
||||
{
|
||||
$message = self::orderProductViolationMessage($orderId, $productId);
|
||||
if ($message !== null) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
public static function orderProductViolationMessage(int $orderId, int $productId): ?string
|
||||
{
|
||||
$context = self::loadOrderProductContext($orderId, $productId);
|
||||
if ($context === null) {
|
||||
return null;
|
||||
}
|
||||
if ((int)($context['product_id'] ?? 0) < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::onlyTankCleaningViolation((bool)((int)($context['has_only_tank_cleaning'] ?? 0)), $context)
|
||||
? self::ONLY_TANKCLEANING_MESSAGE
|
||||
: null;
|
||||
}
|
||||
|
||||
public static function onlyTankCleaningViolation(bool $customerHasOnlyTankCleaning, array $productRow): bool
|
||||
{
|
||||
return $customerHasOnlyTankCleaning && !self::isTankCleaningProductRow($productRow);
|
||||
}
|
||||
|
||||
public static function isTankCleaningProductRow(array $row): bool
|
||||
{
|
||||
return (int)($row['product_category'] ?? $row['category'] ?? 0) === 5
|
||||
|| self::rowMatchesProductTerms($row, ['tank cleaning', 'tankcleaning', 'tankrens']);
|
||||
}
|
||||
|
||||
private static function loadOrderProductContext(int $orderId, int $productId): ?array
|
||||
{
|
||||
global $db;
|
||||
|
||||
if ($orderId < 1 || $productId < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
o.id AS order_id,
|
||||
o.customer_id AS customer_number,
|
||||
p.id AS product_id,
|
||||
p.name AS product_name,
|
||||
p.category AS product_category,
|
||||
c.name AS category_name,
|
||||
MAX(CASE WHEN ca.attribute = '" . self::ONLY_TANKCLEANING_ATTRIBUTE . "' THEN 1 ELSE 0 END) AS has_only_tank_cleaning
|
||||
FROM orders o
|
||||
LEFT JOIN products p ON p.id = {$productId}
|
||||
LEFT JOIN categories c ON c.id = p.category
|
||||
LEFT JOIN users u ON u.customer_number = o.customer_id
|
||||
LEFT JOIN customer_attributes ca ON ca.user_id = u.id
|
||||
AND ca.attribute = '" . self::ONLY_TANKCLEANING_ATTRIBUTE . "'
|
||||
WHERE o.id = {$orderId}
|
||||
GROUP BY o.id, o.customer_id, p.id, p.name, p.category, c.name
|
||||
LIMIT 1
|
||||
";
|
||||
|
||||
$result = $db->query($sql);
|
||||
if (!$result || $result->num_rows < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $result->fetch_assoc();
|
||||
return is_array($row) ? $row : null;
|
||||
}
|
||||
|
||||
private static function rowMatchesProductTerms(array $row, array $terms): bool
|
||||
{
|
||||
$haystack = strtolower(trim(
|
||||
(string)($row['product_name'] ?? $row['name'] ?? '') . ' ' .
|
||||
(string)($row['category_name'] ?? '')
|
||||
));
|
||||
|
||||
foreach ($terms as $term) {
|
||||
if ($term !== '' && str_contains($haystack, strtolower($term))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace classes;
|
||||
|
||||
use objects\orders_o;
|
||||
use objects\products_o;
|
||||
use objects\users_o;
|
||||
|
||||
class customer_product_rule_service
|
||||
{
|
||||
public const BLOCK_MESSAGE = 'This product is not allowed for the selected customer';
|
||||
|
||||
private const ADDON_CATEGORY_ID = 4;
|
||||
private const TANK_CLEANING_CATEGORY_ID = 5;
|
||||
|
||||
/**
|
||||
* @return array{rule:string,message:string}|null
|
||||
*/
|
||||
public function firstViolationForOrderItem(int $orderId, int $productId, ?int $relatedItemId): ?array
|
||||
{
|
||||
$order = (new orders_o())->getOrderById($orderId);
|
||||
if (!$order->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$product = (new products_o())->getProductById($productId);
|
||||
if (!$product->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$customer = (new users_o())->getUserByCustomerNumber((int)$order->customer_id->value());
|
||||
if (!$customer->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$categoryId = (int)$product->category->value();
|
||||
$categoryName = $this->categoryName($categoryId);
|
||||
$searchableProduct = $this->searchableProductText($product, $categoryName);
|
||||
$isTankCleaningProduct = $this->isTankCleaningProduct($categoryId, $searchableProduct);
|
||||
|
||||
if ($customer->doesUserHaveAttribute('restrictAdditionalServices')
|
||||
&& $this->isAdditionalServiceProduct($orderId, $relatedItemId, $categoryId, $searchableProduct)) {
|
||||
return $this->violation('restrictAdditionalServices');
|
||||
}
|
||||
|
||||
if ($customer->doesUserHaveAttribute('restrictTankCleaning') && $isTankCleaningProduct) {
|
||||
return $this->violation('restrictTankCleaning');
|
||||
}
|
||||
|
||||
if ($customer->doesUserHaveAttribute('onlyTankCleaning') && !$isTankCleaningProduct) {
|
||||
return $this->violation('onlyTankCleaning');
|
||||
}
|
||||
|
||||
if ($customer->doesUserHaveAttribute('restrictSpotFree')
|
||||
&& $this->containsAny($searchableProduct, ['spot free', 'spotfree'])) {
|
||||
return $this->violation('restrictSpotFree');
|
||||
}
|
||||
|
||||
if ($customer->doesUserHaveAttribute('restrictInteriorCleaning')
|
||||
&& $this->containsAny($searchableProduct, ['interior', 'indvendig'])) {
|
||||
return $this->violation('restrictInteriorCleaning');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{rule:string,message:string}
|
||||
*/
|
||||
private function violation(string $rule): array
|
||||
{
|
||||
return [
|
||||
'rule' => $rule,
|
||||
'message' => self::BLOCK_MESSAGE,
|
||||
];
|
||||
}
|
||||
|
||||
private function isAdditionalServiceProduct(int $orderId, ?int $relatedItemId, int $categoryId, string $searchableProduct): bool
|
||||
{
|
||||
if ($relatedItemId !== null && $relatedItemId > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($categoryId === self::ADDON_CATEGORY_ID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->containsAny($searchableProduct, ['add-on', 'add on', 'addon', 'tilvalg'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->countStandaloneOrderItems($orderId) > 0;
|
||||
}
|
||||
|
||||
private function isTankCleaningProduct(int $categoryId, string $searchableProduct): bool
|
||||
{
|
||||
if ($categoryId === self::TANK_CLEANING_CATEGORY_ID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->containsAny($searchableProduct, ['tank cleaning', 'tankcleaning', 'tankrens', 'tank rens']);
|
||||
}
|
||||
|
||||
private function searchableProductText(products_o $product, string $categoryName): string
|
||||
{
|
||||
return strtolower(trim((string)$product->name->value() . ' ' . $categoryName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $terms
|
||||
*/
|
||||
private function containsAny(string $value, array $terms): bool
|
||||
{
|
||||
foreach ($terms as $term) {
|
||||
if ($term !== '' && str_contains($value, $term)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function categoryName(int $categoryId): string
|
||||
{
|
||||
global $db;
|
||||
|
||||
if ($categoryId <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$result = $db->query('SELECT name FROM categories WHERE id = ' . $categoryId . ' LIMIT 1');
|
||||
if (!$result || $result->num_rows === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$row = $result->fetch_assoc();
|
||||
return strtolower((string)($row['name'] ?? ''));
|
||||
}
|
||||
|
||||
private function countStandaloneOrderItems(int $orderId): int
|
||||
{
|
||||
global $db;
|
||||
|
||||
$result = $db->query(
|
||||
'SELECT COUNT(*) AS item_count
|
||||
FROM order_items
|
||||
WHERE order_id = ' . $orderId . '
|
||||
AND deleted_at IS NULL
|
||||
AND (related_item_id IS NULL OR related_item_id = 0)'
|
||||
);
|
||||
if (!$result) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$row = $result->fetch_assoc();
|
||||
return (int)($row['item_count'] ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -240,13 +240,7 @@ class economic_transfer_executor
|
||||
'Queued transfer processed successfully for collected invoice #' . $collected_invoice_id
|
||||
);
|
||||
|
||||
$result = $collected_order_invoices->asArray();
|
||||
$transfer_metrics = $collected_order_invoices->getLastEconomicTransferMetrics();
|
||||
if ($transfer_metrics !== null) {
|
||||
$result['economic_transfer_metrics'] = $transfer_metrics;
|
||||
}
|
||||
|
||||
return $result;
|
||||
return $collected_order_invoices->asArray();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2036,7 +2036,8 @@ class invoice_period_flag_service
|
||||
|
||||
private function rowIsTankCleaningProduct(array $row): bool
|
||||
{
|
||||
return customer_order_product_policy::isTankCleaningProductRow($row);
|
||||
return (int)($row['product_category'] ?? 0) === 5
|
||||
|| $this->rowMatchesProductTerms($row, ['tank cleaning', 'tankcleaning', 'tankrens']);
|
||||
}
|
||||
|
||||
private function isIncludedOrderItem(array $row): bool
|
||||
|
||||
@@ -112,124 +112,6 @@ class limited_backoffice_service
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, array{group:string,capability:string}>
|
||||
*/
|
||||
private const ROLE_PERMISSION_CAPABILITIES = [
|
||||
'user' => [
|
||||
'group' => 'account',
|
||||
'capability' => 'sign_in',
|
||||
],
|
||||
'permissions_list_own' => [
|
||||
'group' => 'account',
|
||||
'capability' => 'view_own_permissions',
|
||||
],
|
||||
'list_orders' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'view_orders',
|
||||
],
|
||||
'add_order' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'create_orders',
|
||||
],
|
||||
'edit_order' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'edit_orders',
|
||||
],
|
||||
'delete_order' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'delete_orders',
|
||||
],
|
||||
'list_order_items' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'view_order_items',
|
||||
],
|
||||
'add_order_items' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'create_order_items',
|
||||
],
|
||||
'edit_order_items' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'update_order_lines',
|
||||
],
|
||||
'delete_order_items' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'remove_order_lines',
|
||||
],
|
||||
'charge_order' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'charge_orders',
|
||||
],
|
||||
'list_bookings' => [
|
||||
'group' => 'bookings',
|
||||
'capability' => 'view_department_bookings',
|
||||
],
|
||||
'list_own_bookings' => [
|
||||
'group' => 'bookings',
|
||||
'capability' => 'view_own_bookings',
|
||||
],
|
||||
'edit_bookings' => [
|
||||
'group' => 'bookings',
|
||||
'capability' => 'update_bookings',
|
||||
],
|
||||
'add_booking' => [
|
||||
'group' => 'bookings',
|
||||
'capability' => 'create_bookings',
|
||||
],
|
||||
'complete_bookings' => [
|
||||
'group' => 'bookings',
|
||||
'capability' => 'mark_bookings_complete',
|
||||
],
|
||||
'resend_booking_confirmations' => [
|
||||
'group' => 'bookings',
|
||||
'capability' => 'send_booking_confirmations',
|
||||
],
|
||||
'department_timebookings_entries_get' => [
|
||||
'group' => 'time_bookings',
|
||||
'capability' => 'view_time_booking_entries',
|
||||
],
|
||||
'department_timebookings_entries_post' => [
|
||||
'group' => 'time_bookings',
|
||||
'capability' => 'create_time_booking_entries',
|
||||
],
|
||||
'department_timebookings_entries_put' => [
|
||||
'group' => 'time_bookings',
|
||||
'capability' => 'edit_time_booking_entries',
|
||||
],
|
||||
'statistics_orders_new' => [
|
||||
'group' => 'reports',
|
||||
'capability' => 'view_order_statistics',
|
||||
],
|
||||
'statistics_bookings_new' => [
|
||||
'group' => 'reports',
|
||||
'capability' => 'view_booking_statistics',
|
||||
],
|
||||
self::PERMISSION_ACCESS => [
|
||||
'group' => 'limited_backoffice',
|
||||
'capability' => 'open_limited_backoffice',
|
||||
],
|
||||
self::PERMISSION_MANAGE_PRICES => [
|
||||
'group' => 'limited_backoffice',
|
||||
'capability' => 'manage_department_prices',
|
||||
],
|
||||
self::PERMISSION_MANAGE_EMPLOYEES => [
|
||||
'group' => 'limited_backoffice',
|
||||
'capability' => 'manage_employee_access',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const ROLE_PERMISSION_GROUP_ORDER = [
|
||||
'account',
|
||||
'orders',
|
||||
'bookings',
|
||||
'time_bookings',
|
||||
'reports',
|
||||
'limited_backoffice',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
@@ -241,7 +123,7 @@ class limited_backoffice_service
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{key:string,label:string,description:string,permission_groups:array<int,array{key:string,capabilities:array<int,string>}>}>
|
||||
* @return array<int, array{key:string,label:string,description:string}>
|
||||
*/
|
||||
public function rolePresets(): array
|
||||
{
|
||||
@@ -251,45 +133,11 @@ class limited_backoffice_service
|
||||
'key' => $key,
|
||||
'label' => $preset['label'],
|
||||
'description' => $preset['description'],
|
||||
'permission_groups' => $this->rolePermissionGroups($preset['permissions']),
|
||||
];
|
||||
}
|
||||
return $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $permissions
|
||||
* @return array<int, array{key:string,capabilities:array<int,string>}>
|
||||
*/
|
||||
private function rolePermissionGroups(array $permissions): array
|
||||
{
|
||||
$groups = [];
|
||||
foreach ($permissions as $permission) {
|
||||
$capability = self::ROLE_PERMISSION_CAPABILITIES[$permission] ?? null;
|
||||
if ($capability === null) {
|
||||
throw new \RuntimeException('Missing limited backoffice role capability for permission: ' . $permission);
|
||||
}
|
||||
|
||||
$group = $capability['group'];
|
||||
$groups[$group] ??= [];
|
||||
$groups[$group][] = $capability['capability'];
|
||||
}
|
||||
|
||||
$payload = [];
|
||||
foreach (self::ROLE_PERMISSION_GROUP_ORDER as $group) {
|
||||
if (!isset($groups[$group])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$payload[] = [
|
||||
'key' => $group,
|
||||
'capabilities' => array_values(array_unique($groups[$group])),
|
||||
];
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
|
||||
+5
-33
@@ -61,47 +61,19 @@ class economic_invoices_draft_endpoint
|
||||
* @throws Exception If the request fails
|
||||
*/
|
||||
public function add_order(int $invoiceDraftId, orders_o $order, string $currency = 'DKK'): void
|
||||
{
|
||||
$this->add_orders($invoiceDraftId, [$order], $currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add many orders to a draft invoice and flush their lines in batches.
|
||||
*
|
||||
* @param orders_o[] $orders
|
||||
* @return array{order_count:int,orders_with_invoice_lines:int,line_count:int,batch_count:int,batch_sizes:array<int,int>}
|
||||
* @throws Exception If the request fails
|
||||
*/
|
||||
public function add_orders(int $invoiceDraftId, array $orders, string $currency = 'DKK', int $line_batch_size = 500): array
|
||||
{
|
||||
$draftInvoice = (new economic())->getInvoiceDraft($invoiceDraftId, strtoupper($currency), true);
|
||||
$orders_with_invoice_lines = 0;
|
||||
|
||||
foreach ( $orders as $order ) {
|
||||
if (!$order instanceof orders_o) {
|
||||
throw new Exception('Order payload must contain orders_o instances');
|
||||
}
|
||||
// Check if the order includes any items that should be included in the invoice
|
||||
if ($order->getIncludeInInvoiceCount() <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$orders_with_invoice_lines++;
|
||||
// Check if the order includes any items that should be included in the invoice
|
||||
if ($order->getIncludeInInvoiceCount() > 0) {
|
||||
// Add the transaction header (Timestamp, department, etc.)
|
||||
$draftInvoice->addNewTransactionHeader($order);
|
||||
// Add the order lines
|
||||
$draftInvoice->addOrderItemLines($order);
|
||||
// Add an empty line, so the invoice is not empty
|
||||
$draftInvoice->addTextLine('');
|
||||
// Save the draft invoice lines
|
||||
$draftInvoice->addLines();
|
||||
}
|
||||
|
||||
$metrics = $draftInvoice->flushLinesInBatches($line_batch_size);
|
||||
|
||||
return [
|
||||
'order_count' => count($orders),
|
||||
'orders_with_invoice_lines' => $orders_with_invoice_lines,
|
||||
...$metrics,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,4 +151,4 @@ class economic_invoices_draft_endpoint
|
||||
$draft_invoice->addLines();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ use objects\orders_o;
|
||||
|
||||
class economic_invoice_draft
|
||||
{
|
||||
public const DEFAULT_LINE_BATCH_SIZE = 500;
|
||||
|
||||
/**
|
||||
* The Economic draftInvoiceNumber
|
||||
* @var int $draft_invoice_number
|
||||
@@ -112,55 +110,13 @@ class economic_invoice_draft
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the lines to the draft invoice.
|
||||
* Add the lines to the draft invoice
|
||||
* @return void
|
||||
*/
|
||||
public function addLines(): void
|
||||
{
|
||||
$this->flushLinesInBatches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add queued draft lines using chunked requests.
|
||||
*
|
||||
* @return array{line_count:int,batch_count:int,batch_sizes:array<int,int>}
|
||||
*/
|
||||
public function flushLinesInBatches(int $batch_size = self::DEFAULT_LINE_BATCH_SIZE): array
|
||||
{
|
||||
$lines = array_values($this->draft_lines);
|
||||
$line_count = count($lines);
|
||||
if ($line_count === 0) {
|
||||
return [
|
||||
'line_count' => 0,
|
||||
'batch_count' => 0,
|
||||
'batch_sizes' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$batch_size = max(1, $batch_size);
|
||||
$batch_sizes = [];
|
||||
foreach (array_chunk($lines, $batch_size) as $batch) {
|
||||
$this->sendDraftLines($batch);
|
||||
$batch_sizes[] = count($batch);
|
||||
}
|
||||
|
||||
$this->draft_lines = [];
|
||||
|
||||
return [
|
||||
'line_count' => $line_count,
|
||||
'batch_count' => count($batch_sizes),
|
||||
'batch_sizes' => $batch_sizes,
|
||||
];
|
||||
}
|
||||
|
||||
public function pendingLineCount(): int
|
||||
{
|
||||
return count($this->draft_lines);
|
||||
}
|
||||
|
||||
protected function sendDraftLines(array $draft_lines): object
|
||||
{
|
||||
$economic = new economic();
|
||||
return $economic->invoices->draft->add_lines($this->draft_invoice_number, $draft_lines);
|
||||
$economic->invoices->draft->add_lines($this->draft_invoice_number, $this->draft_lines);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,7 +37,6 @@ class collected_order_invoices_o extends db
|
||||
public object_property $updated_at;
|
||||
public object_property $closed_at;
|
||||
public int $economic_wash_subscription_user_id = 1857;
|
||||
private ?array $last_economic_transfer_metrics = null;
|
||||
/**
|
||||
* The processor types
|
||||
*
|
||||
@@ -713,7 +712,6 @@ class collected_order_invoices_o extends db
|
||||
*/
|
||||
public function addInvoicesToDraft(bool $skip_check = false): self
|
||||
{
|
||||
$this->last_economic_transfer_metrics = null;
|
||||
// Require the invoice collection to be selected
|
||||
self::requireSelected();
|
||||
// Require the invoice collection to be open
|
||||
@@ -738,20 +736,10 @@ class collected_order_invoices_o extends db
|
||||
usort($orders, function ($a, $b) {
|
||||
return strtotime($a['created_at']) - strtotime($b['created_at']);
|
||||
});
|
||||
// Add the invoice lines to the draft in one accumulated batch path.
|
||||
$order_objects = [];
|
||||
// Add the invoices to the invoice draft
|
||||
foreach ( $orders as $order ) {
|
||||
$order_object = new orders_o();
|
||||
$order_object->select((int)$order['id']);
|
||||
$order_object->requireSelected();
|
||||
$order_objects[] = $order_object;
|
||||
self::addInvoiceToDraft($order['id'], true, $draft_id, $currency);
|
||||
}
|
||||
$metrics = (new economic())->invoices->draft->add_orders($draft_id, $order_objects, $currency);
|
||||
$this->last_economic_transfer_metrics = [
|
||||
'draft_invoice_id' => $draft_id,
|
||||
'currency' => (string)$currency,
|
||||
...$metrics,
|
||||
];
|
||||
// If the customer has the onlyTankCleaning attribute, add the environmental fee & oil fees to the invoice draft
|
||||
self::addEnvironmentalAndOilFeesToDraft($draft_id, $currency);
|
||||
// Object changed
|
||||
@@ -760,11 +748,6 @@ class collected_order_invoices_o extends db
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastEconomicTransferMetrics(): ?array
|
||||
{
|
||||
return $this->last_economic_transfer_metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the environmental fee & oil fees to the invoice draft, if the customer has the onlyTankCleaning attribute
|
||||
* @param int $draft_id The invoice draft id
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace objects;
|
||||
|
||||
use classes\db;
|
||||
use classes\customer_order_product_policy;
|
||||
use classes\object_property;
|
||||
use Exception;
|
||||
use traits\db_object_t;
|
||||
@@ -94,7 +93,6 @@ class order_items_o extends db
|
||||
{
|
||||
global $db, $response;
|
||||
try {
|
||||
customer_order_product_policy::assertOrderAllowsProduct($order_id, $product_id);
|
||||
// Avoid SQL injection
|
||||
$reference = $db->escape_string($reference);
|
||||
$notes = $db->escape_string($notes);
|
||||
@@ -169,7 +167,6 @@ class order_items_o extends db
|
||||
try {
|
||||
// Get the order
|
||||
$order = (new orders_o())->getOrderById($order_id);
|
||||
customer_order_product_policy::assertOrderAllowsProduct($order_id, $product_id);
|
||||
// Get the product price
|
||||
$price = (new products_o())->getProductById($product_id)->getDepartmentPrice((int)$order->department_id->value());
|
||||
|
||||
@@ -357,4 +354,4 @@ class order_items_o extends db
|
||||
{
|
||||
return (new products_o())->select((int)$this->product_id->value());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace routes;
|
||||
|
||||
use classes\authentication;
|
||||
use classes\customer_product_rule_service;
|
||||
use objects\logs_o;
|
||||
use objects\order_items_o;
|
||||
use objects\orders_o;
|
||||
@@ -70,6 +71,10 @@ class orderItemsRoute
|
||||
$price = (int)self::getParameter('price');
|
||||
}
|
||||
}
|
||||
$order = (new orders_o())->getOrderById((int)$data['order_id']);
|
||||
if (!$order->exists()) {
|
||||
$response->error('Order not found', 404);
|
||||
}
|
||||
$product = (new products_o())->getProductById((int)$data['product_id']);
|
||||
if (!$product->exists()) {
|
||||
$response->error('Product not found', 404);
|
||||
@@ -77,6 +82,19 @@ class orderItemsRoute
|
||||
if ($product->requiresOrderItemNote() && trim((string)($notes ?? '')) === '') {
|
||||
$response->error('Notes is required for this product', 400);
|
||||
}
|
||||
$customerRuleViolation = (new customer_product_rule_service())
|
||||
->firstViolationForOrderItem((int)$data['order_id'], (int)$data['product_id'], $related_item_id);
|
||||
if ($customerRuleViolation !== null) {
|
||||
(new logs_o())->add(
|
||||
'order_items',
|
||||
'global',
|
||||
1,
|
||||
$user->id,
|
||||
'ORDER_ITEM_RESTRICTED_BY_CUSTOMER_RULE',
|
||||
'Blocked product ' . (int)$data['product_id'] . ' on order ' . (int)$data['order_id'] . ' by rule ' . $customerRuleViolation['rule']
|
||||
);
|
||||
$response->error($customerRuleViolation['message'], 400);
|
||||
}
|
||||
|
||||
// Add the order item to the order This is done individually, to make the notes to the individual order items possible
|
||||
$order_items = (new order_items_o());
|
||||
|
||||
@@ -348,54 +348,7 @@ it('creates updates lists and deactivates scoped employees without exposing raw
|
||||
->assertSuccess();
|
||||
|
||||
expect(array_column($roles->data(), 'key'))->toBe(['viewer', 'cashier', 'booking_coordinator', 'operations_lead', 'department_admin']);
|
||||
$rolesByKey = array_column($roles->data(), null, 'key');
|
||||
expect($rolesByKey['viewer']['permission_groups'] ?? null)->toBe([
|
||||
[
|
||||
'key' => 'account',
|
||||
'capabilities' => ['sign_in', 'view_own_permissions'],
|
||||
],
|
||||
]);
|
||||
$departmentAdminGroups = array_column($rolesByKey['department_admin']['permission_groups'] ?? [], 'capabilities', 'key');
|
||||
expect($departmentAdminGroups['limited_backoffice'] ?? null)->toBe([
|
||||
'open_limited_backoffice',
|
||||
'manage_department_prices',
|
||||
'manage_employee_access',
|
||||
]);
|
||||
expect($roles->body)->not->toContain('department_access_');
|
||||
$rolePayload = $roles->data();
|
||||
$rolePayloadStrings = [];
|
||||
array_walk_recursive($rolePayload, static function ($value) use (&$rolePayloadStrings): void {
|
||||
if (is_string($value)) {
|
||||
$rolePayloadStrings[] = $value;
|
||||
}
|
||||
});
|
||||
foreach ([
|
||||
'list_orders',
|
||||
'add_order',
|
||||
'edit_order',
|
||||
'delete_order',
|
||||
'list_order_items',
|
||||
'add_order_items',
|
||||
'edit_order_items',
|
||||
'delete_order_items',
|
||||
'charge_order',
|
||||
'list_bookings',
|
||||
'list_own_bookings',
|
||||
'edit_bookings',
|
||||
'add_booking',
|
||||
'complete_bookings',
|
||||
'resend_booking_confirmations',
|
||||
'department_timebookings_entries_get',
|
||||
'department_timebookings_entries_post',
|
||||
'department_timebookings_entries_put',
|
||||
'statistics_orders_new',
|
||||
'statistics_bookings_new',
|
||||
'limited_backoffice_access',
|
||||
'limited_backoffice_prices_manage',
|
||||
'limited_backoffice_employees_manage',
|
||||
] as $rawPermission) {
|
||||
expect($rolePayloadStrings)->not->toContain($rawPermission);
|
||||
}
|
||||
|
||||
$created = api_client()->post('/limited-backoffice/employees', [
|
||||
'display_name' => 'Limited Cashier',
|
||||
|
||||
@@ -4,6 +4,38 @@ declare(strict_types=1);
|
||||
|
||||
usesApiSuite();
|
||||
|
||||
function create_order_item_rule_fixture(array $customerAttributes = []): array
|
||||
{
|
||||
$customer = api_fixtures()->createUser(['display_name' => 'Order Item Rule Customer']);
|
||||
foreach ($customerAttributes as $attribute) {
|
||||
api_fixtures()->addCustomerAttribute((int)$customer['id'], (string)$attribute);
|
||||
}
|
||||
|
||||
$department = api_fixtures()->createDepartment();
|
||||
$order = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'reference' => 'RULE-CHECK',
|
||||
]);
|
||||
$session = api_fixtures()->createUserSession([], ['group_id' => 1]);
|
||||
|
||||
return [
|
||||
'customer' => $customer,
|
||||
'department' => $department,
|
||||
'order' => $order,
|
||||
'session' => $session,
|
||||
];
|
||||
}
|
||||
|
||||
function post_order_item(array $order, array $product, array $headers, array $overrides = []): \Tests\Support\Api\ApiResponse
|
||||
{
|
||||
return api_client()->post('/order/items', array_merge([
|
||||
'order_id' => $order['id'],
|
||||
'product_id' => $product['id'],
|
||||
'quantity' => 1,
|
||||
], $overrides), $headers);
|
||||
}
|
||||
|
||||
it('requires notes when adding the extraordinary chemistry product to an order', function (): void {
|
||||
api_test_covers('POST /order/items', 'validation');
|
||||
|
||||
@@ -15,6 +47,7 @@ it('requires notes when adding the extraordinary chemistry product to an order',
|
||||
'reference' => 'NOTE-REQUIRED',
|
||||
]);
|
||||
$product = api_fixtures()->createProduct([
|
||||
'id' => 902701,
|
||||
'name' => \objects\products_o::EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME,
|
||||
'price' => 299,
|
||||
'requires_note' => 0,
|
||||
@@ -48,85 +81,6 @@ it('requires notes when adding the extraordinary chemistry product to an order',
|
||||
expect($response->data()['notes'] ?? null)->toBe('Graffiti removal on left side');
|
||||
});
|
||||
|
||||
it('only allows tankcleaning products for only tankcleaning customers', function (): void {
|
||||
api_test_covers('POST /order/items', 'customer_rules');
|
||||
|
||||
$customer = api_fixtures()->createUser(['display_name' => 'Only Tankcleaning Customer']);
|
||||
api_fixtures()->addCustomerAttribute((int)$customer['id'], 'onlyTankCleaning');
|
||||
$department = api_fixtures()->createDepartment();
|
||||
$order = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'reference' => 'ONLY-TANK',
|
||||
]);
|
||||
$washProduct = api_fixtures()->createProduct([
|
||||
'name' => 'Forvogn',
|
||||
'price' => 649,
|
||||
'category' => 4,
|
||||
]);
|
||||
$tankCleaningProduct = api_fixtures()->createProduct([
|
||||
'name' => 'Saebe/kemi, 1-4 spulehoveder',
|
||||
'price' => 299,
|
||||
'category' => 5,
|
||||
]);
|
||||
$session = api_fixtures()->createUserSession([], ['group_id' => 1]);
|
||||
|
||||
api_client()
|
||||
->post('/order/items', [
|
||||
'order_id' => $order['id'],
|
||||
'product_id' => $washProduct['id'],
|
||||
'quantity' => 1,
|
||||
], $session['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage(\classes\customer_order_product_policy::ONLY_TANKCLEANING_MESSAGE);
|
||||
|
||||
$response = api_client()->post('/order/items', [
|
||||
'order_id' => $order['id'],
|
||||
'product_id' => $tankCleaningProduct['id'],
|
||||
'quantity' => 1,
|
||||
], $session['headers']);
|
||||
|
||||
$response
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
expect((int)($response->data()['product_id'] ?? 0))->toBe((int)$tankCleaningProduct['id']);
|
||||
});
|
||||
|
||||
it('allows non-tankcleaning products for customers without the only tankcleaning attribute', function (): void {
|
||||
api_test_covers('POST /order/items', 'customer_rules');
|
||||
|
||||
$customer = api_fixtures()->createUser(['display_name' => 'Regular Order Item Customer']);
|
||||
$department = api_fixtures()->createDepartment();
|
||||
$order = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'reference' => 'REGULAR-WASH',
|
||||
]);
|
||||
$washProduct = api_fixtures()->createProduct([
|
||||
'name' => 'Forvogn',
|
||||
'price' => 649,
|
||||
'category' => 4,
|
||||
]);
|
||||
$session = api_fixtures()->createUserSession([], ['group_id' => 1]);
|
||||
|
||||
$response = api_client()->post('/order/items', [
|
||||
'order_id' => $order['id'],
|
||||
'product_id' => $washProduct['id'],
|
||||
'quantity' => 1,
|
||||
], $session['headers']);
|
||||
|
||||
$response
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
expect((int)($response->data()['product_id'] ?? 0))->toBe((int)$washProduct['id']);
|
||||
});
|
||||
|
||||
it('does not allow clearing notes for order items whose product requires notes', function (): void {
|
||||
api_test_covers('PUT /order/items', 'validation');
|
||||
|
||||
@@ -140,6 +94,7 @@ it('does not allow clearing notes for order items whose product requires notes',
|
||||
'reference' => 'NOTE-EDIT',
|
||||
]);
|
||||
$product = api_fixtures()->createProduct([
|
||||
'id' => 902702,
|
||||
'name' => 'API Note Required Product',
|
||||
'price' => 199,
|
||||
'requires_note' => 1,
|
||||
@@ -152,7 +107,7 @@ it('does not allow clearing notes for order items whose product requires notes',
|
||||
'quantity' => 1,
|
||||
'notes' => 'Initial note',
|
||||
]);
|
||||
$session = api_fixtures()->createUserSession(['edit_order_items', 'list_order_items']);
|
||||
$session = api_fixtures()->createUserSession(['edit_order_items']);
|
||||
|
||||
api_client()
|
||||
->put('/order/items', [
|
||||
@@ -172,6 +127,7 @@ it('returns the extraordinary chemistry product with requires_note enabled', fun
|
||||
api_test_covers('GET /products', 'happy');
|
||||
|
||||
$product = api_fixtures()->createProduct([
|
||||
'id' => 902703,
|
||||
'name' => \objects\products_o::EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME,
|
||||
'price' => 299,
|
||||
'requires_note' => 0,
|
||||
@@ -187,3 +143,126 @@ it('returns the extraordinary chemistry product with requires_note enabled', fun
|
||||
|
||||
expect($response->data()['requires_note'] ?? null)->toBeTrue();
|
||||
});
|
||||
|
||||
it('blocks addon products added as standalone additional order items for customers restricted from additional services', function (): void {
|
||||
api_test_covers('POST /order/items', 'customer-rule-validation');
|
||||
|
||||
$fixture = create_order_item_rule_fixture(['restrictAdditionalServices']);
|
||||
$primaryProduct = api_fixtures()->createProduct([
|
||||
'name' => 'Primary truck wash',
|
||||
'price' => 200,
|
||||
]);
|
||||
$addonProduct = api_fixtures()->createProduct([
|
||||
'name' => 'Drying add-on',
|
||||
'category' => 4,
|
||||
'price' => 50,
|
||||
]);
|
||||
|
||||
post_order_item($fixture['order'], $primaryProduct, $fixture['session']['headers'])
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
post_order_item($fixture['order'], $addonProduct, $fixture['session']['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage(\classes\customer_product_rule_service::BLOCK_MESSAGE);
|
||||
});
|
||||
|
||||
it('allows standalone additional order items when the customer is not restricted from additional services', function (): void {
|
||||
api_test_covers('POST /order/items', 'customer-rule-validation');
|
||||
|
||||
$fixture = create_order_item_rule_fixture();
|
||||
$primaryProduct = api_fixtures()->createProduct([
|
||||
'name' => 'Primary unrestricted truck wash',
|
||||
'price' => 200,
|
||||
]);
|
||||
$addonProduct = api_fixtures()->createProduct([
|
||||
'name' => 'Unrestricted add-on',
|
||||
'category' => 4,
|
||||
'price' => 50,
|
||||
]);
|
||||
|
||||
post_order_item($fixture['order'], $primaryProduct, $fixture['session']['headers'])
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
post_order_item($fixture['order'], $addonProduct, $fixture['session']['headers'])
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
});
|
||||
|
||||
it('blocks related addon order items for customers restricted from additional services', function (): void {
|
||||
api_test_covers('POST /order/items', 'customer-rule-validation');
|
||||
|
||||
$fixture = create_order_item_rule_fixture(['restrictAdditionalServices']);
|
||||
$cashier = api_fixtures()->createUser(['display_name' => 'Order Item Rule Cashier']);
|
||||
$primaryProduct = api_fixtures()->createProduct([
|
||||
'name' => 'Primary related truck wash',
|
||||
'price' => 200,
|
||||
]);
|
||||
$addonProduct = api_fixtures()->createProduct([
|
||||
'name' => 'Related extra brush',
|
||||
'price' => 35,
|
||||
]);
|
||||
$primaryItem = api_fixtures()->createOrderItem([
|
||||
'order_id' => $fixture['order']['id'],
|
||||
'product_id' => $primaryProduct['id'],
|
||||
'cashier_id' => $cashier['id'],
|
||||
'price' => 200,
|
||||
]);
|
||||
|
||||
post_order_item($fixture['order'], $addonProduct, $fixture['session']['headers'], [
|
||||
'related_item_id' => $primaryItem['id'],
|
||||
])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage(\classes\customer_product_rule_service::BLOCK_MESSAGE);
|
||||
});
|
||||
|
||||
it('blocks named restricted service products for the selected customer', function (string $attribute, array $productAttributes): void {
|
||||
api_test_covers('POST /order/items', 'customer-rule-validation');
|
||||
|
||||
$fixture = create_order_item_rule_fixture([$attribute]);
|
||||
$product = api_fixtures()->createProduct($productAttributes);
|
||||
|
||||
post_order_item($fixture['order'], $product, $fixture['session']['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage(\classes\customer_product_rule_service::BLOCK_MESSAGE);
|
||||
})->with([
|
||||
'spot free' => ['restrictSpotFree', ['name' => 'Spot Free rinse', 'price' => 80]],
|
||||
'interior cleaning' => ['restrictInteriorCleaning', ['name' => 'Indvendig vask', 'price' => 125]],
|
||||
'tank cleaning' => ['restrictTankCleaning', ['name' => 'Tankrens', 'category' => 5, 'price' => 300]],
|
||||
]);
|
||||
|
||||
it('only allows tank cleaning products when the customer has the only tank cleaning rule', function (): void {
|
||||
api_test_covers('POST /order/items', 'customer-rule-validation');
|
||||
|
||||
$fixture = create_order_item_rule_fixture(['onlyTankCleaning']);
|
||||
$nonTankProduct = api_fixtures()->createProduct([
|
||||
'name' => 'Exterior truck wash',
|
||||
'price' => 180,
|
||||
]);
|
||||
$tankProduct = api_fixtures()->createProduct([
|
||||
'name' => 'Tank cleaning',
|
||||
'category' => 5,
|
||||
'price' => 300,
|
||||
]);
|
||||
|
||||
post_order_item($fixture['order'], $nonTankProduct, $fixture['session']['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage(\classes\customer_product_rule_service::BLOCK_MESSAGE);
|
||||
|
||||
post_order_item($fixture['order'], $tankProduct, $fixture['session']['headers'])
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
});
|
||||
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
it('routes collected invoice draft line uploads through the multi-order batch endpoint', function (): void {
|
||||
$content = file_get_contents(dirname(__DIR__, 3) . '/objects/collected_order_invoices_o.php');
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
$content = (string)$content;
|
||||
|
||||
$start = strpos($content, 'public function addInvoicesToDraft');
|
||||
$end = strpos($content, 'public function getLastEconomicTransferMetrics');
|
||||
expect($start)->not->toBeFalse();
|
||||
expect($end)->not->toBeFalse();
|
||||
expect($end)->toBeGreaterThan($start);
|
||||
|
||||
$methodBlock = substr($content, (int)$start, (int)$end - (int)$start);
|
||||
expect($methodBlock)->toContain('$order_objects = [];')
|
||||
->and($methodBlock)->toContain('$metrics = (new economic())->invoices->draft->add_orders($draft_id, $order_objects, $currency);')
|
||||
->and($methodBlock)->toContain('...$metrics')
|
||||
->and($methodBlock)->not->toContain('self::addInvoiceToDraft($order[\'id\'], true, $draft_id, $currency);');
|
||||
});
|
||||
|
||||
it('keeps single-order draft uploads as a wrapper around the batch endpoint', function (): void {
|
||||
$content = file_get_contents(dirname(__DIR__, 3) . '/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php');
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
$content = (string)$content;
|
||||
|
||||
$singleStart = strpos($content, 'public function add_order');
|
||||
$singleEnd = strpos($content, 'public function add_orders');
|
||||
expect($singleStart)->not->toBeFalse();
|
||||
expect($singleEnd)->not->toBeFalse();
|
||||
expect($singleEnd)->toBeGreaterThan($singleStart);
|
||||
|
||||
$singleBlock = substr($content, (int)$singleStart, (int)$singleEnd - (int)$singleStart);
|
||||
expect($singleBlock)->toContain('$this->add_orders($invoiceDraftId, [$order], $currency);');
|
||||
|
||||
$batchBlock = substr($content, (int)$singleEnd);
|
||||
expect($batchBlock)->toContain('$draftInvoice->flushLinesInBatches($line_batch_size);')
|
||||
->and($batchBlock)->toContain("'orders_with_invoice_lines' => \$orders_with_invoice_lines");
|
||||
});
|
||||
|
||||
it('includes collected invoice batch transfer metrics in queue results when available', function (): void {
|
||||
$content = file_get_contents(dirname(__DIR__, 3) . '/classes/economic_transfer_executor.php');
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
$content = (string)$content;
|
||||
|
||||
expect($content)->toContain('$transfer_metrics = $collected_order_invoices->getLastEconomicTransferMetrics();')
|
||||
->and($content)->toContain("\$result['economic_transfer_metrics'] = \$transfer_metrics;");
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
use helpers\economic_invoice_draft;
|
||||
|
||||
if (!class_exists('EconomicInvoiceDraftBatchingProbe')) {
|
||||
class EconomicInvoiceDraftBatchingProbe extends economic_invoice_draft
|
||||
{
|
||||
public array $sentBatches = [];
|
||||
|
||||
protected function sendDraftLines(array $draft_lines): object
|
||||
{
|
||||
$this->sentBatches[] = $draft_lines;
|
||||
return (object)['lines' => $draft_lines];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('EconomicInvoiceDraftFailingBatchingProbe')) {
|
||||
class EconomicInvoiceDraftFailingBatchingProbe extends EconomicInvoiceDraftBatchingProbe
|
||||
{
|
||||
public int $failOnBatch = 1;
|
||||
|
||||
protected function sendDraftLines(array $draft_lines): object
|
||||
{
|
||||
if (count($this->sentBatches) + 1 === $this->failOnBatch) {
|
||||
throw new RuntimeException('Simulated e-conomic line batch failure');
|
||||
}
|
||||
|
||||
return parent::sendDraftLines($draft_lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('does not call e-conomic when flushing an empty draft line buffer', function (): void {
|
||||
$draft = new EconomicInvoiceDraftBatchingProbe(123, 'DKK', true);
|
||||
|
||||
$metrics = $draft->flushLinesInBatches();
|
||||
|
||||
expect($metrics)->toBe([
|
||||
'line_count' => 0,
|
||||
'batch_count' => 0,
|
||||
'batch_sizes' => [],
|
||||
])->and($draft->sentBatches)->toBe([]);
|
||||
});
|
||||
|
||||
it('flushes a small draft line buffer in one request and clears pending lines', function (): void {
|
||||
$draft = new EconomicInvoiceDraftBatchingProbe(123, 'DKK', true);
|
||||
$draft->addTextLine('line-0');
|
||||
$draft->addTextLine('line-1');
|
||||
$draft->addTextLine('line-2');
|
||||
|
||||
$metrics = $draft->flushLinesInBatches(500);
|
||||
|
||||
expect($metrics)->toBe([
|
||||
'line_count' => 3,
|
||||
'batch_count' => 1,
|
||||
'batch_sizes' => [3],
|
||||
])->and($draft->sentBatches)->toHaveCount(1)
|
||||
->and($draft->sentBatches[0][0]['description'])->toBe('line-0')
|
||||
->and($draft->sentBatches[0][2]['description'])->toBe('line-2')
|
||||
->and($draft->pendingLineCount())->toBe(0);
|
||||
});
|
||||
|
||||
it('chunks large draft line buffers while preserving line order', function (): void {
|
||||
$draft = new EconomicInvoiceDraftBatchingProbe(123, 'DKK', true);
|
||||
for ($i = 0; $i < 1201; $i++) {
|
||||
$draft->addTextLine('line-' . $i);
|
||||
}
|
||||
|
||||
$metrics = $draft->flushLinesInBatches(500);
|
||||
|
||||
expect($metrics)->toBe([
|
||||
'line_count' => 1201,
|
||||
'batch_count' => 3,
|
||||
'batch_sizes' => [500, 500, 201],
|
||||
])->and($draft->sentBatches)->toHaveCount(3)
|
||||
->and($draft->sentBatches[0][0]['description'])->toBe('line-0')
|
||||
->and($draft->sentBatches[1][0]['description'])->toBe('line-500')
|
||||
->and($draft->sentBatches[2][200]['description'])->toBe('line-1200')
|
||||
->and($draft->pendingLineCount())->toBe(0);
|
||||
});
|
||||
|
||||
it('bubbles line batch failures and keeps pending lines available', function (): void {
|
||||
$draft = new EconomicInvoiceDraftFailingBatchingProbe(123, 'DKK', true);
|
||||
$draft->addTextLine('line-0');
|
||||
|
||||
expect(fn () => $draft->flushLinesInBatches(500))
|
||||
->toThrow(RuntimeException::class, 'Simulated e-conomic line batch failure');
|
||||
|
||||
expect($draft->sentBatches)->toBe([])
|
||||
->and($draft->pendingLineCount())->toBe(1);
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use classes\customer_order_product_policy;
|
||||
|
||||
it('recognizes tankcleaning products by category and legacy names', function (): void {
|
||||
expect(customer_order_product_policy::isTankCleaningProductRow([
|
||||
'product_category' => 5,
|
||||
'product_name' => 'Saebe/kemi, 1-4 spulehoveder',
|
||||
'category_name' => 'Other',
|
||||
]))->toBeTrue()
|
||||
->and(customer_order_product_policy::isTankCleaningProductRow([
|
||||
'product_category' => 3,
|
||||
'product_name' => 'Tank cleaning 4 spulehoveder',
|
||||
'category_name' => 'Other',
|
||||
]))->toBeTrue()
|
||||
->and(customer_order_product_policy::isTankCleaningProductRow([
|
||||
'product_category' => 3,
|
||||
'product_name' => 'Saebe/kemi, 1-4 spulehoveder',
|
||||
'category_name' => 'Tankrens',
|
||||
]))->toBeTrue();
|
||||
});
|
||||
|
||||
it('detects only tankcleaning violations only for attributed customers and non-tank products', function (): void {
|
||||
$washProduct = [
|
||||
'product_category' => 4,
|
||||
'product_name' => 'Forvogn',
|
||||
'category_name' => 'Udvendig',
|
||||
];
|
||||
$tankCleaningProduct = [
|
||||
'product_category' => 5,
|
||||
'product_name' => 'Tank cleaning 4 spulehoveder',
|
||||
'category_name' => 'Tank cleaning',
|
||||
];
|
||||
|
||||
expect(customer_order_product_policy::onlyTankCleaningViolation(true, $washProduct))->toBeTrue()
|
||||
->and(customer_order_product_policy::onlyTankCleaningViolation(true, $tankCleaningProduct))->toBeFalse()
|
||||
->and(customer_order_product_policy::onlyTankCleaningViolation(false, $washProduct))->toBeFalse();
|
||||
});
|
||||
Reference in New Issue
Block a user