the browser-facing portion of osu!
at master 4.7 kB view raw
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 App\Http\Controllers\Payments; 7 8use App\Libraries\OrderCheckout; 9use App\Libraries\Payments\ShopifySignature; 10use App\Models\Store\Order; 11use App\Models\Store\Payment; 12use Carbon\Carbon; 13use Log; 14use Sentry\State\Scope; 15 16class ShopifyController extends Controller 17{ 18 private $params; 19 20 public function callback() 21 { 22 $signature = new ShopifySignature(request()); 23 $signature->assertValid(); 24 25 // X-Shopify-Hmac-Sha256 26 // X-Shopify-Order-Id 27 // X-Shopify-Shop-Domain 28 // X-Shopify-Test 29 // X-Shopify-Topic 30 31 $type = $this->getWebookType(); 32 $orderId = $this->getOrderId(); 33 34 if ($orderId === null) { 35 $params = $this->getParams(); 36 // just log info that can be used for lookup if necessary. 37 $data = [ 38 'shopify_gid' => $params['id'], 39 'shopify_order_number' => $params['order_number'], 40 'webhook_type' => $type, 41 ]; 42 Log::info('Shopify callback with missing orderId', $data); 43 44 return response([], 204); 45 } 46 47 /** @var Order $order */ 48 $order = Order::findOrFail($orderId); 49 50 switch ($type) { 51 case 'orders/cancelled': 52 // FIXME: We're relying on Shopify not sending cancel multiple times otherwise this will explode. 53 $order->getConnection()->transaction(function () use ($order) { 54 $payment = $order->payments()->where('cancelled', false)->first(); 55 $payment->cancel(); 56 $order->cancel(); 57 }); 58 break; 59 case 'orders/fulfilled': 60 $order->update(['status' => Order::STATUS_SHIPPED, 'shipped_at' => now()]); 61 break; 62 case 'orders/create': 63 if ($order->isShipped() && $this->isDuplicateOrder()) { 64 return response([], 204); 65 } 66 67 (new OrderCheckout($order))->completeCheckout(); 68 break; 69 case 'orders/paid': 70 $this->updateOrderPayment($order); 71 break; 72 default: 73 app('sentry')->getClient()->captureMessage( 74 'Received unknown webhook for order from Shopify', 75 null, 76 (new Scope()) 77 ->setExtra('type', $type) 78 ->setExtra('order_id', $orderId) 79 ); 80 break; 81 } 82 83 return response([], 204); 84 } 85 86 private function getWebookType() 87 { 88 return request()->header('X-Shopify-Topic'); 89 } 90 91 private function getOrderId() 92 { 93 // array of name-value pairs. 94 $attributes = $this->getParams()['note_attributes']; 95 96 foreach ($attributes as $attribute) { 97 if ($attribute['name'] === 'orderId') { 98 return get_int($attribute['value']); 99 } 100 } 101 } 102 103 private function getParams() 104 { 105 if ($this->params === null) { 106 $this->params = static::extractParams(request()); 107 } 108 109 return $this->params; 110 } 111 112 /** 113 * Replacement orders created at the Shopify end by duplicating? the previous order. 114 * 115 * @return bool 116 */ 117 private function isDuplicateOrder() 118 { 119 $params = $this->getParams(); 120 121 return $params['source_name'] === 'shopify_draft_order' && $this->isManualOrder(); 122 } 123 124 /** 125 * Manually created replacement orders created at the Shopify end that might not have 126 * the orderId included. 127 * 128 * @return bool 129 */ 130 private function isManualOrder() 131 { 132 $params = $this->getParams(); 133 134 return array_get($params, 'browser_ip') === null 135 && array_get($params, 'checkout_id') === null 136 && array_get($params, 'gateway') === 'manual' 137 && array_get($params, 'payment_gateway_names') === ['manual'] 138 && array_get($params, 'processing_method') === 'manual'; 139 } 140 141 private function updateOrderPayment(Order $order) 142 { 143 $params = $this->getParams(); 144 $payment = new Payment([ 145 'provider' => Order::PROVIDER_SHOPIFY, 146 'transaction_id' => $order->getProviderReference(), 147 'country_code' => array_get($params, 'billing_address.country_code'), 148 'paid_at' => Carbon::parse(array_get($params, 'processed_at')), 149 ]); 150 151 $order->paid($payment); 152 } 153}