Completed
Push — master ( 3c661d...2a57f9 )
by Will
26s queued 12s
created

tests/php/Model/OrderTest.php (3 issues)

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()
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()
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
            array(
99
                array('ProductID' => $this->mp3player->ID, 'Quantity' => 2, 'CalculatedTotal' => 400),
100
                array('ProductID' => $this->socks->ID, 'Quantity' => 1, 'CalculatedTotal' => 8),
101
            ),
102
            $items
103
        );
104
        $this->assertEquals(3, $items->Quantity(), "Quantity is 3");
105
        $this->assertTrue($items->Plural(), "There is more than one item");
106
        $this->assertEquals(0.7, $items->Sum('Weight', true), "Total order weight sums correctly", 0.0001);
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
            array(
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
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();
138
        $this->assertEquals(408, $order->Total(), "check totals");
139
140
        //make a changes to existing products
141
        $this->mp3player->BasePrice = 100;
142
        $this->mp3player->write();
143
        $this->socks->BasePrice = 20;
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
            array(
160
                array('ProductID' => $this->mp3player->ID, 'Quantity' => 2, 'CalculatedTotal' => 400),
161
                array('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->write();
302
303
        $this->assertEquals(
304
            array(
305
                array('Cart' => 'Unpaid')
306
            ),
307
            OrderTest_TestStatusChangeExtension::$stack
308
        );
309
310
        OrderTest_TestStatusChangeExtension::reset();
311
312
        $order = Order::get()->byID($orderId);
313
        $order->Status = 'Paid';
314
        $order->write();
315
316
        $this->assertEquals(
317
            array(
318
                array('Unpaid' => 'Paid')
319
            ),
320
            OrderTest_TestStatusChangeExtension::$stack
321
        );
322
323
        $this->assertTrue((boolean)$order->Paid, 'Order paid date should be set');
324
    }
325
326
    public function testOrderAddress()
327
    {
328
        $order = $this->objFromFixture(Order::class, 'paid');
329
330
        // assert that order doesn't contain user information
331
        $this->assertNull($order->FirstName);
332
        $this->assertNull($order->Surname);
333
        $this->assertNull($order->Email);
334
335
        // The shipping address should use the members default shipping address
336
        $this->assertEquals(
337
            'Joe Bloggs, 12 Foo Street, Bar, Farmville, New Sandwich, US',
338
            $order->getShippingAddress()->toString()
339
        );
340
341
        $address = $this->objFromFixture(Address::class, 'pukekohe');
342
        $order->ShippingAddressID = $address->ID;
343
        $order->write();
344
345
        // Address doesn't have firstname and surname
346
        $this->assertNull(Address::get()->byID($order->ShippingAddressID)->FirstName);
347
        $this->assertNull(Address::get()->byID($order->ShippingAddressID)->Surname);
348
349
        // Shipping address should contain the name from the member object and new address information
350
        $this->assertEquals(
351
            'Joe Bloggs, 1 Queen Street, Pukekohe, Auckland, 2120',
352
            $order->getShippingAddress()->toString()
353
        );
354
355
        // changing fields on the Order will have precendence!
356
        $order->FirstName = 'Tester';
357
        $order->Surname = 'Mc. Testerson';
358
        $order->write();
359
360
        // Reset caches, otherwise the previously set name will persist (eg. Joe Bloggs)
361
        DataObject::reset();
362
363
        $this->assertEquals(
364
            'Tester Mc. Testerson, 1 Queen Street, Pukekohe, Auckland, 2120',
365
            $order->getShippingAddress()->toString()
366
        );
367
    }
368
}
369