Skip to content

Instantly share code, notes, and snippets.

@kolomiec-valeriy
Created January 9, 2019 13:23
Show Gist options
  • Save kolomiec-valeriy/5bec950e2e10e3b2908166e82d5c3ed4 to your computer and use it in GitHub Desktop.
Save kolomiec-valeriy/5bec950e2e10e3b2908166e82d5c3ed4 to your computer and use it in GitHub Desktop.
<?php
namespace App\Subscription;
use App\Entity\Subscription\IAPAndroidReceipt;
use App\Exception\InvalidAndroidReceiptException;
use App\Exception\InvalidArgumentsException;
use App\Exception\ReceiptNotFoundException;
use Carbon\Carbon;
use Doctrine\Common\Persistence\ManagerRegistry;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Mcfedr\QueueManagerBundle\Exception\JobNotDeletableException;
use Mcfedr\QueueManagerBundle\Manager\QueueManagerRegistry;
use Mcfedr\QueueManagerBundle\Queue\Worker;
use Psr\Log\LoggerInterface;
class AndroidSubscriptionManager implements Worker
{
/**
* @var ManagerRegistry
*/
private $doctrine;
/**
* @var QueueManagerRegistry
*/
private $manager;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var SubscriptionTypeManager
*/
private $typeManager;
/**
* @var Client
*/
private $guzzleClient;
public function __construct(
ManagerRegistry $doctrine,
QueueManagerRegistry $manager,
LoggerInterface $logger,
SubscriptionTypeManager $subscriptionTypeManager,
Client $guzzleClient
) {
$this->doctrine = $doctrine;
$this->manager = $manager;
$this->logger = $logger;
$this->typeManager = $subscriptionTypeManager;
$this->guzzleClient = $guzzleClient;
}
public function handleSubscription(IAPAndroidReceipt $androidReceipt)
{
try {
$subscriptionData = $this->getSubscriptionData($androidReceipt);
$oldExpire = $androidReceipt->getExpirationAt();
$androidReceipt = $this->saveReceipt(
$androidReceipt,
$subscriptionData
);
$this->validateSubscriptionDataStatusCode($androidReceipt);
if ($androidReceipt->isActiveSubscription()) {
if (!$oldExpire) {
$this->logger->info('Subscription_Android_New');
} elseif ($oldExpire != $androidReceipt->getExpirationAt()) {
$this->logger->info('Subscription_Android_Renew');
}
$androidReceipt->setAttemptCount(0);
if ($androidReceipt->getExpirationAt()) {
$expiredDate = clone $androidReceipt->getExpirationAt();
$expiredDate->modify('+1 day');
$this->queue($androidReceipt, $expiredDate);
}
$this->doctrine->getManager()->flush();
$this->typeManager->queue($androidReceipt->getAccount());
} elseif (!$androidReceipt->isCanceled(
) && (IAPAndroidReceipt::PAYMENT_STATE_PENDING === $androidReceipt->getPaymentState(
) || $androidReceipt->getAttemptCount() < 5)) {
$this->logger->warning(
'Android receipt. Payment pending. Trying after 5 mins',
[
'android_receipt_id' => $androidReceipt->getId(),
]
);
$androidReceipt->setAttemptCount(
$androidReceipt->getAttemptCount() + 1
);
$this->queue($androidReceipt, new Carbon('+5 minute'));
} else {
$this->doctrine->getManager()->flush();
$this->typeManager->queue($androidReceipt->getAccount());
}
} catch (InvalidAndroidReceiptException $e) {
$this->logger->error(
'Android Receipt exception',
[
'e' => $e->getMessage(),
'android_receipt_id' => $androidReceipt->getId(),
]
);
$this->typeManager->queue($androidReceipt->getAccount());
}
}
public function queue(
IAPAndroidReceipt $androidReceipt,
\DateTime $when = null
) {
$this->cancel($androidReceipt);
$androidReceipt->setJobReference(
$this->manager->put(
'android_subscription_manager',
[
'receipt_id' => $androidReceipt->getId(),
],
[
'queue' => 'subscriptions',
'time' => $when,
],
'delay'
)
);
$this->doctrine->getManager()->flush();
}
/**
* Updates the validity of the receipt.
*
* @param IAPAndroidReceipt $androidReceipt
*/
private function validateSubscriptionDataStatusCode(
IAPAndroidReceipt $androidReceipt
) {
if (null === $androidReceipt->getCancelReason()) {
$androidReceipt->setCanceled(false);
} else {
if (0 == $androidReceipt->getCancelReason()) {
$this->logger->info(
'Android receipt. User cancelled the subscription',
[
'android_receipt_id' => $androidReceipt->getId(),
]
);
} elseif (1 == $androidReceipt->getCancelReason()) {
$this->logger->info(
'Android receipt. Subscription was cancelled by the system, for example because of a billing problem',
[
'android_receipt_id' => $androidReceipt->getId(),
]
);
}
$androidReceipt->setCanceled(true);
}
if (
IAPAndroidReceipt::PAYMENT_STATE_RECEIVED === $androidReceipt->getPaymentState(
) ||
IAPAndroidReceipt::PURCHASE_STATE_PURCHASED === $androidReceipt->getPurchaseState(
)
) {
$androidReceipt->setValid(true);
} elseif (
IAPAndroidReceipt::PAYMENT_STATE_PENDING === $androidReceipt->getPaymentState(
) ||
IAPAndroidReceipt::PURCHASE_STATE_CANCELLED === $androidReceipt->getPurchaseState(
)
) {
$androidReceipt->setValid(false);
} else {
$this->logger->error(
'Android receipt. Unknown receipt status',
[
'android_receipt_id' => $androidReceipt->getId(),
'payment_state' => $androidReceipt->getPaymentState(),
]
);
$androidReceipt->setValid(false);
}
}
/**
* Store the Google data in the receipt object.
*
* @param IAPAndroidReceipt $androidReceipt
* @param array $subscriptionData
*
* @return IAPAndroidReceipt
*/
private function saveReceipt(
IAPAndroidReceipt $androidReceipt,
array $subscriptionData
) {
$originExpirationDate = $androidReceipt->getExpirationAt();
$androidReceipt->setPurchaseKind($subscriptionData['kind']);
$androidReceipt->setDeveloperPayload(
$subscriptionData['developerPayload']
);
if ($androidReceipt->getProductIdentifier(
) && $androidReceipt->getProductIdentifier()->isLifetime()) {
$androidReceipt
->setPurchaseState($subscriptionData['purchaseState']);
$androidReceipt->setConsumptionState(
$subscriptionData['purchaseState']
);
$androidReceipt->setPurchaseAt(
Carbon::createFromFormat(
'U',
(int) ($subscriptionData['purchaseTimeMillis'] / 1000)
)
);
} else {
$androidReceipt
->setPriceCurrencyCode($subscriptionData['priceCurrencyCode'])
->setPriceAmountMicros($subscriptionData['priceAmountMicros'])
->setCountryCode($subscriptionData['countryCode'])
->setPaymentState(
array_key_exists(
'paymentState',
$subscriptionData
) ? $subscriptionData['paymentState'] : null
)
->setCancelReason(
array_key_exists(
'cancelReason',
$subscriptionData
) ? $subscriptionData['cancelReason'] : null
)
->setExpirationAt(
Carbon::createFromFormat(
'U',
(int) ($subscriptionData['expiryTimeMillis'] / 1000)
)
)
->setPurchaseAt(
Carbon::createFromFormat(
'U',
(int) ($subscriptionData['startTimeMillis'] / 1000)
)
);
}
if (null != $originExpirationDate && $androidReceipt->getExpirationAt(
) > $originExpirationDate) {
$androidReceipt->setRenewCount(
$androidReceipt->getRenewCount() + 1
);
}
return $androidReceipt;
}
/**
* Fetch the android receipt data from Google.
*
* @param IAPAndroidReceipt $androidReceipt
*
* @throws InvalidAndroidReceiptException
*
* @return array
*/
public function getSubscriptionData(IAPAndroidReceipt $androidReceipt)
{
try {
$url = "https://www.googleapis.com/androidpublisher/v2/applications/{$androidReceipt->getPackageName()}/purchases/subscriptions/{$androidReceipt->getProductIdentifier()->getProductIdentifier()}/tokens/{$androidReceipt->getReceiptToken()}";
if ($androidReceipt->getProductIdentifier(
) && $androidReceipt->getProductIdentifier()->isLifetime()) {
$url = "https://www.googleapis.com/androidpublisher/v2/applications/{$androidReceipt->getPackageName()}/purchases/products/{$androidReceipt->getProductIdentifier()->getProductIdentifier()}/tokens/{$androidReceipt->getReceiptToken()}";
}
$responseData = $this->guzzleClient->get($url);
$responseJson = json_decode($responseData->getBody(), true);
$this->logger->info(
'Android receipt data',
[
'androidReceipt_id' => $androidReceipt->getId(),
'response' => $responseJson,
]
);
return $responseJson;
} catch (RequestException $e) {
$this->logger->error(
'Android receipt fetching',
[
'e' => $e->getMessage(),
'androidReceipt_id' => $androidReceipt->getId(),
'product_id' => $androidReceipt->getProductIdentifier(),
]
);
throw new InvalidAndroidReceiptException(
'Failed to fetch android receipt data', 0, $e
);
}
}
private function cancel(IAPAndroidReceipt $androidReceipt)
{
if (($job = $androidReceipt->getJobReference())) {
try {
$androidReceipt->setJobReference(null);
$this->manager->delete($job);
} catch (JobNotDeletableException $e) {
$this->logger->error(
'Failed to delete android receipt job',
[
'e' => $e->getMessage(),
'android_receipt_id' => $androidReceipt->getId(),
'job' => print_r($job, true),
]
);
}
}
}
/**
* Called to start the queued task.
*
* @param array $arguments
*
* @throws \Exception
*/
public function execute(array $arguments)
{
if (!isset($arguments['receipt_id'])) {
throw new InvalidArgumentsException('Missing argument');
}
$receiptId = $arguments['receipt_id'];
$receiptObject = $this->doctrine->getRepository(
IAPAndroidReceipt::class
)->find($receiptId);
if (!$receiptObject) {
throw new ReceiptNotFoundException(
"Android Receipt not found with id {$receiptId}"
);
}
$this->handleSubscription($receiptObject);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment