├── .gitignore ├── src ├── Exceptions │ ├── InvalidTaxInput.php │ ├── InvalidOrderInput.php │ ├── InvalidPriceInput.php │ ├── InvalidMeasureInput.php │ ├── InvalidProductInput.php │ ├── InvalidMechantDetails.php │ ├── InvalidOrderPaymentMethodInput.php │ ├── InvalidProductShippingInput.php │ └── ProductContentAttributesUndefined.php ├── Contents │ ├── Measure.php │ ├── Taxes.php │ ├── Order │ │ ├── OrderPaymentMethod.php │ │ ├── OrderTest.php │ │ ├── OrderShippingDetails.php │ │ ├── OrderLineItem.php │ │ └── Order.php │ ├── Price.php │ ├── Product │ │ ├── ProductShipping.php │ │ └── Product.php │ └── BaseContent.php ├── Facades │ ├── OrderApi.php │ └── ProductApi.php ├── Events │ ├── OrderContentScoutedEvent.php │ ├── ProductDeletedEvent.php │ ├── ProductCreatedOrUpdatedEvent.php │ └── NewOrdersScoutedEvent.php ├── Listeners │ ├── ProductDeletedListener.php │ └── ProductCreatedOrUpdatedListener.php ├── Commands │ └── CssOrdersScout.php ├── GoogleMerchantApiServiceProvider.php └── Api │ ├── ProductApi.php │ ├── OrderApiSandbox.php │ ├── AbstractApi.php │ └── OrderApi.php ├── LICENSE.md ├── composer.json ├── doc └── prodcut-conent-special-methods.md ├── config └── laravel-google-merchant-api.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | composer.phar 4 | composer.lock 5 | tmp*.tmp -------------------------------------------------------------------------------- /src/Exceptions/InvalidTaxInput.php: -------------------------------------------------------------------------------- 1 | product)->catch(function(){ 28 | // 29 | }); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Listeners/ProductCreatedOrUpdatedListener.php: -------------------------------------------------------------------------------- 1 | product)->catch(function(){ 28 | // 29 | }); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Contents/Order/OrderPaymentMethod.php: -------------------------------------------------------------------------------- 1 | attributes['currency'] = config('laravel-google-merchant-api.default_currency', 'AUD'); 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Commands/CssOrdersScout.php: -------------------------------------------------------------------------------- 1 | with($attributes); 30 | } 31 | 32 | $this->product = $product; 33 | } 34 | 35 | /** 36 | * Get the channels the event should broadcast on. 37 | * 38 | * @return \Illuminate\Broadcasting\Channel|array 39 | */ 40 | public function broadcastOn() 41 | { 42 | return []; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Events/ProductCreatedOrUpdatedEvent.php: -------------------------------------------------------------------------------- 1 | with($attributes); 30 | } 31 | 32 | $this->product = $product; 33 | } 34 | 35 | /** 36 | * Get the channels the event should broadcast on. 37 | * 38 | * @return \Illuminate\Broadcasting\Channel|array 39 | */ 40 | public function broadcastOn() 41 | { 42 | return []; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Events/NewOrdersScoutedEvent.php: -------------------------------------------------------------------------------- 1 | orders = $orders; 40 | $this->merchant = $merchant; 41 | $this->merchant_id = $merchant_id; 42 | } 43 | 44 | /** 45 | * Get the channels the event should broadcast on. 46 | * 47 | * @return \Illuminate\Broadcasting\Channel|array 48 | */ 49 | public function broadcastOn() 50 | { 51 | return []; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Contents/Order/OrderTest.php: -------------------------------------------------------------------------------- 1 | attributes['kind'] = 'content#testOrder'; 24 | 25 | $this->attributes[ 'lineItems' ] = array(); 26 | $this->attributes[ 'promotions' ] = array(); 27 | 28 | $this->allowed_attributes = array_merge($this->allowed_attributes, [ 29 | 'notificationMode', 30 | 'country', 'predefinedEmail', 'predefinedDeliveryAddress', 'predefinedBillingAddress', 31 | 'enableOrderinvoices', 'predefinedPickupDetails', 32 | ]); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moirei/laravel-google-merchant-api", 3 | "description": "Laravel Google Merchant Products API for Google Shopping.", 4 | "homepage": "https://github.com/augustusnaz/laravel-google-merchant-api", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Augustus Okoye", 9 | "email": "augustusokoye@moirei.com", 10 | "role": "Developer" 11 | } 12 | ], 13 | "keywords":[ 14 | "Laravel", 15 | "Google Merchant API", 16 | "Google Shopping API", 17 | "moirei.com" 18 | ], 19 | "require": { 20 | "php": "^7.2|^8.0", 21 | "illuminate/support": "~5.0|^6.0|^7.0|^8.0|^9.0", 22 | "illuminate/cache": "~5.0|^6.0|^7.0|^8.0|^9.0", 23 | "guzzlehttp/guzzle":"~5.3|^6.5|^7.0", 24 | "google/apiclient": "^2.4" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "MOIREI\\GoogleMerchantApi\\": "src" 29 | } 30 | }, 31 | "minimum-stability": "dev", 32 | "extra": { 33 | "laravel": { 34 | "providers": [ 35 | "MOIREI\\GoogleMerchantApi\\GoogleMerchantApiServiceProvider" 36 | ], 37 | "aliases": { 38 | "ProductsApi": "MOIREI\\GoogleMerchantApi\\Facades\\ProductsApi" 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Contents/Order/OrderShippingDetails.php: -------------------------------------------------------------------------------- 1 | attributes['shipByDate']){ 33 | return $this->attributes['shipByDate']; 34 | } 35 | 36 | return new Carbon($this->attributes['shipByDate']); 37 | } 38 | 39 | /** 40 | * Mutate the shipping details 41 | * 42 | * @return \Carbon\Carbon|null 43 | */ 44 | public function getDeliverByDate(){ 45 | if(!$this->attributes['deliverByDate']){ 46 | return $this->attributes['deliverByDate']; 47 | } 48 | 49 | return new Carbon($this->attributes['deliverByDate']); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/Contents/Product/ProductShipping.php: -------------------------------------------------------------------------------- 1 | value($price)->currency($currency); 35 | }elseif(is_array($price)){ 36 | $price = (new Price)->with($price); 37 | }elseif (is_callable($price)) { 38 | $callback = $price; 39 | $callback($price = new Price); 40 | }elseif(!($price instanceof Price)){ 41 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidPriceInput; 42 | } 43 | 44 | $this->attributes[ 'price' ] = $price->get(); 45 | 46 | return $this; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Contents/Order/OrderLineItem.php: -------------------------------------------------------------------------------- 1 | with($this->attributes['shippingDetails'])->all(); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/GoogleMerchantApiServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 18 | $this->publishes([ 19 | __DIR__.'/../config/laravel-google-merchant-api.php' => base_path('config/laravel-google-merchant-api.php'), 20 | ], 'google-merchant-api-config'); 21 | } 22 | 23 | $this->app->booted(function () { 24 | if(config('laravel-google-merchant-api.contents.orders.schedule_orders_check', false)){ 25 | $this->registerScheduler(); 26 | } 27 | }); 28 | 29 | } 30 | 31 | /** 32 | * Register the service provider. 33 | */ 34 | public function register() 35 | { 36 | $this->app->instance('productApi', new Api\ProductApi); 37 | $this->app->instance('orderApi', new Api\OrderApi); 38 | 39 | $this->commands([ 40 | Commands\CssOrdersScout::class, 41 | ]); 42 | 43 | } 44 | 45 | /* 46 | * @codeCoverageIgnore 47 | */ 48 | protected function registerScheduler(){ 49 | $schedule = $this->app['Illuminate\Console\Scheduling\Schedule']; 50 | 51 | $schedule_frequency = config('laravel-google-merchant-api.contents.orders.schedule_frequency', 'hourly'); 52 | 53 | if(!in_array($schedule_frequency, [ 54 | 'everyMinute', 'everyFiveMinutes', 'everyTenMinutes', 'everyThirtyMinutes', 55 | 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 56 | 'weekdays', 'mondays', 'tuesdays', 'wednesdays', 'thursdays', 'fridays', 'saturdays', 'sundays', 57 | ])){ 58 | $schedule_frequency = 'hourly'; 59 | } 60 | 61 | $schedule->command('gm-orders:scout')->$schedule_frequency(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Contents/BaseContent.php: -------------------------------------------------------------------------------- 1 | allowed_attributes)){ 34 | $this->attributes[ $name ] = $arguments[0]; 35 | }else{ 36 | throw new \BadMethodCallException("Instance method Content->$name() doesn't exist"); 37 | } 38 | 39 | return $this; 40 | } 41 | 42 | public function __get($attribute){ 43 | 44 | // Mutable attributes 45 | $attributeFunc = 'get' . ucfirst($attribute); 46 | if(method_exists($this, $attributeFunc)) return $this->$attributeFunc(); 47 | 48 | if(isset($this->attributes[ $attribute ]) && !empty($this->attributes[ $attribute ])){ 49 | return $this->attributes[ $attribute ]; 50 | } 51 | 52 | return null; 53 | } 54 | 55 | 56 | /** 57 | * Batch fill with array 58 | * 59 | * @param array $attributes 60 | */ 61 | public function with($attributes){ 62 | 63 | $attributes = collect($attributes)->only($this->allowed_attributes)->all(); 64 | 65 | foreach($attributes as $key => $attribute){ 66 | $this->attributes[ $key ] = $attribute; 67 | } 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Format and retrieve the content variables 74 | * 75 | * @return array 76 | */ 77 | public function get(){ 78 | return array_filter(collect($this->attributes)->only($this->allowed_attributes)->all(), function($value){ 79 | return !($value === null) && !($value === '') && !($value === []); 80 | }); 81 | } 82 | 83 | /** 84 | * Format and retrieve the content variables 85 | * 86 | * @return array 87 | */ 88 | public function all(){ 89 | $attributes = $this->attributes; 90 | foreach($attributes as $key => $attribute){ 91 | $attributeFunc = 'get' . ucfirst($key); 92 | if(method_exists($this, $attributeFunc)){ 93 | $attributes[ $key ] = $this->$attributeFunc(); 94 | } 95 | } 96 | return array_filter($attributes, function($value){ 97 | return !($value === null) && !($value === '') && !($value === []); 98 | }); 99 | } 100 | 101 | /** 102 | * Get content attribute data 103 | * 104 | * @param string $attribute 105 | * @param mix $default 106 | * @return mix 107 | */ 108 | public function dataGet($attribute, $default = null){ 109 | if(isset($this->attributes[ $attribute ]) && !empty($this->attributes[ $attribute ])){ 110 | return $this->attributes[ $attribute ]; 111 | } 112 | return $default; 113 | } 114 | 115 | 116 | } 117 | -------------------------------------------------------------------------------- /doc/prodcut-conent-special-methods.md: -------------------------------------------------------------------------------- 1 | ### `Product` special attribute functions 2 | 3 | Any attribute (as allowed by the content class) can be assigned by simply calling it as function. E.g. to assign offerId, use 4 | 5 | ```php 6 | $product->offerId(1); 7 | ``` 8 | 9 | The allowed attributes are defined in the `MOIREI\GoogleMerchantApi\Contents\Product\Product` class as per [this specification]( https://support.google.com/merchants/answer/7052112 ). 10 | 11 | 12 | 13 | Additionally, special functions are defined for easily assigning certain attributes. 14 | 15 | | Function | Value Type | Description | 16 | | ---------------- | ------------------------------ | ------------------------------------------------------------ | 17 | | image | `string` | Sets the `imageLink`. | 18 | | lang | `string` | Using the 2-letter designation, for example `"en"` or `"fr"`. Default: `"en"` | 19 | | country | `string` | Sets the `targetCountry`. Default: `"AU"` | 20 | | online | `boolean` | Sets `channel` to `“online”` or `"local"` | 21 | | inStock | `boolean` | Sets `availability` to `“in stock”` or `“out of stock”` | 22 | | preorder | none | Sets `availability` to `“preorder”` | 23 | | availabilityDate | `Carbon`/`string` |Takes a Carbon or string | 24 | | expirationDate | `Carbon`/`string` | Takes a Carbon or string | 25 | | category | `string` | Sets `googleProductCategory` | 26 | | price | `Closure`/`string`/`float`/`array` |Sets the `price`. If given an array, array keys must contain, `value` and `currency`. If given float or string, subsequent param (optional) should indicate currency| 27 | | salePrice | `Closure`/`string`/`float`/`array` | Sets the `salePrice`. If given an array, array keys must contain, `value` and `currency`. If given float or string, subsequent param (optional) should indicate currency | 28 | | shipping | `Closure`/`ProductShipping`/`array` |Appends to `shipping`| 29 | | shippingHeight | `Closure`/`Measure`/`double`/`array` | Sets the `shippingHeight` | 30 | | shippingLength | `Closure`/`Measure`/`double`/`array` | Sets the `shippingLength` | 31 | | shippingWeight | `Closure`/`Measure`/`double`/`array` | Sets the `shippingWeight` | 32 | | taxes | `Closure`/`Taxes`/`array` |Sets `taxes`| 33 | | unitPricingBaseMeasure | `Closure`/`Measure`/`double`/`array` |Sets `unitPricingBaseMeasure`| 34 | | unitPricingMeasure | `Closure`/`Measure`/`double`/`array` |Sets `unitPricingMeasure`| 35 | | salePriceEffectiveDate | `Carbon`/`string` |Sets `salePriceEffectiveDate`| 36 | | sizes | `array`/`any` | Sets the `sizes` if given an array. Appends otherwise | 37 | | custom | `string`/`array`, `mix`, `string` |Appends to `customAttributes`. If first param is not an array, subsequent params must indicate `value` and `type` (optional). If array, keys must contain `name`, `value` and `type` (optional). | 38 | | customValues | `array` | Calls the `custom` function per array element | -------------------------------------------------------------------------------- /src/Api/ProductApi.php: -------------------------------------------------------------------------------- 1 | post($product->get()); 35 | } 36 | 37 | /** 38 | * List products. 39 | * 40 | * @return mix 41 | * @throws \GuzzleHttp\Exception\ClientException 42 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidProductInput 43 | */ 44 | public function list($pageToken = null, $maxResults = 25){ 45 | return $this->get(null, ['pageToken' => $pageToken, 'maxResults' => $maxResults]); 46 | } 47 | 48 | /** 49 | * Get product by product. 50 | * 51 | * @param Product|null|Closure $product 52 | * @return mix 53 | * @throws \GuzzleHttp\Exception\ClientException 54 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidProductInput 55 | */ 56 | public function get($product = null, $params = array()){ 57 | $instance = self::getInstance($this); 58 | 59 | if(is_null($product)){ 60 | $id = null; 61 | }else{ 62 | $id = $instance->getId( self::resolveProductInput($product) ); 63 | } 64 | 65 | $instance->setRequestArgs( array( 66 | 'method' => 'GET', 67 | 'path' => $id, 68 | 'params' => $params, 69 | ) ); 70 | 71 | $instance->clearCallbacks(); 72 | 73 | return $instance->execRequest(); 74 | } 75 | 76 | /** 77 | * Delete product(s). 78 | * 79 | * @param Product|Closure $product 80 | * @return mix 81 | * @throws \GuzzleHttp\Exception\ClientException 82 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidProductInput 83 | */ 84 | public function delete($product) { 85 | $instance = self::getInstance($this); 86 | $product = self::resolveProductInput($product); 87 | 88 | if(is_null($id = $instance->getId($product))){ 89 | return $instance; 90 | } 91 | 92 | $instance->setRequestArgs( array( 93 | 'method' => 'DELETE', 94 | 'path' => $id, 95 | ) ); 96 | 97 | $instance->clearCallbacks(); 98 | 99 | return $instance->execRequest(); 100 | } 101 | 102 | /** 103 | * Get product id in format online:en:US:1111111111 104 | * 105 | * @param Product|null $product 106 | * @return string|null 107 | */ 108 | private function getId($product){ 109 | 110 | if(is_null($product) || !($product instanceof Product)) return null; 111 | 112 | $channel = $product->dataGet('channel', config('laravel-google-merchant-api.contents.products.defaults.channel', 'online')); 113 | $contentLanguage = $product->dataGet('contentLanguage', config('laravel-google-merchant-api.contents.products.defaults.contentLanguage', 'en')); 114 | $targetCountry = $product->dataGet('targetCountry', config('laravel-google-merchant-api.contents.products.defaults.targetCountry', 'AU')); 115 | $offerId = $product->dataGet('offerId'); 116 | 117 | if($channel && $contentLanguage && $targetCountry && $offerId){ 118 | return "$channel:$contentLanguage:$targetCountry:$offerId"; 119 | } 120 | 121 | return null; 122 | } 123 | 124 | /** 125 | * Get ProductApi instance 126 | * 127 | * @param ProductApi $productApi 128 | * @return ProductApi 129 | */ 130 | static protected function getInstance(ProductApi $productApi){ 131 | if($productApi->async){ 132 | // duplicate so that callbacks are not overridden 133 | return clone $productApi; 134 | }else{ 135 | return $productApi; 136 | } 137 | } 138 | 139 | /** 140 | * Resolve product input 141 | * 142 | * @param Product|Closure $product 143 | * @return Product 144 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidProductInput 145 | */ 146 | static protected function resolveProductInput($product){ 147 | if (is_callable($product)) { 148 | $callback = $product; 149 | 150 | $callback($product = new Product); 151 | } 152 | 153 | if( !($product instanceof Product) ){ 154 | throw new InvalidProductInput; 155 | } 156 | 157 | return $product; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /config/laravel-google-merchant-api.php: -------------------------------------------------------------------------------- 1 | 'moirei', 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | API Mode 18 | |-------------------------------------------------------------------------- 19 | | 20 | | Set the API version to use. 21 | */ 22 | 23 | 'version' => 'v2.1', 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Client Config 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Optional configurations for the guzzle client. 31 | | Accepts only 'timeout', 'headers', 'proxy', 'allow_redirects', 'http_errors', 'decode_content', 'verify', 'cookies' 32 | */ 33 | 34 | 'client_config' => [ 35 | 'timeout' => 7.0, // in seconds, allow sufficient time on each call since instantiating each request has to also authenticate 36 | 'proxy' => null, // null values are ignored 37 | ], 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Merchant Credentials 42 | |-------------------------------------------------------------------------- 43 | | 44 | | Nerchant credentials' configurations 45 | */ 46 | 47 | 'merchants' => [ 48 | 'moirei' => [ 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Application Name 52 | |-------------------------------------------------------------------------- 53 | | 54 | | The application name to pass to the Google client. 55 | | Set as null to ignore. 56 | */ 57 | 'app_name' => config('app.name'), 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Google Merchant ID 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Your Merchant ID for Google Shopping API. 65 | | This is a numeric value. 66 | */ 67 | 'merchant_id' => env('GOOGLE_MERCHANT_ID_MOIREI', ''), 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Service Account 72 | |-------------------------------------------------------------------------- 73 | | 74 | | The base path url to json file that holds your service account credentials. 75 | | This file should only be accessible to your application. 76 | */ 77 | 'client_credentials_path' => storage_path('app/google-merchant-api/moirei-store-credentials.json'), 78 | ] 79 | ], 80 | 81 | /* 82 | |-------------------------------------------------------------------------- 83 | | Contents Config 84 | |-------------------------------------------------------------------------- 85 | | 86 | | Configuration for the API contents 87 | */ 88 | 89 | 'contents' => [ 90 | 'products' => [ 91 | 'model' => \App\Models\Product::class, 92 | 'attributes' => [ 93 | /* 94 | * Options: 95 | * 96 | * 'offerId', 'title', 'description', 'link', 'imageLink', 97 | * 'contentLanguage' (defaults to "en"), 98 | * 'targetCountry' (defaults to "AU"), 99 | * 'channel' (defaults to "online"), 100 | * 'condition' (defaults to "new"), 101 | * 'availability' (defaults to "in stock"), 102 | * 'ageGroup', 'availabilityDate', 'brand', 'color', 103 | * 'gender', 'googleProductCategory', 'gtin', 'itemGroupId', 'mpn', 104 | * 'price', 'sizes', 'customAttributes', 105 | */ 106 | 107 | 'offerId' => 'id', 108 | 'title' => 'name', 109 | 'description' => 'short_description', 110 | 'link' => 'url', 111 | 'imageLink' => 'image_url', 112 | 'availability' => 'in_stock', // "in stock", "out of stock", or "preorder" 113 | 'brand' => 'brand', 114 | 'mpn' => 'mpn', 115 | 'price' => 'gm_price', // must return array (2): value, and currency 116 | ], 117 | 'defaults' => [ 118 | 'contentLanguage' => 'en', 119 | 'targetCountry' => 'AU', 120 | 'channel' => 'online', 121 | 'availability' => 'in stock', 122 | 'condition' => 'new', 123 | ], 124 | ], 125 | 'orders' => [ 126 | 'schedule_orders_check' => true, 127 | 'schedule_frequency' => 'daily', 128 | 'debug_scout' => false, 129 | ], 130 | ], 131 | 132 | /* 133 | |-------------------------------------------------------------------------- 134 | | Default Currency 135 | |-------------------------------------------------------------------------- 136 | | 137 | | Default product currency 138 | */ 139 | 140 | 'default_currency' => 'AUD', 141 | ]; 142 | -------------------------------------------------------------------------------- /src/Contents/Order/Order.php: -------------------------------------------------------------------------------- 1 | attributes['kind'] = 'content#order'; 54 | 55 | $this->attributes['operationId'] = time(); 56 | } 57 | 58 | /** 59 | * Set the order's shipping cost. 60 | * 61 | * @param Closure|string|float|array $cost 62 | * @param string|null $currency 63 | * @return $this 64 | */ 65 | public function shippingCost($cost, $currency = null) 66 | { 67 | if(is_numeric($cost) || is_string($cost)){ 68 | if(is_null($currency)){ 69 | $currency = config('laravel-google-merchant-api.default_currency', 'AUD'); 70 | } 71 | $cost = (new Price)->value($cost)->currency($currency); 72 | }elseif(is_array($cost)){ 73 | $cost = (new Price)->with($cost); 74 | }elseif (is_callable($cost)) { 75 | $callback = $cost; 76 | $callback($cost = new Price); 77 | }elseif(!($cost instanceof Price)){ 78 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidPriceInput; 79 | } 80 | 81 | $this->attributes[ 'shippingCost' ] = $cost->get(); 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Set the order's shipping cost tax. 88 | * 89 | * @param Closure|string|float|array $cost 90 | * @param string|null $currency 91 | * @return $this 92 | */ 93 | public function shippingCostTax($cost, $currency = null) 94 | { 95 | if(is_numeric($cost) || is_string($cost)){ 96 | if(is_null($currency)){ 97 | $currency = config('laravel-google-merchant-api.default_currency', 'AUD'); 98 | } 99 | $cost = (new Price)->value($cost)->currency($currency); 100 | }elseif(is_array($cost)){ 101 | $cost = (new Price)->with($cost); 102 | }elseif (is_callable($cost)) { 103 | $callback = $cost; 104 | $callback($cost = new Price); 105 | }elseif(!($cost instanceof Price)){ 106 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidPriceInput; 107 | } 108 | 109 | $this->attributes[ 'shippingCostTax' ] = $cost->get(); 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Set the order's payment method 116 | * 117 | * @param Closure|array $paymentMethod 118 | * @return $this 119 | */ 120 | public function paymentMethod($paymentMethod) 121 | { 122 | if(is_array($paymentMethod)){ 123 | $paymentMethod = (new OrderPaymentMethod)->with($paymentMethod); 124 | }elseif (is_callable($paymentMethod)) { 125 | $callback = $paymentMethod; 126 | $callback($paymentMethod = new OrderPaymentMethod); 127 | }elseif(!($paymentMethod instanceof OrderPaymentMethod)){ 128 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidOrderPaymentMethodInput; 129 | } 130 | 131 | $this->attributes[ 'paymentMethod' ] = $paymentMethod->get(); 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * Append the order's line item 138 | * 139 | * @param array $lineItem 140 | * @return $this 141 | */ 142 | public function lineItem(array $lineItem) 143 | { 144 | $this->attributes[ 'lineItems' ][] = $lineItem; 145 | 146 | return $this; 147 | } 148 | 149 | /** 150 | * Mutate the line items by attaching corresponding model 151 | * 152 | * This assumes offerId is set as per model ID 153 | * 154 | * @return array 155 | */ 156 | public function getLineItems(){ 157 | 158 | $lineItems = array_map(function($lineItem){ 159 | return (new OrderLineItem)->with($lineItem)->all(); 160 | }, $this->attributes['lineItems']); 161 | 162 | 163 | if(!($product_model = config('laravel-google-merchant-api.contents.products.model'))){ 164 | return $lineItems; 165 | } 166 | 167 | return array_map(function($lineItem) use($product_model){ 168 | $lineItem['model'] = $product_model::find($lineItem['product']['offerId']); 169 | 170 | return $lineItem; 171 | }, $lineItems); 172 | } 173 | 174 | 175 | } 176 | -------------------------------------------------------------------------------- /src/Api/OrderApiSandbox.php: -------------------------------------------------------------------------------- 1 | kind('content#testOrder') 24 | ->shippingCost(30) 25 | // ->shippingCostTax(10) 26 | // ->paymentMethod(function($method){ 27 | // $method->type('Visa') 28 | // ->lastFourDigits('5555') 29 | // ->predefinedBillingAddress('7 James Ave.') 30 | // ->expirationMonth(5) 31 | // ->expirationYear(2020); 32 | // }) 33 | ->shippingOption('economy') // Allowed values: 'economy', 'expedited', 'oneDay', 'sameDay', 'standard', 'twoDay' 34 | ->predefinedEmail('pog.dwight.schrute@gmail.com') // Allowed values: 'pog.dwight.schrute@gmail.com', 'pog.jim.halpert@gmail.com', 'pog.pam.beesly@gmail.com' 35 | ->predefinedDeliveryAddress('dwight') // Allowed values: 'dwight, 'jim', 'pam' 36 | ->predefinedBillingAddress('dwight') // Allowed values: 'dwight, 'jim', 'pam' 37 | ->lineItem([ 38 | 'product' => (new Product) 39 | ->kind(null)->channel(null)->availability(null) // unset 40 | ->title('Wireless Power Bank') 41 | ->brand('MOIREI') 42 | ->condition('new') 43 | ->contentLanguage('en') 44 | ->targetCountry('US') 45 | ->imageLink('https://mrsc.moirei.com/storage/media/new-moirei-qi-wireless-power-bank-10000-mah-fast-charge-type-c-usb-qc-wireless-pd-charging-mobile-po-1571021317-PMNsS.jpg') 46 | ->offerId(5) 47 | ->price(59, 'USD') 48 | ->get(), 49 | 'quantityOrdered' => 2, 50 | 'returnInfo' => [ 51 | 'isReturnable' => true, 52 | 'daysToReturn' => 15, 53 | 'policyUrl' => 'https://www.moirei.com/shop/returns', 54 | ], 55 | 'shippingDetails' => [ 56 | 'deliverByDate' => '2019-11-20T12:34:02', 57 | 'method' => [ 58 | 'methodName' => 'Post', 59 | 'carrier' => 'Post', 60 | 'minDaysInTransit' => 2, 61 | 'maxDaysInTransit' => 5, 62 | ], 63 | 'shipByDate' => '2019-11-20T12:34:02' 64 | ], 65 | ]); 66 | 67 | return $this->create( $order ); 68 | } 69 | 70 | /** 71 | * Create test order(s). 72 | * 73 | * @param Closure|order $order callback, TestOrder or the ID 74 | * @param string $country 75 | * @return mix 76 | * @throws \GuzzleHttp\Exception\ClientException 77 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidOrderInput 78 | */ 79 | public function create($order, $country = 'US') 80 | { 81 | $order = self::resolveOrderInput($order); 82 | $instance = self::getInstance($this); 83 | 84 | return $instance->post([ 85 | 'country' => $country, // Allowed values: 'US', 'FR' 86 | 'testOrder' => $order->get(), 87 | // 'templateName' => '', 88 | ]); 89 | } 90 | 91 | /** 92 | * Advance test order. 93 | * 94 | * @param Closure|OrderTest|string $order callback, TestOrder or the ID 95 | * @return mix 96 | * @throws \GuzzleHttp\Exception\ClientException 97 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidOrderInput 98 | */ 99 | public function advance($order) 100 | { 101 | $instance = self::getInstance($this); 102 | if(is_string($order)){ 103 | $order_id = $order; 104 | }else{ 105 | $order_id = $instance->getId( self::resolveOrderInput($order) ); 106 | } 107 | 108 | $instance->setRequestArgs([ 109 | 'path' => $order_id . '/advance', 110 | ]); 111 | 112 | return $instance->post(); 113 | } 114 | 115 | /** 116 | * Advance test order by customer. 117 | * 118 | * @param Closure|OrderTest $order 119 | * @param string $reason 120 | * @param string $reason_text 121 | * @return mix 122 | * @throws \GuzzleHttp\Exception\ClientException 123 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidOrderInput 124 | */ 125 | public function cancel($order, string $reason = 'other', string $reason_text = 'Order cancel test') 126 | { 127 | $instance = self::getInstance($this); 128 | if(is_string($order)){ 129 | $order_id = $order; 130 | }else{ 131 | $order_id = $instance->getId( self::resolveOrderInput($order) ); 132 | } 133 | 134 | $instance->setRequestArgs([ 135 | 'path' => $order_id . '/cancelByCustomer', 136 | ]); 137 | 138 | if(!in_array($reason, self::$allowed_reasons)){ 139 | $reason = 'other'; 140 | } 141 | 142 | return $instance->post([ 143 | 'operationId' => $order->operationId, 144 | 'reason' => $reason, 145 | 'reasonText' => $reason_text, 146 | ]); 147 | } 148 | 149 | /** 150 | * Create test order return. 151 | * 152 | * @param Closure|OrderTest|string $order callback, TestOrder or the ID 153 | * @param Closure|array $items 154 | * @return mix 155 | * @throws \GuzzleHttp\Exception\ClientException 156 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidOrderInput 157 | */ 158 | public function createReturn($order, $items) 159 | { 160 | $instance = self::getInstance($this); 161 | if(is_string($order)){ 162 | $order_id = $order; 163 | }else{ 164 | $order_id = $instance->getId( self::resolveOrderInput($order) ); 165 | } 166 | 167 | $instance->setRequestArgs([ 168 | 'path' => $order_id . '/testreturn', 169 | ]); 170 | 171 | /** 172 | * 173 | * items structure: 174 | * 175 | * "lineItemId": string, 176 | * "quantity": unsigned integer 177 | */ 178 | return $instance->post([ 179 | 'items' => $items, 180 | ]); 181 | } 182 | 183 | /** 184 | * Resolve order input 185 | * 186 | * @param OrderTest|Closure $order 187 | * @return OrderTest 188 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidOrderTestInput 189 | */ 190 | static protected function resolveOrderTestInput($order){ 191 | if (is_callable($order)) { 192 | $callback = $order; 193 | 194 | $callback($order = new OrderTest); 195 | } 196 | 197 | if( !($order instanceof OrderTest) ){ 198 | throw new InvalidOrderInput; 199 | } 200 | 201 | return $order; 202 | } 203 | 204 | } -------------------------------------------------------------------------------- /src/Api/AbstractApi.php: -------------------------------------------------------------------------------- 1 | endpoint = $endpoint; 101 | $this->mode = $mode; 102 | 103 | // Backwords compatible 104 | if (is_string(config('laravel-google-merchant-api.default_merchant'))) { 105 | $config = config('laravel-google-merchant-api.default_merchant'); 106 | } else if (!is_null(config('laravel-google-merchant-api.merchant_id'))) { 107 | // Default to 1.0.3 config if `default_merchant` is not set 108 | $config = [ 109 | 'app_name' => config('laravel-google-merchant-api.app_name'), 110 | 'merchant_id' => config('laravel-google-merchant-api.merchant_id'), 111 | 'client_credentials_path' => config('laravel-google-merchant-api.client_credentials_path'), 112 | ]; 113 | $this->merchant($config); 114 | } 115 | 116 | if (isset($config)) { 117 | $this->merchant($config); 118 | } 119 | } 120 | 121 | /** 122 | * Set the async option. 123 | * 124 | * @param boolean $sync 125 | */ 126 | public function sync($sync = true) 127 | { 128 | $this->async = !$sync; 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Switch merchant. 135 | * Either use preconfigured merchant or provide new credentials. 136 | * 137 | * @param string|array $config 138 | * @throws \MOIREI\GoogleMerchantApi\Exceptions\InvalidMechantDetails 139 | */ 140 | public function merchant($config) 141 | { 142 | if (is_string($config)) { 143 | $config = config("laravel-google-merchant-api.merchants.$config"); 144 | } elseif (!is_array($config)) { 145 | throw new InvalidMechantDetails; 146 | } 147 | 148 | $app_name = data_get($config, 'app_name'); 149 | $merchant_id = data_get($config, 'merchant_id'); 150 | $client_credentials_path = data_get($config, 'client_credentials_path'); 151 | 152 | if (!($merchant_id && $client_credentials_path)) { 153 | throw new InvalidMechantDetails; 154 | } 155 | 156 | $this->client = $this->initClient($app_name, $merchant_id, $client_credentials_path); 157 | 158 | return $this; 159 | } 160 | 161 | /** 162 | * Set the arguments for the request, required: 163 | * 164 | * `method` 165 | * `path` 166 | * 167 | * optional: 168 | * 169 | * `params` 170 | * `body` 171 | * 172 | * @param array $args 173 | */ 174 | protected function setRequestArgs($args) 175 | { 176 | if (isset($args['method'])) { 177 | $this->request_method = $args['method']; 178 | } 179 | if (isset($args['path'])) { 180 | $this->request_path = $args['path']; 181 | } 182 | if (isset($args['params'])) { 183 | $this->request_params = $args['params']; 184 | } 185 | if (isset($args['body'])) { 186 | $this->request_body = $args['body']; 187 | } 188 | } 189 | 190 | 191 | /** 192 | * Get the API url 193 | * 194 | * @return string 195 | */ 196 | protected function getUrl() 197 | { 198 | return empty($this->request_path) ? $this->endpoint : $this->endpoint . '/' . $this->request_path; 199 | } 200 | 201 | 202 | /** 203 | * Return the request data, either query parameters (for GET/DELETE requests) 204 | * or the request body (for PUT/POST requests) 205 | * 206 | * @return array 207 | */ 208 | protected function getRequestData() 209 | { 210 | if ('GET' === $this->request_method || 'DELETE' === $this->request_method) { 211 | return [ 212 | 'query' => $this->request_params 213 | ]; 214 | } else { 215 | return [ 216 | 'body' => json_encode($this->request_body) 217 | ]; 218 | } 219 | } 220 | 221 | 222 | /** 223 | * Perform the request and return the response 224 | * 225 | * @return mix 226 | * @throws \GuzzleHttp\Exception\ClientException 227 | */ 228 | protected function execRequest() 229 | { 230 | 231 | if ($this->async) { 232 | 233 | $promise = new Promise(); 234 | $promise->then( 235 | 236 | function ($response) { 237 | if ($response->getStatusCode() === 200) { 238 | if (is_callable($this->then)) { 239 | $callback = $this->then; 240 | $callback(json_decode($response->getBody(), true)); 241 | } 242 | } else { 243 | if (is_callable($this->otherwise)) { 244 | $callback = $this->otherwise; 245 | $callback($response); 246 | } 247 | } 248 | }, 249 | 250 | function ($e) { 251 | if (is_callable($this->catch)) { 252 | $callback = $this->catch; 253 | $callback($e); 254 | } else { 255 | throw $e; 256 | } 257 | } 258 | ); 259 | } 260 | 261 | try { 262 | 263 | $response = $this->client->request($this->request_method, $this->getUrl(), $this->getRequestData()); 264 | 265 | if ($this->async) { 266 | $promise->resolve($response); 267 | return $this; 268 | } 269 | 270 | return $response; 271 | } catch (\GuzzleHttp\Exception\ClientException $e) { 272 | if ($this->async) { 273 | $promise->reject($e); 274 | return $this; 275 | } else { 276 | throw $e; 277 | } 278 | } 279 | } 280 | 281 | /** 282 | * POST resource 283 | * 284 | * POST /resource 285 | * POST /resource/#{id} 286 | * 287 | * @param array $params 288 | * @return mix 289 | * @throws \GuzzleHttp\Exception\ClientException 290 | */ 291 | public function post($params = array()) 292 | { 293 | 294 | $this->setRequestArgs([ 295 | 'method' => 'POST', 296 | // 'params' => $params, 297 | 'body' => $params, 298 | ]); 299 | 300 | $this->clearCallbacks(); 301 | 302 | return $this->execRequest(); 303 | } 304 | 305 | /** 306 | * Get resource 307 | * 308 | * GET /{resource} 309 | * GET /{resource}/#{id} 310 | * 311 | * @param null|int $id resource ID or null to get all 312 | * @return mix 313 | * @throws \GuzzleHttp\Exception\ClientException 314 | */ 315 | public function get($id = null, $params = array()) 316 | { 317 | 318 | $this->setRequestArgs([ 319 | 'method' => 'GET', 320 | 'path' => $id, 321 | 'params' => $params, 322 | ]); 323 | 324 | $this->clearCallbacks(); 325 | 326 | return $this->execRequest(); 327 | } 328 | 329 | 330 | /** 331 | * Delete a resource. Creates a Promise 332 | * 333 | * DELETE /{resource}/#{id} 334 | * 335 | * @param int $id product ID 336 | * @return instance 337 | */ 338 | // public function delete( $id ) { 339 | 340 | // $this->setRequestArgs([ 341 | // 'method' => 'DELETE', 342 | // 'path' => $id, 343 | // ]); 344 | 345 | // $this->clearCallbacks(); 346 | 347 | // return $this->execRequest(); 348 | // } 349 | 350 | /** 351 | * Closure callback on success for request client 352 | * 353 | * @param Closure $callback 354 | * @return this 355 | */ 356 | public function then(Closure $callback) 357 | { 358 | $this->then = $callback; 359 | 360 | return $this; 361 | } 362 | 363 | /** 364 | * Closure callback on unsuccess for request client 365 | * 366 | * @param Closure $callback 367 | * @return this 368 | */ 369 | public function otherwise(Closure $callback) 370 | { 371 | $this->otherwise = $callback; 372 | 373 | return $this; 374 | } 375 | 376 | /** 377 | * Closure callback on exception for request client 378 | * 379 | * @param Closure $callback 380 | * @return this 381 | */ 382 | public function catch(Closure $callback) 383 | { 384 | $this->catch = $callback; 385 | 386 | return $this; 387 | } 388 | 389 | /** 390 | * Clear callbacks 391 | */ 392 | protected function clearCallbacks() 393 | { 394 | $this->then = null; 395 | $this->otherwise = null; 396 | $this->catch = null; 397 | } 398 | 399 | /** 400 | * Switch between merchants. 401 | * Either use preconfigured merchant or provide new credentials. 402 | * 403 | * @param string $app_name 404 | * @param string $merchant_id 405 | * @param string $client_credentials_path 406 | * @return \GuzzleHttp\Client 407 | */ 408 | protected function initClient($app_name, $merchant_id, $client_credentials_path): Client 409 | { 410 | 411 | $version = config('laravel-google-merchant-api.version', 'v2'); 412 | 413 | if ($this->mode === 'sandbox') { 414 | $version = $version . 'sandbox'; 415 | } 416 | 417 | $client_config = collect(config('laravel-google-merchant-api.client_config'))->only([ 418 | 'timeout', 'headers', 'proxy', 419 | 'allow_redirects', 'http_errors', 'decode_content', 'verify', 'cookies', 420 | ])->filter()->all(); 421 | $client_config['base_uri'] = "https://www.googleapis.com/content/$version/$merchant_id/"; 422 | 423 | $client_config['headers'] = array_merge($client_config['headers'] ?? [], [ 424 | 'Accept' => 'application/json', 425 | 'Content-Type' => 'application/json', 426 | ]); 427 | 428 | if ((strpos($client_credentials_path, '.json') !== false) && file_exists($client_credentials_path)) { 429 | $client = new \Google_Client(); 430 | $client->setHttpClient(new Client($client_config)); 431 | 432 | $client->setApplicationName($app_name); 433 | 434 | $client->setAuthConfig($client_credentials_path); 435 | $client->addScope('https://www.googleapis.com/auth/content'); 436 | 437 | return $client->authorize(); 438 | } else { 439 | return new Client($client_config); 440 | } 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /src/Api/OrderApi.php: -------------------------------------------------------------------------------- 1 | setRequestArgs([ 55 | 'path' => $instance->getId($order) . '/acknowledge', 56 | ]); 57 | 58 | return $instance->post([ 59 | 'operationId' => $order->operationId, 60 | ]); 61 | } 62 | 63 | /** 64 | * Advance test order. 65 | * 66 | * @param Closure|Order $order 67 | * @param string $reason 68 | * @param string $reason_text 69 | * @return mix 70 | * @throws \GuzzleHttp\Exception\ClientException 71 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidOrderInput 72 | */ 73 | public function cancel($order, string $reason = 'other', string $reason_text = 'Other') 74 | { 75 | $order = self::resolveOrderInput($order); 76 | $instance = self::getInstance($this); 77 | 78 | $instance->setRequestArgs([ 79 | 'path' => $instance->getId($order) . '/cancel', 80 | ]); 81 | 82 | if(!in_array($reason, self::$allowed_reasons)){ 83 | $reason = 'other'; 84 | } 85 | 86 | return $instance->post([ 87 | 'operationId' => $order->operationId, 88 | 'reason' => $reason, 89 | 'reasonText' => $reason_text, 90 | ]); 91 | } 92 | 93 | /** 94 | * Cancel order line item. 95 | * 96 | * @param Closure|Order $order 97 | * @param string $lineItemId 98 | * @param string $productId 99 | * @param integer $quantity 100 | * @param string $reason 101 | * @param string $reason_text 102 | * @return mix 103 | * @throws \GuzzleHttp\Exception\ClientException 104 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidOrderInput 105 | */ 106 | public function cancelLineItem($order, string $lineItemId, string $productId, $quantity = 1, string $reason = 'other', string $reason_text = 'Other') 107 | { 108 | $order = self::resolveOrderInput($order); 109 | $instance = self::getInstance($this); 110 | 111 | $instance->setRequestArgs([ 112 | 'path' => $instance->getId($order) . '/cancelLineItem', 113 | ]); 114 | 115 | if(!in_array($reason, self::$allowed_reasons)){ 116 | $reason = 'other'; 117 | } 118 | 119 | return $instance->post([ 120 | 'operationId' => $order->operationId, 121 | 'lineItemId' => $lineItemId, 122 | 'productId' => $productId, 123 | 'quantity' => $quantity, // unsigned integer 124 | 'reason' => $reason, // string 125 | 'reasonText' => $reason_text, // string 126 | ]); 127 | } 128 | 129 | /** 130 | * Reject return on an line item. 131 | * 132 | * @param Closure|Order $order 133 | * @param string $lineItemId 134 | * @param string $productId 135 | * @param integer $quantity 136 | * @param string $reason 137 | * @param string $reason_text 138 | * @return mix 139 | * @throws \GuzzleHttp\Exception\ClientException 140 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidOrderInput 141 | */ 142 | public function rejectReturnLineItem($order, string $lineItemId, string $productId, $quantity = 1, string $reason = 'other', string $reason_text = 'Other') 143 | { 144 | $order = self::resolveOrderInput($order); 145 | $instance = self::getInstance($this); 146 | 147 | $instance->setRequestArgs([ 148 | 'path' => $instance->getId($order) . '/rejectReturnLineItem', 149 | ]); 150 | 151 | if(!in_array($reason, self::$allowed_reasons)){ 152 | $reason = 'other'; 153 | } 154 | 155 | return $instance->post([ 156 | 'operationId' => $order->operationId, 157 | 'lineItemId' => $lineItemId, 158 | 'productId' => $productId, 159 | 'quantity' => $quantity, // unsigned integer 160 | 'reason' => $reason, // string 161 | 'reasonText' => $reason_text, // string 162 | ]); 163 | } 164 | 165 | /** 166 | * Reject return on an line item. 167 | * 168 | * @param Closure|Order $order 169 | * @param string $lineItemId 170 | * @param string $productId 171 | * @param Closure|Price $priceAmount 172 | * @param Closure|Price $taxAmount 173 | * @param integer $quantity 174 | * @param string $reason 175 | * @param string $reason_text 176 | * @return mix 177 | * @throws \GuzzleHttp\Exception\ClientException 178 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidOrderInput 179 | */ 180 | public function returnRefundLineItem($order, string $lineItemId, string $productId, $priceAmount, $taxAmount, $quantity = 1, string $reason = 'other', string $reason_text = 'Other') 181 | { 182 | $order = self::resolveOrderInput($order); 183 | $instance = self::getInstance($this); 184 | 185 | $instance->setRequestArgs([ 186 | 'path' => $instance->getId($order) . '/returnRefundLineItem', 187 | ]); 188 | 189 | if(!in_array($reason, self::$allowed_reasons)){ 190 | $reason = 'other'; 191 | } 192 | 193 | if(is_array($priceAmount)){ 194 | $priceAmount = (new Price)->with($priceAmount); 195 | }elseif (is_callable($priceAmount)) { 196 | $callback = $priceAmount; 197 | $callback($priceAmount = new Price); 198 | }elseif(!($priceAmount instanceof Price)){ 199 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidPriceInput; 200 | } 201 | 202 | if(is_array($taxAmount)){ 203 | $taxAmount = (new Price)->with($taxAmount); 204 | }elseif (is_callable($taxAmount)) { 205 | $callback = $taxAmount; 206 | $callback($taxAmount = new Price); 207 | }elseif(!($taxAmount instanceof Price)){ 208 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidPriceInput; 209 | } 210 | 211 | return $instance->post([ 212 | 'operationId' => $order->operationId, 213 | 'lineItemId' => $lineItemId, 214 | 'productId' => $productId, 215 | 'quantity' => $quantity, // unsigned integer 216 | 'reason' => $reason, // string 217 | 'reasonText' => $reason_text, // string 218 | 'priceAmount' => $priceAmount->get(), 219 | 'taxAmount' => $taxAmount->get(), 220 | ]); 221 | } 222 | 223 | /** 224 | * Get order by order. 225 | * 226 | * @param Order|null|Closure $order 227 | * @return mix 228 | * @throws \GuzzleHttp\Exception\ClientException 229 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidOrderInput 230 | */ 231 | public function get($order = null, $params = array()){ 232 | $instance = self::getInstance($this); 233 | 234 | if(is_null($order)){ 235 | $id = null; 236 | }else{ 237 | $id = $instance->getId( self::resolveOrderInput($order) ); 238 | } 239 | 240 | $instance->setRequestArgs( array( 241 | 'method' => 'GET', 242 | 'path' => $id, 243 | ) ); 244 | 245 | $instance->clearCallbacks(); 246 | 247 | return $instance->execRequest(); 248 | } 249 | 250 | /** 251 | * List un-acknowledged orders. 252 | * 253 | * @return mix 254 | * @throws \GuzzleHttp\Exception\ClientException 255 | */ 256 | public function list(){ 257 | return $this->get(); 258 | } 259 | 260 | /** 261 | * List acknowledged orders. 262 | * 263 | * @return mix 264 | * @throws \GuzzleHttp\Exception\ClientException 265 | */ 266 | public function listAcknowledged(){ 267 | $instance = self::getInstance($this); 268 | 269 | $instance->setRequestArgs( array( 270 | 'method' => 'GET', 271 | 'path' => '?acknowledged=true', 272 | ) ); 273 | 274 | $instance->clearCallbacks(); 275 | 276 | return $instance->execRequest(); 277 | } 278 | 279 | /** 280 | * Scout Google Merchant for un-acknowledged orders and take actions 281 | * 282 | * @throws \GuzzleHttp\Exception\ClientException 283 | */ 284 | public function scout(){ 285 | 286 | function handleResponse($response, $merchant, $merchant_id) { 287 | if($response->getStatusCode() === 200){ 288 | $data = json_decode($response->getBody(), true); 289 | if(count($resource)){ 290 | $orders = array_map(function($resource){ 291 | return (new Order)->with($resource); 292 | }, $data->resources); 293 | event(new NewOrdersScoutedEvent($orders, $merchant, $merchant_id)); 294 | } 295 | }else{ 296 | // 297 | } 298 | } 299 | 300 | $client = $this->sync(); 301 | if($merchants = config('laravel-google-merchant-api.merchants')){ 302 | // Backwords compatible 303 | // Default to 1.0.3 config if `merchants` is not set 304 | foreach($merchants as $merchant => $merchant_config){ 305 | $response = $client->merchant($merchant_config)->list(); 306 | handleResponse($response, $merchant, data_get($merchant_config, 'merchant_id')); 307 | } 308 | }else{ 309 | $response = $client->list(); 310 | handleResponse($response, null, config('laravel-google-merchant-api.merchant_id')); 311 | } 312 | 313 | if(config('laravel-google-merchant-api.contents.orders.debug_scout', false)){ 314 | event(new OrderContentScoutedEvent()); 315 | } 316 | } 317 | 318 | /** 319 | * Get order id 320 | * 321 | * @param Order|null $order 322 | * @return string|null 323 | */ 324 | protected function getId($order){ 325 | if(is_null($order)) return null; 326 | 327 | return $order->id; 328 | } 329 | 330 | /** 331 | * Get OrderApi instance 332 | * 333 | * @param OrderApi $orderApi 334 | * @return OrderApi 335 | */ 336 | static protected function getInstance(OrderApi $orderApi){ 337 | if($orderApi->async){ 338 | // duplicate so that callbacks are not overridden 339 | return clone $orderApi; 340 | }else{ 341 | return $orderApi; 342 | } 343 | } 344 | 345 | /** 346 | * Resolve order input 347 | * 348 | * @param Order|Closure $order 349 | * @return Order 350 | * @throws MOIREI\GoogleMerchantApi\Exceptions\InvalidOrderInput 351 | */ 352 | static protected function resolveOrderInput($order){ 353 | if (is_callable($order)) { 354 | $callback = $order; 355 | 356 | $callback($order = new Order); 357 | } 358 | 359 | if( !($order instanceof Order) ){ 360 | throw new InvalidOrderInput; 361 | } 362 | 363 | return $order; 364 | } 365 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Google API 2 | 3 | A sweet package for managing Google Merchant Center feeds for Google Shopping. This package is prepared to implement the advanced [Content API](https://developers.google.com/shopping-content/v2.1/quickstart) for Merchants. 4 | 5 | 6 | 7 | Example usage: 8 | 9 | ```php 10 | use MOIREI\GoogleMerchantApi\Facades\ProductApi; 11 | use MOIREI\GoogleMerchantApi\Facades\OrderApi; 12 | 13 | ... 14 | 15 | ProductApi::insert(function($product){ 16 | $product->offerId(1) 17 | ->title('Purple Shoes') 18 | ->description('What are thooose!!') 19 | ->price(10) 20 | ->custom('purchase_quantity_limit', 1000) 21 | ->availabilityDate( today()->addDays(7) ); 22 | })->then(function($response){ 23 | echo 'Product inserted'; 24 | })->otherwise(function($response){ 25 | echo 'Insert failed'; 26 | })->catch(function($e){ 27 | echo($e->getResponse()->getBody()->getContents()); 28 | }); 29 | 30 | OrderApi::list()->then(function($response){ 31 | // 32 | })->otherwise(function($response){ 33 | echo 'List failed'; 34 | })->catch(function($e){ 35 | echo($e->getResponse()->getBody()->getContents()); 36 | }); 37 | 38 | 39 | OrderApi::scout(); // Scout and fire event 40 | ``` 41 | 42 | 43 | 44 | ## Features 45 | 46 | * **Products API** 47 | * Implements the `insert`, `get`, `delete` and `list` API calls 48 | * Uses defined schema interface for directly working with eloquent models (a product model) 49 | * Event listeners to respond to changes made to eloquent models, and `insert` them automatically 50 | * **Orders API** 51 | * Implements the `acknowledge`, `cancel`, `cancelLineItem`, `rejectReturnLineItem`, `returnRefundLineItem`, `get` and `list` API calls. 52 | * Internal schedule scouts un-acknowledged orders and fires an event. This means new orders on your Google Shopping can be automatically acknowledged and registered. 53 | * Includes sandbox functions for `testOrders`. 54 | * Multiple Merchants (1.1.0) 55 | 56 | 57 | 58 | ## Updating to 1.1.x 59 | 60 | Although backwards compatible, be sure to update your config to be able to use multiple merchants. 61 | 62 | 63 | 64 | 65 | ## Installation 66 | 67 | Via composer: 68 | 69 | ```bash 70 | composer require moirei/laravel-google-merchant-api 71 | ``` 72 | 73 | Install the service provider (skip for Laravel>=5.5); 74 | 75 | ``` 76 | // config/app.php 77 | 'providers' => [ 78 | ... 79 | MOIREI\GoogleMerchantApi\GoogleMerchantApiServiceProvider::class, 80 | ], 81 | ``` 82 | 83 | Publish the config 84 | 85 | ```php 86 | php artisan vendor:publish --tag="google-merchant-api-config" 87 | ``` 88 | 89 | 90 | 91 | ## Setup & Authorisation 92 | 93 | * Follow the instructions [here]( https://developers.google.com/shopping-content/v2/quickstart ) and create a service account key. Create `storage/app/google-merchant-api/service-account-credentials.json` in your app root and store the downloaded json contents 94 | * Obtain your numeric Merchant ID 95 | * Add your Merchant ID and the path to your service account credentials to the config 96 | * In the config, setup the attributes section in product content if you need to use arrays or models 97 | 98 | 99 | 100 | ## Usage 101 | 102 | ### Multiple Merchants 103 | 104 | From `1.1.0` we can now define multiple merchants and switch between them by simply calling the `merchant` method from either the Order or Product API class. 105 | 106 | ```php 107 | ProductApi::merchant('my-pet-store')->insert($product); 108 | ``` 109 | 110 | ```php 111 | // config/laravel-google-merchant-api.php 112 | ... 113 | 'merchants' => [ 114 | 'my-pet-store' => [ 115 | 'app_name' => config('app.name'), 116 | 'merchant_id' => '000000000', 117 | 'client_credentials_path' => storage_path('app/my-pet-store-credentials.json'), 118 | ] 119 | ], 120 | ... 121 | ``` 122 | 123 | Or 124 | 125 | ```php 126 | ProductApi::merchant([ 127 | 'app_name' => 'My Pet Store', 128 | 'merchant_id' => '000000000', 129 | 'client_credentials_path' => storage_path('app/my-pet-store-credentials.json') 130 | ])->insert($product); 131 | ``` 132 | 133 | 134 | 135 | ### Product API 136 | 137 | The Google Merchant contents can be queried via the `insert`, `get`, `delete`, and `list` methods. The product content is contained and handled via the `Product` class. An instance of this class can be passed directly or resolved in a Closure callback. An instance can be population by 138 | 139 | * Directly accessing underlying attributes. See [special functions](doc/prodcut-conent-special-methods.md). 140 | * Passing an eloquent model, or by 141 | * Passing a raw array 142 | 143 | To pass an array or a model, the attributes relationships must be defined in the config. 144 | 145 | #### Insert 146 | 147 | The insert method creates a new content, as well as updates an old content if the `channel`, `contentLanguage`, `targetCountry` and `offerId` are the same. 148 | 149 | ```php 150 | $attributes = [ 151 | 'id' => 1, // maps to offerId (if set in config) 152 | 'name' => 'Product 1', // likewise maps to title 153 | ]; 154 | ProductApi::insert(function($product) use($attributes){ 155 | $product->with($attributes) 156 | ->link('https://moirei.com/mg001') 157 | ->price(60, 'USD'); 158 | })->then(function($data){ 159 | echo 'Product inserted'; 160 | })->otherwise(function(){ 161 | echo 'Insert failed'; 162 | })->catch(function($e){ 163 | dump($e); 164 | }); 165 | ``` 166 | 167 | **With arrays**: 168 | 169 | ```php 170 | use MOIREI\GoogleMerchantApi\Contents\Product\Product as GMProduct; 171 | 172 | ... 173 | $attributes = [ 174 | 'id' => 1, 175 | 'name' => 'Product 1', 176 | ]; 177 | $product = (new GMProduct)->with($attributes); 178 | ``` 179 | 180 | The `attributes` values must be defined as per the attributes map in the config. 181 | 182 | **With Eloquent Models**: 183 | 184 | ```php 185 | use App\Models\Product; 186 | use MOIREI\GoogleMerchantApi\Contents\Product\Product as GMProduct; 187 | 188 | 189 | ... 190 | $model = Product::find(1); 191 | $product = (new GMProduct)->with($model); 192 | 193 | ProductApi::insert($product)->catch(function($e){ 194 | // always catch exceptions 195 | }); 196 | ``` 197 | 198 | The model `attributes` values must be defined as per the attributes map in the config. For accessing undefined models attributes, use Accessors and custom Model attributes: 199 | 200 | ```php 201 | protected $appends = [ 202 | 'availability', 203 | 'gm_price', 204 | ]; 205 | 206 | ... 207 | 208 | public function getAvailabilityAttribute(){ 209 | return 'in stock'; // calculate 210 | } 211 | public function getGmPriceAttribute(){ 212 | return [ 213 | 'value' => $this->price, 214 | 'currency' => $this->currency->code, 215 | ]; 216 | } 217 | ``` 218 | 219 | For setting custom Product contents (`customAttributes`), you're probably better off using the `custom()` method. Likewise for `availabilityDate` use the `availabilityUntil()` method. 220 | 221 | **With Events & Listeners**: 222 | 223 | The provided event and listener can be setup such that when your application creates or updates a model, the product content is automatically inserted. 224 | 225 | To set this up, add the following snippet to your eloquent mode. The `product` variable can be a model or an array. 226 | 227 | ```php 228 | use MOIREI\GoogleMerchantApi\Events\ProductCreatedOrUpdatedEvent; 229 | 230 | ... 231 | 232 | /** 233 | * The "booting" method of the model. 234 | * 235 | * @return void 236 | */ 237 | protected static function boot() { 238 | parent::boot(); 239 | 240 | // when a product is created 241 | static::created(function(Product $product){ 242 | // perhaps a logic to ignore drafts and private products 243 | if($product->is_active && (config('app.env') === 'production')){ 244 | event(new ProductCreatedOrUpdatedEvent($product)); 245 | } 246 | }); 247 | 248 | // when a product is updated 249 | static::updated(function(Product $product){ 250 | // perhaps a logic to ignore drafts and private products 251 | if($product->is_active && (config('app.env') === 'production')){ 252 | event(new ProductCreatedOrUpdatedEvent(function($gm_product) use ($product){ 253 | $gm_product->with($product) 254 | ->preorder() 255 | ->availabilityDate($product->preorder_date); 256 | })); 257 | } 258 | }); 259 | } 260 | ``` 261 | 262 | Next, define the events relationship in `EventServiceProvider.php`. 263 | 264 | ```php 265 | use MOIREI\GoogleMerchantApi\Listeners\ProductCreatedOrUpdatedListener; 266 | 267 | ... 268 | 269 | /** 270 | * The event listener mappings for the application. 271 | * 272 | * @var array 273 | */ 274 | protected $listen = [ 275 | ..., 276 | /** 277 | * Product events 278 | */ 279 | ProductCreatedOrUpdatedEvent::class => [ 280 | ProductCreatedOrUpdatedListener::class, 281 | ], 282 | 283 | ]; 284 | ``` 285 | 286 | #### Get & List 287 | 288 | ```php 289 | ProductApi::get($product)->then(function($data){ 290 | // 291 | })->catch(function($e){ 292 | // always catch exceptions 293 | }); 294 | ``` 295 | 296 | The `list` method calls the `get` method without any parameters; 297 | 298 | ```php 299 | ProductApi::list()->then(function($data){ 300 | // 301 | }); 302 | ``` 303 | 304 | So the following should likewise retrieve the product list: 305 | 306 | ```php 307 | ProductApi::get()->then(function($data){ 308 | // 309 | }); 310 | ``` 311 | 312 | #### Delete 313 | 314 | ```php 315 | ProductApi::delete($product)->then(function($data){ 316 | // 317 | }); 318 | ``` 319 | 320 | To set up with the event listener, add the following to your eloquent model: 321 | 322 | ```php 323 | use MOIREI\GoogleMerchantApi\Events\ProductDeletedEvent; 324 | 325 | ... 326 | 327 | protected static function boot() { 328 | parent::boot(); 329 | 330 | ... 331 | 332 | // when a product is deleted 333 | static::deleted(function(Product $product){ 334 | if(config('app.env') === 'production'){ 335 | event(new ProductDeletedEvent($product)); 336 | } 337 | }); 338 | } 339 | ``` 340 | 341 | Then define the relationship in `EventServiceProvider.php`: 342 | 343 | ```php 344 | use MOIREI\GoogleMerchantApi\Listeners\ProductDeletedListener; 345 | 346 | ... 347 | 348 | protected $listen = [ 349 | ..., 350 | ProductDeletedEvent::class => [ 351 | ProductDeletedListener::class, 352 | ], 353 | ]; 354 | ``` 355 | 356 | 357 | 358 | # Order API 359 | 360 | ***Please note that these implementations have not been properly tested.*** 361 | 362 | #### Using the API methods 363 | 364 | The `acknowledge`, `cancel`, `cancelLineItem`, `rejectReturnLineItem`, `returnRefundLineItem`, `get`, `list` methods are currently implemented for interacting with your Google Merchant. 365 | 366 | The format for using these methods are standard across the entire package. For example, an order can be acknowledged by 367 | 368 | ```php 369 | OrderApi::acknowledge(function($order){ 370 | $order->id('TEST-1953-43-0514'); 371 | }); 372 | ``` 373 | 374 | or by 375 | 376 | ```php 377 | $order = (new Order)->with([ 378 | 'id' => 'TEST-1953-43-0514', 379 | ]); 380 | OrderApi::acknowledge($order); 381 | ``` 382 | 383 | Additionally the `listAcknowledged` method is provided so one can list acknowledged orders if needed. 384 | 385 | #### Scheduled Scouts 386 | 387 | If `schedule_orders_check` is set as true in the config, the package will regularly scout un-acknowledged orders and will fire a `\MOIREI\GoogleMerchantApi\Events\NewOrdersScoutedEvent` event. This event includes an **array** of orders of class `\MOIREI\GoogleMerchantApi\Contents\Order`. The orders are structured as per the [Order Resource](https://developers.google.com/shopping-content/v2/reference/v2.1/orders#resource). 388 | 389 | Example handle in your listener: 390 | 391 | ```php 392 | use MOIREI\GoogleMerchantApi\Events\NewOrdersScoutedEvent; 393 | use MOIREI\GoogleMerchantApi\Facades\OrderApi; 394 | 395 | ... 396 | public function handle(NewOrdersScoutedEvent $event) 397 | { 398 | $merchant = $event->merchant; // array key as defined in config 399 | $merchant_id = $event->merchant_id; 400 | 401 | foreach($event->orders as $gm_order){ 402 | OrderApi::acknowledge($gm_order); 403 | 404 | $gm_order = $gm_order->all(); // get all attributes, including mutated attributes 405 | foreach($gm_order['lineItems'] as $line_item){ 406 | $model = $line_item['model']; // retrieves model 407 | $quantity = $line_item['quantityOrdered']; 408 | $shipping = $line_item['shippingDetails']; 409 | $delivery_date = $shipping['deliverByDate']->diffForHumans(); 410 | 411 | // register new order item 412 | } 413 | 414 | // register new order 415 | } 416 | } 417 | ``` 418 | 419 | **Notes**: 420 | 421 | * Accessing the `lineItems` will automatically resolve and attach the corresponding model to each item. Of course this assumes your inserted products' `offerId` correspond to the model's ID & primary key. 422 | * If you haven't already started Laravel scheduler, you'll need to add the following Cron entry to your server. `* * * * * php artisan schedule:run >> /dev/null 2>&1`. 423 | * It's important you test that the scheduler is set up correctly. For this reason, the `MOIREI\GoogleMerchantApi\Events\OrderContentScoutedEvent` event is provided. If `debug_scout` is set to true in the config, this event is fired whenever the scheduler fires. 424 | 425 | #### Sandboxing 426 | 427 | The OrderApi class provide a way of calling some of the sandbox operations. Example: 428 | 429 | ```php 430 | OrderApi::sandbox()->create(function($order){ 431 | $order->shippingCost(30) 432 | ->shippingOption('economy') 433 | ->predefinedEmail('pog.dwight.schrute@gmail.com') 434 | ->predefinedDeliveryAddress('dwight'); 435 | }) 436 | ``` 437 | 438 | You may use 439 | 440 | ```php 441 | OrderApi::sandbox()->testCreate(); 442 | ``` 443 | 444 | to use a preset example. 445 | 446 | Implemented sandbox actions: 447 | 448 | | Function | Sandbox Action | 449 | | -------------- | ---------------- | 450 | | `create` | createtestorder | 451 | | `advance` | advancetestorder | 452 | | `cancel` | createtestorder | 453 | | `createReturn` | createtestreturn | 454 | 455 | 456 | 457 | ## Commands 458 | 459 | This package provides an artisan command for scouting orders. 460 | 461 | ```bash 462 | php artisan gm-orders:scout 463 | ``` 464 | 465 | 466 | 467 | ## Handling Errors 468 | 469 | Methods that throw exceptions 470 | 471 | * `MOIREI\GoogleMerchantApi\Contents\Product::with()` 472 | 473 | throws `MOIREI\GoogleMerchantApi\Exceptions\ProductContentAttributesUndefined` if the supplied attributes is not a Model or array. 474 | 475 | * The `insert`, `get`, `delete`, `list`, `listAcknowledged` and `scout` methods in the API classes will throw `GuzzleHttp\Exception\ClientException` if the client request is corrupted, fails, not defined or not authorised. 476 | 477 | * The `MOIREI\GoogleMerchantApi\Exceptions\Invalid**Input` exceptions are thrown if an unresolvable entity is passed as a content attribute. 478 | 479 | * The `merchant` method throws `MOIREI\GoogleMerchantApi\Exceptions\InvalidMechantDetails` if unable to resolve a merchant ID or credentials path. 480 | 481 | Exceptions should be handled using the `catch` function. If making synchronous calls, use the try-catch block. You'd be well advised to always catch requests (and notify your business logic), seeing that Google has a million reasons to deny any request. 482 | 483 | 484 | 485 | ## Design Notes 486 | 487 | * Insert, List, Get, Delete methods will always return a clone of the original instance if using the default asynchronous feature. This allows the then, otherwise, and catch callbacks of multiple requests to not override. These methods return a Guzzle response if set to synchronous mode. 488 | * If the delete method is called and the resolved content ID is invalid, it returns without making any requests or throwing any errors. If the get method, it returns a list of products or orders. 489 | * A valid product content ID follows the pattern *online:en:AU:1* i.e. `channel:contentLanguage:targetCountry:offerId`. This ID is of course auto generated; and the attributes, except for `offerId`, have default values. 490 | * Requests can take up to 2 hours before they reflect on your Google Merchant Center. Patience! 491 | * Unlike the ProductApi or OrderApi classes, the events constructor may take a Model, array or callback. 492 | * Calling the `all` method on a `Product`, `Order` or any content class will resolve all mutated attributes. e.g. `$order['lineItems'][0]['shippingDetails']['deliverByDate']` returns a `Carbon`. 493 | 494 | 495 | 496 | #### Synchronous Calls 497 | 498 | All the above are by default asynchronous. To make synchronous calls, use the `sync` method: 499 | 500 | ```php 501 | try{ 502 | $response = ProductApi::sync()->insert(function($product){ 503 | $product->offerId(1) 504 | ->country('AU') 505 | ->inStock(false); 506 | }); 507 | }catch(\GuzzleHttp\Exception\ClientException $e){ 508 | // 509 | } 510 | ``` 511 | 512 | **Note**: In this case, methods such as `insert`, `get`, `delete`, and `list`, etc, returns a Guzzle response when called asynchronously (rather than an instance of `ProductApi` or `OrderApi`. This means your exception blocks should be wrapped around requests. 513 | 514 | 515 | 516 | ## Contributing 517 | 518 | This package is intended to provide a Laravel solution for the Google Shopping API for Google Merchant. Currently, only the Product Content has been adequately implemented and tested. For orders, refunds, etc., ideas and pull-requests are welcome. 519 | 520 | 521 | 522 | 523 | ## Credits 524 | 525 | - [Augustus Okoye](https://github.com/augustusnaz) 526 | - [All Contributors](https://github.com/augustusnaz/laravel-google-merchant-api/graphs/contributors) 527 | 528 | 529 | 530 | ## License 531 | 532 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 533 | -------------------------------------------------------------------------------- /src/Contents/Product/Product.php: -------------------------------------------------------------------------------- 1 | attributes['kind'] = 'content#product'; 112 | $this->attributes['sizes'] = array(); 113 | $this->attributes['customAttributes'] = array(); 114 | $this->attributes['shipping'] = array(); 115 | $this->attributes['taxes'] = array(); 116 | 117 | // Set defaults 118 | foreach(config('laravel-google-merchant-api.contents.products.defaults', []) as $attribute => $default){ 119 | if(in_array($attribute, $this->allowed_attributes)){ 120 | $this->attributes[ $attribute ] = $default; 121 | } 122 | } 123 | 124 | } 125 | 126 | public function __call($name, $arguments) { 127 | if(in_array($name, $this->allowed_attributes)){ 128 | $this->attributes[ $name ] = $arguments[0]; 129 | }else{ 130 | throw new \BadMethodCallException("Instance method Product->$name() doesn't exist"); 131 | } 132 | 133 | return $this; 134 | } 135 | 136 | public function __get($attribute){ 137 | if(isset($this->attributes[ $attribute ]) && !empty($this->attributes[ $attribute ])){ 138 | return $this->attributes[ $attribute ]; 139 | } 140 | 141 | return null; 142 | } 143 | 144 | 145 | /** 146 | * Batch fill with array 147 | * 148 | * @param array|Model $attributes 149 | * @throws MOIREI\GoogleMerchantApi\Exceptions\ProductContentAttributesUndefined 150 | */ 151 | public function with($attributes){ 152 | 153 | if( !($attributes_map = config('laravel-google-merchant-api.contents.products.attributes')) ){ 154 | throw new \MOIREI\GoogleMerchantApi\Exceptions\ProductContentAttributesUndefined; 155 | } 156 | 157 | if($attributes instanceof \Illuminate\Database\Eloquent\Model){ 158 | $attributes = $attributes->toArray(); 159 | } 160 | 161 | $attributes_map = collect($attributes_map)->only($this->allowed_attributes)->all(); 162 | 163 | $attributes = collect($attributes)->only(array_values($attributes_map))->all(); 164 | 165 | foreach($attributes_map as $key => $attribute){ 166 | if(isset($attributes[ $attribute ])){ 167 | $this->attributes[ $key ] = $attributes[ $attribute ]; 168 | } 169 | } 170 | 171 | return $this; 172 | } 173 | 174 | /** 175 | * Set the product image link. 176 | * 177 | * @param string $imageLink 178 | * @return $this 179 | */ 180 | public function image(string $imageLink) 181 | { 182 | $this->attributes[ 'imageLink' ] = $imageLink; 183 | 184 | return $this; 185 | } 186 | 187 | /** 188 | * Set the product content language. 189 | * 190 | * @param string $contentLanguage 191 | * @return $this 192 | */ 193 | public function lang(string $contentLanguage) 194 | { 195 | $this->attributes[ 'contentLanguage' ] = $contentLanguage; 196 | 197 | return $this; 198 | } 199 | 200 | /** 201 | * Set the product target country. 202 | * 203 | * @param string $targetCountry 204 | * @return $this 205 | */ 206 | public function country(string $targetCountry) 207 | { 208 | $this->attributes[ 'targetCountry' ] = $targetCountry; 209 | 210 | return $this; 211 | } 212 | 213 | /** 214 | * Set the product as online. 215 | * 216 | * @param boolean $online 217 | * @return $this 218 | */ 219 | public function online($online = true) 220 | { 221 | if($online){ 222 | $this->attributes[ 'channel' ] = 'online'; 223 | }else{ 224 | $this->attributes[ 'channel' ] = 'local'; 225 | } 226 | 227 | return $this; 228 | } 229 | 230 | /** 231 | * Set the product availability. 232 | * 233 | * @param boolean $inStock 234 | * @return $this 235 | */ 236 | public function inStock($inStock = true) 237 | { 238 | if($inStock){ 239 | $this->attributes[ 'availability' ] = 'in stock'; 240 | } 241 | else{ 242 | $this->attributes[ 'availability' ] = 'out of stock'; 243 | } 244 | 245 | return $this; 246 | } 247 | 248 | /** 249 | * Set the product availability. 250 | * 251 | * @return $this 252 | */ 253 | public function preorder() 254 | { 255 | $this->attributes[ 'availability' ] = 'preorder'; 256 | 257 | return $this; 258 | } 259 | 260 | /** 261 | * Set the product stock availability date. 262 | * 263 | * @param Carbon\Carbon|string $until 264 | * @return $this 265 | */ 266 | public function availabilityDate($until) 267 | { 268 | if( !($until instanceof Carbon) ){ 269 | $until = new Carbon($until); 270 | } 271 | 272 | $this->attributes[ 'availabilityDate' ] = $until->format('Y-m-d') . 'T' . $until->format('h:i:s'); 273 | 274 | return $this; 275 | } 276 | 277 | /** 278 | * Set the product stock availability date. 279 | * 280 | * Date on which the product should expire, in ISO 8601 format. 281 | * Google: "The actual expiration date in Google Shopping is exposed in productstatuses as googleExpirationDate and might be earlier if expirationDate is too far in the future" 282 | * 283 | * @param Carbon\Carbon|string $until 284 | * @return $this 285 | */ 286 | public function expirationDate($date) 287 | { 288 | if( !($date instanceof Carbon) ){ 289 | $date = new Carbon($date); 290 | } 291 | 292 | $this->attributes[ 'expirationDate' ] = $date->format('Y-m-d') . 'T' . $date->format('h:i:s'); 293 | 294 | return $this; 295 | } 296 | 297 | /** 298 | * Set the product's google product category. 299 | * 300 | * @param string $googleProductCategory 301 | * @return $this 302 | */ 303 | public function category(string $googleProductCategory) 304 | { 305 | $this->attributes[ 'googleProductCategory' ] = $googleProductCategory; 306 | 307 | return $this; 308 | } 309 | 310 | /** 311 | * Set the product's price. 312 | * 313 | * @param Closure|string|float|array $price 314 | * @param string|null $currency 315 | * @return $this 316 | */ 317 | public function price($price, $currency = null) 318 | { 319 | if(is_numeric($price) || is_string($price)){ 320 | if(is_null($currency)){ 321 | $currency = config('laravel-google-merchant-api.default_currency', 'AUD'); 322 | } 323 | $price = (new Price)->value($price)->currency($currency); 324 | }elseif(is_array($price)){ 325 | $price = (new Price)->with($price); 326 | }elseif (is_callable($price)) { 327 | $callback = $price; 328 | $callback($price = new Price); 329 | }elseif(!($price instanceof Price)){ 330 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidPriceInput; 331 | } 332 | 333 | $this->attributes[ 'price' ] = $price->get(); 334 | 335 | return $this; 336 | } 337 | 338 | /** 339 | * Set the product's sale price. 340 | * 341 | * @param Closure|string|float|array $price 342 | * @param string|null $currency 343 | * @return $this 344 | */ 345 | public function salePrice($price, $currency = null) 346 | { 347 | if(is_numeric($price) || is_string($price)){ 348 | if(is_null($currency)){ 349 | $currency = config('laravel-google-merchant-api.default_currency', 'AUD'); 350 | } 351 | $price = (new Price)->value($price)->currency($currency); 352 | }elseif(is_array($price)){ 353 | $price = (new Price)->with($price); 354 | }elseif (is_callable($price)) { 355 | $callback = $price; 356 | $callback($price = new Price); 357 | }elseif(!($price instanceof Price)){ 358 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidPriceInput; 359 | } 360 | 361 | $this->attributes[ 'salePrice' ] = $price->get(); 362 | 363 | return $this; 364 | } 365 | 366 | /** 367 | * Append product's shipping. 368 | * 369 | * @param Closure|ProductShipping|array $shipping 370 | * @return $this 371 | */ 372 | public function shipping($shipping) 373 | { 374 | if(is_array($shipping)){ 375 | $shipping = (new ProductShipping)->with($shipping); 376 | }elseif (is_callable($shipping)) { 377 | $callback = $shipping; 378 | $callback($shipping = new ProductShipping); 379 | }elseif(!($shipping instanceof ProductShipping)){ 380 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidProductShippingInput; 381 | } 382 | 383 | $this->attributes[ 'shipping' ][] = $shipping->get(); 384 | 385 | return $this; 386 | } 387 | 388 | /** 389 | * Set the product's shipping height. 390 | * 391 | * @param Closure|Measure|double|array $shippingHeight 392 | * @param string $unit 393 | * @return $this 394 | */ 395 | public function shippingHeight($shippingHeight, $unit = 'cm') 396 | { 397 | if(is_numeric($shippingHeight)){ 398 | $shippingHeight = (new Measure)->value($shippingHeight)->unit($unit); 399 | }elseif(is_array($shippingHeight)){ 400 | $shippingHeight = (new Measure)->with($shippingHeight); 401 | }elseif (is_callable($shippingHeight)) { 402 | $callback = $shippingHeight; 403 | $shippingHeight = (new Measure)->unit($unit); // default 404 | $callback($shippingHeight); 405 | }elseif(!($shippingHeight instanceof Measure)){ 406 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidMeasureInput; 407 | } 408 | 409 | $this->attributes[ 'shippingHeight' ] = $shippingHeight->get(); 410 | 411 | return $this; 412 | } 413 | /** 414 | * Set the product's shipping length. 415 | * 416 | * @param Closure|Measure|double|array $shippingLength 417 | * @param string $unit 418 | * @return $this 419 | */ 420 | public function shippingLength($shippingLength, $unit = 'cm') 421 | { 422 | if(is_numeric($shippingLength)){ 423 | $shippingLength = (new Measure)->value($shippingLength)->unit($unit); 424 | }elseif(is_array($shippingLength)){ 425 | $shippingLength = (new Measure)->with($shippingLength); 426 | }elseif (is_callable($shippingLength)) { 427 | $callback = $shippingLength; 428 | $shippingLength = (new Measure)->unit($unit); // default 429 | $callback($shippingLength); 430 | }elseif(!($shippingLength instanceof Measure)){ 431 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidMeasureInput; 432 | } 433 | 434 | $this->attributes[ 'shippingLength' ] = $shippingLength->get(); 435 | 436 | return $this; 437 | } 438 | 439 | /** 440 | * Set the product's shipping weight. 441 | * 442 | * @param Closure|Measure|double|array $shippingWeight 443 | * @param string $unit 444 | * @return $this 445 | */ 446 | public function shippingWeight($shippingWeight, $unit = 'kg') 447 | { 448 | if(is_numeric($shippingWeight)){ 449 | $shippingWeight = (new Measure)->value($shippingWeight)->unit($unit); 450 | }elseif(is_array($shippingWeight)){ 451 | $shippingWeight = (new Measure)->with($shippingWeight); 452 | }elseif (is_callable($shippingWeight)) { 453 | $callback = $shippingWeight; 454 | $shippingWeight = (new Measure)->unit($unit); // default 455 | $callback($shippingWeight); 456 | }elseif(!($shippingWeight instanceof Measure)){ 457 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidMeasureInput; 458 | } 459 | 460 | $this->attributes[ 'shippingWeight' ] = $shippingWeight->get(); 461 | 462 | return $this; 463 | } 464 | 465 | /** 466 | * Append the product's tax. 467 | * 468 | * @param Closure|Taxes|array $tax 469 | * @return $this 470 | */ 471 | public function taxes($tax) 472 | { 473 | if (is_callable($tax)) { 474 | $callback = $tax; 475 | $callback($tax = new Taxes); 476 | }elseif(is_array($tax)){ 477 | $tax = (new Taxes)->with($tax); 478 | } 479 | elseif(!($tax instanceof Taxes)){ 480 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidTaxInput; 481 | } 482 | 483 | $this->attributes[ 'taxes' ][] = $tax->get(); 484 | 485 | return $this; 486 | } 487 | 488 | /** 489 | * Set the product's unit pricing base measure. 490 | * 491 | * @param Closure|Measure|long|array $value 492 | * @param string $unit 493 | * @return $this 494 | */ 495 | public function unitPricingBaseMeasure($value, $unit = null) 496 | { 497 | if(is_numeric($value)){ 498 | $value = (new Measure)->value($value)->unit($unit); 499 | }elseif(is_array($value)){ 500 | $value = (new Measure)->with($value); 501 | }elseif (is_callable($value)) { 502 | $callback = $value; 503 | $value = (new Measure)->unit($unit); // default 504 | $callback($value); 505 | }elseif(!($value instanceof Measure)){ 506 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidMeasureInput; 507 | } 508 | 509 | $this->attributes[ 'unitPricingBaseMeasure' ] = $value->get(); 510 | 511 | return $this; 512 | } 513 | 514 | /** 515 | * Set the product's unit pricing measure. 516 | * 517 | * @param Closure|Measure|double|array $value 518 | * @param string $unit 519 | * @return $this 520 | */ 521 | public function unitPricingMeasure($value, $unit = null) 522 | { 523 | if(is_numeric($value)){ 524 | $value = (new Measure)->value($value)->unit($unit); 525 | }elseif(is_array($value)){ 526 | $value = (new Measure)->with($value); 527 | }elseif (is_callable($value)) { 528 | $callback = $value; 529 | $value = (new Measure)->unit($unit); // default 530 | $callback($value); 531 | }elseif(!($value instanceof Measure)){ 532 | throw new \MOIREI\GoogleMerchantApi\Exceptions\InvalidMeasureInput; 533 | } 534 | 535 | $this->attributes[ 'unitPricingMeasure' ] = $value->get(); 536 | 537 | return $this; 538 | } 539 | 540 | /** 541 | * Set the product's sale price effective date. 542 | * 543 | * Date range during which the item is on sale. 544 | * 545 | * @param Carbon\Carbon|string $until 546 | * @param Carbon\Carbon|string|null $from 547 | * @return $this 548 | */ 549 | public function salePriceEffectiveDate($until, $from = null) 550 | { 551 | 552 | if( !($until instanceof Carbon) ){ 553 | $until = new Carbon($until); 554 | } 555 | 556 | if(is_null($from)){ 557 | $from = now(); 558 | }elseif(!($from instanceof Carbon)){ 559 | $from = new Carbon($from); 560 | } 561 | 562 | $this->attributes[ 'salePriceEffectiveDate' ] = $from->format('Y-m-d') . 'T' . $from->format('h:i:s') . '/' . 563 | $until->format('Y-m-d') . 'T' . $until->format('h:i:s'); 564 | 565 | return $this; 566 | } 567 | 568 | /** 569 | * Set the product's sizes. 570 | * 571 | * @param array $sizes 572 | * @return $this 573 | */ 574 | public function sizes($sizes) 575 | { 576 | if(is_array($sizes)){ 577 | $this->attributes[ 'sizes' ] = $sizes; 578 | }else{ 579 | $this->attributes[ 'sizes' ][] = $sizes; 580 | } 581 | 582 | return $this; 583 | } 584 | 585 | /** 586 | * Set a custom value. 587 | * 588 | * @param string|array $custom 589 | * @param mix $value 590 | * @param string $type 591 | * @return $this 592 | */ 593 | public function custom($custom, $value = null, $type = null) 594 | { 595 | $name = ''; 596 | 597 | if(is_array($custom)){ 598 | $name = $custom['name']; 599 | $value = $custom['value']; 600 | $type = $custom['type']?? null; 601 | }else{ 602 | $name = $custom; 603 | } 604 | 605 | if(is_null($type)){ 606 | if(is_int($value)){ 607 | $type = 'int'; 608 | }elseif(is_float($value)){ 609 | $type = 'float'; 610 | }elseif(is_bool($value)){ 611 | $type = 'boolean'; 612 | }else{ 613 | $type = 'text'; 614 | } 615 | }else{ 616 | if(!in_array($type, [ 617 | 'boolean', 'datetimerange', 'float', 'group', 618 | 'int', 'price', 'text', 'time', 'url', 619 | ])){ 620 | $type = 'text'; 621 | } 622 | } 623 | 624 | $this->attributes[ 'customAttributes' ][] = [ 625 | 'name' => $name, 626 | 'type' => $type, 627 | 'value' => $value, 628 | ]; 629 | 630 | return $this; 631 | } 632 | 633 | /** 634 | * Set the product's custom values. 635 | * 636 | * @param array $customValues 637 | * @return $this 638 | */ 639 | public function customValues(array $customValues) 640 | { 641 | foreach($customValues as $customValue){ 642 | $this->custom($customValues); 643 | } 644 | 645 | return $this; 646 | } 647 | 648 | } 649 | --------------------------------------------------------------------------------