paysystemList->getList() as $k => $p) { // if ($p->getId() == $this->getId()) //$p->setPublic(false); } $di->billingPlanTable->customFields()->add( new Am_CustomFieldText('apple_product_id', "Apple Product Identifier")); } function getSupportedCurrencies() { return array_keys(Am_Currency::getFullList()); } function _initSetupForm(\Am_Form_Setup $form) { $form->addSecretText('password') ->setLabel(___('Your app’s shared secret')); $form->addSelect('testing')->setLabel(___('Plugin mode'))->loadOptions(['PROD' => 'Production', 'Sandbox'=> 'Sandbox']); $form->setDefault('testing', 'PROD'); } function createTransaction(/* Am_Mvc_Request */ $request, /* Am_Mvc_Response */ $response, array $invokeArgs ) { return new Am_Paysystem_Transaction_Incoming_AppleIap($this, $request, $response, $invokeArgs); } /** * Do not send failure emails for in-app purchases * @param Invoice $invoice * @param $failedReason * @param $nextRebill */ function sendRebillFailedToUser(Invoice $invoice, $failedReason, $nextRebill) { } public function getToken( \Invoice $invoice ) { $value = $invoice->data()->get(self::RECEIPT_ID); if ($value == Am_DataFieldStorage::BLOB_VALUE) { return $invoice->data()->getBlob(self::RECEIPT_ID); } return $value; } public function processTokenPayment( \Invoice $invoice, $token = null ) { $token = $this->validateToken($invoice, $token); $log = $this->getDi()->invoiceLogRecord; $log->setInvoice($invoice); $receiptResponse = $this->validateReceipt($token, $log); //Update info with latest receipt $this->saveToken($invoice, $receiptResponse['latest_receipt']); $lastReceipt = $receiptResponse['latest_receipt_info'][0]; $transactions = $invoice->data()->getBlob(self::TRANSACTIONS); if (empty($transactions)) { $transactions = []; } else { $transactions = json_decode($transactions, true); if (!is_array($transactions)) { $transactions = []; } } $result = new Am_Paysystem_Result(); if (in_array($lastReceipt['transaction_id'], $transactions)) { $result->setFailed(___("Transaction was processed already")); return $result; } if ($this->getDi()->db->selectRow("select * from ?_invoice_payment where receipt_id=? and paysys_id=? limit 1", $lastReceipt['transaction_id'], $this->getId())) { $result->setFailed(___("Transaction was processed already")); return $result; } // Invoice Renewal failed. If apple pay won;t try to renew invoice again, set it as failed. if (isset($lastReceipt['expiration_intent']) && $lastReceipt['expiration_intent']) { if (!isset($lastReceipt['is_in_billing_retry_period']) || !$lastReceipt['is_in_billing_retry_period']) { $invoice->updateQuick([ 'status' => Invoice::RECURRING_FAILED ]); } $result->setFailed(___('Subscirption expired')); return $result; } // Invoice won;t be rebilled anymore, set it as cancelled. if (isset($lastReceipt["auto_renew_status"]) && !$lastReceipt['auto_renew_status'] && ($invoice->status == Invoice::RECURRING_ACTIVE)) { $invoice->setCancelled(true); $result->setFailed(___('Subscirption Cancelled')); return $result; } $tr = new Am_Paysystem_Transaction_AppleIap($this, $lastReceipt); $tr->setInvoice($invoice); $tr->process(); // Store receipt to avoid duplicate processing; $transactions[] = $lastReceipt['transaction_id']; $invoice->data()->setBlob(self::TRANSACTIONS, json_encode($transactions))->update(); $invoice->data()->setBlob(self::ORIG_TRANSACTION_ID, $lastReceipt['original_transaction_id']?:$lastReceipt['transaction_id'])->update(); $result->setSuccess($tr); return $result; } public function saveToken( \Invoice $invoice, $token ) { $invoice->data()->setBlob(self::RECEIPT_ID, $token)->update(); return $token; } function supportsTokenPayment() { return true; } /** * * @param type $transaction_id * @return Invoice $invoice */ function findInvoice($transaction_id) { $invoice = $this->getDi()->invoiceTable->findFirstByData(self::ORIG_TRANSACTION_ID, $transaction_id); return $invoice; } function getReceiptResponse($endpoint, $receipt, InvoiceLog $log) { $req = new Am_HttpRequest($endpoint, Am_HttpRequest::METHOD_POST); $req->setHeader('Content-type', 'application/json'); $req->setBody($data = json_encode([ 'receipt-data' => $receipt, 'password' => $this->getConfig('password'), 'exclude-old-transactions' => true ])); $resp = $req->send(); $log->add([ 'Validate Receipt Request' => $receipt, 'Validate Receipt Response' => $resp->getBody() ], true); if ($resp->getStatus() != 200) { throw new Am_Exception_InternalError(___('Unable to contact apple.com to validate receipt')); } $ret = @json_decode($resp->getBody(), true); if (!$ret) { throw new Am_Exception_InternalError(___('Incorrect response received from apple.com: %s', $resp->getBody())); } return $ret; } function validateReceipt($receipt, InvoiceLog $log) { $ret = $this->getReceiptResponse(self::ENDPOINT_LIVE, $receipt, $log); // Sandbox receipt received, try to validate it through sandbox if ($ret['status'] == 21007) { $ret = $this->getReceiptResponse(self::ENDPOINT_SANDBOX, $receipt, $log); } if ($ret['status'] != 0) { throw new Am_Exception_InternalError(___('Receipt Status is not success: %s', $ret['status'])); } return $ret; } public function doBill( Invoice $invoice, $doFirst, CcRecord $cc = null) { if ($doFirst) { return; } $this->invoice = $invoice; try { return $this->processTokenPayment($invoice); } catch (Exception $ex) { $this->getDi()->errorLogTable->logException($ex); $result = new Am_Paysystem_Result(); $result->setFailed($ex->getMessage()); return $result; } } function _doBill(\Invoice $invoice, $doFirst, \CcRecord $cc, \Am_Paysystem_Result $result) { } function storesCcInfo() { return false; } } class Am_Paysystem_Transaction_AppleIap extends Am_Paysystem_Transaction_Abstract { protected $receipt; function __construct(Am_Paysystem_AppleIap $plugin, $receipt) { parent::__construct($plugin); $this->receipt = $receipt; } public function getUniqId() { return $this->receipt['transaction_id']; } function processValidated() { if (isset($this->receipt["is_trial_period"]) && ($this->receipt['is_trial_period'] === true || $this->receipt['is_trial_period'] === 'true')) { $this->invoice->addAccessPeriod($this); } else { $this->invoice->addPayment($this); } } } class Am_Paysystem_Transaction_Incoming_AppleIap extends Am_Paysystem_Transaction_Incoming { protected $parsedResponse = []; protected $receipt = null; /** * * @param type $plugin * @param Am_Mvc_Request $request * @param type $response * @param type $invokeArgs */ function __construct($plugin, $request, $response, $invokeArgs) { parent::__construct($plugin, $request, $response, $invokeArgs); $this->parsedResponse = json_decode($request->getRawBody(), true); } public function getUniqId() { return $this->parsedResponse['latest_expired_receipt_info']['transaction_id'] . '-RFND'; } public function validateSource() { $env = $this->getPlugin()->getConfig('testing', 'PROD') ? 'Sandbox' : 'PROD'; if ($this->parsedResponse['environment'] != $env) { throw new Am_Exception_Paysystem_TransactionSource(___('Test notification was received but test mode is not enabled')); } if ($this->parsedResponse['password'] != $this->getPlugin()->getConfig('password')) { throw new Am_Exception_Paysystem_TransactionSource(___('Received password is not the same as stored in config')); } return true; } public function validateStatus() { return true; } public function validateTerms() { return true; } function findInvoiceId() { $invoice = $this->getPlugin()->findInvoice($this->parsedResponse['original_transaction_id'] ?: $this->parsedResponse['latest_expired_receipt_info']['original_transaction_id']); if (empty($invoice)) { return null; } return $invoice->public_id; } function processValidated() { switch ($this->parsedResponse['notification_type']) { case 'CANCEL' : $this->invoice->addRefund($this, $this->parsedResponse['latest_expired_receipt_info']['transaction_id']); break; case 'DID_CHANGE_RENEWAL_PREF' : $this->handleUpgrade(); break; } $this->invoice->data() ->setBlob(Am_Paysystem_AppleIap::RECEIPT_ID, $this->parsedResponse['latest_expired_receipt'] ?: $this->parsedResponse['latest_receipt'])->update(); } function handleUpgrade() { // Cancel current invoice and create new one with free period till the end of current invoice period. // Copy transactions from previous invoice to new one. $billingPlan = Am_Di::getInstance()->billingPlanTable->findFirstByData('apple_product_id', $this->parsedResponse['auto_renew_product_id']); $newInvoice = Am_Di::getInstance()->invoiceRecord; $newInvoice->setUser($this->invoice->getUser()); $product = $billingPlan->getProduct(); $newInvoice->add($product); $newInvoice->setPaysystem($this->getPlugin()->getId()); $newInvoice->calculate(); foreach ($newInvoice->getItems() as $item) { $item->first_total = $item->first_discount = $item->first_price = $item->first_shipping = $item->first_tax = 0; $item->first_period = $this->invoice->rebill_date; } $newInvoice->first_tax = $newInvoice->first_discount = $newInvoice->first_shipping = $newInvoice->first_discount = 0; $newInvoice->first_period = $this->invoice->rebill_date; foreach ([ Am_Paysystem_AppleIap::TRANSACTIONS, Am_Paysystem_AppleIap::RECEIPT_ID, Am_Paysystem_AppleIap::ORIG_TRANSACTION_ID ] as $key) { $newInvoice->data()->setBlob($key, $this->invoice->data()->getBlob($key)); $this->invoice->data()->setBlob($key, null); } $newInvoice->insert(); $tr = new Am_Paysystem_Transaction_Manual($this->getPlugin()); $tr->process(); $this->invoice->setCancelled(true); $this->invoice->data()->update(); } }