1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace Tests\Libraries;
7
8use App\Exceptions\InvariantException;
9use App\Libraries\OrderCheckout;
10use App\Models\Country;
11use App\Models\Store\Order;
12use App\Models\Store\OrderItem;
13use App\Models\Store\Product;
14use App\Models\Tournament;
15use Carbon\Carbon;
16use Tests\TestCase;
17
18// TODO: should split into Tournament-specific and generic Checkout tests.
19class OrderCheckoutTest extends TestCase
20{
21 public function testTournamentBannerWhenAvailable()
22 {
23 $tournament = Tournament::factory()->create();
24 $product = $this->createTournamentProduct($tournament, Carbon::now()->addDays(1));
25 $orderItem = OrderItem::factory()->create([
26 'product_id' => $product,
27 'extra_data' => [
28 'tournament_id' => $tournament->getKey(),
29 ],
30 ]);
31 $tournament->update(['tournament_banner_product_id' => $product->getKey()]);
32
33 $checkout = new OrderCheckout($orderItem->order);
34
35 $this->assertEmpty($checkout->validate());
36 }
37
38 public function testTournamentBannerWhenNoEndDate()
39 {
40 $tournament = Tournament::factory()->create();
41 $product = $this->createTournamentProduct($tournament);
42 $orderItem = OrderItem::factory()->create([
43 'product_id' => $product,
44 'extra_data' => [
45 'tournament_id' => $tournament->getKey(),
46 ],
47 ]);
48 $tournament->update(['tournament_banner_product_id' => $product->getKey()]);
49
50 $checkout = new OrderCheckout($orderItem->order);
51
52 $this->assertEmpty($checkout->validate());
53 }
54
55 public function testTournamentBannerWhenNotAvailable()
56 {
57 $tournament = Tournament::factory()->create();
58 $product = $this->createTournamentProduct($tournament, Carbon::now()->subDays(1));
59 $orderItem = OrderItem::factory()->create([
60 'product_id' => $product,
61 'extra_data' => [
62 'tournament_id' => $tournament->getKey(),
63 ],
64 ]);
65 $tournament->update(['tournament_banner_product_id' => $product->getKey()]);
66
67 $checkout = new OrderCheckout($orderItem->order);
68
69 $this->assertNotEmpty($checkout->validate());
70 }
71
72 public function testShippableItemRequiresShopify()
73 {
74 $product = Product::factory()->create(['stock' => 5, 'max_quantity' => 5, 'shopify_id' => null]);
75 $orderItem = OrderItem::factory()->create([
76 'product_id' => $product,
77 'quantity' => 1,
78 ]);
79
80 $order = Order::factory()->create();
81 $order->items()->save($orderItem);
82
83 $checkout = new OrderCheckout($order);
84 $result = $checkout->validate();
85
86 $this->assertSame(
87 [osu_trans('model_validation/store/product.not_available')],
88 array_get($result, "orderItems.{$orderItem->getKey()}")
89 );
90 }
91
92 public function testShopifyItemDoesNotMix()
93 {
94 $product1 = Product::factory()->create(['stock' => 5, 'max_quantity' => 5, 'shopify_id' => 1]);
95 $product2 = Product::factory()->virtual()->create(['stock' => 5, 'max_quantity' => 5, 'shopify_id' => null]);
96 $orderItem1 = OrderItem::factory()->create([
97 'product_id' => $product1,
98 'quantity' => 1,
99 ]);
100
101 $orderItem2 = OrderItem::factory()->create([
102 'product_id' => $product2,
103 'quantity' => 1,
104 ]);
105
106 $order = Order::factory()->create();
107 $order->items()->save($orderItem1);
108 $order->items()->save($orderItem2);
109
110 $checkout = new OrderCheckout($order);
111 $result = $checkout->validate();
112
113 $this->assertSame(
114 [osu_trans('model_validation/store/product.must_separate')],
115 array_get($result, "orderItems.{$orderItem2->getKey()}")
116 );
117 }
118
119 public function testTotalNonZeroDoesNotAllowFreeCheckout()
120 {
121 $product1 = Product::factory()->create(['stock' => 5, 'max_quantity' => 5, 'cost' => 1]);
122 $orderItem1 = OrderItem::factory()->create([
123 'product_id' => $product1,
124 'quantity' => 1,
125 'cost' => $product1->cost,
126 ]);
127
128 $order = Order::factory()->create();
129 $order->items()->save($orderItem1);
130
131 $checkout = new OrderCheckout($order, Order::PROVIDER_FREE);
132 $result = $checkout->allowedCheckoutProviders();
133
134 $this->expectException(InvariantException::class);
135 $checkout->beginCheckout();
136
137 $this->assertNotContains(Order::PROVIDER_FREE, $result);
138 }
139
140 public function testTotalZeroOnlyAllowsFreeCheckout()
141 {
142 $product1 = Product::factory()->create(['stock' => 5, 'max_quantity' => 5, 'cost' => 0]);
143 $orderItem1 = OrderItem::factory()->create([
144 'product_id' => $product1,
145 'quantity' => 1,
146 'cost' => $product1->cost,
147 ]);
148
149 $order = Order::factory()->create();
150 $order->items()->save($orderItem1);
151 $checkout = new OrderCheckout($order, Order::PROVIDER_PAYPAL);
152 $result = $checkout->allowedCheckoutProviders();
153
154 $this->expectException(InvariantException::class);
155 $checkout->beginCheckout();
156
157 $this->assertSame([Order::PROVIDER_FREE], $result);
158 }
159
160 private function createTournamentProduct(Tournament $tournament, Carbon $availableUntil = null)
161 {
162 $country = Country::inRandomOrder()->first() ?? Country::factory()->create();
163
164 $product = Product::factory()->childBanners()->create([
165 'available_until' => $availableUntil,
166 'name' => "{$tournament->name} Support Banner ({$country->name})",
167 ]);
168
169 $type_mappings_json = [
170 $product->product_id => [
171 'country' => $country->acronym,
172 'tournament_id' => $tournament->tournament_id,
173 ],
174 ];
175
176 $product->type_mappings_json = json_encode($type_mappings_json, JSON_PRETTY_PRINT);
177 $product->saveOrExplode();
178
179 return $product;
180 }
181}