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
|
|||
58 | $this->mp3player->publishSingle(); |
||
59 | $this->socks = $this->objFromFixture(Product::class, 'socks'); |
||
0 ignored issues
–
show
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
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 |
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..