Skip to content

Commit a75ef83

Browse files
committed
AC-15419::Order remains in status processing after shipping, if items get partially refunded
1 parent efbad7e commit a75ef83

File tree

2 files changed

+117
-156
lines changed
  • app/code/Magento/Sales

2 files changed

+117
-156
lines changed

app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ private function checkForClosedState(Order $order, ?string $currentState): bool
118118
*
119119
* @param Order $order
120120
* @return bool
121+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
121122
*/
122123
private function areAllItemsFulfilled(Order $order): bool
123124
{

app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php

Lines changed: 116 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -83,119 +83,17 @@ public function testChildOfBundleShippedTogetherIsSkippedAndParentDeterminesFulf
8383

8484
$order->method('getState')->willReturn(Order::STATE_PROCESSING);
8585
$order->method('getIsInProcess')->willReturn(false);
86-
87-
// Avoid early return
8886
$order->method('isCanceled')->willReturn(false);
8987
$order->method('canUnhold')->willReturn(false);
9088
$order->method('canInvoice')->willReturn(false);
9189
$order->method('getInvoiceCollection')->willReturn($this->createInvoiceCollection([]));
9290
$order->method('getTotalDue')->willReturn(0);
93-
94-
// Closed state preconditions (first branch)
9591
$order->method('canCreditmemo')->willReturn(false);
9692
$order->method('getIsNotVirtual')->willReturn(true);
9793
$order->method('canShip')->willReturn(true);
9894

99-
// Build a parent bundle item with shipment together and fully fulfilled
100-
$bundleProduct = new class {
101-
public function getShipmentType()
102-
{
103-
return AbstractType::SHIPMENT_TOGETHER;
104-
}
105-
};
106-
107-
$parentItem = new class($bundleProduct) {
108-
/** @var object */
109-
private $bundleProduct;
110-
public function __construct($bundleProduct)
111-
{
112-
$this->bundleProduct = $bundleProduct;
113-
}
114-
public function getIsVirtual()
115-
{
116-
return false;
117-
}
118-
public function getLockedDoShip()
119-
{
120-
return false;
121-
}
122-
public function getParentItem()
123-
{
124-
return null;
125-
}
126-
public function getProductType()
127-
{
128-
return Type::TYPE_BUNDLE;
129-
}
130-
public function getProduct()
131-
{
132-
return $this->bundleProduct;
133-
}
134-
public function getQtyOrdered()
135-
{
136-
return 1;
137-
}
138-
public function getQtyCanceled()
139-
{
140-
return 0;
141-
}
142-
public function getQtyShipped()
143-
{
144-
return 1;
145-
}
146-
public function getQtyRefunded()
147-
{
148-
return 0;
149-
}
150-
};
151-
152-
// Child item should be skipped due to parent bundle shipped together
153-
$childItem = new class($parentItem) {
154-
/** @var object */
155-
private $parentItem;
156-
public function __construct($parentItem)
157-
{
158-
$this->parentItem = $parentItem;
159-
}
160-
public function getIsVirtual()
161-
{
162-
return false;
163-
}
164-
public function getLockedDoShip()
165-
{
166-
return false;
167-
}
168-
public function getParentItem()
169-
{
170-
return $this->parentItem;
171-
}
172-
public function getProductType()
173-
{
174-
return 'simple';
175-
}
176-
public function getProduct()
177-
{
178-
return null;
179-
}
180-
public function getQtyOrdered()
181-
{
182-
return 1;
183-
}
184-
public function getQtyCanceled()
185-
{
186-
return 0;
187-
}
188-
public function getQtyShipped()
189-
{
190-
return 0;
191-
} // would be open if not skipped
192-
public function getQtyRefunded()
193-
{
194-
return 0;
195-
}
196-
};
197-
198-
// Ensure the loop hits the child first to exercise the continue branch
95+
$parentItem = $this->createBundleParentShippedTogetherFulfilled();
96+
$childItem = $this->createChildItemForParent($parentItem, 0);
19997
$order->method('getAllItems')->willReturn([$childItem, $parentItem]);
20098

20199
$order->expects($this->once())
@@ -423,12 +321,46 @@ public function testSubjectChoosesParentWhenSecondEvaluationMatchesBundleShipped
423321
$order->method('getIsNotVirtual')->willReturn(true);
424322
$order->method('canShip')->willReturn(true);
425323

426-
// Parent whose methods return different values across calls
427-
$parentWithSwitchingBehavior = new class {
428-
/** @var int */
429-
private $typeCall = 0;
430-
/** @var int */
431-
private $productCall = 0;
324+
$parentWithSwitchingBehavior = $this->createSwitchingParentForBundleSelection();
325+
$childItem = $this->createChildItemForParent($parentWithSwitchingBehavior, 0);
326+
$order->method('getAllItems')->willReturn([$childItem]);
327+
328+
$order->expects($this->once())
329+
->method('setState')
330+
->with(Order::STATE_CLOSED)
331+
->willReturnSelf();
332+
333+
$config->expects($this->once())
334+
->method('getStateDefaultStatus')
335+
->with(Order::STATE_CLOSED)
336+
->willReturn('closed');
337+
338+
$order->expects($this->once())
339+
->method('setStatus')
340+
->with('closed')
341+
->willReturnSelf();
342+
343+
$order->method('getConfig')->willReturn($config);
344+
345+
$this->subject->check($order);
346+
}
347+
348+
private function createBundleParentShippedTogetherFulfilled(): object
349+
{
350+
$bundleProduct = new class {
351+
public function getShipmentType()
352+
{
353+
return AbstractType::SHIPMENT_TOGETHER;
354+
}
355+
};
356+
357+
return new class($bundleProduct) {
358+
/** @var object */
359+
private $bundleProduct;
360+
public function __construct($bundleProduct)
361+
{
362+
$this->bundleProduct = $bundleProduct;
363+
}
432364
public function getIsVirtual()
433365
{
434366
return false;
@@ -443,27 +375,11 @@ public function getParentItem()
443375
}
444376
public function getProductType()
445377
{
446-
$this->typeCall++;
447-
return $this->typeCall === 1 ? 'simple' : Type::TYPE_BUNDLE;
378+
return Type::TYPE_BUNDLE;
448379
}
449380
public function getProduct()
450381
{
451-
$this->productCall++;
452-
$callIndex = $this->productCall;
453-
return new class($callIndex) {
454-
/** @var int */
455-
private $callIndex;
456-
public function __construct($callIndex)
457-
{
458-
$this->callIndex = $callIndex;
459-
}
460-
public function getShipmentType()
461-
{
462-
return $this->callIndex === 1
463-
? AbstractType::SHIPMENT_SEPARATELY
464-
: AbstractType::SHIPMENT_TOGETHER;
465-
}
466-
};
382+
return $this->bundleProduct;
467383
}
468384
public function getQtyOrdered()
469385
{
@@ -482,15 +398,15 @@ public function getQtyRefunded()
482398
return 0;
483399
}
484400
};
401+
}
485402

486-
// Child referencing the parent; child has open qty but should use parent as subject
487-
$childItem = new class($parentWithSwitchingBehavior) {
403+
private function createChildItemForParent(object $parent, int $shipped): object
404+
{
405+
$child = new class {
488406
/** @var object */
489-
private $parent;
490-
public function __construct($parent)
491-
{
492-
$this->parent = $parent;
493-
}
407+
public $parent;
408+
/** @var int */
409+
public $shipped = 0;
494410
public function getIsVirtual()
495411
{
496412
return false;
@@ -521,34 +437,78 @@ public function getQtyCanceled()
521437
}
522438
public function getQtyShipped()
523439
{
524-
return 0;
440+
return $this->shipped;
525441
}
526442
public function getQtyRefunded()
527443
{
528444
return 0;
529445
}
530446
};
447+
$child->parent = $parent;
448+
$child->shipped = $shipped;
449+
return $child;
450+
}
531451

532-
$order->method('getAllItems')->willReturn([$childItem]);
533-
534-
$order->expects($this->once())
535-
->method('setState')
536-
->with(Order::STATE_CLOSED)
537-
->willReturnSelf();
538-
539-
$config->expects($this->once())
540-
->method('getStateDefaultStatus')
541-
->with(Order::STATE_CLOSED)
542-
->willReturn('closed');
543-
544-
$order->expects($this->once())
545-
->method('setStatus')
546-
->with('closed')
547-
->willReturnSelf();
548-
549-
$order->method('getConfig')->willReturn($config);
550-
551-
$this->subject->check($order);
452+
private function createSwitchingParentForBundleSelection(): object
453+
{
454+
return new class {
455+
/** @var int */
456+
private $typeCall = 0;
457+
/** @var int */
458+
private $productCall = 0;
459+
public function getIsVirtual()
460+
{
461+
return false;
462+
}
463+
public function getLockedDoShip()
464+
{
465+
return false;
466+
}
467+
public function getParentItem()
468+
{
469+
return null;
470+
}
471+
public function getProductType()
472+
{
473+
$this->typeCall++;
474+
return $this->typeCall === 1 ? 'simple' : Type::TYPE_BUNDLE;
475+
}
476+
public function getProduct()
477+
{
478+
$this->productCall++;
479+
$callIndex = $this->productCall;
480+
return new class($callIndex) {
481+
/** @var int */
482+
private $callIndex;
483+
public function __construct($callIndex)
484+
{
485+
$this->callIndex = $callIndex;
486+
}
487+
public function getShipmentType()
488+
{
489+
return $this->callIndex === 1
490+
? AbstractType::SHIPMENT_SEPARATELY
491+
: AbstractType::SHIPMENT_TOGETHER;
492+
}
493+
};
494+
}
495+
public function getQtyOrdered()
496+
{
497+
return 1;
498+
}
499+
public function getQtyCanceled()
500+
{
501+
return 0;
502+
}
503+
public function getQtyShipped()
504+
{
505+
return 1;
506+
}
507+
public function getQtyRefunded()
508+
{
509+
return 0;
510+
}
511+
};
552512
}
553513

554514
public function testEarlyReturnWhenOpenInvoiceAndTotalDue(): void

0 commit comments

Comments
 (0)