commit 7f4b49d98d07486f0809e072eb94d8de61a28429 Author: Daan Geurts Date: Wed Oct 16 14:20:07 2019 +0200 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9d49909 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bb6265e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore +/.scrutinizer.yml export-ignore +/tests export-ignore +/.editorconfig export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08320b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build +composer.lock +vendor +coverage.clover +.phpunit.result.cache diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..1165e72 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,18 @@ +filter: + excluded_paths: [tests/*] + +checks: + php: + remove_extra_empty_lines: true + remove_php_closing_tag: true + remove_trailing_whitespace: true + fix_use_statements: + remove_unused: true + preserve_multiple: false + preserve_blanklines: true + order_alphabetically: true + fix_php_opening_tag: true + fix_linefeed: true + fix_line_ending: true + fix_identation_4spaces: true + fix_doc_comments: true diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..f4d3cbc --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,4 @@ +preset: laravel + +disabled: + - single_class_element_per_statement diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7f54b2c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +language: php + +cache: + directories: + - $HOME/.composer/cache + +matrix: + fast_finish: true + include: + - php: 7.2 + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.2 + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.3 + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.3 + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.2 + env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.2 + env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.3 + env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.3 + env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-stable' + +before_install: + - travis_retry composer self-update + - travis_retry composer require --no-update --no-interaction "illuminate/support:${LARAVEL}" "orchestra/testbench:${TESTBENCH}" + +install: + - travis_retry composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction --no-suggest + +script: + - vendor/bin/phpunit diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..3d75413 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) App-vise V.O.F. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e684dc4 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Handle Appstore server-to-server notifications for auto-renewable subscriptions + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/app-vise/laravel-appstore-notifications.svg?style=flat-square)](https://packagist.org/packages/app-vise/laravel-appstore-notifications) +[![Build Status](https://img.shields.io/travis/app-vise/laravel-appstore-notifications/master.svg?style=flat-square)](https://travis-ci.org/app-vise/laravel-appstore-notifications) +[![StyleCI](https://styleci.io/repos/105920179/shield?branch=master)](https://styleci.io/repos/105920179) +[![Quality Score](https://img.shields.io/scrutinizer/g/app-vise/laravel-appstore-notifications.svg?style=flat-square)](https://scrutinizer-ci.com/g/app-vise/laravel-appstore-notifications) +[![Total Downloads](https://img.shields.io/packagist/dt/app-vise/laravel-appstore-notifications.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-appstore-notifications) + +## Installation +You can install this package via composer + +```bash +composer require app-vise/laravel-appstore-server-notifications + ``` + +The service provider will register itself. +You have to publish the config file with: + +```bash +php artisan vendor:publish --provider="Appvise\AppStoreNotifications\NotificationsServiceProvider" --tag="config" + ``` + +## Usage + +## Changelog + +Please see CHANGELOG for more information about what has changed recently. + +## Testing + +```bash +composer test +``` + +## Security + +If you discover any security related issues, please email daan@app-vise.nl instead of using the issue tracker. + +## Credits + +- [Daan Geurts](https://github.com/DaanGeurts) +- [All Contributors](../../contributors) + +A big thanks to [Spatie's](https://spatie.be) laravel-stripe-webhooks which was a huge inspiration and starting point for this package +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..e69de29 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7cacab1 --- /dev/null +++ b/composer.json @@ -0,0 +1,53 @@ +{ + "name": "app-vise/laravel-appstore-server-notifications", + "description": "Handling Appstore server to server notifications", + "keywords": [ + "app-vise", + "appvise", + "laravel-appstore-server-to-server-notifications", + "laravel-appstore-server-notifications", + "laravel in app subscriptions", + "laravel-appstore-server-notifications" + ], + "license": "MIT", + "authors": [ + { + "name": "Daan Geurts", + "email": "daan@app-vise.nl", + "homepage": "https://www.app-vise.nl", + "role": "Developer" + } + ], + "require": { + "php": "^7.2", + "illuminate/support": "~5.7.0|^6.0", + "bensampo/laravel-enum": "^1.0" + }, + "require-dev": { + "orchestra/testbench": "~3.8.0|^4.0", + "phpunit/phpunit": "^8.2" + }, + "autoload": { + "psr-4": { + "Appvise\\AppStoreNotifications\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Appvise\\AppStoreNotifications\\Tests\\": "tests" + } + }, + "scripts": { + "test": "vendor/bin/phpunit --verbose" + }, + "config": { + "sort-packages": true + }, + "extra": { + "laravel": { + "providers": [ + "Appvise\\AppStoreNotifications\\NotificationsServiceProvider" + ] + } + } +} diff --git a/config/appstore-server-notifications.php b/config/appstore-server-notifications.php new file mode 100644 index 0000000..b0b4a6b --- /dev/null +++ b/config/appstore-server-notifications.php @@ -0,0 +1,24 @@ + env('APPLE_SHARED_SECRET'), + /* + * All the events that should be handeled by your application. + * Typically you should uncomment all jobs + * + * You can find a list of all notification types here: + * https://developer.apple.com/documentation/storekit/in-app_purchase/enabling_server-to-server_notifications?language=objc#3162176 + */ + 'jobs' => [ + // 'initial_buy' => \App\Jobs\AppstoreNotifications\HandleInitialBuy::class, + // 'cancel' => \App\Jobs\AppstoreNotifications\HandleCancellation::class, + // 'renewal' => \App\Jobs\AppstoreNotifications\HandleRenewal::class, + // 'interactive_renewal' => \App\Jobs\AppstoreNotifications\HandleInteractiveRenewal::class, + // 'did_change_renewal_pref' => \App\Jobs\AppstoreNotifications\HandleDidChangeRenewalPreferences::class, + // 'did_change_renewal_status' => \App\Jobs\AppstoreNotifications\HandleDidChangeRenewalStatus::class, + ], +]; diff --git a/database/migrations/create_apple_notifications_table.php.stub b/database/migrations/create_apple_notifications_table.php.stub new file mode 100644 index 0000000..9a05887 --- /dev/null +++ b/database/migrations/create_apple_notifications_table.php.stub @@ -0,0 +1,25 @@ +bigIncrements('id'); + + $table->string('type'); + $table->text('payload')->nullable(); + $table->text('exception')->nullable(); + + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('apple_notifications'); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..8ec1761 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + tests + + + + + src/ + + + + + + + + + + diff --git a/src/NotificationsServiceProvider.php b/src/NotificationsServiceProvider.php new file mode 100644 index 0000000..106df8a --- /dev/null +++ b/src/NotificationsServiceProvider.php @@ -0,0 +1,30 @@ +app->runningInConsole()) { + $this->publishes([ + __DIR__.'/../config/appstore-server-notifications.php' => config_path('appstore-server-notifications.php'), + ], 'config'); + } + + if (! class_exists('CreateAppleNotificationsTable')) { + $timestamp = date('Y_m_d_His', time()); + $this->publishes([ + __DIR__.'/../database/migrations/create_apple_notifications_table.php.stub' => database_path("migrations/{$timestamp}_create_apple_notifications_table.php"), + ], 'migrations'); + } + + $this->loadRoutesFrom(__DIR__.'/routes.php'); + } + + public function register() + { + $this->mergeConfigFrom(__DIR__.'/../config/appstore-server-notifications.php', 'appstore-server-notifications'); + } +} diff --git a/src/WebhooksController.php b/src/WebhooksController.php new file mode 100644 index 0000000..724a7b2 --- /dev/null +++ b/src/WebhooksController.php @@ -0,0 +1,43 @@ +input('notification_type')}(); + $payload = NotificationPayload::createFromRequest($request); + + $this->determineValidRequest($payload); + + AppleNotification::storeNotification($jobConfigKey, $payload); + + $jobClass = config("appstore-server-notifications.jobs.{$jobConfigKey}", null); + + if (is_null($jobClass)) { + throw WebhookFailed::jobClassDoesNotExist($jobConfigKey); + } + + + $job = new $jobClass($payload); + dispatch($job); + + return response()->json(); + } + + private function determineValidRequest(NotificationPayload $notificationPayload): bool + { + if ($notificationPayload->getPassword() !== config('appstore-server-notifications.shared_secret')) { + throw WebhookFailed::nonValidRequest(); + } + + return true; + } +} diff --git a/src/exceptions/WebhookFailed.php b/src/exceptions/WebhookFailed.php new file mode 100644 index 0000000..6f48133 --- /dev/null +++ b/src/exceptions/WebhookFailed.php @@ -0,0 +1,23 @@ + $this->getMessage()], 400); + } +} diff --git a/src/model/AppleNotification.php b/src/model/AppleNotification.php new file mode 100644 index 0000000..52b22eb --- /dev/null +++ b/src/model/AppleNotification.php @@ -0,0 +1,19 @@ + $notificationType, + 'payload' => serialize($notificationPayload), + ]); + } + +} diff --git a/src/model/NotificationPayload.php b/src/model/NotificationPayload.php new file mode 100644 index 0000000..a22ad6e --- /dev/null +++ b/src/model/NotificationPayload.php @@ -0,0 +1,193 @@ +environment = $request->input('environment'); + $instance->password = $request->input('password'); + $instance->notificationType = $request->input('notification_type'); + $instance->cancellationDate = $request->input('cancellation_date'); + $instance->cancellationDatePst = $request->input('cancellation_date_pst'); + $instance->cancellationDateMs = $request->input('cancellation_date_ms'); + $instance->webOrderLineItemId = $request->input('web_order_line_item_id'); + $instance->latestReceipt = $request->input('latest_receipt'); + $instance->latestReceiptInfo = Receipt::createFromArray($request->input('latest_receipt_info')); + $instance->latestExpiredReceipt = $request->input('latest_expired_receipt'); + $instance->latestExpiredReceiptInfo = Receipt::createFromArray($request->input('latest_expired_receipt_info')); + $instance->autoRenewStatus = $request->input('auto_renew_status'); + $instance->autoRenewProductId = $request->input('auto_renew_product_id'); + $instance->autoRenewStatusChangeDate = $request->input('auto_renew_status_change_date'); + $instance->autoRenewStatusChangeDatePst = $request->input('auto_renew_status_change_date_pst'); + $instance->autoRenewStatusChangeDateMs = $request->input('auto_renew_status_change_date_ms'); + foreach ($request->input('pending_renewal_info') as $pendingRenewalInfo) { + $instance->pendingRenewalInfo[] = RenewalInfo::createFromRequest($pendingRenewalInfo); + } + + return $instance; + } + + /** + * Get the value of environment + */ + public function getEnvironment() + { + return $this->environment; + } + + /** + * Get the value of notificationType + */ + public function getNotificationType() + { + return $this->notificationType; + } + + /** + * Get the value of pendingRenewalInfo + */ + public function getPendingRenewalInfo() + { + return $this->pendingRenewalInfo; + } + + /** + * Get the value of autoRenewStatusChangeDateMs + */ + public function getAutoRenewStatusChangeDateMs() + { + return $this->autoRenewStatusChangeDateMs; + } + + /** + * Get the value of autoRenewStatusChangeDatePst + */ + public function getAutoRenewStatusChangeDatePst() + { + return $this->autoRenewStatusChangeDatePst; + } + + /** + * Get the value of autoRenewStatusChangeDate + */ + public function getAutoRenewStatusChangeDate() + { + return $this->autoRenewStatusChangeDate; + } + + /** + * Get the value of autoRenewProductId + */ + public function getAutoRenewProductId() + { + return $this->autoRenewProductId; + } + + /** + * Get the value of autoRenewStatus + */ + public function getAutoRenewStatus() + { + return $this->autoRenewStatus; + } + + /** + * Get the value of latestExpiredReceiptInfo + */ + public function getLatestExpiredReceiptInfo() + { + return $this->latestExpiredReceiptInfo; + } + + /** + * Get the value of latestExpiredReceipt + */ + public function getLatestExpiredReceipt() + { + return $this->latestExpiredReceipt; + } + + /** + * Get the value of latestReceiptInfo + */ + public function getLatestReceiptInfo() + { + return $this->latestReceiptInfo; + } + + /** + * Get the value of latestReceipt + */ + public function getLatestReceipt() + { + return $this->latestReceipt; + } + + /** + * Get the value of webOrderLineItemId + */ + public function getWebOrderLineItemId() + { + return $this->webOrderLineItemId; + } + + /** + * Get the value of cancellationDateMs + */ + public function getCancellationDateMs() + { + return $this->cancellationDateMs; + } + + /** + * Get the value of cancellationDatePst + */ + public function getCancellationDatePst() + { + return $this->cancellationDatePst; + } + + /** + * Get the value of cancellationDate + */ + public function getCancellationDate() + { + return $this->cancellationDate; + } + + /** + * Get the value of password + */ + public function getPassword() + { + return $this->password; + } +} diff --git a/src/model/NotificationType.php b/src/model/NotificationType.php new file mode 100644 index 0000000..fd1d552 --- /dev/null +++ b/src/model/NotificationType.php @@ -0,0 +1,18 @@ +originalTransactionId = $receiptInfo['original_transaction_id'] ?? null; + $instance->webOrderLineItemId = $receiptInfo['web_order_line_item_id'] ?? null; + $instance->productId = $receiptInfo['product_id'] ?? null; + $instance->purchaseDateMs = $receiptInfo['purchase_date_ms'] ?? null; + $instance->purchaseDate = $receiptInfo['purchase_date'] ?? null; + $instance->purchaseDatePst = $receiptInfo['purchase_date_pst'] ?? null; + $instance->originalPurchaseDate = $receiptInfo['original_purchase_date'] ?? null; + $instance->originalPurchaseDateMs = $receiptInfo['original_purchase_date_ms'] ?? null; + $instance->originalPurchaseDatePst = $receiptInfo['original_purchase_date_pst'] ?? null; + $instance->cancellationReason = $receiptInfo['cancellation_reason'] ?? null; + $instance->cancellationDate = $receiptInfo['cancellation_date'] ?? null; + $instance->cancellationDateMs = $receiptInfo['cancellation_date_ms'] ?? null; + $instance->cancellationDatePst = $receiptInfo['cancellation_date_pst'] ?? null; + $instance->expiresDate = $receiptInfo['expires_date'] ?? null; + $instance->expiresDateMs = $receiptInfo['expires_date_ms'] ?? null; + $instance->expiresDateFormatted = $receiptInfo['expires_date_formatted'] ?? null; + $instance->expiresDateFormattedPst = $receiptInfo['expires_date_formatted_pst'] ?? null; + $instance->quantity = $receiptInfo['quantity'] ?? null; + $instance->uniqueIdentifier = $receiptInfo['unique_identifier'] ?? null; + $instance->uniqueVendorIdentifier = $receiptInfo['unique_vendor_identifier'] ?? null; + $instance->isInIntroOfferPeriod = $receiptInfo['is_in_intro_offer_period'] ?? null; + $instance->isTrialPeriod = $receiptInfo['is_trial_period'] ?? null; + $instance->itemId = $receiptInfo['item_id'] ?? null; + $instance->appItemId = $receiptInfo['app_item_id'] ?? null; + $instance->versionExternalIdentifier = $receiptInfo['version_external_identifier'] ?? null; + $instance->transactionId = $receiptInfo['transaction_id'] ?? null; + $instance->bvrs = $receiptInfo['bvrs'] ?? null; + $instance->bid = $receiptInfo['bid'] ?? null; + return $instance; + } + + + /** + * Get the value of bid + */ + public function getBid() + { + return $this->bid; + } + + /** + * Get the value of bvrs + */ + public function getBvrs() + { + return $this->bvrs; + } + + /** + * Get the value of transactionId + */ + public function getTransactionId() + { + return $this->transactionId; + } + + /** + * Get the value of versionExternalIdentifier + */ + public function getVersionExternalIdentifier() + { + return $this->versionExternalIdentifier; + } + + /** + * Get the value of appItemId + */ + public function getAppItemId() + { + return $this->appItemId; + } + + /** + * Get the value of itemId + */ + public function getItemId() + { + return $this->itemId; + } + + /** + * Get the value of isTrialPeriod + */ + public function getIsTrialPeriod() + { + return $this->isTrialPeriod; + } + + /** + * Get the value of isInIntroOfferPeriod + */ + public function getIsInIntroOfferPeriod() + { + return $this->isInIntroOfferPeriod; + } + + /** + * Get the value of uniqueVendorIdentifier + */ + public function getUniqueVendorIdentifier() + { + return $this->uniqueVendorIdentifier; + } + + /** + * Get the value of uniqueIdentifier + */ + public function getUniqueIdentifier() + { + return $this->uniqueIdentifier; + } + + /** + * Get the value of quantity + */ + public function getQuantity() + { + return $this->quantity; + } + + /** + * Get the value of expiresDateFormattedPst + */ + public function getExpiresDateFormattedPst() + { + return $this->expiresDateFormattedPst; + } + + /** + * Get the value of expiresDateFormatted + */ + public function getExpiresDateFormatted() + { + return $this->expiresDateFormatted; + } + + /** + * Get the value of expiresDateMs + */ + public function getExpiresDateMs() + { + return $this->expiresDateMs; + } + + /** + * Get the value of expiresDate + */ + public function getExpiresDate() + { + return $this->expiresDate; + } + + /** + * Get the value of cancellationDatePst + */ + public function getCancellationDatePst() + { + return $this->cancellationDatePst; + } + + /** + * Get the value of cancellationDateMs + */ + public function getCancellationDateMs() + { + return $this->cancellationDateMs; + } + + /** + * Get the value of cancellationDate + */ + public function getCancellationDate() + { + return $this->cancellationDate; + } + + /** + * Get the value of cancellationReason + */ + public function getCancellationReason() + { + return $this->cancellationReason; + } + + /** + * Get the value of originalPurchaseDatePst + */ + public function getOriginalPurchaseDatePst() + { + return $this->originalPurchaseDatePst; + } + + /** + * Get the value of originalPurchaseDateMs + */ + public function getOriginalPurchaseDateMs() + { + return $this->originalPurchaseDateMs; + } + + /** + * Get the value of originalPurchaseDate + */ + public function getOriginalPurchaseDate() + { + return $this->originalPurchaseDate; + } + + /** + * Get the value of purchaseDatePst + */ + public function getPurchaseDatePst() + { + return $this->purchaseDatePst; + } + + /** + * Get the value of purchaseDate + */ + public function getPurchaseDate() + { + return $this->purchaseDate; + } + + /** + * Get the value of purchaseDateMs + */ + public function getPurchaseDateMs() + { + return $this->purchaseDateMs; + } + + /** + * Get the value of productId + */ + public function getProductId() + { + return $this->productId; + } + + /** + * Get the value of webOrderLineItemId + */ + public function getWebOrderLineItemId() + { + return $this->webOrderLineItemId; + } + + /** + * Get the value of originalTransactionId + */ + public function getOriginalTransactionId() + { + return $this->originalTransactionId; + } +} diff --git a/src/model/RenewalInfo.php b/src/model/RenewalInfo.php new file mode 100644 index 0000000..619a090 --- /dev/null +++ b/src/model/RenewalInfo.php @@ -0,0 +1,116 @@ +autoRenewProductId = $pendingRenewalInfo['auto_renew_product_id'] ?? null; + $instance->autoRenewStatus = $pendingRenewalInfo['auto_renew_status'] ?? null; + $instance->expirationIntent = $pendingRenewalInfo['expiration_intent'] ?? null; + $instance->originalTransactionId = $pendingRenewalInfo['original_transaction_id'] ?? null; + $instance->isInBillingRetryPeriod = $pendingRenewalInfo['is_in_billing_retry_period'] ?? null; + $instance->productId = $pendingRenewalInfo['product_id'] ?? null; + $instance->priceConsentStatus = $pendingRenewalInfo['price_consent_status'] ?? null; + $instance->gracePeriodExpiresDate = $pendingRenewalInfo['grace_period_expires_date'] ?? null; + $instance->gracePeriodExpiresDatePst = $pendingRenewalInfo['grace_period_expires_date_pst'] ?? null; + return $instance; + } + /** + * Get the value of autoRenewProductId + */ + public function getAutoRenewProductId() + { + return $this->autoRenewProductId; + } + + /** + * Get the value of autoRenewStatus + */ + public function getAutoRenewStatus() + { + return $this->autoRenewStatus; + } + + /** + * Get the value of expirationIntent + */ + public function getExpirationIntent() + { + return $this->expirationIntent; + } + + /** + * Get the value of originalTransactionId + */ + public function getOriginalTransactionId() + { + return $this->originalTransactionId; + } + + /** + * Get the value of isInBillingRetryPeriod + */ + public function getIsInBillingRetryPeriod() + { + return $this->isInBillingRetryPeriod; + } + + /** + * Get the value of productId + */ + public function getProductId() + { + return $this->productId; + } + + /** + * Get the value of priceConsentStatus + */ + public function getPriceConsentStatus() + { + return $this->priceConsentStatus; + } + + /** + * Get the value of gracePeriodExpiresDate + */ + public function getGracePeriodExpiresDate() + { + return $this->gracePeriodExpiresDate; + } + + /** + * Get the value of gracePeriodExpiresDateMs + */ + public function getGracePeriodExpiresDateMs() + { + return $this->gracePeriodExpiresDateMs; + } + + /** + * Get the value of gracePeriodExpiresDatePst + */ + public function getGracePeriodExpiresDatePst() + { + return $this->gracePeriodExpiresDatePst; + } +} diff --git a/src/routes.php b/src/routes.php new file mode 100644 index 0000000..9630ab7 --- /dev/null +++ b/src/routes.php @@ -0,0 +1,3 @@ +payload = $payload; + } + + function handle() + { + } +} diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php new file mode 100644 index 0000000..247672b --- /dev/null +++ b/tests/IntegrationTest.php @@ -0,0 +1,79 @@ + [ + 'initial_buy' => DummyJob::class + ], + 'appstore-server-notifications.shared_secret' => 'VALID_APPLE_PASSWORD', + ] + ); + + } + + /** @test */ + public function it_can_handle_a_valid_request() + { + $payload = include_once __DIR__ . '/__fixtures__/request.php'; + + $payload['password'] = "VALID_APPLE_PASSWORD"; + + $this + ->postJson('/apple/server/notifications', $payload) + ->assertSuccessful(); + + $this->assertCount(1, AppleNotification::get()); + + $notification = AppleNotification::first(); + + $this->assertEquals('initial_buy', $notification->type); + $this->assertInstanceOf(AppleNotification::class, $notification); + + Queue::assertPushed(DummyJob::class); + } + + /** @test */ + public function a_request_with_an_invalid_password_wont_be_logged() + { + $payload = include_once __DIR__ . '/__fixtures__/request.php'; + $payload['password'] = "NON_VALID_APPLE_PASSWORD"; + + $this + ->postJson('/apple/server/notifications', $payload) + ->assertStatus(400); + + $this->assertCount(0, AppleNotification::get()); + $this->assertNull(AppleNotification::first()); + + Queue::assertNotPushed(DummyJob::class); + } + + /** @test */ + public function a_request_with_an_invalid_payload_will_be_logged_but_jobs_will_not_be_dispatched() + { + $payload = ['payload' => 'INVALID']; + + $this + ->postJson('/apple/server/notifications', $payload) + ->assertStatus(500); + + Queue::assertNotPushed(DummyJob::class); + } + +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..66fe3fa --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,75 @@ +setUpDatabase(); + } + + /** + * Set up the environment. + * + * @param \Illuminate\Foundation\Application $app + */ + protected function getEnvironmentSetUp($app) + { + $app['config']->set('database.default', 'sqlite'); + $app['config']->set('database.connections.sqlite', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + + config(['appstore-server-notifications.shared_secret' => 'test_shared_secret']); + } + + protected function setUpDatabase() + { + include_once __DIR__.'/../database/migrations/create_apple_notifications_table.php.stub'; + + (new CreateAppleNotificationsTable())->up(); + } + + /** + * @param \Illuminate\Foundation\Application $app + * + * @return array + */ + protected function getPackageProviders($app) + { + return [ + NotificationsServiceProvider::class, + ]; + } + + protected function disableExceptionHandling() + { + $this->app->instance(ExceptionHandler::class, new class extends Handler { + public function __construct() + { + } + + public function report(Exception $e) + { + } + + public function render($request, Exception $exception) + { + throw $exception; + } + }); + } + +} diff --git a/tests/__fixtures__/request.php b/tests/__fixtures__/request.php new file mode 100644 index 0000000..bc45863 --- /dev/null +++ b/tests/__fixtures__/request.php @@ -0,0 +1,126 @@ +