From 7f4b49d98d07486f0809e072eb94d8de61a28429 Mon Sep 17 00:00:00 2001 From: Daan Geurts Date: Wed, 16 Oct 2019 14:20:07 +0200 Subject: [PATCH] Initial commit --- .editorconfig | 15 + .gitattributes | 11 + .gitignore | 5 + .scrutinizer.yml | 18 ++ .styleci.yml | 4 + .travis.yml | 35 ++ CHANGELOG.md | 0 CONTRIBUTING.md | 0 LICENSE.md | 21 ++ README.md | 47 +++ UPGRADING.md | 0 composer.json | 53 +++ config/appstore-server-notifications.php | 24 ++ .../create_apple_notifications_table.php.stub | 25 ++ phpunit.xml.dist | 29 ++ src/NotificationsServiceProvider.php | 30 ++ src/WebhooksController.php | 43 +++ src/exceptions/WebhookFailed.php | 23 ++ src/model/AppleNotification.php | 19 ++ src/model/NotificationPayload.php | 193 +++++++++++ src/model/NotificationType.php | 18 ++ src/model/Receipt.php | 301 ++++++++++++++++++ src/model/RenewalInfo.php | 116 +++++++ src/routes.php | 3 + tests/DummyJob.php | 27 ++ tests/IntegrationTest.php | 79 +++++ tests/TestCase.php | 75 +++++ tests/__fixtures__/request.php | 126 ++++++++ 28 files changed, 1340 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .scrutinizer.yml create mode 100644 .styleci.yml create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 UPGRADING.md create mode 100644 composer.json create mode 100644 config/appstore-server-notifications.php create mode 100644 database/migrations/create_apple_notifications_table.php.stub create mode 100644 phpunit.xml.dist create mode 100644 src/NotificationsServiceProvider.php create mode 100644 src/WebhooksController.php create mode 100644 src/exceptions/WebhookFailed.php create mode 100644 src/model/AppleNotification.php create mode 100644 src/model/NotificationPayload.php create mode 100644 src/model/NotificationType.php create mode 100644 src/model/Receipt.php create mode 100644 src/model/RenewalInfo.php create mode 100644 src/routes.php create mode 100644 tests/DummyJob.php create mode 100644 tests/IntegrationTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/__fixtures__/request.php 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 @@ +