Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions _build/elements/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@
'xtype' => 'numberfield',
'area' => 'ms3_statuses',
],
'ms3_order_status_transitions' => [
'value' => '',
'xtype' => 'textfield',
'area' => 'ms3_statuses',
],
'ms3_customer_cancel_allowed_statuses' => [
'value' => '2,3',
'xtype' => 'textfield',
Expand Down
2 changes: 2 additions & 0 deletions core/components/minishop3/lexicon/en/default.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@
$_lang['ms3_err_status_fixed'] = 'Fixed status is set. You cannot change it to earlier one.';
$_lang['ms3_err_status_wrong'] = 'Invalid order status.';
$_lang['ms3_err_status_same'] = 'This status is already set.';
$_lang['ms3_err_status_transition'] = 'This status transition is not allowed.';
$_lang['ms3_err_status_transitions_invalid'] = 'Order status transition allow-list is invalid.';
$_lang['ms3_err_register_globals'] = 'Error: php parameter <b>register_globals</b> must be disabled.';
$_lang['ms3_err_link_equal'] = 'You are trying to add product link to itself';
$_lang['ms3_err_no_link'] = 'Link type not found';
Expand Down
4 changes: 3 additions & 1 deletion core/components/minishop3/lexicon/en/setting.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,15 @@
$_lang['setting_ms3_status_canceled'] = 'Canceled order status ID';
$_lang['setting_ms3_status_canceled_desc'] = 'What status to set when canceling order';
$_lang['setting_ms3_status_sent'] = 'Sent order status ID';
$_lang['setting_ms3_status_sent_desc'] = 'Order status to set when a shipment becomes shipped (if the transition is allowed).';
$_lang['setting_ms3_status_sent_desc'] = 'Order status to set when a shipment becomes shipped (if the transition is allowed). Also used as the shipped status for order lifecycle ports (default seed: 4).';
$_lang['setting_ms3_shipment_enabled'] = 'Enable shipment lifecycle';
$_lang['setting_ms3_shipment_enabled_desc'] = 'Off (default): checkout and order statuses are unchanged. On: shipment shipped maps to ms3_status_sent via OrderStatusService, cancelled/failed maps to ms3_status_canceled. Create/setTracking still work when off. Webhook is 404 when off. Replace ms3_shipment_lifecycle to use an external WMS.';
$_lang['setting_ms3_shipment_on_delivered_status'] = 'Order status ID on delivered shipment';
$_lang['setting_ms3_shipment_on_delivered_status_desc'] = 'Optional. 0 (default) keeps order status unchanged when the shipment becomes delivered. Seed sent is final, so leave 0 unless you use a non-final sent status.';
$_lang['setting_ms3_shipment_on_in_transit_status'] = 'Order status ID on in-transit shipment';
$_lang['setting_ms3_shipment_on_in_transit_status_desc'] = 'Optional. 0 (default) keeps order status unchanged when the shipment becomes in_transit.';
$_lang['setting_ms3_order_status_transitions'] = 'Allowed order status transitions';
$_lang['setting_ms3_order_status_transitions_desc'] = 'Optional allow-list of status edges in addition to final/fixed rules. Empty = no matrix (default final/fixed only). Format: CSV pairs from:to (e.g. 2:3,3:4,2:5) or JSON [[2,3],[3,4]].';
$_lang['setting_ms3_customer_cancel_allowed_statuses'] = 'Statuses from which customer can cancel order';
$_lang['setting_ms3_customer_cancel_allowed_statuses_desc'] = 'Comma-separated status IDs. Default: New and Paid (2,3). Empty = use ms3_status_new and ms3_status_paid.';
$_lang['setting_ms3_status_for_stat'] = 'Status IDs for statistics';
Expand Down
2 changes: 2 additions & 0 deletions core/components/minishop3/lexicon/ru/default.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@
$_lang['ms3_err_status_fixed'] = 'Установлен фиксирующий статус. Вы не можете сменить его на более ранний.';
$_lang['ms3_err_status_wrong'] = 'Неверный статус заказа.';
$_lang['ms3_err_status_same'] = 'Этот статус уже установлен.';
$_lang['ms3_err_status_transition'] = 'Такой переход статуса не разрешён.';
$_lang['ms3_err_status_transitions_invalid'] = 'Некорректный allow-list переходов статусов заказа.';
$_lang['ms3_err_register_globals'] = 'Ошибка: php параметр <b>register_globals</b> должен быть выключен.';
$_lang['ms3_err_link_equal'] = 'Вы пытаетесь добавить товару ссылку на самого себя';
$_lang['ms3_err_no_link'] = 'Тип связи не найден';
Expand Down
4 changes: 3 additions & 1 deletion core/components/minishop3/lexicon/ru/setting.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,15 @@
$_lang['setting_ms3_status_canceled'] = 'ID статуса отмены заказа';
$_lang['setting_ms3_status_canceled_desc'] = 'Какой статус нужно устанавливать при отмене заказа';
$_lang['setting_ms3_status_sent'] = 'ID статуса «отправлен»';
$_lang['setting_ms3_status_sent_desc'] = 'Статус заказа при переходе отгрузки в shipped, если переход разрешён.';
$_lang['setting_ms3_status_sent_desc'] = 'Статус заказа при переходе отгрузки в shipped, если переход разрешён. Также используется как shipped-статус для order lifecycle ports (по умолчанию seed id 4).';
$_lang['setting_ms3_shipment_enabled'] = 'Включить lifecycle отгрузки';
$_lang['setting_ms3_shipment_enabled_desc'] = 'Выкл. (по умолчанию): оформление и статусы заказа как сейчас. Вкл.: shipped ставит ms3_status_sent через OrderStatusService, cancelled/failed — ms3_status_canceled. create/setTracking работают и при выкл. Webhook при выкл. отвечает 404. Внешний WMS подменяется через ms3_shipment_lifecycle.';
$_lang['setting_ms3_shipment_on_delivered_status'] = 'ID статуса заказа при delivered';
$_lang['setting_ms3_shipment_on_delivered_status_desc'] = 'Необязательно. 0 (по умолчанию) не меняет статус заказа, когда отгрузка становится delivered. Сид sent финальный, поэтому оставьте 0, если не используете нефинальный sent.';
$_lang['setting_ms3_shipment_on_in_transit_status'] = 'ID статуса заказа при in_transit';
$_lang['setting_ms3_shipment_on_in_transit_status_desc'] = 'Необязательно. 0 (по умолчанию) не меняет статус заказа, когда отгрузка становится in_transit.';
$_lang['setting_ms3_order_status_transitions'] = 'Разрешённые переходы статусов заказа';
$_lang['setting_ms3_order_status_transitions_desc'] = 'Опциональный allow-list рёбер поверх правил final/fixed. Пусто — только final/fixed. Формат: CSV пары from:to (например 2:3,3:4,2:5) или JSON [[2,3],[3,4]].';
$_lang['setting_ms3_customer_cancel_allowed_statuses'] = 'Статусы, из которых покупатель может отменить заказ';
$_lang['setting_ms3_customer_cancel_allowed_statuses_desc'] = 'ID статусов через запятую. По умолчанию: «Новый» и «Оплачен» (2,3). Пусто — использовать ms3_status_new и ms3_status_paid.';
$_lang['setting_ms3_status_for_stat'] = 'ID статусов для статистики';
Expand Down
25 changes: 13 additions & 12 deletions core/components/minishop3/src/ServiceRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class ServiceRegistry
'ms3_order_number_generator',
],
'ms3_order_finalize' => ['ms3_order_number_generator'],
'ms3_order_status' => ['ms3_order_log'],
'ms3_order_status' => ['ms3_order_log', 'ms3_order_lifecycle_ports'],
'ms3_payment_lifecycle' => ['ms3_order_status'],
'ms3_cart_mutation_handler' => [
'ms3_order_draft_manager',
Expand Down Expand Up @@ -286,6 +286,10 @@ class ServiceRegistry
'class' => \MiniShop3\Services\Order\OrderLogService::class,
'interface' => null,
],
'ms3_order_lifecycle_ports' => [
'class' => \MiniShop3\Services\Order\NullOrderLifecyclePorts::class,
'interface' => \MiniShop3\Services\Order\OrderLifecyclePortsInterface::class,
],
'ms3_order_status' => [
'class' => \MiniShop3\Services\Order\OrderStatusService::class,
'interface' => null,
Expand Down Expand Up @@ -722,19 +726,16 @@ protected function validateClass(
return $fallbackClass;
}

if ($requiredInterface) {
$interfaces = class_implements($className);
if (!in_array($requiredInterface, $interfaces ?: [])) {
$this->modx->log(
modX::LOG_LEVEL_ERROR,
"[MiniShop3 ServiceRegistry] Class '{$className}' must implement {$requiredInterface}, "
. 'using fallback'
);
return $fallbackClass;
}
if ($requiredInterface && !is_a($className, $requiredInterface, true)) {
$this->modx->log(
modX::LOG_LEVEL_ERROR,
"[MiniShop3 ServiceRegistry] Class '{$className}' must implement {$requiredInterface}, "
. 'using fallback'
);
return $fallbackClass;
}

if (!is_subclass_of($className, $fallbackClass)) {
if (!$requiredInterface && !is_subclass_of($className, $fallbackClass)) {
$this->modx->log(
modX::LOG_LEVEL_ERROR,
"[MiniShop3 ServiceRegistry] Class '{$className}' must extend {$fallbackClass}, using fallback"
Expand Down
7 changes: 6 additions & 1 deletion core/components/minishop3/src/ServiceRegistryFactories.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,16 @@ public static function map(): array
);
},

'ms3_order_lifecycle_ports' => static function (modX $modx, object $services, string $class): object {
return new $class();
},

'ms3_order_status' => static function (modX $modx, object $services, string $class): object {
return new $class(
$modx,
self::ms3($modx),
$services->get('ms3_order_log')
$services->get('ms3_order_log'),
$services->get('ms3_order_lifecycle_ports')
);
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,19 @@ public function update(array $params = []): array

// Store old values for logging
$oldStatusId = (int)$order->get('status_id');
$pendingStatusId = array_key_exists('status_id', $params)
? (int) $params['status_id']
: null;

// Get editable order fields from msModelField configuration
$orderFields = $this->presenter->getModelFieldNames('msOrder');
$changedOrderFields = [];

foreach ($orderFields as $field) {
// Non-draft status changes go only through OrderStatusService (#592).
if ($field === 'status_id') {
continue;
}
if (array_key_exists($field, $params)) {
$oldValue = $order->get($field);
$newValue = $params[$field];
Expand Down Expand Up @@ -257,8 +264,7 @@ public function update(array $params = []): array
return $this->error('Failed to update order', HttpStatus::INTERNAL_SERVER_ERROR);
}

// Log order field changes (excluding status_id which is logged separately)
unset($changedOrderFields['status_id']);
// Log order field changes (status_id is logged by OrderStatusService)
if (!empty($changedOrderFields)) {
$this->getOrderLog()->addEntry(
$id,
Expand Down Expand Up @@ -325,15 +331,10 @@ public function update(array $params = []): array
}

// Handle status change via OrderStatusService (sends notifications)
$newStatusId = (int)$order->get('status_id');
if ($oldStatusId !== $newStatusId) {
// Revert status to old value - OrderStatusService will change it properly
$order->set('status_id', $oldStatusId);
$order->save();

if ($pendingStatusId !== null && $pendingStatusId !== $oldStatusId) {
/** @var OrderStatusService $orderStatusService */
$orderStatusService = $this->modx->services->get('ms3_order_status');
$result = $orderStatusService->change((int)$order->get('id'), $newStatusId);
$result = $orderStatusService->change((int)$order->get('id'), $pendingStatusId);

if ($result !== true) {
return $this->error(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace MiniShop3\Services\Order;

use MiniShop3\Model\msOrder;

/**
* No-op lifecycle ports until inventory / payment / shipment domains are wired (#589–#591).
*/
final class NullOrderLifecyclePorts implements OrderLifecyclePortsInterface
{
public function onOrderBecamePaid(msOrder $order, ?int $previousStatusId): ?string
{
return null;
}

public function onOrderCancelled(msOrder $order, ?int $previousStatusId): ?string
{
return null;
}

public function onOrderShipped(msOrder $order, ?int $previousStatusId): ?string
{
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace MiniShop3\Services\Order;

use MiniShop3\Model\msOrder;

/**
* In-TX domain ports invoked by {@see OrderStatusService} before status_id is persisted.
*
* Implementations land with inventory (#589 / #603), payment (#590), and shipment (#591).
* Core ships {@see NullOrderLifecyclePorts} until those domains exist.
*
* A non-null return aborts the transition (transaction rollback); status_id is not saved.
* Ports run inside the same DB transaction as the status write when the connection supports it.
*/
interface OrderLifecyclePortsInterface
{
/**
* Order is transitioning to the configured "paid" status (ms3_status_paid).
*
* @return string|null Lexicon/error message on failure; null on success
*/
public function onOrderBecamePaid(msOrder $order, ?int $previousStatusId): ?string;

/**
* Order is transitioning to the configured canceled status (ms3_status_canceled).
*
* @return string|null Lexicon/error message on failure; null on success
*/
public function onOrderCancelled(msOrder $order, ?int $previousStatusId): ?string;

/**
* Order is transitioning to the configured shipped/sent status (ms3_status_sent, default 4).
*
* @return string|null Lexicon/error message on failure; null on success
*/
public function onOrderShipped(msOrder $order, ?int $previousStatusId): ?string;
}
Loading
Loading