Browse Source

Initial commit

proxy_request
Daan Geurts 5 years ago
commit
7f4b49d98d
  1. 15
      .editorconfig
  2. 11
      .gitattributes
  3. 5
      .gitignore
  4. 18
      .scrutinizer.yml
  5. 4
      .styleci.yml
  6. 35
      .travis.yml
  7. 0
      CHANGELOG.md
  8. 0
      CONTRIBUTING.md
  9. 21
      LICENSE.md
  10. 47
      README.md
  11. 0
      UPGRADING.md
  12. 53
      composer.json
  13. 24
      config/appstore-server-notifications.php
  14. 25
      database/migrations/create_apple_notifications_table.php.stub
  15. 29
      phpunit.xml.dist
  16. 30
      src/NotificationsServiceProvider.php
  17. 43
      src/WebhooksController.php
  18. 23
      src/exceptions/WebhookFailed.php
  19. 19
      src/model/AppleNotification.php
  20. 193
      src/model/NotificationPayload.php
  21. 18
      src/model/NotificationType.php
  22. 301
      src/model/Receipt.php
  23. 116
      src/model/RenewalInfo.php
  24. 3
      src/routes.php
  25. 27
      tests/DummyJob.php
  26. 79
      tests/IntegrationTest.php
  27. 75
      tests/TestCase.php
  28. 126
      tests/__fixtures__/request.php

15
.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

11
.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

5
.gitignore

@ -0,0 +1,5 @@
build
composer.lock
vendor
coverage.clover
.phpunit.result.cache

18
.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

4
.styleci.yml

@ -0,0 +1,4 @@
preset: laravel
disabled:
- single_class_element_per_statement

35
.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

0
CHANGELOG.md

0
CONTRIBUTING.md

21
LICENSE.md

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) App-vise V.O.F. <info@app-vise.nl>
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.

47
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 [email protected] 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.

0
UPGRADING.md

53
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": "[email protected]",
"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"
]
}
}
}

24
config/appstore-server-notifications.php

@ -0,0 +1,24 @@
<?php
return [
/*
* Apple will send the shared secret with the request that should match
* the one you use when validating receipts.
* https://developer.apple.com/documentation/storekit/in-app_purchase/enabling_server-to-server_notifications?language=objc#overview
*/
'shared_secret' => 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,
],
];

25
database/migrations/create_apple_notifications_table.php.stub

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateAppleNotificationsTable extends Migration
{
public function up()
{
Schema::create('apple_notifications', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('type');
$table->text('payload')->nullable();
$table->text('exception')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('apple_notifications');
}
}

29
phpunit.xml.dist

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
verbose="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="true"
stopOnFailure="false">
<testsuites>
<testsuite name="App-vise Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">src/</directory>
</whitelist>
</filter>
<logging>
<log type="tap" target="build/report.tap"/>
<log type="junit" target="build/report.junit.xml"/>
<log type="coverage-html" target="build/coverage"/>
<log type="coverage-text" target="build/coverage.txt"/>
<log type="coverage-clover" target="build/logs/clover.xml"/>
</logging>
</phpunit>

30
src/NotificationsServiceProvider.php

@ -0,0 +1,30 @@
<?php
namespace Appvise\AppStoreNotifications;
use Illuminate\Support\ServiceProvider;
class NotificationsServiceProvider extends ServiceProvider
{
public function boot()
{
if ($this->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');
}
}

43
src/WebhooksController.php

@ -0,0 +1,43 @@
<?php
namespace Appvise\AppStoreNotifications;
use Appvise\AppStoreNotifications\Exceptions\WebhookFailed;
use Appvise\AppStoreNotifications\Model\AppleNotification;
use Appvise\AppStoreNotifications\Model\NotificationPayload;
use Appvise\AppStoreNotifications\Model\NotificationType;
use Illuminate\Http\Request;
class WebhooksController
{
public function __invoke(Request $request)
{
$jobConfigKey = NotificationType::{$request->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;
}
}

23
src/exceptions/WebhookFailed.php

@ -0,0 +1,23 @@
<?php
namespace Appvise\AppStoreNotifications\Exceptions;
use Exception;
class WebhookFailed extends Exception
{
public static function nonValidRequest()
{
return new static("Your shared secret does not match password in Apple's request", 400);
}
public static function jobClassDoesNotExist(string $jobClass)
{
return new static("Could not process webhook because the configured job `$jobClass` does not exist.", 501);
}
public function render($request)
{
return response(['error' => $this->getMessage()], 400);
}
}

19
src/model/AppleNotification.php

@ -0,0 +1,19 @@
<?php
namespace Appvise\AppStoreNotifications\Model;
use Illuminate\Database\Eloquent\Model;
class AppleNotification extends Model
{
public $guarded = [];
public static function storeNotification(String $notificationType, NotificationPayload $notificationPayload): AppleNotification
{
return self::create([
'type' => $notificationType,
'payload' => serialize($notificationPayload),
]);
}
}

193
src/model/NotificationPayload.php

@ -0,0 +1,193 @@
<?php
namespace Appvise\AppStoreNotifications\Model;
use Illuminate\Http\Request;
class NotificationPayload
{
private $environment;
private $notificationType;
private $password;
private $cancellationDate;
private $cancellationDatePst;
private $cancellationDateMs;
private $webOrderLineItemId;
private $latestReceipt;
private $latestReceiptInfo;
private $latestExpiredReceipt;
private $latestExpiredReceiptInfo;
private $autoRenewStatus;
private $autoRenewProductId;
private $autoRenewStatusChangeDate;
private $autoRenewStatusChangeDatePst;
private $autoRenewStatusChangeDateMs;
private $pendingRenewalInfo;
public function __construct()
{
}
static function createFromRequest(Request $request)
{
$instance = new self();
$instance->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;
}
}

18
src/model/NotificationType.php

@ -0,0 +1,18 @@
<?php
namespace Appvise\AppStoreNotifications\Model;
use BenSampo\Enum\Enum;
class NotificationType extends Enum
{
const INITIAL_BUY = 'initial_buy';
const CANCEL = 'cancel';
const RENEWAL = 'renewal';
const INTERACTIVE_RENEWAL = 'interactive_renewal';
const DID_CHANGE_RENEWAL_PREF = 'did_change_renewal_pref';
const DID_CHANGE_RENEWAL_STATUS = 'did_change_renewal_status';
const DID_FAIL_TO_RENEW = 'did_fail_to_renew';
const DID_RECOVER = 'did_recover'; // replaces RENEWAL
const PRICE_INCREASE_CONSENT = 'price_increase_consent';
}

301
src/model/Receipt.php

@ -0,0 +1,301 @@
<?php
namespace Appvise\AppStoreNotifications\Model;
class Receipt
{
private $originalTransactionId;
private $webOrderLineItemId;
private $productId;
private $purchaseDateMs;
private $purchaseDate;
private $purchaseDatePst;
private $originalPurchaseDate;
private $originalPurchaseDateMs;
private $originalPurchaseDatePst;
private $cancellationReason;
private $cancellationDate;
private $cancellationDateMs;
private $cancellationDatePst;
private $expiresDate;
private $expiresDateMs;
private $expiresDateFormatted;
private $expiresDateFormattedPst;
private $quantity;
private $uniqueIdentifier;
private $uniqueVendorIdentifier;
private $isInIntroOfferPeriod;
private $isTrialPeriod;
private $itemId;
private $appItemId;
private $versionExternalIdentifier;
private $transactionId;
private $bvrs;
private $bid;
public function __construct()
{
}
static function createFromArray(array $receiptInfo)
{
$instance = new self();
$instance->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;
}
}

116
src/model/RenewalInfo.php

@ -0,0 +1,116 @@
<?php
namespace Appvise\AppStoreNotifications\Model;
class RenewalInfo
{
private $autoRenewProductId;
private $autoRenewStatus;
private $expirationIntent;
private $originalTransactionId;
private $isInBillingRetryPeriod;
private $productId;
private $priceConsentStatus;
private $gracePeriodExpiresDate;
private $gracePeriodExpiresDateMs;
private $gracePeriodExpiresDatePst;
public function __construct()
{
}
public static function createFromRequest(array $pendingRenewalInfo) {
$instance = new self();
$instance->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;
}
}

3
src/routes.php

@ -0,0 +1,3 @@
<?php
Route::post('/apple/server/notifications', "\Appvise\AppStoreNotifications\WebhooksController");

27
tests/DummyJob.php

@ -0,0 +1,27 @@
<?php
namespace Appvise\AppStoreNotifications\Tests;
use Appvise\AppStoreNotifications\Model\NotificationPayload;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class DummyJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $payload;
public function __construct(NotificationPayload $payload)
{
$this->payload = $payload;
}
function handle()
{
}
}

79
tests/IntegrationTest.php

@ -0,0 +1,79 @@
<?php
namespace Appvise\AppStoreNotifications\Tests;
use Appvise\AppStoreNotifications\Model\AppleNotification;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Route;
class IntegrationTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
Queue::fake();
Route::post('/apple/server/notifications', "\Appvise\AppStoreNotifications\WebhooksController");
config(
[
'appstore-server-notifications.jobs' => [
'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);
}
}

75
tests/TestCase.php

@ -0,0 +1,75 @@
<?php
namespace Appvise\AppStoreNotifications\Tests;
use Appvise\AppStoreNotifications\NotificationsServiceProvider;
use Exception;
use Illuminate\Foundation\Exceptions\Handler;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Orchestra\Testbench\TestCase as OrchestraTestCase;
use CreateAppleNotificationsTable;
abstract class TestCase extends OrchestraTestCase
{
public function setUp(): void
{
parent::setUp();
$this->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;
}
});
}
}

126
tests/__fixtures__/request.php

@ -0,0 +1,126 @@
<?php
return json_decode('{
"environment": "Sandbox",
"notification_type": "INITIAL_BUY",
"password": "TEST_SHARED_SECRET",
"cancellation_date": "2018-03-27 07:11:12 Etc/GMT",
"cancellation_date_pst": "2018-03-27 00:11:12 America/Los_Angeles",
"cancellation_date_ms": "1522134672000",
"web_order_line_item_id": "1000000047417113",
"latest_receipt": "BASE64ENCODED_RECEIPT_INFO",
"latest_receipt_info": {},
"latest_expired_receipt" : "BASE64ENCODED_LATEST_RECEIPT_INFO",
"latest_expired_receipt_info": {
"purchase_date_ms": "1521893342000",
"original_transaction_id": "1000000577061006",
"web_order_line_item_id": "1000000047417113",
"product_id": "PRODUCT_ID",
"purchase_date": "2018-03-24 12:09:02 Etc/GMT",
"purchase_date_pst": "2018-03-24 05:09:02 America/Los_Angeles",
"original_purchase_date": "2018-03-17 12:09:03 Etc/GMT",
"original_purchase_date_ms": "1521288543000",
"original_purchase_date_pst": "2018-03-17 05:09:03 America/Los_Angeles",
"cancellation_reason": "0",
"cancellation_date": "2018-03-27 07:11:12 Etc/GMT",
"cancellation_date_ms": "1522134672000",
"cancellation_date_pst": "2018-03-27 00:11:12 America/Los_Angeles",
"expires_date": "2019-10-09 07:43:26 Etc/GMT",
"expires_date_ms": "1570607006000",
"expires_date_formatted": "2019-03-24 12:09:02 Etc/GMT",
"expires_date_formatted_pst": "2019-03-24 05:09:02 America/Los_Angeles",
"quantity": "1",
"unique_identifier": "UNIQUE_IDENTIFIER",
"unique_vendor_identifier": "UNIQUE_VENDOR_IDENTIFIER",
"is_in_intro_offer_period": "false",
"is_trial_period": "false",
"item_id": "ITEM_ID",
"app_item_id": "APP_ITEM_ID",
"version_external_identifier": "VERSION_EXTERNAL_IDENTIFIER",
"transaction_id": "1000000577069202",
"bvrs": "2",
"bid": "com.example.app.ios"
},
"auto_renew_status": "false",
"auto_renew_product_id": "PRODUCT_ID",
"auto_renew_status_change_date": "",
"auto_renew_status_change_date_pst": "",
"auto_renew_status_change_date_ms": "",
"pending_renewal_info": [
{
"auto_renew_product_id": "PRODUCT_ID",
"auto_renew_status": "1",
"expiration_intent": "",
"original_transaction_id": "1000000577061006",
"is_in_billing_retry_period": "1",
"product_id": "PRODUCT_ID",
"price_consent_status": "0",
"grace_period_expires_date": "",
"grace_period_expires_date_ms": "",
"grace_period_expires_date_pst": ""
}
]
}', true
);
//{
// "environment": "Sandbox",
// "notification_type": "INITIAL_BUY",
// "password": "TEST_SHARED_SECRET",
// "cancellation_date": "2018-03-27 07:11:12 Etc/GMT",
// "cancellation_date_pst": "2018-03-27 00:11:12 America/Los_Angeles",
// "cancellation_date_ms": "1522134672000", // important for cancel
// "web_order_line_item_id": "1000000047417113",
// "latest_receipt": "BASE64ENCODED_RECEIPT_INFO",
// "latest_receipt_info": "ARRAY_WITH_RECEIPT_INFO",
// "latest_expired_receipt" : "BASE64ENCODED_LATEST_RECEIPT_INFO",
// "latest_expired_receipt_info": {
// "purchase_date_ms": "1521893342000", // important for initial buy, interactive_renewal, did_recover
// "original_transaction_id": "1000000577061006", // important for initial buy, interactive_renewal, did_change_renewal_info, cancel, did_change_renewal_status, did_fail_to_renew, did_recover, price_increase_consent
// "web_order_line_item_id": "1000000047417113", // important for initial buy, interactive_renewal
// "product_id": "PRODUCT_ID", // important for initial buy, interactive_renewal, cancel, did_change_renewal_status
// "purchase_date": "2018-03-24 12:09:02 Etc/GMT",
// "purchase_date_pst": "2018-03-24 05:09:02 America/Los_Angeles",
// "original_purchase_date": "2018-03-17 12:09:03 Etc/GMT",
// "original_purchase_date_ms": "1521288543000",
// "original_purchase_date_pst": "2018-03-17 05:09:03 America/Los_Angeles",
// "cancellation_reason": "0",
// "cancellation_date": "2018-03-27 07:11:12 Etc/GMT",
// "cancellation_date_ms": "1522134672000",
// "cancellation_date_pst": "2018-03-27 00:11:12 America/Los_Angeles",
// "expires_date": "2019-10-09 07:43:26 Etc/GMT",
// "expires_date_ms": "1570607006000", // important for did_recover, price_increase_consent
// "expires_date_formatted": "2019-03-24 12:09:02 Etc/GMT",
// "expires_date_formatted_pst": "2019-03-24 05:09:02 America/Los_Angeles",
// "quantity": "1",
// "unique_identifier": "UNIQUE_IDENTIFIER",
// "unique_vendor_identifier": "UNIQUE_VENDOR_IDENTIFIER",
// "is_in_intro_offer_period": "false",
// "is_trial_period": "false",
// "item_id": "ITEM_ID",
// "app_item_id": "APP_ITEM_ID",
// "version_external_identifier": "VERSION_EXTERNAL_IDENTIFIER",
// "transaction_id": "1000000577069202",
// "bvrs": "2",
// "bid": "com.example.ios.app"
// },
// "auto_renew_status": "false", // important for did_change_renewal_status
// "auto_renew_product_id": "PRODUCT_ID", // important for did_change_renewal_info
// "auto_renew_status_change_date": "",
// "auto_renew_status_change_date_pst": "",
// "auto_renew_status_change_date_ms": "", // important for did_change_renewal_status
// "pending_renewal_info": [ // important for did_fail_to_renew
// {
// "auto_renew_product_id": "PRODUCT_ID",
// "auto_renew_status": "1",
// "expiration_intent": "",
// "original_transaction_id": "1000000577061006",
// "is_in_billing_retry_period": "1",
// "product_id": "PRODUCT_ID",
// "price_consent_status": "0", // important for price_increase_consent
// "grace_period_expires_date": "",
// "grace_period_expires_date_ms": "",
// "grace_period_expires_date_pst": ""
// }
// ]
// }
Loading…
Cancel
Save