From 8f4cf2820bb5d688a4b78cdd303c22086bbf7de6 Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Thu, 30 Jul 2026 09:42:19 +0600 Subject: [PATCH 1/2] feat(settings): consolidate combo lists and drop duplicate member processors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Share SettingsComboListService between ExtJS combo GetList and Manager REST dropdowns; remove unused Delivery↔Payment membership processors that duplicated Vue REST with hardcoded TODOs. --- .../minishop3/config/routes/manager.php | 6 + .../Api/Manager/DeliveriesController.php | 11 +- .../Api/Manager/PaymentsController.php | 72 ++++++++ .../Controllers/Api/ReferencesController.php | 47 ++--- .../Processors/Settings/Delivery/GetList.php | 35 +++- .../Settings/Delivery/Payments/Disable.php | 44 ----- .../Settings/Delivery/Payments/Enable.php | 27 --- .../Settings/Delivery/Payments/GetList.php | 74 -------- .../Settings/Delivery/Payments/Multiple.php | 32 ---- .../Settings/Payment/Deliveries/Disable.php | 45 ----- .../Settings/Payment/Deliveries/Enable.php | 27 --- .../Settings/Payment/Deliveries/GetList.php | 74 -------- .../Settings/Payment/Deliveries/Multiple.php | 32 ---- .../Processors/Settings/Payment/GetList.php | 36 +++- .../Processors/Settings/Vendor/GetList.php | 99 +++++----- .../minishop3/src/ServiceRegistry.php | 4 + .../Settings/SettingsComboListService.php | 173 ++++++++++++++++++ .../PaymentsActiveDropdownFieldsTest.php | 96 ++++++++++ .../SettingsDualApiConsolidationTest.php | 105 +++++++++++ phpstan-baseline.neon | 24 --- 20 files changed, 577 insertions(+), 486 deletions(-) delete mode 100644 core/components/minishop3/src/Processors/Settings/Delivery/Payments/Disable.php delete mode 100644 core/components/minishop3/src/Processors/Settings/Delivery/Payments/Enable.php delete mode 100644 core/components/minishop3/src/Processors/Settings/Delivery/Payments/GetList.php delete mode 100644 core/components/minishop3/src/Processors/Settings/Delivery/Payments/Multiple.php delete mode 100644 core/components/minishop3/src/Processors/Settings/Payment/Deliveries/Disable.php delete mode 100644 core/components/minishop3/src/Processors/Settings/Payment/Deliveries/Enable.php delete mode 100644 core/components/minishop3/src/Processors/Settings/Payment/Deliveries/GetList.php delete mode 100644 core/components/minishop3/src/Processors/Settings/Payment/Deliveries/Multiple.php create mode 100644 core/components/minishop3/src/Services/Settings/SettingsComboListService.php create mode 100644 core/components/minishop3/tests/PaymentsActiveDropdownFieldsTest.php create mode 100644 core/components/minishop3/tests/SettingsDualApiConsolidationTest.php diff --git a/core/components/minishop3/config/routes/manager.php b/core/components/minishop3/config/routes/manager.php index 1b0f0ffe..c2711340 100644 --- a/core/components/minishop3/config/routes/manager.php +++ b/core/components/minishop3/config/routes/manager.php @@ -964,6 +964,12 @@ return $controller->getActiveDropdown($params); }); + // Dropdown list of active payments (optional delivery_id filter via msDeliveryMember) + $router->get('/payments-active', function($params) use ($modx) { + $controller = new \MiniShop3\Controllers\Api\Manager\PaymentsController($modx); + return $controller->getActiveDropdown($params); + }); + // Read: any mgr who can open grids (view_document). Write is global msGridField — mssetting_save. $router->group('/grid-config', function ($router) use ($modx) { $router->get('/{grid_key}', function ($params) use ($modx) { diff --git a/core/components/minishop3/src/Controllers/Api/Manager/DeliveriesController.php b/core/components/minishop3/src/Controllers/Api/Manager/DeliveriesController.php index 436696ff..15f33b3e 100644 --- a/core/components/minishop3/src/Controllers/Api/Manager/DeliveriesController.php +++ b/core/components/minishop3/src/Controllers/Api/Manager/DeliveriesController.php @@ -7,6 +7,7 @@ use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; use MiniShop3\Services\Grid\ManagerListFilterPolicy; +use MiniShop3\Services\Settings\SettingsComboListService; use MiniShop3\Utils\PriceAdjustment; use MODX\Revolution\modX; @@ -47,11 +48,13 @@ public function getActiveDropdown(array $params = []): array { $this->modx->lexicon->load('minishop3:default'); - $q = $this->modx->newQuery(msDelivery::class, ['active' => 1]); - $q->sortby('position', 'ASC'); - + /** @var SettingsComboListService $comboList */ + $comboList = $this->modx->services->get('ms3_settings_combo_list'); $results = []; - foreach ($this->modx->getIterator(msDelivery::class, $q) as $delivery) { + foreach ($comboList->iterateActiveDeliveries( + (int) ($params['id'] ?? 0), + trim((string) ($params['query'] ?? '')) + ) as $delivery) { $results[] = $this->formatActiveDropdownItem($delivery); } diff --git a/core/components/minishop3/src/Controllers/Api/Manager/PaymentsController.php b/core/components/minishop3/src/Controllers/Api/Manager/PaymentsController.php index 1e1b2487..d4ebba88 100644 --- a/core/components/minishop3/src/Controllers/Api/Manager/PaymentsController.php +++ b/core/components/minishop3/src/Controllers/Api/Manager/PaymentsController.php @@ -7,6 +7,7 @@ use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; use MiniShop3\Services\Grid\ManagerListFilterPolicy; +use MiniShop3\Services\Settings\SettingsComboListService; use MiniShop3\Utils\PriceAdjustment; use MODX\Revolution\modX; @@ -19,6 +20,19 @@ */ class PaymentsController { + /** + * Public fields for GET /api/mgr/payments-active (order-form dropdown). + * + * Must never include integration secrets: properties, class, validation_rules. + */ + public const ACTIVE_DROPDOWN_FIELDS = [ + 'id', + 'name', + 'price', + 'active', + 'position', + ]; + protected modX $modx; public function __construct(modX $modx) @@ -26,6 +40,41 @@ public function __construct(modX $modx) $this->modx = $modx; } + /** + * Active payments for order-form dropdowns (no integration secrets). + * GET /api/mgr/payments-active + * + * Optional query: delivery_id — restrict to payments linked via msDeliveryMember. + */ + public function getActiveDropdown(array $params = []): array + { + $this->modx->lexicon->load('minishop3:default'); + + /** @var SettingsComboListService $comboList */ + $comboList = $this->modx->services->get('ms3_settings_combo_list'); + $results = []; + foreach ($comboList->iterateActivePayments( + (int) ($params['id'] ?? 0), + (int) ($params['delivery_id'] ?? 0), + trim((string) ($params['query'] ?? '')) + ) as $payment) { + $results[] = $this->formatActiveDropdownItem($payment); + } + + return Response::success(['results' => $results])->getData(); + } + + /** + * Project a payment row to the dropdown whitelist (no MODX required). + * + * @param array $data + * @return array + */ + public static function projectActiveDropdownFields(array $data): array + { + return array_intersect_key($data, array_flip(self::ACTIVE_DROPDOWN_FIELDS)); + } + /** * Get list of payments with pagination and search * GET /api/mgr/payments @@ -356,6 +405,29 @@ protected function formatPayment(msPayment $payment): array ]; } + protected function formatActiveDropdownItem(msPayment $payment): array + { + $data = []; + foreach (self::ACTIVE_DROPDOWN_FIELDS as $field) { + $raw = $payment->get($field); + $data[$field] = match ($field) { + 'id', 'position' => (int) $raw, + 'active' => (int) (bool) $raw, + default => $raw, + }; + } + + $name = (string) ($data['name'] ?? ''); + if ($name !== '' && str_starts_with($name, 'ms3_')) { + $translated = $this->modx->lexicon($name); + if ($translated !== $name) { + $data['name'] = $translated; + } + } + + return $data; + } + /** * Get deliveries for specific payment * GET /api/mgr/payments/{id}/deliveries diff --git a/core/components/minishop3/src/Controllers/Api/ReferencesController.php b/core/components/minishop3/src/Controllers/Api/ReferencesController.php index c5da69c1..e368f2ee 100644 --- a/core/components/minishop3/src/Controllers/Api/ReferencesController.php +++ b/core/components/minishop3/src/Controllers/Api/ReferencesController.php @@ -4,6 +4,7 @@ use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; +use MiniShop3\Services\Settings\SettingsComboListService; /** * API controller for working with reference data (vendors, categories, etc) @@ -20,37 +21,21 @@ class ReferencesController extends BaseApiController public function getVendors(array $params): Response { try { - $query = $this->modx->newQuery('MiniShop3\\Model\\msVendor'); - - $query->select(['id', 'name']); - - $query->sortby('name', 'ASC'); - - $searchQuery = $_GET['query'] ?? null; - if (!empty($searchQuery)) { - $query->where([ - 'name:LIKE' => "%{$searchQuery}%", - 'OR:description:LIKE' => "%{$searchQuery}%", - ]); - } - - $collection = $this->modx->getCollection('MiniShop3\\Model\\msVendor', $query); - - $vendors = []; - foreach ($collection as $vendor) { - $vendors[] = [ - 'id' => (int)$vendor->get('id'), - 'name' => $vendor->get('name'), - ]; - } - - $options = []; - foreach ($vendors as $row) { - $options[] = [ - 'value' => $row['id'], - 'label' => (string)$row['name'], - ]; - } + /** @var SettingsComboListService $comboList */ + $comboList = $this->modx->services->get('ms3_settings_combo_list'); + $includeId = SettingsComboListService::resolvePinnedIncludeId( + (int) ($params['id'] ?? $_GET['id'] ?? 0), + $params['limit'] ?? $_GET['limit'] ?? null + ); + $vendors = $comboList->listVendorsForCombo( + $includeId, + trim((string) ($params['query'] ?? $_GET['query'] ?? '')) + ); + + $options = array_map( + static fn(array $row): array => ['value' => $row['id'], 'label' => (string) $row['name']], + $vendors + ); return Response::success([ 'vendors' => $vendors, diff --git a/core/components/minishop3/src/Processors/Settings/Delivery/GetList.php b/core/components/minishop3/src/Processors/Settings/Delivery/GetList.php index a8005bc8..15889028 100644 --- a/core/components/minishop3/src/Processors/Settings/Delivery/GetList.php +++ b/core/components/minishop3/src/Processors/Settings/Delivery/GetList.php @@ -4,6 +4,7 @@ use MiniShop3\Model\msDelivery; use MiniShop3\Model\msDeliveryMember; +use MiniShop3\Services\Settings\SettingsComboListService; use MODX\Revolution\Processors\Model\GetListProcessor; use xPDO\Om\xPDOObject; use xPDO\Om\xPDOQuery; @@ -15,6 +16,26 @@ class GetList extends GetListProcessor public $defaultSortDirection = 'asc'; public $permission = 'mssetting_list'; + /** + * Combo mode delegates to SettingsComboListService (same source as REST dropdowns). + * + * @return array|string + */ + public function process() + { + if ($this->getProperty('combo')) { + /** @var SettingsComboListService $comboList */ + $comboList = $this->modx->services->get('ms3_settings_combo_list'); + $rows = $comboList->listActiveDeliveriesForCombo( + (int) $this->getProperty('id'), + trim((string) $this->getProperty('query', '')) + ); + + return $this->outputArray($rows, count($rows)); + } + + return parent::process(); + } /** * @param xPDOQuery $c @@ -23,15 +44,10 @@ class GetList extends GetListProcessor */ public function prepareQueryBeforeCount(xPDOQuery $c) { - if ($this->getProperty('combo')) { - $c->select('id,name'); - $c->where(['active' => 1, 'OR:id:=' => $this->getProperty('id')]); - } else { - $c->leftJoin(msDeliveryMember::class, 'Payments'); - $c->groupby('msDelivery' . '.id'); - $c->select($this->modx->getSelectColumns($this->classKey, 'msDelivery')); - $c->select('COUNT(Payments.payment_id) as payments'); - } + $c->leftJoin(msDeliveryMember::class, 'Payments'); + $c->groupby('msDelivery' . '.id'); + $c->select($this->modx->getSelectColumns($this->classKey, 'msDelivery')); + $c->select('COUNT(Payments.payment_id) as payments'); if ($query = trim($this->getProperty('query'))) { $c->where([ 'name:LIKE' => "%{$query}%", @@ -43,7 +59,6 @@ public function prepareQueryBeforeCount(xPDOQuery $c) return $c; } - /** * @param xPDOObject $object * diff --git a/core/components/minishop3/src/Processors/Settings/Delivery/Payments/Disable.php b/core/components/minishop3/src/Processors/Settings/Delivery/Payments/Disable.php deleted file mode 100644 index 4fa22bcb..00000000 --- a/core/components/minishop3/src/Processors/Settings/Delivery/Payments/Disable.php +++ /dev/null @@ -1,44 +0,0 @@ -object = $this->modx->getObject($this->classKey, $this->getProperties()); - if (empty($this->object)) { - return $this->modx->lexicon($this->objectType . '_err_nfs'); - } - - return true; - } - - - /** - * @return bool - */ - public function beforeSave() - { - $this->object->fromArray($this->getProperties(), '', true, true); - - return true; - } -} diff --git a/core/components/minishop3/src/Processors/Settings/Delivery/Payments/Enable.php b/core/components/minishop3/src/Processors/Settings/Delivery/Payments/Enable.php deleted file mode 100644 index 83b50b96..00000000 --- a/core/components/minishop3/src/Processors/Settings/Delivery/Payments/Enable.php +++ /dev/null @@ -1,27 +0,0 @@ -object->fromArray($this->getProperties(), '', true, true); - - return true; - } -} diff --git a/core/components/minishop3/src/Processors/Settings/Delivery/Payments/GetList.php b/core/components/minishop3/src/Processors/Settings/Delivery/Payments/GetList.php deleted file mode 100644 index 75af3203..00000000 --- a/core/components/minishop3/src/Processors/Settings/Delivery/Payments/GetList.php +++ /dev/null @@ -1,74 +0,0 @@ -leftJoin( - msDeliveryMember::class, - 'Deliveries', - "Deliveries.payment_id = msPayment.id AND Deliveries.delivery_id = {$this->getProperty('delivery')}" - ); - $c->select($this->modx->getSelectColumns($this->classKey, 'msPayment')); - $c->select('(Deliveries.payment_id is not null) as active'); - $c->groupby($this->sortAlias . '.id'); - - return $c; - } - - - /** - * @param xPDOObject $object - * - * @return array - */ - public function prepareRow(xPDOObject $object) - { - $data = $object->toArray(); - $data['actions'] = []; - - if (empty($data['active'])) { - $data['actions'][] = [ - 'cls' => 'fw-900', - 'icon' => 'icon icon-power-off action-green', - 'title' => $this->modx->lexicon('ms3_menu_enable'), - 'multiple' => $this->modx->lexicon('ms3_menu_enable'), - 'action' => 'enablePayment', - 'button' => true, - 'menu' => true, - ]; - } else { - $data['actions'][] = [ - 'cls' => 'fw-900', - 'icon' => 'icon icon-power-off action-gray', - 'title' => $this->modx->lexicon('ms3_menu_disable'), - 'multiple' => $this->modx->lexicon('ms3_menu_disable'), - 'action' => 'disablePayment', - 'button' => true, - 'menu' => true, - ]; - } - - return $data; - } -} diff --git a/core/components/minishop3/src/Processors/Settings/Delivery/Payments/Multiple.php b/core/components/minishop3/src/Processors/Settings/Delivery/Payments/Multiple.php deleted file mode 100644 index e382187f..00000000 --- a/core/components/minishop3/src/Processors/Settings/Delivery/Payments/Multiple.php +++ /dev/null @@ -1,32 +0,0 @@ -getProperty('method', false); - if (!$method) { - return $this->failure(); - } - $method = ucfirst($method); - $ids = json_decode($this->getProperty('ids'), true); - if (empty($ids)) { - return $this->success(); - } - - foreach ($ids as $id) { - $this->modx->runProcessor('MiniShop3\\Processors\\Settings\\Delivery\\Payments\\' . $method, $id); - } - - return $this->success(); - } -} diff --git a/core/components/minishop3/src/Processors/Settings/Payment/Deliveries/Disable.php b/core/components/minishop3/src/Processors/Settings/Payment/Deliveries/Disable.php deleted file mode 100644 index 5ef3ea7a..00000000 --- a/core/components/minishop3/src/Processors/Settings/Payment/Deliveries/Disable.php +++ /dev/null @@ -1,45 +0,0 @@ -object = $this->modx->getObject($this->classKey, $this->getProperties()); - if (empty($this->object)) { - return $this->modx->lexicon($this->objectType . '_err_nfs'); - } - - return true; - } - - - /** - * @return bool - */ - public function beforeSave() - { - $this->object->fromArray($this->getProperties(), '', true, true); - - return true; - } -} diff --git a/core/components/minishop3/src/Processors/Settings/Payment/Deliveries/Enable.php b/core/components/minishop3/src/Processors/Settings/Payment/Deliveries/Enable.php deleted file mode 100644 index 4b122cc1..00000000 --- a/core/components/minishop3/src/Processors/Settings/Payment/Deliveries/Enable.php +++ /dev/null @@ -1,27 +0,0 @@ -object->fromArray($this->getProperties(), '', true, true); - - return true; - } -} diff --git a/core/components/minishop3/src/Processors/Settings/Payment/Deliveries/GetList.php b/core/components/minishop3/src/Processors/Settings/Payment/Deliveries/GetList.php deleted file mode 100644 index 61209693..00000000 --- a/core/components/minishop3/src/Processors/Settings/Payment/Deliveries/GetList.php +++ /dev/null @@ -1,74 +0,0 @@ -leftJoin( - msDeliveryMember::class, - 'Payments', - "Payments.delivery_id = msDelivery.id AND Payments.payment_id = {$this->getProperty('payment')}" - ); - $c->select($this->modx->getSelectColumns($this->classKey, 'msDelivery')); - $c->select('(Payments.delivery_id is not null) as active'); - $c->groupby($this->sortAlias . '.id'); - - return $c; - } - - - /** - * @param xPDOObject $object - * - * @return array - */ - public function prepareRow(xPDOObject $object) - { - $data = $object->toArray(); - $data['actions'] = []; - - if (empty($data['active'])) { - $data['actions'][] = [ - 'cls' => 'fw-900', - 'icon' => 'icon icon-power-off action-green', - 'title' => $this->modx->lexicon('ms3_menu_enable'), - 'multiple' => $this->modx->lexicon('ms3_menu_enable'), - 'action' => 'enableDelivery', - 'button' => true, - 'menu' => true, - ]; - } else { - $data['actions'][] = [ - 'cls' => 'fw-900', - 'icon' => 'icon icon-power-off action-gray', - 'title' => $this->modx->lexicon('ms3_menu_disable'), - 'multiple' => $this->modx->lexicon('ms3_menu_disable'), - 'action' => 'disableDelivery', - 'button' => true, - 'menu' => true, - ]; - } - - return $data; - } -} diff --git a/core/components/minishop3/src/Processors/Settings/Payment/Deliveries/Multiple.php b/core/components/minishop3/src/Processors/Settings/Payment/Deliveries/Multiple.php deleted file mode 100644 index 97691a20..00000000 --- a/core/components/minishop3/src/Processors/Settings/Payment/Deliveries/Multiple.php +++ /dev/null @@ -1,32 +0,0 @@ -getProperty('method', false); - if (!$method) { - return $this->failure(); - } - $method = ucfirst($method); - $ids = json_decode($this->getProperty('ids'), true); - if (empty($ids)) { - return $this->success(); - } - - foreach ($ids as $id) { - $this->modx->runProcessor('MiniShop3\\Processors\\Settings\\Payment\\Deliveries\\' . $method, $id); - } - - return $this->success(); - } -} diff --git a/core/components/minishop3/src/Processors/Settings/Payment/GetList.php b/core/components/minishop3/src/Processors/Settings/Payment/GetList.php index 42a20b0f..6dbbd581 100644 --- a/core/components/minishop3/src/Processors/Settings/Payment/GetList.php +++ b/core/components/minishop3/src/Processors/Settings/Payment/GetList.php @@ -4,6 +4,7 @@ use MiniShop3\Model\msDeliveryMember; use MiniShop3\Model\msPayment; +use MiniShop3\Services\Settings\SettingsComboListService; use MODX\Revolution\Processors\Model\GetListProcessor; use xPDO\Om\xPDOObject; use xPDO\Om\xPDOQuery; @@ -15,6 +16,27 @@ class GetList extends GetListProcessor public $defaultSortDirection = 'asc'; public $permission = 'mssetting_list'; + /** + * Combo mode delegates to SettingsComboListService (same source as REST dropdowns). + * + * @return array|string + */ + public function process() + { + if ($this->getProperty('combo')) { + /** @var SettingsComboListService $comboList */ + $comboList = $this->modx->services->get('ms3_settings_combo_list'); + $rows = $comboList->listActivePaymentsForCombo( + (int) $this->getProperty('id'), + (int) $this->getProperty('delivery_id'), + trim((string) $this->getProperty('query', '')) + ); + + return $this->outputArray($rows, count($rows)); + } + + return parent::process(); + } /** * @param xPDOQuery $c @@ -23,15 +45,10 @@ class GetList extends GetListProcessor */ public function prepareQueryBeforeCount(xPDOQuery $c) { - if ($this->getProperty('combo')) { - $c->select('id,name'); - $c->where(['active' => 1, 'OR:id:=' => $this->getProperty('id')]); - } else { - $c->leftJoin(msDeliveryMember::class, 'Deliveries'); - $c->groupby('msPayment.id'); - $c->select($this->modx->getSelectColumns($this->classKey, 'msPayment')); - $c->select('COUNT(Deliveries.delivery_id) as deliveries'); - } + $c->leftJoin(msDeliveryMember::class, 'Deliveries'); + $c->groupby('msPayment.id'); + $c->select($this->modx->getSelectColumns($this->classKey, 'msPayment')); + $c->select('COUNT(Deliveries.delivery_id) as deliveries'); if ($query = trim($this->getProperty('query'))) { $c->where( [ @@ -45,7 +62,6 @@ public function prepareQueryBeforeCount(xPDOQuery $c) return $c; } - /** * @param xPDOObject $object * diff --git a/core/components/minishop3/src/Processors/Settings/Vendor/GetList.php b/core/components/minishop3/src/Processors/Settings/Vendor/GetList.php index 43310735..06b8cfea 100644 --- a/core/components/minishop3/src/Processors/Settings/Vendor/GetList.php +++ b/core/components/minishop3/src/Processors/Settings/Vendor/GetList.php @@ -3,6 +3,7 @@ namespace MiniShop3\Processors\Settings\Vendor; use MiniShop3\Model\msVendor; +use MiniShop3\Services\Settings\SettingsComboListService; use MODX\Revolution\modResource; use MODX\Revolution\Processors\Model\GetListProcessor; use xPDO\Om\xPDOObject; @@ -15,19 +16,30 @@ class GetList extends GetListProcessor public $defaultSortField = 'id'; public $defaultSortDirection = 'asc'; public $permission = 'mssetting_list'; - protected $item_id = 0; - /** - * @return bool|null|string + * Combo mode delegates to SettingsComboListService (same source as REST /references/vendors). + * + * @return array|string */ - public function initialize() + public function process() { - if ($this->getProperty('combo') && !$this->getProperty('limit') && $id = (int)$this->getProperty('id')) { - $this->item_id = $id; + if ($this->getProperty('combo')) { + $includeId = SettingsComboListService::resolvePinnedIncludeId( + (int) $this->getProperty('id'), + $this->getProperty('limit') + ); + /** @var SettingsComboListService $comboList */ + $comboList = $this->modx->services->get('ms3_settings_combo_list'); + $rows = $comboList->listVendorsForCombo( + $includeId, + trim((string) $this->getProperty('query', '')) + ); + + return $this->outputArray($rows, count($rows)); } - return parent::initialize(); + return parent::process(); } /** @@ -37,18 +49,9 @@ public function initialize() */ public function prepareQueryBeforeCount(xPDOQuery $c) { - if ($this->getProperty('combo')) { - $c->select('id,name'); - } else { - $c->leftJoin(modResource::class, 'Resource'); - $c->select($this->modx->getSelectColumns($this->classKey, 'msVendor')); - $c->select('Resource.pagetitle'); - } - - if (!empty($this->item_id)) { - $c->where(['id' => $this->item_id]); - return $c; - } + $c->leftJoin(modResource::class, 'Resource'); + $c->select($this->modx->getSelectColumns($this->classKey, 'msVendor')); + $c->select('Resource.pagetitle'); $query = trim($this->getProperty('query')); if (!empty($query)) { @@ -64,7 +67,6 @@ public function prepareQueryBeforeCount(xPDOQuery $c) return $c; } - /** * @param xPDOObject $object * @@ -72,40 +74,33 @@ public function prepareQueryBeforeCount(xPDOQuery $c) */ public function prepareRow(xPDOObject $object) { - if ($this->getProperty('combo')) { - $data = [ - 'id' => $object->get('id'), - 'name' => $object->get('name'), - ]; - } else { - $data = $object->toArray(); - if (!$data['resource_id']) { - $data['resource_id'] = null; - } - $data['actions'] = []; + $data = $object->toArray(); + if (!$data['resource_id']) { + $data['resource_id'] = null; + } + $data['actions'] = []; - $data['actions'][] = [ - 'cls' => '', - 'icon' => 'icon icon-edit', - 'title' => $this->modx->lexicon('ms3_menu_update'), - 'action' => 'updateVendor', - 'button' => true, - 'menu' => true, - ]; + $data['actions'][] = [ + 'cls' => '', + 'icon' => 'icon icon-edit', + 'title' => $this->modx->lexicon('ms3_menu_update'), + 'action' => 'updateVendor', + 'button' => true, + 'menu' => true, + ]; - $data['actions'][] = [ - 'cls' => [ - 'menu' => 'red', - 'button' => 'red', - ], - 'icon' => 'icon icon-trash-o', - 'title' => $this->modx->lexicon('ms3_menu_remove'), - 'multiple' => $this->modx->lexicon('ms3_menu_remove_multiple'), - 'action' => 'removeVendor', - 'button' => true, - 'menu' => true, - ]; - } + $data['actions'][] = [ + 'cls' => [ + 'menu' => 'red', + 'button' => 'red', + ], + 'icon' => 'icon icon-trash-o', + 'title' => $this->modx->lexicon('ms3_menu_remove'), + 'multiple' => $this->modx->lexicon('ms3_menu_remove_multiple'), + 'action' => 'removeVendor', + 'button' => true, + 'menu' => true, + ]; return $data; } diff --git a/core/components/minishop3/src/ServiceRegistry.php b/core/components/minishop3/src/ServiceRegistry.php index 04980615..6624d50c 100644 --- a/core/components/minishop3/src/ServiceRegistry.php +++ b/core/components/minishop3/src/ServiceRegistry.php @@ -202,6 +202,10 @@ class ServiceRegistry 'class' => \MiniShop3\Services\Category\CategoryTreeService::class, 'interface' => null, ], + 'ms3_settings_combo_list' => [ + 'class' => \MiniShop3\Services\Settings\SettingsComboListService::class, + 'interface' => null, + ], 'ms3_filter_config' => [ 'class' => \MiniShop3\Services\FilterConfigManager::class, 'interface' => null, diff --git a/core/components/minishop3/src/Services/Settings/SettingsComboListService.php b/core/components/minishop3/src/Services/Settings/SettingsComboListService.php new file mode 100644 index 00000000..e217467b --- /dev/null +++ b/core/components/minishop3/src/Services/Settings/SettingsComboListService.php @@ -0,0 +1,173 @@ + + */ + public function listVendorsForCombo(int $includeId = 0, string $query = ''): array + { + return $this->toComboRows($this->iterateVendors($includeId, $query)); + } + + /** + * @return list + */ + public function listActiveDeliveriesForCombo(int $includeId = 0, string $query = ''): array + { + return $this->toComboRows($this->iterateActiveDeliveries($includeId, $query)); + } + + /** + * @return list + */ + public function listActivePaymentsForCombo(int $includeId = 0, int $deliveryId = 0, string $query = ''): array + { + return $this->toComboRows($this->iterateActivePayments($includeId, $deliveryId, $query)); + } + + /** + * @return iterable + */ + public function iterateActiveDeliveries(int $includeId = 0, string $query = ''): iterable + { + $c = $this->modx->newQuery(msDelivery::class); + $c->where($this->activeOrIncludeCriteria($includeId)); + $c->sortby('position', 'ASC'); + $this->applyTextQuery($c, $query, ['name', 'description', 'class']); + + yield from $this->modx->getIterator(msDelivery::class, $c); + } + + /** + * @return iterable + */ + private function iterateVendors(int $includeId = 0, string $query = ''): iterable + { + if ($includeId > 0) { + $c = $this->modx->newQuery(msVendor::class, ['id' => $includeId]); + $c->select('id,name'); + yield from $this->modx->getIterator(msVendor::class, $c); + + return; + } + + $c = $this->modx->newQuery(msVendor::class); + $c->select('id,name'); + $c->sortby('name', 'ASC'); + $this->applyTextQuery($c, $query, ['name', 'description', 'country', 'email', 'address']); + + yield from $this->modx->getIterator(msVendor::class, $c); + } + + /** + * @return iterable + */ + public function iterateActivePayments(int $includeId = 0, int $deliveryId = 0, string $query = ''): iterable + { + $c = $this->modx->newQuery(msPayment::class); + $c->where($this->activeOrIncludeCriteria($includeId)); + + if ($deliveryId > 0) { + $c->innerJoin( + msDeliveryMember::class, + 'Member', + 'Member.payment_id = msPayment.id AND Member.delivery_id = ' . $deliveryId + ); + } + + $c->sortby('position', 'ASC'); + $this->applyTextQuery($c, $query, ['name', 'description', 'class']); + + yield from $this->modx->getIterator(msPayment::class, $c); + } + + /** + * Pin a single record by id when the client is not browsing a paged list. + * Matches ExtJS combo: includeId only when limit is unset/0. + */ + public static function resolvePinnedIncludeId(int $id, mixed $limit = null): int + { + if ($id <= 0) { + return 0; + } + + if ($limit !== null && $limit !== '' && (int) $limit > 0) { + return 0; + } + + return $id; + } + + /** + * @param iterable $objects + * @return list + */ + private function toComboRows(iterable $objects): array + { + $rows = []; + foreach ($objects as $object) { + $rows[] = [ + 'id' => (int) $object->get('id'), + 'name' => (string) $object->get('name'), + ]; + } + + return $rows; + } + + /** + * @return array + */ + private function activeOrIncludeCriteria(int $includeId): array + { + if ($includeId > 0) { + return [ + 'active' => 1, + 'OR:id:=' => $includeId, + ]; + } + + return ['active' => 1]; + } + + /** + * @param list $fields + */ + private function applyTextQuery(xPDOQuery $c, string $query, array $fields): void + { + $query = trim($query); + if ($query === '' || $fields === []) { + return; + } + + $criteria = []; + foreach ($fields as $i => $field) { + $key = $i === 0 ? "{$field}:LIKE" : "OR:{$field}:LIKE"; + $criteria[$key] = "%{$query}%"; + } + $c->where($criteria); + } +} diff --git a/core/components/minishop3/tests/PaymentsActiveDropdownFieldsTest.php b/core/components/minishop3/tests/PaymentsActiveDropdownFieldsTest.php new file mode 100644 index 00000000..cfeca6dd --- /dev/null +++ b/core/components/minishop3/tests/PaymentsActiveDropdownFieldsTest.php @@ -0,0 +1,96 @@ + 3, + 'name' => 'Card', + 'price' => '0', + 'active' => 1, + 'position' => 1, + 'properties' => ['secret' => 'x'], + 'class' => 'MiniShop3\\Controllers\\Payment\\Payment', + 'validation_rules' => '{}', + 'description' => 'should not leak', +]; + +$projected = PaymentsController::projectActiveDropdownFields($raw); + +foreach ($forbidden as $field) { + if (array_key_exists($field, $projected)) { + $fail("projected payload must not contain \"{$field}\""); + } +} + +if (array_key_exists('description', $projected)) { + $fail('projected payload must not contain non-whitelist field "description"'); +} + +foreach ($required as $field) { + if (!array_key_exists($field, $projected)) { + $fail("projected payload must keep \"{$field}\""); + } +} + +$controllerSource = file_get_contents(__DIR__ . '/../src/Controllers/Api/Manager/PaymentsController.php'); +if ($controllerSource === false) { + $fail('cannot read PaymentsController.php'); +} +if (!str_contains($controllerSource, 'foreach (self::ACTIVE_DROPDOWN_FIELDS as $field)')) { + $fail('formatActiveDropdownItem must iterate ACTIVE_DROPDOWN_FIELDS'); +} + +// Vendor pin semantics shared by processor + REST. +if (SettingsComboListService::resolvePinnedIncludeId(5, null) !== 5) { + $fail('resolvePinnedIncludeId must pin when limit is null'); +} +if (SettingsComboListService::resolvePinnedIncludeId(5, 0) !== 5) { + $fail('resolvePinnedIncludeId must pin when limit is 0'); +} +if (SettingsComboListService::resolvePinnedIncludeId(5, 20) !== 0) { + $fail('resolvePinnedIncludeId must not pin when browsing with limit>0'); +} +if (SettingsComboListService::resolvePinnedIncludeId(0, null) !== 0) { + $fail('resolvePinnedIncludeId must ignore non-positive id'); +} + +fwrite(STDOUT, "OK PaymentsActiveDropdownFieldsTest\n"); +exit(0); diff --git a/core/components/minishop3/tests/SettingsDualApiConsolidationTest.php b/core/components/minishop3/tests/SettingsDualApiConsolidationTest.php new file mode 100644 index 00000000..02010e05 --- /dev/null +++ b/core/components/minishop3/tests/SettingsDualApiConsolidationTest.php @@ -0,0 +1,105 @@ + Date: Thu, 30 Jul 2026 09:44:03 +0600 Subject: [PATCH 2/2] style(routes): PSR-12 space after function in payments-active route --- core/components/minishop3/config/routes/manager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/components/minishop3/config/routes/manager.php b/core/components/minishop3/config/routes/manager.php index c2711340..3112ccd9 100644 --- a/core/components/minishop3/config/routes/manager.php +++ b/core/components/minishop3/config/routes/manager.php @@ -965,7 +965,7 @@ }); // Dropdown list of active payments (optional delivery_id filter via msDeliveryMember) - $router->get('/payments-active', function($params) use ($modx) { + $router->get('/payments-active', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Manager\PaymentsController($modx); return $controller->getActiveDropdown($params); });