OrderTest   A
last analyzed

Complexity

Total Complexity 13

Size/Duplication

Total Lines 341
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 187
c 3
b 0
f 0
dl 0
loc 341
rs 10
wmc 13

13 Methods

Rating   Name   Duplication   Size   Complexity  
A testSearchFields() 0 4 1
A setUp() 0 11 1
A testDebug() 0 5 1
A testDelete() 0 51 1
A testCMSFields() 0 4 1
A testCanFunctions() 0 61 1
A testTotals() 0 7 1
A tearDown() 0 6 1
A testRounding() 0 13 1
A testOrderAddress() 0 40 1
A testOrderItems() 0 15 1
A testPlacedOrderImmutability() 0 42 1
A testStatusChange() 0 32 1
1
<?php
2
3
namespace SilverShop\Tests\Model;
4
5
use SilverShop\Checkout\OrderProcessor;
6
use SilverShop\Model\Address;
7
use SilverShop\Model\Modifiers\OrderModifier;
8
use SilverShop\Model\Modifiers\Tax\FlatTax;
9
use SilverShop\Model\Order;
10
use SilverShop\Model\OrderStatusLog;
11
use SilverShop\Model\Product\OrderItem;
12
use SilverShop\Page\Product;
13
use SilverShop\Tests\Model\Product\CustomProduct_OrderItem;
14
use SilverShop\Tests\ShopTest;
15
use SilverStripe\Core\Config\Config;
16
use SilverStripe\Dev\SapphireTest;
17
use SilverStripe\Omnipay\Model\Payment;
18
use SilverStripe\ORM\DataObject;
19
20
/**
21
 * Order Unit Tests
22
 *
23
 * @package    silvershop
24
 * @subpackage tests
25
 */
26
class OrderTest extends SapphireTest
27
{
28
    public static $fixture_file = __DIR__ . '/../Fixtures/shop.yml';
29
30
    // This seems to be required, because we query the OrderItem table and thus this gets included…
31
    // TODO: Remove once we figure out how to circumvent that…
32
    protected static $extra_dataobjects = [
33
        CustomProduct_OrderItem::class,
34
    ];
35
36
    /**
37
     * @var Product
38
     */
39
    protected $mp3player;
40
41
    /**
42
     * @var Product
43
     */
44
    protected $socks;
45
46
    /**
47
     * @var Product
48
     */
49
    protected $beachball;
50
51
52
    public function setUp(): void
53
    {
54
        parent::setUp();
55
        ShopTest::setConfiguration();
56
        $this->logInWithPermission('ADMIN');
57
        $this->mp3player = $this->objFromFixture(Product::class, 'mp3player');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->objFromFixture(Si...ct::class, 'mp3player') of type SilverStripe\ORM\DataObject is incompatible with the declared type SilverShop\Page\Product of property $mp3player.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
58
        $this->mp3player->publishSingle();
59
        $this->socks = $this->objFromFixture(Product::class, 'socks');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->objFromFixture(Si...roduct::class, 'socks') of type SilverStripe\ORM\DataObject is incompatible with the declared type SilverShop\Page\Product of property $socks.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
60
        $this->socks->publishSingle();
61
        $this->beachball = $this->objFromFixture(Product::class, 'beachball');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->objFromFixture(Si...ct::class, 'beachball') of type SilverStripe\ORM\DataObject is incompatible with the declared type SilverShop\Page\Product of property $beachball.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
62
        $this->beachball->publishSingle();
63
    }
64
65
    public function tearDown(): void
66
    {
67
        parent::tearDown();
68
        unset($this->mp3player);
69
        unset($this->socks);
70
        unset($this->beachball);
71
    }
72
73
    public function testCMSFields()
74
    {
75
        //singleton(Order::class)->getCMSFields();
76
        $this->markTestIncomplete('assertions!');
77
    }
78
79
    public function testSearchFields()
80
    {
81
        //singleton(Order::class)->scaffoldSearchFields();
82
        $this->markTestIncomplete('assertions!');
83
    }
84
85
    public function testDebug()
86
    {
87
        //$order = $this->objFromFixture(Order::class, "cart");
88
        //$order->debug();
89
        $this->markTestIncomplete('assertions!');
90
    }
91
92
    public function testOrderItems()
93
    {
94
        $order = $this->objFromFixture(Order::class, "paid");
95
        $items = $order->Items();
96
        $this->assertNotNull($items);
97
        $this->assertListEquals(
98
            [
99
                ['ProductID' => $this->mp3player->ID, 'Quantity' => 2, 'CalculatedTotal' => 400],
100
                ['ProductID' => $this->socks->ID, 'Quantity' => 1, 'CalculatedTotal' => 8],
101
            ],
102
            $items
103
        );
104
        $this->assertEquals(3, $items->Quantity(), "Quantity is 3");
0 ignored issues
show
Bug introduced by
The method Quantity() does not exist on SilverStripe\ORM\SS_List. It seems like you code against a sub-type of said class. However, the method does not exist in SilverStripe\ORM\Sortable or SilverStripe\ORM\Filterable or SilverStripe\ORM\Relation or SilverStripe\ORM\Limitable or SilverStripe\ORM\Relation or SilverStripe\ORM\Relation or SilverStripe\ORM\Relation. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

104
        $this->assertEquals(3, $items->/** @scrutinizer ignore-call */ Quantity(), "Quantity is 3");
Loading history...
105
        $this->assertTrue($items->Plural(), "There is more than one item");
0 ignored issues
show
Bug introduced by
The method Plural() does not exist on SilverStripe\ORM\SS_List. It seems like you code against a sub-type of said class. However, the method does not exist in SilverStripe\ORM\Sortable or SilverStripe\ORM\Filterable or SilverStripe\ORM\Relation or SilverStripe\ORM\Limitable or SilverStripe\ORM\Relation or SilverStripe\ORM\Relation or SilverStripe\ORM\Relation. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

105
        $this->assertTrue($items->/** @scrutinizer ignore-call */ Plural(), "There is more than one item");
Loading history...
106
        $this->assertEquals(0.7, $items->Sum('Weight', true), "Total order weight sums correctly", 0.0001);
0 ignored issues
show
Bug introduced by
The method Sum() does not exist on SilverStripe\ORM\SS_List. It seems like you code against a sub-type of said class. However, the method does not exist in SilverStripe\ORM\Sortable or SilverStripe\ORM\Filterable or SilverStripe\ORM\Relation or SilverStripe\ORM\Limitable or SilverStripe\ORM\Relation or SilverStripe\ORM\Relation or SilverStripe\ORM\Relation. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

106
        $this->assertEquals(0.7, $items->/** @scrutinizer ignore-call */ Sum('Weight', true), "Total order weight sums correctly", 0.0001);
Loading history...
107
    }
108
109
    public function testTotals()
110
    {
111
        $order = $this->objFromFixture(Order::class, "paid");
112
        $this->assertEquals(408, $order->SubTotal(), "Subtotal is correct"); // 200 + 200 + 8
113
        $this->assertEquals(408, $order->GrandTotal(), "Grand total is correct");
114
        $this->assertEquals(200, $order->TotalPaid(), "Outstanding total is correct");
115
        $this->assertEquals(208, $order->TotalOutstanding(), "Outstanding total is correct");
116
    }
117
118
    public function testRounding()
119
    {
120
        //create an order with unrounded total
121
        $order = new Order(
122
            [
123
                'Total' => 123.257323,
124
                //NOTE: setTotal isn't called here, so un-rounded data *could* get in to the object
125
                'Status' => 'Unpaid',
126
            ]
127
        );
128
        $order->Total = 123.257323; //setTotal IS called here
0 ignored issues
show
Documentation Bug introduced by
It seems like 123.257323 of type double is incompatible with the declared type SilverStripe\ORM\FieldType\DBCurrency of property $Total.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
129
        $this->assertEquals(123.26, $order->Total(), "Check total rounds appropriately");
130
        $this->assertEquals(123.26, $order->TotalOutstanding(), "Check total outstanding rounds appropriately");
131
    }
132
133
    public function testPlacedOrderImmutability()
134
    {
135
136
        $order = $this->objFromFixture(Order::class, "paid");
137
        $processor = OrderProcessor::create($order)->placeOrder();
0 ignored issues
show
Unused Code introduced by
The assignment to $processor is dead and can be removed.
Loading history...
138
        $this->assertEquals(408, $order->Total(), "check totals");
139
140
        //make a changes to existing products
141
        $this->mp3player->BasePrice = 100;
0 ignored issues
show
Documentation Bug introduced by
It seems like 100 of type integer is incompatible with the declared type SilverStripe\ORM\FieldType\DBCurrency of property $BasePrice.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
142
        $this->mp3player->write();
143
        $this->socks->BasePrice = 20;
0 ignored issues
show
Documentation Bug introduced by
It seems like 20 of type integer is incompatible with the declared type SilverStripe\ORM\FieldType\DBCurrency of property $BasePrice.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
144
        $this->socks->write();
145
146
        //total doesn't change
147
        $this->assertEquals(408, $order->Total());
148
        $this->assertFalse($order->isCart());
149
150
        //item values don't change
151
        $items = $order->Items()
152
            //hack join to make thigns work
153
            ->innerJoin(
154
                "SilverShop_Product_OrderItem",
155
                '"SilverShop_OrderItem"."ID" = "SilverShop_Product_OrderItem"."ID"'
156
            );
157
        $this->assertNotNull($items);
158
        $this->assertListEquals(
159
            [
160
                ['ProductID' => $this->mp3player->ID, 'Quantity' => 2, 'CalculatedTotal' => 400],
161
                ['ProductID' => $this->socks->ID, 'Quantity' => 1, 'CalculatedTotal' => 8],
162
            ],
163
            $items
164
        );
165
166
        $mp3player = $items->find('ProductID', $this->mp3player->ID);//join needed to provide ProductID
167
        $this->assertNotNull($mp3player, "MP3 player is in order");
168
        $this->assertEquals(200, $mp3player->UnitPrice(), "Unit price remains the same");
169
        $this->assertEquals(400, $mp3player->Total(), "Total remains the same");
170
171
        $socks = $items->find('ProductID', $this->socks->ID);
172
        $this->assertNotNull($socks, "Socks are in order");
173
        $this->assertEquals(8, $socks->UnitPrice(), "Unit price remains the same");
174
        $this->assertEquals(8, $socks->Total(), "Total remains the same");
175
    }
176
177
    public function testCanFunctions()
178
    {
179
        $order = $this->objFromFixture(Order::class, "cart");
180
        $order->calculate();
181
        $this->assertTrue($order->canPay(), "can pay when order is in cart");
182
        $this->assertFalse($order->canCancel(), "can't cancel when order is in cart");
183
        $this->assertFalse($order->canDelete(), "never allow deleting orders");
184
        $this->assertTrue($order->canEdit(), "orders can be edited by anyone");
185
        $this->assertFalse($order->canCreate(), "no body can create orders manually");
186
187
        $order = $this->objFromFixture(Order::class, "unpaid");
188
        $this->assertTrue($order->canPay(), "can pay an order that is unpaid");
189
        $this->assertTrue($order->canCancel());
190
        $this->assertFalse($order->canDelete(), "never allow deleting orders");
191
192
        // Override config
193
        Config::modify()->set(Order::class, 'cancel_before_payment', false);
194
        $this->assertFalse($order->canCancel());
195
196
        $order = $this->objFromFixture(Order::class, "paid");
197
        $this->assertFalse($order->canPay(), "paid order can't be paid for");
198
        $this->assertFalse($order->canCancel(), "paid order can't be cancelled");
199
        $this->assertFalse($order->canDelete(), "never allow deleting orders");
200
201
        Config::modify()->set(Order::class, 'cancel_before_processing', true);
202
        $this->assertTrue($order->canCancel(), "paid order can be cancelled when expcicitly set via config");
203
204
        $order->Status = 'Processing';
205
        $this->assertFalse($order->canPay(), "Processing order can't be paid for");
206
        $this->assertFalse($order->canCancel(), "Processing order can't be cancelled");
207
        $this->assertFalse($order->canDelete(), "never allow deleting orders");
208
209
        Config::modify()->set(Order::class, 'cancel_before_sending', true);
210
        $this->assertTrue($order->canCancel(), "Processing order can be cancelled when expcicitly set via config");
211
212
        $order->Status = 'Sent';
213
        $this->assertFalse($order->canPay(), "Sent order can't be paid for");
214
        $this->assertFalse($order->canCancel(), "Sent order can't be cancelled");
215
        $this->assertFalse($order->canDelete(), "never allow deleting orders");
216
217
        Config::modify()->set(Order::class, 'cancel_after_sending', true);
218
        $this->assertTrue($order->canCancel(), "Sent order can be cancelled when expcicitly set via config");
219
        Config::modify()->set(Order::class, 'cancel_after_sending', false);
220
221
        $order->Status = 'Complete';
222
        $this->assertFalse($order->canPay(), "Complete order can't be paid for");
223
        $this->assertFalse($order->canCancel(), "Complete order can't be cancelled");
224
        $this->assertFalse($order->canDelete(), "never allow deleting orders");
225
226
        Config::modify()->set(Order::class, 'cancel_after_sending', true);
227
        $this->assertTrue($order->canCancel(), "Completed order can be cancelled when expcicitly set via config");
228
229
        $order->Status = 'AdminCancelled';
230
        $this->assertFalse($order->canPay(), "Cancelled order can't be paid for");
231
        $this->assertFalse($order->canCancel(), "Cancelled order can't be cancelled");
232
        $this->assertFalse($order->canDelete(), "never allow deleting orders");
233
234
        $order->Status = 'MemberCancelled';
235
        $this->assertFalse($order->canPay(), "Cancelled order can't be paid for");
236
        $this->assertFalse($order->canCancel(), "Cancelled order can't be cancelled");
237
        $this->assertFalse($order->canDelete(), "never allow deleting orders");
238
    }
239
240
    public function testDelete()
241
    {
242
        Config::modify()
243
            ->set(FlatTax::class, 'rate', 0.25)
244
            ->merge(Order::class, 'modifiers', [FlatTax::class]);
245
246
        $order = Order::create();
247
        $shirt = $this->objFromFixture(Product::class, "tshirt");
248
        $mp3player = $this->objFromFixture(Product::class, "mp3player");
249
        $order->Items()->add($shirt->createItem(3));
250
        $order->Items()->add($mp3player->createItem(1));
251
        $order->write();
252
        $order->calculate();
253
254
        $statusLogId = OrderStatusLog::create()->update(
255
            [
256
                'Title' => 'Test status log',
257
                'OrderID' => $order->ID
258
            ]
259
        )->write();
260
261
        $paymentId = Payment::create()->update(
262
            [
263
                'OrderID' => $order->ID
264
            ]
265
        )->init('Manual', 343.75, 'NZD')->write();
266
267
268
        $this->assertEquals(4, $order->Items()->Quantity());
269
        $this->assertEquals(1, $order->Modifiers()->count());
270
        $this->assertEquals(1, $order->OrderStatusLogs()->count());
271
        $this->assertEquals(1, $order->Payments()->count());
272
273
        $itemIds = OrderItem::get()->filter('OrderID', $order->ID)->column('ID');
274
        $modifierIds = OrderModifier::get()->filter('OrderID', $order->ID)->column('ID');
275
276
        $order->delete();
277
278
        // Items should no longer be linked to order
279
        $this->assertEquals(0, $order->Items()->count());
280
        $this->assertEquals(0, $order->Modifiers()->count());
281
        $this->assertEquals(0, $order->OrderStatusLogs()->count());
282
        $this->assertEquals(0, $order->Payments()->count());
283
284
        // Ensure the order items have been deleted!
285
        $this->assertEquals(0, OrderItem::get()->filter('ID', $itemIds)->count());
286
        $this->assertEquals(0, OrderModifier::get()->filter('ID', $modifierIds)->count());
287
        $this->assertEquals(0, OrderStatusLog::get()->filter('ID', $statusLogId)->count());
288
289
        // Keep the payment… it might be relevant for book keeping
290
        $this->assertEquals(1, Payment::get()->filter('ID', $paymentId)->count());
291
    }
292
293
    public function testStatusChange()
294
    {
295
        Config::modify()->merge(Order::class, 'extensions', [OrderTest_TestStatusChangeExtension::class]);
296
297
        $order = Order::create();
298
        $orderId = $order->write();
299
300
        $order->Status = 'Unpaid';
301
        $order->Email = '[email protected]';
302
        $order->write();
303
304
        $this->assertEquals(
305
            [
306
                ['Cart' => 'Unpaid']
307
            ],
308
            OrderTest_TestStatusChangeExtension::$stack
309
        );
310
311
        OrderTest_TestStatusChangeExtension::reset();
312
313
        $order = Order::get()->byID($orderId);
314
        $order->Status = 'Paid';
315
        $order->write();
316
317
        $this->assertEquals(
318
            [
319
                ['Unpaid' => 'Paid']
320
            ],
321
            OrderTest_TestStatusChangeExtension::$stack
322
        );
323
324
        $this->assertTrue((boolean)$order->Paid, 'Order paid date should be set');
325
    }
326
327
    public function testOrderAddress()
328
    {
329
        $order = $this->objFromFixture(Order::class, 'paid');
330
331
        // assert that order doesn't contain user information
332
        $this->assertNull($order->FirstName);
333
        $this->assertNull($order->Surname);
334
        $this->assertNull($order->Email);
335
336
        // The shipping address should use the members default shipping address
337
        $this->assertEquals(
338
            'Joe Bloggs, 12 Foo Street, Bar, Farmville, New Sandwich, US',
339
            $order->getShippingAddress()->toString()
340
        );
341
342
        $address = $this->objFromFixture(Address::class, 'pukekohe');
343
        $order->ShippingAddressID = $address->ID;
344
        $order->write();
345
346
        // Address doesn't have firstname and surname
347
        $this->assertNull(Address::get()->byID($order->ShippingAddressID)->FirstName);
348
        $this->assertNull(Address::get()->byID($order->ShippingAddressID)->Surname);
349
350
        // Shipping address should contain the name from the member object and new address information
351
        $this->assertEquals(
352
            'Joe Bloggs, 1 Queen Street, Pukekohe, Auckland, 2120',
353
            $order->getShippingAddress()->toString()
354
        );
355
356
        // changing fields on the Order will have precendence!
357
        $order->FirstName = 'Tester';
358
        $order->Surname = 'Mc. Testerson';
359
        $order->write();
360
361
        // Reset caches, otherwise the previously set name will persist (eg. Joe Bloggs)
362
        DataObject::reset();
363
364
        $this->assertEquals(
365
            'Tester Mc. Testerson, 1 Queen Street, Pukekohe, Auckland, 2120',
366
            $order->getShippingAddress()->toString()
367
        );
368
    }
369
}
370