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\Controllers\Payments;
7
8use App\Libraries\Payments\ShopifySignature;
9use App\Models\Country;
10use App\Models\Store\Order;
11use App\Models\Store\Payment;
12use Tests\TestCase;
13
14class ShopifyControllerTest extends TestCase
15{
16 public function testWebhookOrdersCancelled()
17 {
18 $order = Order::factory()->paid()->shopify()->create();
19 $payment = new Payment([
20 'provider' => Order::PROVIDER_SHOPIFY,
21 'transaction_id' => $order->getProviderReference(),
22 'country_code' => Country::UNKNOWN,
23 'paid_at' => now(),
24 ]);
25 $order->payments()->save($payment);
26 $order->paid($payment);
27
28 $this->setShopifyPayload([
29 'note_attributes' => [['name' => 'orderId', 'value' => $order->getKey()]],
30 ]);
31
32 $response = $this->sendCallbackRequest(['X-Shopify-Topic' => 'orders/cancelled']);
33
34 $order->refresh();
35 $response->assertStatus(204);
36
37 $this->assertTrue($order->isCancelled());
38 $this->assertTrue(Payment::where('order_id', $order->getKey())->where('cancelled', true)->exists());
39 }
40
41 public function testWebhookOrdersCreate()
42 {
43 $order = Order::factory()->shopify()->processing()->create();
44 $this->setShopifyPayload([
45 'note_attributes' => [['name' => 'orderId', 'value' => $order->getKey()]],
46 ]);
47
48 $response = $this->sendCallbackRequest(['X-Shopify-Topic' => 'orders/create']);
49
50 $order->refresh();
51 $response->assertStatus(204);
52 $this->assertTrue($order->status === 'checkout');
53 }
54
55 public function testWebhookOrdersFulfilled()
56 {
57 $order = Order::factory()->shopify()->checkout()->create();
58 $this->setShopifyPayload([
59 'note_attributes' => [['name' => 'orderId', 'value' => $order->getKey()]],
60 ]);
61
62 $response = $this->sendCallbackRequest(['X-Shopify-Topic' => 'orders/fulfilled']);
63
64 $order->refresh();
65 $response->assertStatus(204);
66 $this->assertTrue($order->isShipped());
67 $this->assertNotNull($order->shipped_at);
68 }
69
70 public function testWebhookOrdersPaid()
71 {
72 $order = Order::factory()->shopify()->processing()->create();
73 $this->setShopifyPayload([
74 'note_attributes' => [['name' => 'orderId', 'value' => $order->getKey()]],
75 ]);
76
77 $response = $this->sendCallbackRequest(['X-Shopify-Topic' => 'orders/paid']);
78
79 $order->refresh();
80 $response->assertStatus(204);
81 $this->assertTrue($order->isPaidOrDelivered());
82 $this->assertTrue(Payment::where('order_id', $order->getKey())->where('cancelled', false)->exists());
83 }
84
85 public function testReplacementOrdersManuallyCreatedShouldBeIgnored()
86 {
87 $this->setShopifyPayload([
88 'note_attributes' => [],
89 'gateway' => 'manual',
90 'payment_gateway_names' => ['manual'],
91 'processing_method' => 'manual',
92 ]);
93
94 $response = $this->sendCallbackRequest();
95
96 $response->assertStatus(204);
97 $this->assertSame(Order::withoutGlobalScopes()->count(), 0);
98 }
99
100 public function testReplacementOrdersCreatedByDuplicatingShopifyOrderShouldBeIgnored()
101 {
102 // Orders are already shipped when the replacement gets created.
103 $order = Order::factory()->shopify()->shipped()->create();
104 $oldUpdatedAt = $order->updated_at->copy();
105
106 $this->setShopifyPayload([
107 'note_attributes' => [['name' => 'orderId', 'value' => $order->getKey()]],
108 'gateway' => 'manual',
109 'payment_gateway_names' => ['manual'],
110 'processing_method' => 'manual',
111 'source_name' => 'shopify_draft_order',
112 ]);
113
114 $response = $this->sendCallbackRequest();
115
116 $order->refresh();
117 $response->assertStatus(204);
118 $this->assertSame($order->status, 'shipped');
119 $this->assertEquals($order->updated_at, $oldUpdatedAt);
120 $this->assertSame(Order::withoutGlobalScopes()->count(), 1);
121 }
122
123 protected function setUp(): void
124 {
125 parent::setUp();
126 config_set('payments.shopify.webhook_key', 'magic');
127
128 $this->url = route('payments.shopify.callback');
129 }
130
131 private function sendCallbackRequest(array $extraHeaders = [])
132 {
133 $validSignature = ShopifySignature::calculateSignature(json_encode($this->payload));
134 $headers = array_merge(['X-Shopify-Hmac-Sha256' => $validSignature], $extraHeaders);
135
136 return $this->json('POST', $this->url, $this->payload, $headers);
137 }
138
139 private function setShopifyPayload(array $params)
140 {
141 $this->payload = array_merge([
142 'id' => 1,
143 'order_number' => 1,
144 ], $params);
145 }
146}