diff --git a/app/PaymentDrivers/PayPalExpressPaymentDriver.php b/app/PaymentDrivers/PayPalExpressPaymentDriver.php index b4fcf438f93d..752ebf48c69d 100644 --- a/app/PaymentDrivers/PayPalExpressPaymentDriver.php +++ b/app/PaymentDrivers/PayPalExpressPaymentDriver.php @@ -12,41 +12,28 @@ namespace App\PaymentDrivers; -use App\Events\Payment\PaymentWasCreated; +use App\Exceptions\PaymentFailed; use App\Jobs\Mail\PaymentFailureMailer; use App\Jobs\Util\SystemLogger; -use App\Models\ClientGatewayToken; use App\Models\GatewayType; -use App\Models\Payment; -use App\Models\PaymentHash; +use App\Models\Invoice; use App\Models\PaymentType; use App\Models\SystemLog; -use App\Utils\Ninja; use App\Utils\Traits\MakesHash; -use Exception; use Omnipay\Common\Item; -use stdClass; +use Omnipay\Omnipay; -class PayPalExpressPaymentDriver extends BasePaymentDriver +class PayPalExpressPaymentDriver extends BaseDriver { use MakesHash; - public $payment_hash; + public $token_billing = false; - public $required_fields = []; + public $can_authorise_credit_card = false; - protected $refundable = true; + private $omnipay_gateway; - protected $token_billing = false; - - protected $can_authorise_credit_card = false; - - protected $customer_reference = ''; - - public function setPaymentMethod($payment_method_id = null) - { - return $this; - } + const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYPAL; public function gatewayTypes() { @@ -55,131 +42,43 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver ]; } - const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYPAL; - - public function checkRequirements() + /** + * Initialize Omnipay PayPal_Express gateway. + * + * @return void + */ + private function initializeOmnipayGateway(): void { - if ($this->company_gateway->require_billing_address) { - if ($this->checkRequiredResource(auth()->user('contact')->client->address1)) { - $this->required_fields[] = 'billing_address1'; - } + $this->omnipay_gateway = Omnipay::create( + $this->company_gateway->gateway->provider + ); - if ($this->checkRequiredResource(auth()->user('contact')->client->address2)) { - $this->required_fields[] = 'billing_address2'; - } + $this->omnipay_gateway->initialize((array) $this->company_gateway->getConfig()); + } - if ($this->checkRequiredResource(auth()->user('contact')->client->city)) { - $this->required_fields[] = 'billing_city'; - } - - if ($this->checkRequiredResource(auth()->user('contact')->client->state)) { - $this->required_fields[] = 'billing_state'; - } - - if ($this->checkRequiredResource(auth()->user('contact')->client->postal_code)) { - $this->required_fields[] = 'billing_postal_code'; - } - - if ($this->checkRequiredResource(auth()->user('contact')->client->country_id)) { - $this->required_fields[] = 'billing_country'; - } - } - - if ($this->company_gateway->require_shipping_address) { - if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_address1)) { - $this->required_fields[] = 'shipping_address1'; - } - - if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_address2)) { - $this->required_fields[] = 'shipping_address2'; - } - - if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_city)) { - $this->required_fields[] = 'shipping_city'; - } - - if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_state)) { - $this->required_fields[] = 'shipping_state'; - } - - if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_postal_code)) { - $this->required_fields[] = 'shipping_postal_code'; - } - - if ($this->checkRequiredResource(auth()->user('contact')->client->shipping_country_id)) { - $this->required_fields[] = 'shipping_country'; - } - } - - if ($this->company_gateway->require_client_name) { - if ($this->checkRequiredResource(auth()->user('contact')->client->name)) { - $this->required_fields[] = 'name'; - } - } - - if ($this->company_gateway->require_client_phone) { - if ($this->checkRequiredResource(auth()->user('contact')->client->phone)) { - $this->required_fields[] = 'phone'; - } - } - - if ($this->company_gateway->require_contact_email) { - if ($this->checkRequiredResource(auth()->user('contact')->email)) { - $this->required_fields[] = 'contact_email'; - } - } - - if ($this->company_gateway->require_contact_name) { - if ($this->checkRequiredResource(auth()->user('contact')->first_name)) { - $this->required_fields[] = 'contact_first_name'; - } - - if ($this->checkRequiredResource(auth()->user('contact')->last_name)) { - $this->required_fields[] = 'contact_last_name'; - } - } - - if ($this->company_gateway->require_postal_code) { - // In case "require_postal_code" is true, we don't need billing address. - - foreach ($this->required_fields as $position => $field) { - if (Str::startsWith($field, 'billing')) { - unset($this->required_fields[$position]); - } - } - - if ($this->checkRequiredResource(auth()->user('contact')->client->postal_code)) { - $this->required_fields[] = 'postal_code'; - } - } + public function setPaymentMethod($payment_method_id) + { + // PayPal doesn't have multiple ways of paying. + // There's just one, off-site redirect. return $this; } - /** - * Wrapper method for checking if resource is good. - * - * @param mixed $resource - * @return bool - */ - public function checkRequiredResource($resource): bool + public function authorizeView($payment_method) { - if (is_null($resource) || empty($resource)) { - return true; - } + // PayPal doesn't support direct authorization. - return false; + return $this; } - /** - * Processes the payment with this gateway. - * - * - * @param array $data variables required to build payment page - * @return void Gateway and payment method specific view - * @throws Exception - */ - public function processPaymentView(array $data) + public function authorizeResponse($request) + { + // PayPal doesn't support direct authorization. + + return $this; + } + + public function processPaymentView($data) { if (count($this->required_fields) > 0) { return redirect() @@ -187,37 +86,36 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver ->with('missing_required_fields', $this->required_fields); } - $response = $this->purchase($this->paymentDetails($data), $this->paymentItems($data)); + $this->initializeOmnipayGateway(); + + $this->payment_hash->data = array_merge((array) $this->payment_hash->data, ['amount' => $data['amount_with_fee']]); + $this->payment_hash->save(); + + $response = $this->omnipay_gateway + ->purchase($this->generatePaymentDetails($data)) + ->setItems($this->generatePaymentItems($data)) + ->send(); if ($response->isRedirect()) { - // redirect to offsite payment gateway - $response->redirect(); - } elseif ($response->isSuccessful()) { - // payment was successful: update database - /* for this driver this method wont be hit*/ - } else { - // payment failed: display message to customer - - SystemLogger::dispatch( - [ - 'server_response' => $response->getData(), - 'data' => $data, - ], - SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_FAILURE, - SystemLog::TYPE_PAYPAL, - $this->client - ); - - throw new Exception('Error Processing Payment', 1); + return $response->redirect(); } - } - public function setPaymentHash(PaymentHash $payment_hash) - { - $this->payment_hash = $payment_hash; + PaymentFailureMailer::dispatch($this->client, $response->getData(), $this->client->company, $data['amount_with_fee']); - return $this; + $message = [ + 'server_response' => $response->getMessage(), + 'data' => $this->checkout->payment_hash->data, + ]; + + SystemLogger::dispatch( + $message, + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_PAYPAL, + $this->client + ); + + throw new PaymentFailed($response->getMessage(), $response->getCode()); } public function processPaymentResponse($request) @@ -227,210 +125,113 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver ->route('client.profile.edit', ['client_contact' => auth()->user()->hashed_id]) ->with('missing_required_fields', $this->required_fields); } - - $response = $this->completePurchase($request->all()); - $transaction_reference = $response->getTransactionReference() ?: $request->input('token'); + $this->initializeOmnipayGateway(); + + $response = $this->omnipay_gateway + ->completePurchase(['amount' => $this->payment_hash->data->amount]) + ->send(); if ($response->isCancelled()) { return redirect()->route('client.invoices.index')->with('warning', ctrans('texts.status_cancelled')); - } elseif ($response->isSuccessful()) { + } + + if ($response->isSuccessful()) { + $data = [ + 'payment_method' => $response->getData()['TOKEN'], + 'payment_type' => PaymentType::PAYPAL, + 'amount' => $this->payment_hash->data->amount, + 'transaction_reference' => $response->getTransactionReference(), + ]; + + $payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED); + SystemLogger::dispatch( - [ - 'server_response' => $response->getData(), - 'data' => $request->all(), - ], + ['response' => $response, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_PAYPAL, $this->client ); - } elseif (! $response->isSuccessful()) { - PaymentFailureMailer::dispatch($this->client, $response->getMessage, $this->client->company, $response['PAYMENTINFO_0_AMT']); + + return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); + } + + if (!$response->isSuccessful()) { + PaymentFailureMailer::dispatch($this->client, $response->getMessage(), $this->client->company, $response['PAYMENTINFO_0_AMT']); + + $message = [ + 'server_response' => $response->getMessage(), + 'data' => $this->payment_hash->data, + ]; SystemLogger::dispatch( - [ - 'data' => $request->all(), - 'server_response' => $response->getData(), - ], + $message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_PAYPAL, $this->client ); - throw new Exception($response->getMessage()); + throw new PaymentFailed($response->getMessage(), $response->getCode()); } - - $payment = $this->createPayment($response->getData()); - $payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->input('payment_hash')])->firstOrFail(); - - $payment_hash->payment_id = $payment->id; - $payment_hash->save(); - - $this->attachInvoices($payment, $payment_hash); - $payment->service()->updateInvoicePayment($payment_hash); - - event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); - - return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); } - protected function paymentDetails($input): array + public function generatePaymentDetails(array $data) { - $data = parent::paymentDetails($input); - - $data['amount'] = $input['amount_with_fee']; - $data['returnUrl'] = $this->buildReturnUrl($input); - $data['cancelUrl'] = $this->buildCancelUrl($input); - $data['description'] = $this->buildDescription($input); - $data['transactionId'] = $this->buildTransactionId($input); - - $data['ButtonSource'] = 'InvoiceNinja_SP'; - $data['solutionType'] = 'Sole'; // show 'Pay with credit card' option - $data['transactionId'] = $data['transactionId'].'-'.time(); - - return $data; - } - - private function buildReturnUrl($input): string - { - return route('client.payments.response', [ - 'company_gateway_id' => $this->company_gateway->id, - 'payment_hash' => $this->payment_hash->hash, - 'payment_method_id' => GatewayType::PAYPAL, - ]); - } - - private function buildCancelUrl($input): string - { - $url = $this->client->company->domain().'/client/invoices'; - - return $url; - } - - private function buildDescription($input): string - { - $invoice_numbers = ''; - - foreach ($input['invoices'] as $invoice) { - $invoice_numbers .= $invoice->number.' '; - } - - return ctrans('texts.invoice_number').": {$invoice_numbers}"; - } - - private function buildTransactionId($input): string - { - return implode(',', $input['hashed_ids']); - } - - private function paymentItems($input): array - { - $items = []; - $total = 0; - - foreach ($input['invoices'] as $invoice) { - foreach ($invoice->line_items as $invoiceItem) { - // Some gateways require quantity is an integer - if (floatval($invoiceItem->quantity) != intval($invoiceItem->quantity)) { - return null; - } - - $item = new Item([ - 'name' => $invoiceItem->product_key, - 'description' => substr($invoiceItem->notes, 0, 100), - 'price' => $invoiceItem->cost, - 'quantity' => $invoiceItem->quantity, - ]); - - $items[] = $item; - - $total += $invoiceItem->cost * $invoiceItem->quantity; - } - } - - if ($total != $input['amount_with_fee']) { - $item = new Item([ - 'name' => trans('texts.taxes_and_fees'), - 'description' => '', - 'price' => $input['amount_with_fee'] - $total, - 'quantity' => 1, - ]); - - $items[] = $item; - } - - return $items; - } - - public function createPayment($data, $status = Payment::STATUS_COMPLETED): Payment - { - $payment_meta = new stdClass; - $payment_meta->exp_month = 'xx'; - $payment_meta->exp_year = 'xx'; - $payment_meta->brand = 'PayPal'; - $payment_meta->last4 = 'xxxx'; - $payment_meta->type = GatewayType::PAYPAL; - - $payment = parent::createPayment($data, $status); - - $client_contact = $this->getContact(); - $client_contact_id = $client_contact ? $client_contact->id : null; - - $payment->amount = $data['PAYMENTINFO_0_AMT']; - $payment->type_id = PaymentType::PAYPAL; - $payment->transaction_reference = $data['PAYMENTINFO_0_TRANSACTIONID']; - $payment->client_contact_id = $client_contact_id; - $payment->meta = $payment_meta; - $payment->save(); - - return $payment; - } - - public function refund(Payment $payment, $amount) - { - $this->gateway(); - - $response = $this->gateway - ->refund(['transactionReference' => $payment->transaction_reference, 'amount' => $amount]) - ->send(); - - if ($response->isSuccessful()) { - SystemLogger::dispatch([ - 'server_response' => $response->getMessage(), 'data' => request()->all(), - ], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_PAYPAL, $this->client); - - return [ - 'transaction_reference' => $response->getData()['REFUNDTRANSACTIONID'], - 'transaction_response' => json_encode($response->getData()), - 'success' => true, - 'description' => $response->getData()['ACK'], - 'code' => $response->getCode(), - ]; - } - - SystemLogger::dispatch([ - 'server_response' => $response->getMessage(), 'data' => request()->all(), - ], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_PAYPAL, $this->client); - return [ - 'transaction_reference' => $response->getData()['CORRELATIONID'], - 'transaction_response' => json_encode($response->getData()), - 'success' => false, - 'description' => $response->getData()['L_LONGMESSAGE0'], - 'code' => $response->getData()['L_ERRORCODE0'], + 'currency' => $this->client->getCurrencyCode(), + 'transactionType' => 'Purchase', + 'clientIp' => request()->getClientIp(), + 'amount' => $data['amount_with_fee'], + 'returnUrl' => route('client.payments.response', [ + 'company_gateway_id' => $this->company_gateway->id, + 'payment_hash' => $this->payment_hash->hash, + 'payment_method_id' => GatewayType::PAYPAL, + ]), + 'cancelUrl' => $this->client->company->domain() . '/client/invoices', + 'description' => implode(',', collect($this->payment_hash->data->invoices) + ->map(function ($invoice) { + return sprintf('%s: %s', ctrans('texts.invoice_number'), $invoice->invoice_number); + })->toArray()), + 'transactionId' => $this->payment_hash->hash . '-' . time(), + 'ButtonSource' => 'InvoiceNinja_SP', + 'solutionType' => 'Sole', ]; } - /** - * Detach payment method from PayPal. - * - * @param ClientGatewayToken $token - * @return void - */ - public function detach(ClientGatewayToken $token) + public function generatePaymentItems(array $data) { - // PayPal doesn't support this feature. + $total = 0; + + $items = collect($this->payment_hash->data->invoices)->map(function ($i) use (&$total) { + $invoice = Invoice::findOrFail($this->decodePrimaryKey($i->invoice_id)); + + return collect($invoice->line_items)->map(function ($lineItem) use (&$total) { + if (floatval($lineItem->quantity) != intval($lineItem->quantity)) { + return null; + } + + $total += $lineItem->cost * $lineItem->quantity; + + return new Item([ + 'name' => $lineItem->product_key, + 'description' => substr($lineItem->notes, 0, 100), + 'price' => $lineItem->cost, + 'quantity' => $lineItem->quantity, + ]); + }); + }); + + if ($total != $data['amount_with_fee']) { + $items[0][] = new Item([ + 'name' => trans('texts.taxes_and_fees'), + 'description' => '', + 'price' => $data['amount_with_fee'] - $total, + 'quantity' => 1, + ]); + } + + return $items[0]->toArray(); } }