/** * @Route ("/profile") * @HttpMethod ({"GET"}) * @Secure ({"USER"}) * * @param array $params * @param ViewModel $model * @return string */ public function profile(array $params, ViewModel $model) { $userService = UserService::instance(); $orderService = OrdersService::instance(); $subscriptionsService = SubscriptionsService::instance(); $userId = Session::getCredentials()->getUserId(); $subscription = $subscriptionsService->getUserActiveSubscription($userId); if (empty($subscription)) { $subscription = $subscriptionsService->getUserPendingSubscription($userId); } $paymentProfile = null; $subscriptionType = null; if (!empty($subscription) && !empty($subscription['subscriptionType'])) { $subscriptionType = $subscriptionsService->getSubscriptionType($subscription['subscriptionType']); $paymentProfile = $this->getPaymentProfile($subscription); } $address = $userService->getAddressByUserId($userId); if (empty($address)) { $address = array(); $address['fullName'] = ''; $address['line1'] = ''; $address['line2'] = ''; $address['city'] = ''; $address['region'] = ''; $address['zip'] = ''; $address['country'] = ''; } if (Session::get('modelSuccess')) { $model->success = Session::get('modelSuccess'); Session::set('modelSuccess'); } if (Session::get('modelError')) { $model->error = Session::get('modelError'); Session::set('modelError'); } $model->title = 'Profile'; $model->user = $userService->getUserById($userId); $model->subscription = $subscription; $model->subscriptionType = $subscriptionType; $gifts = $subscriptionsService->getActiveSubscriptionsByGifterId($userId); for ($i = 0; $i < count($gifts); $i++) { $gifts[$i]['type'] = $subscriptionsService->getSubscriptionType($gifts[$i]['subscriptionType']); } $model->gifts = $gifts; $model->paymentProfile = $paymentProfile; $model->address = $address; $model->title = 'Account'; return 'profile'; }
/** * @Route ("/subscription/process") * @Secure ({"USER"}) * * We were redirected here from PayPal after the buyer approved/cancelled the payment * * @param array $params * @return string * @throws Exception * @throws \Destiny\Common\Utils\FilterParamsException * TODO clean this method up */ public function subscriptionProcess(array $params) { FilterParams::required($params, 'subscriptionId'); FilterParams::required($params, 'token'); FilterParams::declared($params, 'success'); $userId = Session::getCredentials()->getUserId(); $userService = UserService::instance(); $ordersService = OrdersService::instance(); $subscriptionsService = SubscriptionsService::instance(); $payPalApiService = PayPalApiService::instance(); $chatIntegrationService = ChatIntegrationService::instance(); $authenticationService = AuthenticationService::instance(); $log = Application::instance()->getLogger(); $subscription = $subscriptionsService->getSubscriptionById($params['subscriptionId']); if (empty($subscription) || strcasecmp($subscription['status'], SubscriptionStatus::_NEW) !== 0) { throw new Exception('Invalid subscription record'); } try { $subscriptionType = $subscriptionsService->getSubscriptionType($subscription['subscriptionType']); $user = $userService->getUserById($subscription['userId']); if ($user['userId'] != $userId && $subscription['gifter'] != $userId) { throw new Exception('Invalid subscription'); } if ($params['success'] == '0' || $params['success'] == 'false' || $params['success'] === false) { throw new Exception('Order request failed'); } if (!$payPalApiService->retrieveCheckoutInfo($params['token'])) { throw new Exception('Failed to retrieve express checkout details'); } FilterParams::required($params, 'PayerID'); // if the order status is an error, the payerID is not returned Session::set('subscriptionId'); Session::set('token'); // Create the payment profile // Payment date is 1 day before subscription rolls over. if ($subscription['recurring'] == 1 || $subscription['recurring'] == true) { $startPaymentDate = Date::getDateTime(); $nextPaymentDate = Date::getDateTime(); $nextPaymentDate->modify('+' . $subscriptionType['billingFrequency'] . ' ' . strtolower($subscriptionType['billingPeriod'])); $nextPaymentDate->modify('-1 DAY'); $reference = $subscription['userId'] . '-' . $subscription['subscriptionId']; $paymentProfileId = $payPalApiService->createRecurringPaymentProfile($params['token'], $reference, $user['username'], $nextPaymentDate, $subscriptionType); if (empty($paymentProfileId)) { throw new Exception('Invalid recurring payment profileId returned from Paypal'); } $subscriptionsService->updateSubscription(array('subscriptionId' => $subscription['subscriptionId'], 'paymentStatus' => PaymentStatus::ACTIVE, 'paymentProfileId' => $paymentProfileId, 'billingStartDate' => $startPaymentDate->format('Y-m-d H:i:s'), 'billingNextDate' => $nextPaymentDate->format('Y-m-d H:i:s'))); } // Record the payments as well as check if any are not in the completed state // we put the subscription into "PENDING" state if a payment is found not completed $subscriptionStatus = SubscriptionStatus::ACTIVE; $DoECResponse = $payPalApiService->getECPaymentResponse($params['PayerID'], $params['token'], $subscriptionType['amount']); $payments = $payPalApiService->getResponsePayments($DoECResponse); foreach ($payments as $payment) { $payment['subscriptionId'] = $subscription['subscriptionId']; $payment['payerId'] = $params['PayerID']; $ordersService->addPayment($payment); // TODO: Payment provides no way of telling if the transaction with ALL payments was successful if ($payment['paymentStatus'] != PaymentStatus::COMPLETED) { $subscriptionStatus = SubscriptionStatus::PENDING; } } // Update subscription status $subscriptionsService->updateSubscription(array('subscriptionId' => $subscription['subscriptionId'], 'status' => $subscriptionStatus)); } catch (Exception $e) { $subscriptionsService->updateSubscription(array('subscriptionId' => $subscription['subscriptionId'], 'status' => SubscriptionStatus::ERROR)); $log->critical($e->getMessage(), $subscription); return 'redirect: /subscription/' . urlencode($subscription['subscriptionId']) . '/error'; } // only unban the user if the ban is non-permanent or the tier of the subscription is >= 2 // we unban the user if no ban is found because it also unmutes $ban = $userService->getUserActiveBan($user['userId']); if (empty($ban) or (!empty($ban['endtimestamp']) or $subscriptionType['tier'] >= 2)) { $chatIntegrationService->sendUnban($user['userId']); } // Broadcast $randomEmote = Config::$a['chat']['customemotes'][array_rand(Config::$a['chat']['customemotes'])]; if (!empty($subscription['gifter'])) { $gifter = $userService->getUserById($subscription['gifter']); $userName = $gifter['username']; $chatIntegrationService->sendBroadcast(sprintf("%s is now a %s subscriber! gifted by %s %s", $user['username'], $subscriptionType['tierLabel'], $gifter['username'], $randomEmote)); } else { $userName = $user['username']; $chatIntegrationService->sendBroadcast(sprintf("%s is now a %s subscriber! %s", $user['username'], $subscriptionType['tierLabel'], $randomEmote)); } $subMessage = Session::set('subMessage'); if (!empty($subMessage)) { $chatIntegrationService->sendBroadcast(sprintf("%s: %s", $userName, $subMessage)); } // Update the user $authenticationService->flagUserForUpdate($user['userId']); // Redirect to completion page return 'redirect: /subscription/' . urlencode($subscription['subscriptionId']) . '/complete'; }
/** * @Route ("/admin/user/{id}/subscription/{subscriptionId}/edit") * @Secure ({"ADMIN"}) * @HttpMethod ({"GET"}) * * @param array $params * @param ViewModel $model * @throws Exception * @return string */ public function subscriptionEdit(array $params, ViewModel $model) { FilterParams::required($params, 'id'); FilterParams::required($params, 'subscriptionId'); $subscriptionsService = SubscriptionsService::instance(); $userService = UserService::instance(); $ordersService = OrdersService::instance(); $subscription = array(); $payments = array(); $order = array(); if (!empty($params['subscriptionId'])) { $subscription = $subscriptionsService->getSubscriptionById($params['subscriptionId']); $order = $ordersService->getOrderById($subscription['orderId']); $payments = $ordersService->getPaymentsByOrderId($subscription['orderId']); } if (Session::get('modelSuccess')) { $model->success = Session::get('modelSuccess'); Session::set('modelSuccess'); } $model->user = $userService->getUserById($params['id']); $model->subscriptions = Config::$a['commerce']['subscriptions']; $model->subscription = $subscription; $model->order = $order; $model->payments = $payments; $model->title = 'Subsription'; return "admin/subscription"; }
/** * @Route ("/subscription/process") * @Secure ({"USER"}) * @Transactional * * We were redirected here from PayPal after the buyer approved/cancelled the payment * * @param array $params */ public function subscriptionProcess(array $params, ViewModel $model) { FilterParams::isRequired($params, 'orderId'); FilterParams::isRequired($params, 'token'); FilterParams::isThere($params, 'success'); $ordersService = OrdersService::instance(); $userService = UserService::instance(); $subscriptionsService = SubscriptionsService::instance(); $payPalApiService = PayPalApiService::instance(); $chatIntegrationService = ChatIntegrationService::instance(); $authenticationService = AuthenticationService::instance(); $userId = Session::getCredentials()->getUserId(); // Get the order $order = $ordersService->getOrderByIdAndUserId($params['orderId'], $userId); if (empty($order) || strcasecmp($order['state'], OrderStatus::_NEW) !== 0) { throw new Exception('Invalid order record'); } try { // If we got a failed response URL if ($params['success'] == '0' || $params['success'] == 'false' || $params['success'] === false) { throw new Exception('Order request failed'); } // Get the subscription from the order $orderSubscription = $subscriptionsService->getSubscriptionByOrderId($order['orderId']); $subscriptionUser = $userService->getUserById($orderSubscription['userId']); // Make sure the subscription is valid if (empty($orderSubscription)) { throw new Exception('Invalid order subscription'); } // Make sure the subscription is either owned or gifted by the user if ($subscriptionUser['userId'] != $userId && $orderSubscription['gifter'] != $userId) { throw new Exception('Invalid order subscription'); } $subscriptionType = $subscriptionsService->getSubscriptionType($orderSubscription['subscriptionType']); $paymentProfile = $ordersService->getPaymentProfileByOrderId($order['orderId']); // Get the checkout info $ecResponse = $payPalApiService->retrieveCheckoutInfo($params['token']); if (!isset($ecResponse) || $ecResponse->Ack != 'Success') { throw new Exception('Failed to retrieve express checkout details'); } // Moved this down here, as if the order status is error, the payerID is not returned FilterParams::isRequired($params, 'PayerID'); // Point of no return - we only every want a person to get here if their order was a successful sequence Session::set('token'); Session::set('orderId'); // Recurring payment if (!empty($paymentProfile)) { $createRPProfileResponse = $payPalApiService->createRecurringPaymentProfile($paymentProfile, $params['token'], $subscriptionType); if (!isset($createRPProfileResponse) || $createRPProfileResponse->Ack != 'Success') { throw new Exception('Failed to create recurring payment request'); } $paymentProfileId = $createRPProfileResponse->CreateRecurringPaymentsProfileResponseDetails->ProfileID; $paymentStatus = $createRPProfileResponse->CreateRecurringPaymentsProfileResponseDetails->ProfileStatus; if (empty($paymentProfileId)) { throw new Exception('Invalid recurring payment profileId returned from Paypal'); } // Set the payment profile to active, and paymetProfileId $ordersService->updatePaymentProfileId($paymentProfile['profileId'], $paymentProfileId, $paymentStatus); // Update the payment profile $subscriptionsService->updateSubscriptionPaymentProfile($orderSubscription['subscriptionId'], $paymentProfile['profileId'], true); } // Complete the checkout $DoECResponse = $payPalApiService->getECPaymentResponse($params['PayerID'], $params['token'], $order); if (isset($DoECResponse) && $DoECResponse->Ack == 'Success') { if (isset($DoECResponse->DoExpressCheckoutPaymentResponseDetails->PaymentInfo)) { $payPalApiService->recordECPayments($DoECResponse, $params['PayerID'], $order); $ordersService->updateOrderState($order['orderId'], $order['state']); } else { throw new Exception('No payments for express checkout order'); } } else { throw new Exception($DoECResponse->Errors[0]->LongMessage); } // If the user already has a subscription and ONLY if this subscription was NOT a gift if (!isset($orderSubscription['gifter']) || empty($orderSubscription['gifter'])) { $activeSubscription = $subscriptionsService->getUserActiveSubscription($subscriptionUser['userId']); if (!empty($activeSubscription)) { // Cancel any attached payment profiles $ordersService = OrdersService::instance(); $paymentProfile = $ordersService->getPaymentProfileById($activeSubscription['paymentProfileId']); if (!empty($paymentProfile)) { $payPalApiService->cancelPaymentProfile($activeSubscription, $paymentProfile); $subscriptionsService->updateSubscriptionRecurring($activeSubscription['subscriptionId'], false); } // Cancel the active subscription $subscriptionsService->updateSubscriptionState($activeSubscription['subscriptionId'], SubscriptionStatus::CANCELLED); } } // Check if this is a gift, check that the giftee is still eligable if (!empty($orderSubscription['gifter']) && !$subscriptionsService->getCanUserReceiveGift($userId, $subscriptionUser['userId'])) { // Update the state to ERROR and log a critical error Application::instance()->getLogger()->critical('Duplicate subscription attempt, Gifter: %d GifteeId: %d, OrderId: %d', $userId, $subscriptionUser['userId'], $order['orderId']); $subscriptionsService->updateSubscriptionState($orderSubscription['subscriptionId'], SubscriptionStatus::ERROR); } else { // Unban the user if a ban is found $ban = $userService->getUserActiveBan($subscriptionUser['userId']); // only unban the user if the ban is non-permanent or the tier of the subscription is >= 2 // we unban the user if no ban is found because it also unmutes if (empty($ban) or (!empty($ban['endtimestamp']) or $orderSubscription['subscriptionTier'] >= 2)) { $chatIntegrationService->sendUnban($subscriptionUser['userId']); } // Activate the subscription (state) $subscriptionsService->updateSubscriptionState($orderSubscription['subscriptionId'], SubscriptionStatus::ACTIVE); // Flag the user for 'update' $authenticationService->flagUserForUpdate($subscriptionUser['userId']); // Random emote $randomEmote = Config::$a['chat']['customemotes'][array_rand(Config::$a['chat']['customemotes'])]; // Broadcast if (!empty($orderSubscription['gifter'])) { $gifter = $userService->getUserById($orderSubscription['gifter']); $userName = $gifter['username']; $chatIntegrationService->sendBroadcast(sprintf("%s is now a %s subscriber! gifted by %s %s", $subscriptionUser['username'], $subscriptionType['tierLabel'], $gifter['username'], $randomEmote)); } else { $userName = $subscriptionUser['username']; $chatIntegrationService->sendBroadcast(sprintf("%s is now a %s subscriber! %s", $subscriptionUser['username'], $subscriptionType['tierLabel'], $randomEmote)); } // Get the subscription message, and remove it from the session $subMessage = Session::set('subMessage'); if (!empty($subMessage)) { $chatIntegrationService->sendBroadcast(sprintf("%s: %s", $userName, $subMessage)); } } // Redirect to completion page return 'redirect: /subscription/' . urlencode($order['orderId']) . '/complete'; } catch (Exception $e) { if (!empty($order)) { $ordersService->updateOrderState($order['orderId'], OrderStatus::ERROR); } if (!empty($paymentProfile)) { $ordersService->updatePaymentStatus($paymentProfile['paymentId'], PaymentStatus::ERROR); } if (!empty($orderSubscription)) { $subscriptionsService->updateSubscriptionState($orderSubscription['subscriptionId'], SubscriptionStatus::ERROR); } $log = Application::instance()->getLogger(); $log->error($e->getMessage(), $order); return 'redirect: /subscription/' . urlencode($order['orderId']) . '/error'; } }
/** * Get payment profile from IPN * * @param array $data * @return unknown */ protected function getPaymentProfile(array $data) { if (!isset($data['recurring_payment_id']) || empty($data['recurring_payment_id'])) { throw new Exception('Invalid recurring_payment_id'); } $orderService = OrdersService::instance(); $paymentProfile = $orderService->getPaymentProfileByPaymentProfileId($data['recurring_payment_id']); if (empty($paymentProfile)) { throw new Exception('Invalid payment profile'); } return $paymentProfile; }
/** * @param string $txnId * @param string $txnType * @param array $data * @throws Exception */ protected function handleIPNTransaction($txnId, $txnType, array $data) { $log = Application::instance()->getLogger(); $orderService = OrdersService::instance(); $subscriptionsService = SubscriptionsService::instance(); switch (strtoupper($txnType)) { // This is sent when a express checkout has been performed by a user // We need to handle the case where orders go through, but have pending payments. case 'EXPRESS_CHECKOUT': $payment = $orderService->getPaymentByTransactionId($txnId); if (!empty($payment)) { // Make sure the payment values are the same if (number_format($payment['amount'], 2) != number_format($data['mc_gross'], 2)) { throw new Exception('Amount for payment do not match'); } // Update the payment status $orderService->updatePayment(array('paymentId' => $payment['paymentId'], 'paymentStatus' => $data['payment_status'])); // Update the subscription paymentStatus to active (may have been pending) // TODO we set the paymentStatus to active without checking it because we get the opportunity to check it after subscription completion $subscription = $subscriptionsService->getSubscriptionById($payment['subscriptionId']); if (!empty($subscription)) { $subscriptionsService->updateSubscription(array('subscriptionId' => $subscription['subscriptionId'], 'paymentStatus' => PaymentStatus::ACTIVE)); } } else { $log->info(sprintf('Express checkout IPN called, but no payment found [%s]', $txnId)); } break; // This is sent from paypal when a recurring payment is billed // This is sent from paypal when a recurring payment is billed case 'RECURRING_PAYMENT': if (!isset($data['payment_status'])) { throw new Exception('Invalid payment status'); } if (!isset($data['next_payment_date'])) { throw new Exception('Invalid next_payment_date'); } $nextPaymentDate = Date::getDateTime($data['next_payment_date']); $subscription = $this->getSubscriptionByPaymentProfileData($data); $subscriptionsService->updateSubscription(array('subscriptionId' => $subscription['subscriptionId'], 'billingNextDate' => $nextPaymentDate->format('Y-m-d H:i:s'), 'paymentStatus' => PaymentStatus::ACTIVE)); $orderService->addPayment(array('subscriptionId' => $subscription['subscriptionId'], 'payerId' => $data['payer_id'], 'amount' => $data['mc_gross'], 'currency' => $data['mc_currency'], 'transactionId' => $txnId, 'transactionType' => $txnType, 'paymentType' => $data['payment_type'], 'paymentStatus' => $data['payment_status'], 'paymentDate' => Date::getDateTime($data['payment_date'])->format('Y-m-d H:i:s'))); $log->notice(sprintf('Added order payment %s status %s', $data['recurring_payment_id'], $data['profile_status'])); break; case 'RECURRING_PAYMENT_SKIPPED': $subscription = $this->getSubscriptionByPaymentProfileData($data); $subscriptionsService->updateSubscription(array('subscriptionId' => $subscription['subscriptionId'], 'paymentStatus' => PaymentStatus::SKIPPED)); $log->debug(sprintf('Payment skipped %s', $data['recurring_payment_id'])); break; case 'RECURRING_PAYMENT_PROFILE_CANCEL': $subscription = $this->getSubscriptionByPaymentProfileData($data); $subscriptionsService->updateSubscription(array('subscriptionId' => $subscription['subscriptionId'], 'paymentStatus' => PaymentStatus::CANCELLED)); $log->debug(sprintf('Payment profile cancelled %s status %s', $data['recurring_payment_id'], $data['profile_status'])); break; case 'RECURRING_PAYMENT_FAILED': $subscription = $this->getSubscriptionByPaymentProfileData($data); $subscriptionsService->updateSubscription(array('subscriptionId' => $subscription['subscriptionId'], 'paymentStatus' => PaymentStatus::FAILED)); $log->debug(sprintf('Payment profile cancelled %s status %s', $data['recurring_payment_id'], $data['profile_status'])); break; // Sent on first post-back when the user subscribes // Sent on first post-back when the user subscribes case 'RECURRING_PAYMENT_PROFILE_CREATED': $subscription = $this->getSubscriptionByPaymentProfileData($data); $subscriptionsService->updateSubscription(array('subscriptionId' => $subscription['subscriptionId'], 'paymentStatus' => PaymentStatus::ACTIVE)); $log->debug(sprintf('Updated payment profile %s status %s', $data['recurring_payment_id'], $data['profile_status'])); break; } }
/** * Record the payments from a EC payment response * * @param PayPalAPI\DoExpressCheckoutPaymentResponseType $DoECResponse * @param string $payerId * @param array $order * @return array */ public function recordECPayments(DoExpressCheckoutPaymentResponseType $DoECResponse, $payerId, array &$order) { $payments = array(); $orderService = OrdersService::instance(); $orderStatus = OrderStatus::COMPLETED; for ($i = 0; $i < count($DoECResponse->DoExpressCheckoutPaymentResponseDetails->PaymentInfo); ++$i) { $paymentInfo = $DoECResponse->DoExpressCheckoutPaymentResponseDetails->PaymentInfo[$i]; $payment = array(); $payment['orderId'] = $order['orderId']; $payment['payerId'] = $payerId; $payment['amount'] = $paymentInfo->GrossAmount->value; $payment['currency'] = $paymentInfo->GrossAmount->currencyID; $payment['transactionId'] = $paymentInfo->TransactionID; $payment['transactionType'] = $paymentInfo->TransactionType; $payment['paymentType'] = $paymentInfo->PaymentType; $payment['paymentStatus'] = $paymentInfo->PaymentStatus; $payment['paymentDate'] = Date::getDateTime($paymentInfo->PaymentDate)->format('Y-m-d H:i:s'); $orderService->addOrderPayment($payment); $payments[] = $payment; if ($paymentInfo->PaymentStatus != PaymentStatus::COMPLETED) { $orderStatus = OrderStatus::PENDING; } } $order['state'] = $orderStatus; return $order; }
/** * Create a new payment * * @param unknown $userId * @param array $order * @param array $subscriptionType * @param \DateTime $billingStartDate * @return array */ public function createPaymentProfile($userId, array $order, array $subscriptionType, \DateTime $billingStartDate) { $ordersService = OrdersService::instance(); $paymentProfile = array(); $paymentProfile['paymentProfileId'] = ''; $paymentProfile['userId'] = $userId; $paymentProfile['orderId'] = $order['orderId']; $paymentProfile['amount'] = $order['amount']; $paymentProfile['currency'] = $order['currency']; $paymentProfile['billingFrequency'] = $subscriptionType['billingFrequency']; $paymentProfile['billingPeriod'] = $subscriptionType['billingPeriod']; $paymentProfile['billingStartDate'] = $billingStartDate->format('Y-m-d H:i:s'); $paymentProfile['billingNextDate'] = $billingStartDate->format('Y-m-d H:i:s'); $paymentProfile['state'] = PaymentStatus::_NEW; $paymentProfile['profileId'] = $ordersService->addPaymentProfile($paymentProfile); return $paymentProfile; }