- Refactor machine_1 drawing logic: optimize highlighted button rendering and deferred processing.
- Add branding management feature: API routes, payload handling, and OpenAPI schema updates. - Implement department branding logic: CRUD operations, validation, and permissions. - Add order deletion confirmation support with conflict handling and OpenAPI schema updates. - Enhance tests and API methods for improved order handling and branding workflows.
This commit is contained in:
+57
-3
@@ -10961,12 +10961,23 @@ paths:
|
|||||||
name: {type: string}
|
name: {type: string}
|
||||||
description: {type: string}
|
description: {type: string}
|
||||||
cvr: {type: integer}
|
cvr: {type: integer}
|
||||||
|
address: {type: string, nullable: true}
|
||||||
|
phone_country_code: {type: integer, nullable: true}
|
||||||
|
phone: {type: integer, nullable: true}
|
||||||
|
email: {type: string, nullable: true}
|
||||||
|
website: {type: string, nullable: true}
|
||||||
|
banner: {type: string, nullable: true}
|
||||||
|
logo: {type: string, nullable: true}
|
||||||
|
favicon: {type: string, nullable: true}
|
||||||
|
signature: {type: string, nullable: true}
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Branding option added successfully
|
description: Branding option added successfully
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema: {}
|
schema: {}
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
'403':
|
'403':
|
||||||
$ref: '#/components/responses/Forbidden'
|
$ref: '#/components/responses/Forbidden'
|
||||||
put:
|
put:
|
||||||
@@ -10984,17 +10995,30 @@ paths:
|
|||||||
required: [id]
|
required: [id]
|
||||||
properties:
|
properties:
|
||||||
id: {type: integer}
|
id: {type: integer}
|
||||||
name: {type: string}
|
name: {type: string, nullable: true}
|
||||||
description: {type: string}
|
description: {type: string, nullable: true}
|
||||||
cvr: {type: integer}
|
cvr: {type: integer, nullable: true}
|
||||||
|
address: {type: string, nullable: true}
|
||||||
|
phone_country_code: {type: integer, nullable: true}
|
||||||
|
phone: {type: integer, nullable: true}
|
||||||
|
email: {type: string, nullable: true}
|
||||||
|
website: {type: string, nullable: true}
|
||||||
|
banner: {type: string, nullable: true}
|
||||||
|
logo: {type: string, nullable: true}
|
||||||
|
favicon: {type: string, nullable: true}
|
||||||
|
signature: {type: string, nullable: true}
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Branding option updated successfully
|
description: Branding option updated successfully
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema: {}
|
schema: {}
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
'403':
|
'403':
|
||||||
$ref: '#/components/responses/Forbidden'
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
/roles:
|
/roles:
|
||||||
get:
|
get:
|
||||||
@@ -11168,6 +11192,36 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema: {}
|
schema: {}
|
||||||
|
|
||||||
|
/superuser/department/branding:
|
||||||
|
put:
|
||||||
|
tags:
|
||||||
|
- Departments
|
||||||
|
summary: Set department branding
|
||||||
|
description: Assign an existing branding option to a department, or clear the department branding by sending a null branding_id.
|
||||||
|
operationId: setDepartmentBranding
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [department_id, branding_id]
|
||||||
|
properties:
|
||||||
|
department_id: {type: integer}
|
||||||
|
branding_id: {type: integer, nullable: true}
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Department branding updated successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
/superuser/department/prices:
|
/superuser/department/prices:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -74,10 +74,9 @@ class machine_1 extends dynamicimages_image
|
|||||||
$this->drawProgramWheel();
|
$this->drawProgramWheel();
|
||||||
$this->drawThumb();
|
$this->drawThumb();
|
||||||
$this->drawStepThumb();
|
$this->drawStepThumb();
|
||||||
$this->drawHighlightedResetButton();
|
$deferredStartButtons = $this->drawHighlightedButtonSequence();
|
||||||
$this->drawHighlightedButtons();
|
|
||||||
$this->drawAsset($this->getAsset(self::IMAGE_POWER_BUTTON), 0, 0);
|
$this->drawAsset($this->getAsset(self::IMAGE_POWER_BUTTON), 0, 0);
|
||||||
$this->drawHighlightedStartButton();
|
$this->drawDeferredHighlightedButtons($deferredStartButtons);
|
||||||
$this->drawCropOutLine(true);
|
$this->drawCropOutLine(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +193,161 @@ class machine_1 extends dynamicimages_image
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function normalizeHighlightedButtonToken(mixed $button): int|string|null
|
||||||
|
{
|
||||||
|
if (is_string($button)) {
|
||||||
|
$trimmed = trim($button);
|
||||||
|
$specialButton = strtolower($trimmed);
|
||||||
|
if ($specialButton === self::BUTTON_RESET || $specialButton === self::BUTTON_START) {
|
||||||
|
return $specialButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($trimmed !== '' && ctype_digit($trimmed)) {
|
||||||
|
$button = (int)$trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_int($button)) {
|
||||||
|
return $button >= 0 && $button < ($this->button_rows * $this->button_columns) ? $button : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($button) && (int)$button == $button) {
|
||||||
|
$button = (int)$button;
|
||||||
|
return $button >= 0 && $button < ($this->button_rows * $this->button_columns) ? $button : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getOrderedHighlightedButtonTokens(): array
|
||||||
|
{
|
||||||
|
$tokens = [];
|
||||||
|
foreach ($this->highlighted_buttons as $button) {
|
||||||
|
$token = $this->normalizeHighlightedButtonToken($button);
|
||||||
|
if ($token !== null) {
|
||||||
|
$tokens[] = $token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRegularButtonCoordinates(int $buttonIndex): ?array
|
||||||
|
{
|
||||||
|
if ($buttonIndex < 0 || $buttonIndex >= ($this->button_rows * $this->button_columns)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = intdiv($buttonIndex, $this->button_columns);
|
||||||
|
$col = $buttonIndex % $this->button_columns;
|
||||||
|
$x = 2815 + ($col * ($this->button_highlight_size + $this->button_columns_spacing));
|
||||||
|
$y = 1265 + ($row * ($this->button_highlight_size + $this->button_rows_spacing));
|
||||||
|
if ($row === $this->button_rows - 1) {
|
||||||
|
$y += $this->button_last_row_spacing_buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'x' => $x,
|
||||||
|
'y' => $y,
|
||||||
|
'size' => $this->button_highlight_size,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getHighlightedButtonCoordinates(int|string $button): ?array
|
||||||
|
{
|
||||||
|
if ($button === self::BUTTON_RESET) {
|
||||||
|
return [
|
||||||
|
'x' => 1736,
|
||||||
|
'y' => 599,
|
||||||
|
'size' => $this->reset_button_highlight_size,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($button === self::BUTTON_START) {
|
||||||
|
return [
|
||||||
|
'x' => 4965,
|
||||||
|
'y' => 1980,
|
||||||
|
'size' => $this->start_button_highlight_size,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_int($button) ? $this->getRegularButtonCoordinates($button) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function buildHighlightedButtonDraw(int|string $button): ?array
|
||||||
|
{
|
||||||
|
$coordinates = $this->getHighlightedButtonCoordinates($button);
|
||||||
|
if ($coordinates === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmp = new dynamicimages_asset($this->getAssetPath(self::getHighlightedAssetName()));
|
||||||
|
$tmp->resize($coordinates['size'], $coordinates['size']);
|
||||||
|
$tmp = $this->drawStepCounterOnButton($tmp);
|
||||||
|
if ($this->only_generate_current_step && $this->button_counter != $this->current_step + 1) {
|
||||||
|
if (method_exists($tmp, 'clearMemoryImage')) {
|
||||||
|
$tmp->clearMemoryImage();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'asset' => $tmp,
|
||||||
|
'x' => $coordinates['x'],
|
||||||
|
'y' => $coordinates['y'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function drawHighlightedButtonDraw(array $draw): void
|
||||||
|
{
|
||||||
|
$tmp = $draw['asset'];
|
||||||
|
$this->drawAsset($tmp, (int)$draw['x'], (int)$draw['y']);
|
||||||
|
if (method_exists($tmp, 'clearMemoryImage')) {
|
||||||
|
$tmp->clearMemoryImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int,array{asset: dynamicimages_asset, x: int, y: int}>
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function drawHighlightedButtonSequence(): array
|
||||||
|
{
|
||||||
|
$deferredStartButtons = [];
|
||||||
|
foreach ($this->getOrderedHighlightedButtonTokens() as $button) {
|
||||||
|
$draw = $this->buildHighlightedButtonDraw($button);
|
||||||
|
if ($draw === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($button === self::BUTTON_START) {
|
||||||
|
$deferredStartButtons[] = $draw;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->drawHighlightedButtonDraw($draw);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $deferredStartButtons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int,array{asset: dynamicimages_asset, x: int, y: int}> $deferredStartButtons
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function drawDeferredHighlightedButtons(array $deferredStartButtons): void
|
||||||
|
{
|
||||||
|
foreach ($deferredStartButtons as $draw) {
|
||||||
|
$this->drawHighlightedButtonDraw($draw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
@@ -545,12 +699,8 @@ class machine_1 extends dynamicimages_image
|
|||||||
$rad = deg2rad($angle);
|
$rad = deg2rad($angle);
|
||||||
$stepX = $centerX + (int)round($radius * cos($rad)) - (int)floor($stepSize / 2);
|
$stepX = $centerX + (int)round($radius * cos($rad)) - (int)floor($stepSize / 2);
|
||||||
$stepY = $centerY + (int)round($radius * sin($rad)) - (int)floor($stepSize / 2);
|
$stepY = $centerY + (int)round($radius * sin($rad)) - (int)floor($stepSize / 2);
|
||||||
$tmp = new dynamicimages_asset($this->getAssetPath(self::getHighlightedAssetName()));
|
$tmp = new dynamicimages_asset($this->getAssetPath(self::IMAGE_BUTTON_HIGHLIGHTED_BLUE));
|
||||||
$tmp->resize($stepSize, $stepSize);
|
$tmp->resize($stepSize, $stepSize);
|
||||||
$tmp = $this->drawStepCounterOnButton($tmp);
|
|
||||||
if ($this->only_generate_current_step && $this->button_counter != $this->current_step +1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$this->drawAsset($tmp, $stepX, $stepY);
|
$this->drawAsset($tmp, $stepX, $stepY);
|
||||||
// Free memory used by temporary asset image, if any
|
// Free memory used by temporary asset image, if any
|
||||||
if (method_exists($tmp, 'clearMemoryImage')) {
|
if (method_exists($tmp, 'clearMemoryImage')) {
|
||||||
|
|||||||
@@ -154,6 +154,80 @@ class orders_o extends db
|
|||||||
// Save the object
|
// Save the object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the data needed to decide whether deletion needs explicit confirmation.
|
||||||
|
*
|
||||||
|
* @return array{
|
||||||
|
* requires_confirmation: bool,
|
||||||
|
* protected_reasons: array<int, string>,
|
||||||
|
* order_item_count: int,
|
||||||
|
* attachment_count: int,
|
||||||
|
* completed_at: mixed
|
||||||
|
* }
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function getDeleteProtectionSummary(): array
|
||||||
|
{
|
||||||
|
self::requireSelected();
|
||||||
|
|
||||||
|
$completedAt = $this->completed_at->value();
|
||||||
|
$orderItemCount = $this->countActiveOrderItems();
|
||||||
|
$attachmentCount = $this->countActiveOrderAttachments();
|
||||||
|
$protectedReasons = [];
|
||||||
|
|
||||||
|
if ($completedAt !== null) {
|
||||||
|
$protectedReasons[] = 'completed';
|
||||||
|
}
|
||||||
|
if ($orderItemCount > 0) {
|
||||||
|
$protectedReasons[] = 'order_items';
|
||||||
|
}
|
||||||
|
if ($attachmentCount > 0) {
|
||||||
|
$protectedReasons[] = 'attachments';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'requires_confirmation' => count($protectedReasons) > 0,
|
||||||
|
'protected_reasons' => $protectedReasons,
|
||||||
|
'order_item_count' => $orderItemCount,
|
||||||
|
'attachment_count' => $attachmentCount,
|
||||||
|
'completed_at' => $completedAt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function countActiveOrderItems(): int
|
||||||
|
{
|
||||||
|
self::requireSelected();
|
||||||
|
global $db;
|
||||||
|
|
||||||
|
$orderId = (int)$this->id;
|
||||||
|
$result = $db->query("SELECT COUNT(*) AS total FROM order_items WHERE order_id = {$orderId} AND deleted_at IS NULL");
|
||||||
|
if ($result && $row = $result->fetch_assoc()) {
|
||||||
|
return (int)($row['total'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function countActiveOrderAttachments(): int
|
||||||
|
{
|
||||||
|
self::requireSelected();
|
||||||
|
global $db;
|
||||||
|
|
||||||
|
$orderId = (int)$this->id;
|
||||||
|
$result = $db->query("SELECT COUNT(*) AS total FROM object_attachments WHERE object_type = 'orders' AND object_id = {$orderId} AND deleted_at IS NULL");
|
||||||
|
if ($result && $row = $result->fetch_assoc()) {
|
||||||
|
return (int)($row['total'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Exception If the order is not selected
|
* @throws Exception If the order is not selected
|
||||||
* This function is called when the order object is changed.
|
* This function is called when the order object is changed.
|
||||||
@@ -278,6 +352,7 @@ class orders_o extends db
|
|||||||
}
|
}
|
||||||
// Set the completed_at property to the current timestamp
|
// Set the completed_at property to the current timestamp
|
||||||
$this->completed_at->set(date('Y-m-d H:i:s'));
|
$this->completed_at->set(date('Y-m-d H:i:s'));
|
||||||
|
$this->setPendingHandheldIndicator(false);
|
||||||
$washCertificateCreated = $this->completeWashCertificateIfNeeded(
|
$washCertificateCreated = $this->completeWashCertificateIfNeeded(
|
||||||
$operator,
|
$operator,
|
||||||
(string)$this->completed_at->value()
|
(string)$this->completed_at->value()
|
||||||
|
|||||||
@@ -3146,12 +3146,55 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
|
- name: confirmed
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: Must be true to delete an order that is completed or has active items or attachments.
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Order deleted successfully
|
description: Order deleted successfully
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema: {}
|
schema: {}
|
||||||
|
'409':
|
||||||
|
description: Order deletion requires explicit confirmation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: Order deletion requires confirmation
|
||||||
|
requires_confirmation:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
protected_reasons:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum: [completed, order_items, attachments]
|
||||||
|
order_item_count:
|
||||||
|
type: integer
|
||||||
|
example: 2
|
||||||
|
attachment_count:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
completed_at:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
meta:
|
||||||
|
type: object
|
||||||
|
includes:
|
||||||
|
type: object
|
||||||
'401':
|
'401':
|
||||||
$ref: '#/components/responses/Unauthorized'
|
$ref: '#/components/responses/Unauthorized'
|
||||||
'404':
|
'404':
|
||||||
@@ -11125,12 +11168,23 @@ paths:
|
|||||||
name: {type: string}
|
name: {type: string}
|
||||||
description: {type: string}
|
description: {type: string}
|
||||||
cvr: {type: integer}
|
cvr: {type: integer}
|
||||||
|
address: {type: string, nullable: true}
|
||||||
|
phone_country_code: {type: integer, nullable: true}
|
||||||
|
phone: {type: integer, nullable: true}
|
||||||
|
email: {type: string, nullable: true}
|
||||||
|
website: {type: string, nullable: true}
|
||||||
|
banner: {type: string, nullable: true}
|
||||||
|
logo: {type: string, nullable: true}
|
||||||
|
favicon: {type: string, nullable: true}
|
||||||
|
signature: {type: string, nullable: true}
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Branding option added successfully
|
description: Branding option added successfully
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema: {}
|
schema: {}
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
'403':
|
'403':
|
||||||
$ref: '#/components/responses/Forbidden'
|
$ref: '#/components/responses/Forbidden'
|
||||||
put:
|
put:
|
||||||
@@ -11148,17 +11202,30 @@ paths:
|
|||||||
required: [id]
|
required: [id]
|
||||||
properties:
|
properties:
|
||||||
id: {type: integer}
|
id: {type: integer}
|
||||||
name: {type: string}
|
name: {type: string, nullable: true}
|
||||||
description: {type: string}
|
description: {type: string, nullable: true}
|
||||||
cvr: {type: integer}
|
cvr: {type: integer, nullable: true}
|
||||||
|
address: {type: string, nullable: true}
|
||||||
|
phone_country_code: {type: integer, nullable: true}
|
||||||
|
phone: {type: integer, nullable: true}
|
||||||
|
email: {type: string, nullable: true}
|
||||||
|
website: {type: string, nullable: true}
|
||||||
|
banner: {type: string, nullable: true}
|
||||||
|
logo: {type: string, nullable: true}
|
||||||
|
favicon: {type: string, nullable: true}
|
||||||
|
signature: {type: string, nullable: true}
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Branding option updated successfully
|
description: Branding option updated successfully
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema: {}
|
schema: {}
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
'403':
|
'403':
|
||||||
$ref: '#/components/responses/Forbidden'
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
/roles:
|
/roles:
|
||||||
get:
|
get:
|
||||||
@@ -11332,6 +11399,36 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema: {}
|
schema: {}
|
||||||
|
|
||||||
|
/superuser/department/branding:
|
||||||
|
put:
|
||||||
|
tags:
|
||||||
|
- Departments
|
||||||
|
summary: Set department branding
|
||||||
|
description: Assign an existing branding option to a department, or clear the department branding by sending a null branding_id.
|
||||||
|
operationId: setDepartmentBranding
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [department_id, branding_id]
|
||||||
|
properties:
|
||||||
|
department_id: {type: integer}
|
||||||
|
branding_id: {type: integer, nullable: true}
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Department branding updated successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
/superuser/department/prices:
|
/superuser/department/prices:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -11,6 +11,87 @@ class BrandingRoute
|
|||||||
{
|
{
|
||||||
use route_t;
|
use route_t;
|
||||||
|
|
||||||
|
private const BRANDING_FIELDS = [
|
||||||
|
'name' => 'string',
|
||||||
|
'description' => 'string',
|
||||||
|
'cvr' => 'int',
|
||||||
|
'address' => 'string',
|
||||||
|
'phone_country_code' => 'int',
|
||||||
|
'phone' => 'int',
|
||||||
|
'email' => 'string',
|
||||||
|
'website' => 'string',
|
||||||
|
'banner' => 'string',
|
||||||
|
'logo' => 'string',
|
||||||
|
'favicon' => 'string',
|
||||||
|
'signature' => 'string',
|
||||||
|
];
|
||||||
|
|
||||||
|
private function readBrandingPayload(array $requiredFields = []): array
|
||||||
|
{
|
||||||
|
global $response;
|
||||||
|
|
||||||
|
$payload = [];
|
||||||
|
foreach (self::BRANDING_FIELDS as $field => $type) {
|
||||||
|
if (!self::isParametersSet([$field])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = self::getParameter($field);
|
||||||
|
if ($value === '') {
|
||||||
|
$value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value === null) {
|
||||||
|
if (in_array($field, $requiredFields, true)) {
|
||||||
|
$response->error($field . ' is required', 400);
|
||||||
|
}
|
||||||
|
$payload[$field] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'int') {
|
||||||
|
if (is_int($value)) {
|
||||||
|
$payload[$field] = $value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value) && preg_match('/^-?\d+$/', $value) === 1) {
|
||||||
|
$payload[$field] = (int)$value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->error($field . ' must be an integer', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($value)) {
|
||||||
|
$response->error($field . ' must be a string', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload[$field] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($requiredFields as $requiredField) {
|
||||||
|
if (!array_key_exists($requiredField, $payload)) {
|
||||||
|
$response->error($requiredField . ' is required', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyBrandingPayload(branding_o $branding, array $payload): void
|
||||||
|
{
|
||||||
|
foreach ($payload as $field => $value) {
|
||||||
|
if (!array_key_exists($field, self::BRANDING_FIELDS) || !property_exists($branding, $field)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$branding->{$field}->set($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$branding->objectChanged();
|
||||||
|
}
|
||||||
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$this->get('/branding', function () {
|
$this->get('/branding', function () {
|
||||||
@@ -35,7 +116,7 @@ class BrandingRoute
|
|||||||
if ($branding->exists()) {
|
if ($branding->exists()) {
|
||||||
// Return the object as an array
|
// Return the object as an array
|
||||||
$response->success(
|
$response->success(
|
||||||
(new branding_o())->select(self::getParameter('id'))->__toString()
|
$branding->asArray()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Log the incident
|
// Log the incident
|
||||||
@@ -77,25 +158,15 @@ class BrandingRoute
|
|||||||
if ($user) {
|
if ($user) {
|
||||||
// Check if the required parameters are set
|
// Check if the required parameters are set
|
||||||
self::requireParameters(['name', 'description', 'cvr']);
|
self::requireParameters(['name', 'description', 'cvr']);
|
||||||
// Check if the parameters are of the correct type
|
|
||||||
self::requireType(self::getParameter('name'), self::TYPE_STRING());
|
|
||||||
self::requireType(self::getParameter('description'), self::TYPE_STRING());
|
|
||||||
self::requireType(self::getParameter('cvr'), self::TYPE_INT());
|
|
||||||
// Create the object
|
// Create the object
|
||||||
$branding = new branding_o();
|
$branding = new branding_o();
|
||||||
// Add the object
|
// Add the object
|
||||||
$branding->add(
|
$branding->add($this->readBrandingPayload(['name', 'description', 'cvr']));
|
||||||
[
|
|
||||||
'name' => self::getParameter('name'),
|
|
||||||
'description' => self::getParameter('description'),
|
|
||||||
'cvr' => self::getParameter('cvr')
|
|
||||||
]
|
|
||||||
);
|
|
||||||
// Log the incident
|
// Log the incident
|
||||||
(new logs_o())->add('branding', 'global', 1, $user->id, 'ADD_BRANDING_OPTION', 'User added a branding option');
|
(new logs_o())->add('branding', 'global', 1, $user->id, 'ADD_BRANDING_OPTION', 'User added a branding option');
|
||||||
// Return the object
|
// Return the object
|
||||||
$response->success(
|
$response->success(
|
||||||
$branding->__toString()
|
$branding->asArray()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Log the incident
|
// Log the incident
|
||||||
@@ -125,41 +196,18 @@ class BrandingRoute
|
|||||||
$branding = new branding_o();
|
$branding = new branding_o();
|
||||||
// Select the object
|
// Select the object
|
||||||
$branding->select(self::getParameter('id'));
|
$branding->select(self::getParameter('id'));
|
||||||
// Check what the user wants to edit
|
if (!$branding->exists()) {
|
||||||
|
$response->error('Invalid id', 400);
|
||||||
|
}
|
||||||
|
|
||||||
// Option name
|
$payload = $this->readBrandingPayload();
|
||||||
if (self::isParametersSet(['name'])) {
|
$this->applyBrandingPayload($branding, $payload);
|
||||||
// Check if the parameters are of the correct type
|
|
||||||
self::requireType(self::getParameter('name'), self::TYPE_STRING());
|
|
||||||
// Set the name
|
|
||||||
$branding->name->set(
|
|
||||||
(string)self::getParameter('name')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Option description
|
|
||||||
if (self::isParametersSet(['description'])) {
|
|
||||||
// Check if the parameters are of the correct type
|
|
||||||
self::requireType(self::getParameter('description'), self::TYPE_STRING());
|
|
||||||
// Set the description
|
|
||||||
$branding->description->set(
|
|
||||||
(string)self::getParameter('description')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Option cvr
|
|
||||||
if (self::isParametersSet(['cvr'])) {
|
|
||||||
// Check if the parameters are of the correct type
|
|
||||||
self::requireType(self::getParameter('cvr'), self::TYPE_INT());
|
|
||||||
// Set the cvr value
|
|
||||||
$branding->cvr->set(
|
|
||||||
(int)self::getParameter('cvr')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the incident
|
// Log the incident
|
||||||
(new logs_o())->add('branding', 'global', 1, $user->id, 'EDIT_BRANDING_OPTION', 'User edited a branding option');
|
(new logs_o())->add('branding', 'global', 1, $user->id, 'EDIT_BRANDING_OPTION', 'User edited a branding option');
|
||||||
// Return the object
|
// Return the object
|
||||||
$response->success(
|
$response->success(
|
||||||
$branding->__toString()
|
$branding->asArray()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Log the incident
|
// Log the incident
|
||||||
@@ -173,4 +221,4 @@ class BrandingRoute
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class departmentLanesRoute
|
|||||||
* Query parameters:
|
* Query parameters:
|
||||||
* - department (int, required)
|
* - department (int, required)
|
||||||
* - lane (int, required)
|
* - lane (int, required)
|
||||||
* - buttons (array|json|csv, optional) → highlighted button IDs (0-indexed)
|
* - buttons (array|json|csv, optional) → ordered highlighted button IDs (0-indexed), "reset", or "start"
|
||||||
* - current_step (int >= 0, optional) → current click/step indicator
|
* - current_step (int >= 0, optional) → current click/step indicator
|
||||||
* - only_current_step (bool/int, optional) → if true, only draw current step highlight
|
* - only_current_step (bool/int, optional) → if true, only draw current step highlight
|
||||||
* - vehicle_type (int|null, optional) → normalized but currently not used by machine_1
|
* - vehicle_type (int|null, optional) → normalized but currently not used by machine_1
|
||||||
|
|||||||
@@ -218,15 +218,48 @@ class ordersRoute
|
|||||||
// Get the current order
|
// Get the current order
|
||||||
$order = (new orders_o())->getOrderById((int)$id);
|
$order = (new orders_o())->getOrderById((int)$id);
|
||||||
// Check if the order exists
|
// Check if the order exists
|
||||||
if (!$order->exists()) {
|
if (!isset($order->id) || (int)$order->id < 1 || !$order->exists()) {
|
||||||
$response->error('Order not found', 400);
|
$response->error('Order not found', 400);
|
||||||
}
|
}
|
||||||
// Check if the user has access to the department
|
// Check if the user has access to the department
|
||||||
self::requireDepartmentAccess((int)$order->department_id->value());
|
self::requireDepartmentAccess((int)$order->department_id->value());
|
||||||
|
$confirmed = filter_var($this->fromRequest('confirmed'), FILTER_VALIDATE_BOOLEAN) === true;
|
||||||
|
$deleteProtection = $order->getDeleteProtectionSummary();
|
||||||
|
if ($deleteProtection['requires_confirmation'] && !$confirmed) {
|
||||||
|
(new logs_o())->add(
|
||||||
|
'orders',
|
||||||
|
$order->department_id->value(),
|
||||||
|
1,
|
||||||
|
$user->id,
|
||||||
|
'DELETE_ORDER_CONFIRMATION_REQUIRED',
|
||||||
|
'Order deletion requires confirmation (ID: ' . $id
|
||||||
|
. '; reasons: ' . implode(',', $deleteProtection['protected_reasons'])
|
||||||
|
. '; order_items: ' . $deleteProtection['order_item_count']
|
||||||
|
. '; attachments: ' . $deleteProtection['attachment_count'] . ')'
|
||||||
|
);
|
||||||
|
$response->error([
|
||||||
|
'message' => 'Order deletion requires confirmation',
|
||||||
|
...$deleteProtection,
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
// Delete the order
|
// Delete the order
|
||||||
$order->delete();
|
$order->delete();
|
||||||
// Log the incident
|
// Log the incident
|
||||||
(new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'DELETE_ORDER', 'Successfully deleted an order (ID: ' . $id . ')');
|
if ($deleteProtection['requires_confirmation']) {
|
||||||
|
(new logs_o())->add(
|
||||||
|
'orders',
|
||||||
|
$order->department_id->value(),
|
||||||
|
1,
|
||||||
|
$user->id,
|
||||||
|
'DELETE_ORDER_CONFIRMED',
|
||||||
|
'Successfully deleted a protected order after confirmation (ID: ' . $id
|
||||||
|
. '; reasons: ' . implode(',', $deleteProtection['protected_reasons'])
|
||||||
|
. '; order_items: ' . $deleteProtection['order_item_count']
|
||||||
|
. '; attachments: ' . $deleteProtection['attachment_count'] . ')'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
(new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'DELETE_ORDER', 'Successfully deleted an order (ID: ' . $id . ')');
|
||||||
|
}
|
||||||
// Return a success message
|
// Return a success message
|
||||||
$response->success(['message' => 'Order deleted successfully']);
|
$response->success(['message' => 'Order deleted successfully']);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace routes;
|
namespace routes;
|
||||||
|
|
||||||
use classes\authentication;
|
use classes\authentication;
|
||||||
|
use objects\branding_o;
|
||||||
use objects\department_variables_o;
|
use objects\department_variables_o;
|
||||||
use objects\departments_o;
|
use objects\departments_o;
|
||||||
use objects\logs_o;
|
use objects\logs_o;
|
||||||
@@ -50,6 +51,63 @@ class superuserDepartmentRoute
|
|||||||
'superuser_fetch_department' => 'Fetch department'
|
'superuser_fetch_department' => 'Fetch department'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->put('/superuser/department/branding', function () {
|
||||||
|
global $response;
|
||||||
|
$this->requirePermission('superuser_set_department_branding');
|
||||||
|
|
||||||
|
$user = (new authentication())->get_user();
|
||||||
|
if (!$user) {
|
||||||
|
(new logs_o())->add('departments', 'global', 1, 0, 'SUPERUSER_SET_DEPARTMENT_BRANDING', 'No user found, or invalid session');
|
||||||
|
$response->error('Invalid session', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::requireParameters(['department_id', 'branding_id']);
|
||||||
|
|
||||||
|
$departmentId = self::getParameter('department_id');
|
||||||
|
if (!is_int($departmentId) && !(is_string($departmentId) && preg_match('/^\d+$/', $departmentId) === 1)) {
|
||||||
|
$response->error('Department ID must be a number', 400);
|
||||||
|
}
|
||||||
|
$departmentId = (int)$departmentId;
|
||||||
|
if ($departmentId <= 0) {
|
||||||
|
$response->error('Department ID must be a positive number', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$department = (new departments_o())->selectId($departmentId);
|
||||||
|
if (!$department->exists()) {
|
||||||
|
$response->error('Department not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$brandingId = self::getParameter('branding_id');
|
||||||
|
if ($brandingId === '' || $brandingId === null || $brandingId === 0 || $brandingId === '0') {
|
||||||
|
$department->branding->set(null);
|
||||||
|
} else {
|
||||||
|
if (!is_int($brandingId) && !(is_string($brandingId) && preg_match('/^\d+$/', $brandingId) === 1)) {
|
||||||
|
$response->error('Branding ID must be a number', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$brandingId = (int)$brandingId;
|
||||||
|
if ($brandingId <= 0) {
|
||||||
|
$response->error('Branding ID must be a positive number', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$branding = (new branding_o())->select($brandingId);
|
||||||
|
if (!$branding->exists()) {
|
||||||
|
$response->error('Branding not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$department->branding->set($brandingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$department->objectChanged();
|
||||||
|
(new logs_o())->add('departments', 'global', 1, $user->id, 'SUPERUSER_SET_DEPARTMENT_BRANDING', 'Successfully set department branding');
|
||||||
|
|
||||||
|
$response->success(
|
||||||
|
(new departments_o())->getDepartmentById($departmentId, true)
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
'superuser_set_department_branding' => 'Set department branding'
|
||||||
|
]);
|
||||||
|
|
||||||
$this->post('/superuser/department/prices', function () {
|
$this->post('/superuser/department/prices', function () {
|
||||||
// Require the user to be logged in
|
// Require the user to be logged in
|
||||||
global $response;
|
global $response;
|
||||||
@@ -237,4 +295,4 @@ class superuserDepartmentRoute
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -798,6 +798,86 @@ it('deletes orders through the real endpoint', function (): void {
|
|||||||
expect($row['deleted_at'] ?? null)->not->toBeNull();
|
expect($row['deleted_at'] ?? null)->not->toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('requires explicit confirmation before deleting protected orders', function (): void {
|
||||||
|
api_test_covers('DELETE /orders', 'failure');
|
||||||
|
api_test_covers('DELETE /orders', 'happy');
|
||||||
|
|
||||||
|
$department = api_fixtures()->createDepartment();
|
||||||
|
$customer = api_fixtures()->createUser();
|
||||||
|
$cashier = api_fixtures()->createUser(['display_name' => 'Protected Delete Cashier']);
|
||||||
|
$session = api_fixtures()->createUserSession([
|
||||||
|
'delete_order',
|
||||||
|
'department_access_' . $department['id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cases = [
|
||||||
|
'completed' => function () use ($department, $customer, $cashier): array {
|
||||||
|
return api_fixtures()->createOrder([
|
||||||
|
'customer_id' => $customer['customer_number'],
|
||||||
|
'cashier_id' => $cashier['id'],
|
||||||
|
'department_id' => $department['id'],
|
||||||
|
'completed_at' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
'order_items' => function () use ($department, $customer, $cashier): array {
|
||||||
|
$order = api_fixtures()->createOrder([
|
||||||
|
'customer_id' => $customer['customer_number'],
|
||||||
|
'cashier_id' => $cashier['id'],
|
||||||
|
'department_id' => $department['id'],
|
||||||
|
]);
|
||||||
|
api_fixtures()->createOrderItem([
|
||||||
|
'order_id' => $order['id'],
|
||||||
|
'product_id' => 53,
|
||||||
|
'cashier_id' => $cashier['id'],
|
||||||
|
'quantity' => 1,
|
||||||
|
]);
|
||||||
|
return $order;
|
||||||
|
},
|
||||||
|
'attachments' => function () use ($department, $customer, $cashier): array {
|
||||||
|
$order = api_fixtures()->createOrder([
|
||||||
|
'customer_id' => $customer['customer_number'],
|
||||||
|
'cashier_id' => $cashier['id'],
|
||||||
|
'department_id' => $department['id'],
|
||||||
|
]);
|
||||||
|
api_fixtures()->createOrderAttachment([
|
||||||
|
'order_id' => $order['id'],
|
||||||
|
]);
|
||||||
|
return $order;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($cases as $expectedReason => $createProtectedOrder) {
|
||||||
|
$order = $createProtectedOrder();
|
||||||
|
|
||||||
|
$unconfirmed = api_client()->delete('/orders', [
|
||||||
|
'id' => $order['id'],
|
||||||
|
], $session['headers']);
|
||||||
|
|
||||||
|
$unconfirmed
|
||||||
|
->assertStatus(409)
|
||||||
|
->assertEnvelope()
|
||||||
|
->assertSuccess(false)
|
||||||
|
->assertMessage('Order deletion requires confirmation');
|
||||||
|
|
||||||
|
expect($unconfirmed->data()['requires_confirmation'] ?? null)->toBeTrue();
|
||||||
|
expect($unconfirmed->data()['protected_reasons'] ?? [])->toContain($expectedReason);
|
||||||
|
expect(api_fixtures()->fetchRowById('orders', (int)$order['id'])['deleted_at'] ?? null)->toBeNull();
|
||||||
|
|
||||||
|
$confirmed = api_client()->delete('/orders', [
|
||||||
|
'id' => $order['id'],
|
||||||
|
'confirmed' => 'true',
|
||||||
|
], $session['headers']);
|
||||||
|
|
||||||
|
$confirmed
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertEnvelope()
|
||||||
|
->assertSuccess()
|
||||||
|
->assertMessage('Order deleted successfully');
|
||||||
|
|
||||||
|
expect(api_fixtures()->fetchRowById('orders', (int)$order['id'])['deleted_at'] ?? null)->not->toBeNull();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects invalid order delete requests', function (): void {
|
it('rejects invalid order delete requests', function (): void {
|
||||||
api_test_covers('DELETE /orders', 'failure');
|
api_test_covers('DELETE /orders', 'failure');
|
||||||
|
|
||||||
@@ -820,8 +900,8 @@ it('rejects invalid order delete requests', function (): void {
|
|||||||
], $session['headers']);
|
], $session['headers']);
|
||||||
|
|
||||||
$missingOrder
|
$missingOrder
|
||||||
->assertStatus(500)
|
->assertStatus(400)
|
||||||
->assertEnvelope()
|
->assertEnvelope()
|
||||||
->assertSuccess(false)
|
->assertSuccess(false)
|
||||||
->assertMessageContains('must not be accessed before initialization');
|
->assertMessage('Order not found');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ return [
|
|||||||
'PUT /orders',
|
'PUT /orders',
|
||||||
'PUT /numberplatescanners',
|
'PUT /numberplatescanners',
|
||||||
'DELETE /orders',
|
'DELETE /orders',
|
||||||
|
'GET /branding',
|
||||||
|
'POST /branding',
|
||||||
|
'PUT /branding',
|
||||||
|
'PUT /superuser/department/branding',
|
||||||
'POST /bird/voice/calls/webhook/inbound',
|
'POST /bird/voice/calls/webhook/inbound',
|
||||||
],
|
],
|
||||||
'manual_operations' => [
|
'manual_operations' => [
|
||||||
|
|||||||
@@ -141,6 +141,32 @@ final class ApiFixtures
|
|||||||
return ['id' => $departmentId];
|
return ['id' => $departmentId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $attributes
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function createBranding(array $attributes = []): array
|
||||||
|
{
|
||||||
|
$brandingId = $this->insertRow('branding', [
|
||||||
|
'name' => (string)($attributes['name'] ?? ('API Brand ' . $this->uniqueSuffix())),
|
||||||
|
'description' => (string)($attributes['description'] ?? 'API branding'),
|
||||||
|
'cvr' => (int)($attributes['cvr'] ?? 41004355),
|
||||||
|
'address' => $attributes['address'] ?? null,
|
||||||
|
'phone_country_code' => $attributes['phone_country_code'] ?? null,
|
||||||
|
'phone' => $attributes['phone'] ?? null,
|
||||||
|
'email' => $attributes['email'] ?? null,
|
||||||
|
'website' => $attributes['website'] ?? null,
|
||||||
|
'banner' => $attributes['banner'] ?? null,
|
||||||
|
'logo' => $attributes['logo'] ?? null,
|
||||||
|
'favicon' => $attributes['favicon'] ?? null,
|
||||||
|
'signature' => $attributes['signature'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cleanup->add(fn() => $this->deleteById('branding', $brandingId));
|
||||||
|
|
||||||
|
return array_merge(['id' => $brandingId], $this->fetchRowById('branding', $brandingId) ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $attributes
|
* @param array<string, mixed> $attributes
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
@@ -729,6 +755,34 @@ final class ApiFixtures
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $attributes
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function createOrderAttachment(array $attributes): array
|
||||||
|
{
|
||||||
|
$orderId = (int)($attributes['order_id'] ?? 0);
|
||||||
|
if ($orderId <= 0) {
|
||||||
|
throw new RuntimeException('Order attachments require order_id.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachmentId = $this->insertRow('object_attachments', [
|
||||||
|
'object_type' => 'orders',
|
||||||
|
'object_id' => $orderId,
|
||||||
|
'content' => $attributes['content'] ?? '{"document":"api-test.pdf","other":"api-test.pdf"}',
|
||||||
|
'created_at' => $attributes['created_at'] ?? $this->now(),
|
||||||
|
'updated_at' => $attributes['updated_at'] ?? $this->now(),
|
||||||
|
'deleted_at' => $attributes['deleted_at'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cleanup->add(fn() => $this->deleteById('object_attachments', $attachmentId));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $attachmentId,
|
||||||
|
'order_id' => $orderId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $attributes
|
* @param array<string, mixed> $attributes
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
|
|||||||
+69
-8
@@ -1,11 +1,72 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
it('allows studio lane dynamic image previews to override the saved image id', function (): void {
|
namespace classes {
|
||||||
$content = file_get_contents(app_path('routes/departmentLanesRoute.php'));
|
if (!class_exists('classes\\db')) {
|
||||||
|
class db {}
|
||||||
|
}
|
||||||
|
|
||||||
expect($content)->not->toBeFalse();
|
if (!class_exists('classes\\object_property')) {
|
||||||
expect($content)->toContain("isRequestParameterSet('dynamic_image_id')");
|
class object_property
|
||||||
expect($content)->toContain("\$dynamic_image_override = \$response->getRequestParameter('dynamic_image_id')");
|
{
|
||||||
expect($content)->toContain("self::requireMinValue(\$dynamic_image_id, 1)");
|
public function __construct(...$args) {}
|
||||||
expect($content)->toContain("switch (\$dynamic_image_id)");
|
public function value() { return null; }
|
||||||
});
|
public function set($v) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace traits {
|
||||||
|
if (!trait_exists('traits\\db_object_t')) {
|
||||||
|
trait db_object_t {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
app_require('objects/department_selfserve_tasks_o.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
use objects\department_selfserve_tasks_o;
|
||||||
|
|
||||||
|
it('allows studio lane dynamic image previews to override the saved image id', function (): void {
|
||||||
|
$content = file_get_contents(app_path('routes/departmentLanesRoute.php'));
|
||||||
|
|
||||||
|
expect($content)->not->toBeFalse();
|
||||||
|
expect($content)->toContain("isRequestParameterSet('dynamic_image_id')");
|
||||||
|
expect($content)->toContain("\$dynamic_image_override = \$response->getRequestParameter('dynamic_image_id')");
|
||||||
|
expect($content)->toContain("self::requireMinValue(\$dynamic_image_id, 1)");
|
||||||
|
expect($content)->toContain("switch (\$dynamic_image_id)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts ordered dynamic image button tokens including reset start and zero', function (): void {
|
||||||
|
expect(department_selfserve_tasks_o::normalizeButtonsInput('["reset",0,2,"start",5]'))->toBe([
|
||||||
|
'reset',
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
'start',
|
||||||
|
5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$route = file_get_contents(app_path('routes/departmentLanesRoute.php'));
|
||||||
|
expect($route)->not->toBeFalse();
|
||||||
|
expect($route)->toContain('ordered highlighted button IDs');
|
||||||
|
expect($route)->toContain('"reset", or "start"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders machine one dynamic image steps from the ordered button payload', function (): void {
|
||||||
|
$content = file_get_contents(app_path('modules/dynamicimages/images/machine_1.php'));
|
||||||
|
|
||||||
|
expect($content)->not->toBeFalse();
|
||||||
|
expect($content)->toContain('$deferredStartButtons = $this->drawHighlightedButtonSequence();');
|
||||||
|
expect($content)->toContain('getOrderedHighlightedButtonTokens');
|
||||||
|
expect($content)->toContain('normalizeHighlightedButtonToken');
|
||||||
|
expect($content)->toContain('drawDeferredHighlightedButtons($deferredStartButtons)');
|
||||||
|
|
||||||
|
$thumbOffset = strpos($content, 'public function drawStepThumb');
|
||||||
|
expect($thumbOffset)->not->toBeFalse();
|
||||||
|
$thumbBody = substr($content, (int)$thumbOffset, 1800);
|
||||||
|
expect($thumbBody)->toContain('self::IMAGE_BUTTON_HIGHLIGHTED_BLUE');
|
||||||
|
expect($thumbBody)->not->toContain('drawStepCounterOnButton');
|
||||||
|
expect($thumbBody)->not->toContain('only_generate_current_step');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user