AJenbo/agcms

View on GitHub
application/inc/Http/Controllers/Payment.php

Summary

Maintainability
C
1 day
Test Coverage
A
91%
<?php

namespace App\Http\Controllers;

use App\Countries;
use App\Enums\InvoiceStatus;
use App\Exceptions\Exception;
use App\Exceptions\Handler as ExceptionHandler;
use App\Exceptions\InvalidInput;
use App\Http\Request;
use App\Models\CustomPage;
use App\Models\Email;
use App\Models\Invoice;
use App\Models\VolatilePage;
use App\Services\ConfigService;
use App\Services\EmailService;
use App\Services\EpaymentService;
use App\Services\InvoiceService;
use App\Services\OrmService;
use App\Services\RenderService;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

class Payment extends Base
{
    private InvoiceService $invoiceService;

    /**
     * Initialize needed services.
     */
    public function __construct()
    {
        $this->invoiceService = new InvoiceService();
    }

    /**
     * Page for manually entering the id and checkid code.
     */
    public function index(Request $request): Response
    {
        $data = $this->basicPageData();
        $crumbs = $data['crumbs'] ?? null;
        if (!is_array($crumbs)) {
            $crumbs = [];
        }

        $renderable = new VolatilePage(_('Payment'), $request->getRequestUri());
        $crumbs[] = $renderable;
        $data['crumbs'] = $crumbs;
        $data['renderable'] = $renderable;
        $data['id'] = $request->get('id');
        $data['checkid'] = $request->get('checkid');
        $response = $this->render('payment-manual', $data);

        return $this->cachedResponse($response);
    }

    /**
     * Show the items in the shopping basket.
     */
    public function basket(Request $request, int $id, string $checkId): Response
    {
        $invoice = app(OrmService::class)->getOne(Invoice::class, $id);
        if ($redirect = $this->checkStatus($id, $checkId, $invoice)) {
            return $redirect;
        }
        if (!$invoice) {
            throw new InvalidInput('Invoice not found', Response::HTTP_NOT_FOUND);
        }

        $invoice->setStatus(InvoiceStatus::Locked)->save();

        $data = $this->basicPageData();
        $crumbs = $data['crumbs'] ?? null;
        if (!is_array($crumbs)) {
            $crumbs = [];
        }

        $renderable = new VolatilePage(_('Order #') . $id, $invoice->getLink());
        $crumbs[] = $renderable;
        $data['crumbs'] = $crumbs;
        $data['renderable'] = $renderable;
        $data['invoice'] = $invoice;

        $response = $this->render('payment-form0', $data);

        return $this->cachedResponse($response);
    }

    /**
     * Page for user to correct there contact info.
     */
    public function address(Request $request, int $id, string $checkId): Response
    {
        $invoice = app(OrmService::class)->getOne(Invoice::class, $id);
        if ($redirect = $this->checkStatus($id, $checkId, $invoice)) {
            return $redirect;
        }
        if (!$invoice) {
            throw new InvalidInput('Invoice not found', Response::HTTP_NOT_FOUND);
        }

        $data = $this->basicPageData();
        $crumbs = $data['crumbs'] ?? null;
        if (!is_array($crumbs)) {
            $crumbs = [];
        }

        $data['countries'] = Countries::getOrdered();
        $crumbs[] = new VolatilePage(_('Order #') . $id, $invoice->getLink());
        $renderable = new VolatilePage(_('Address'), $invoice->getLink() . 'address/');
        $crumbs[] = $renderable;
        $data['crumbs'] = $crumbs;
        $data['renderable'] = $renderable;
        $data['newsletter'] = $request->query->getBoolean('newsletter');
        $data['invoice'] = $invoice;
        $data['invalid'] = $invoice->getInvalid();
        $data['action'] = $invoice->getLink() . 'address/';
        $data['actionLable'] = _('Continue');

        $response = $this->render('order-form1', $data);

        return $this->cachedResponse($response);
    }

    /**
     * Update the contact infor and forwared the user in the payment process.
     */
    public function addressSave(Request $request, int $id, string $checkId): Response
    {
        $invoice = app(OrmService::class)->getOne(Invoice::class, $id);
        if ($redirect = $this->checkStatus($id, $checkId, $invoice)) {
            return $redirect;
        }
        if (!$invoice) {
            throw new InvalidInput('Invoice not found', Response::HTTP_NOT_FOUND);
        }

        $data = $request->request->all();
        $data = $this->invoiceService->cleanAddressData($data);

        $invoice->setStatus(InvoiceStatus::Locked)
            ->setName(strval($data['name']))
            ->setAttn(strval($data['attn']))
            ->setAddress(strval($data['address']))
            ->setPostbox(strval($data['postbox']))
            ->setPostcode(strval($data['postcode']))
            ->setCity(strval($data['city']))
            ->setCountry(strval($data['country']))
            ->setEmail(strval($data['email']))
            ->setPhone1(strval($data['phone1']))
            ->setPhone2(strval($data['phone2']))
            ->setHasShippingAddress(intval($data['has_shipping_address']) === 1)
            ->setShippingPhone(strval($data['shipping_phone']))
            ->setShippingName(strval($data['shipping_name']))
            ->setShippingAttn(strval($data['shipping_attn']))
            ->setShippingAddress(strval($data['shipping_address']))
            ->setShippingAddress2(strval($data['shipping_address2']))
            ->setShippingPostbox(strval($data['shipping_postbox']))
            ->setShippingPostcode(strval($data['shipping_postcode']))
            ->setShippingCity(strval($data['shipping_city']))
            ->setShippingCountry(strval($data['shipping_country']))
            ->save();

        if ($invoice->getInvalid()) {
            if ($request->request->getBoolean('newsletter')) {
                return redirect($invoice->getLink() . 'address/?newsletter=1', Response::HTTP_SEE_OTHER);
            }

            return redirect($invoice->getLink() . 'address/', Response::HTTP_SEE_OTHER);
        }

        if ($request->request->getBoolean('newsletter')) {
            $this->invoiceService->addToAddressBook($invoice, $request->getClientIp());
        }

        return redirect($invoice->getLink() . 'terms/', Response::HTTP_SEE_OTHER);
    }

    /**
     * Show the terms of condition.
     */
    public function terms(Request $request, int $id, string $checkId): Response
    {
        $invoice = app(OrmService::class)->getOne(Invoice::class, $id);
        if ($redirect = $this->checkStatus($id, $checkId, $invoice)) {
            return $redirect;
        }
        if (!$invoice) {
            throw new InvalidInput('Invoice not found', Response::HTTP_NOT_FOUND);
        }

        $invoice->setStatus(InvoiceStatus::Locked)->save();

        $data = $this->basicPageData();
        $crumbs = $data['crumbs'] ?? null;
        if (!is_array($crumbs)) {
            $crumbs = [];
        }

        $crumbs[] = new VolatilePage(_('Order #') . $id, $invoice->getLink());
        $crumbs[] = new VolatilePage(_('Address'), $invoice->getLink() . 'address/');
        $renderable = new VolatilePage(_('Trade Conditions'), $invoice->getLink() . 'terms/');
        $crumbs[] = $renderable;
        $data['crumbs'] = $crumbs;
        $data['renderable'] = $renderable;

        $inputs = [
            'group'          => ConfigService::getString('pbsfix'),
            'merchantnumber' => ConfigService::getString('pbsid'),
            'orderid'        => ConfigService::getString('pbsfix') . $invoice->getId(),
            'currency'       => 208,
            'amount'         => number_format($invoice->getAmount(), 2, '', ''),
            'ownreceipt'     => 1,
            'accepturl'      => $invoice->getLink() . 'status/',
            'cancelurl'      => $invoice->getLink() . 'terms/',
            'callbackurl'    => $invoice->getLink() . 'callback/',
            'windowstate'    => 3,
            'windowid'       => ConfigService::getInt('pbswindow'),
        ];
        $inputs['hash'] = md5(implode('', $inputs) . ConfigService::getString('pbspassword'));
        $data['inputs'] = $inputs;
        $data['html'] = $this->getTermsHtml();

        $response = $this->render('payment-form2', $data);

        return $this->cachedResponse($response);
    }

    /**
     * Get the terms and conditions text.
     */
    public function getTermsHtml(): string
    {
        $shoppingTerms = app(OrmService::class)->getOne(CustomPage::class, 3);
        if (!$shoppingTerms) {
            app(ExceptionHandler::class)->report(new Exception(_('Missing terms and conditions')));

            return '';
        }

        return $shoppingTerms->getHtml();
    }

    /**
     * Show the order status page.
     *
     * Also set the order payment status if txnid is provided
     */
    public function status(Request $request, int $id, string $checkId): Response
    {
        $invoice = app(OrmService::class)->getOne(Invoice::class, $id);
        if (!$invoice || $checkId !== $invoice->getCheckId()) {
            return redirect('/betaling/?id=' . $id . '&checkid=' . rawurlencode($checkId), Response::HTTP_SEE_OTHER);
        }

        if (!$invoice->isHandled() && InvoiceStatus::PbsOk !== $invoice->getStatus() && !$request->query->has('txnid')) {
            return redirect($invoice->getLink(), Response::HTTP_SEE_OTHER);
        }

        if ($request->query->has('txnid')) {
            if (!$this->isHashValid($request)) {
                return redirect($invoice->getLink(), Response::HTTP_SEE_OTHER);
            }

            $this->setPaymentStatus($request, $invoice);
        }

        $data = $this->basicPageData();
        $crumbs = $data['crumbs'] ?? null;
        if (!is_array($crumbs)) {
            $crumbs = [];
        }

        $crumbs[] = new VolatilePage(_('Order #') . $id, $invoice->getLink());
        $crumbs[] = new VolatilePage(_('Address'), $invoice->getLink() . 'address/');
        $crumbs[] = new VolatilePage(_('Trade Conditions'), $invoice->getLink() . 'terms/');
        $renderable = new VolatilePage(_('Receipt'), $invoice->getLink() . 'status/');
        $crumbs[] = $renderable;
        $data['crumbs'] = $crumbs;
        $data['renderable'] = $renderable;
        $data['newsletter'] = $request->query->getBoolean('newsletter');
        $data['invoice'] = $invoice;
        $data['invalid'] = $invoice->getInvalid();
        $data['action'] = $invoice->getLink() . 'address/';
        $data['statusMessage'] = $this->getStatusMessage($invoice);

        $response = $this->render('payment-status', $data);

        return $this->cachedResponse($response);
    }

    /**
     * Get the status message.
     *
     * @throws Exception
     */
    private function getStatusMessage(Invoice $invoice): string
    {
        switch ($invoice->getStatus()) {
            case InvoiceStatus::PbsOk:
                return _('Payment is now accepted. We will send your goods by mail as soon as possible.')
                    . '<br />' . _('A copy of your order has been sent to your email.');
            case InvoiceStatus::Canceled:
                return _('The transaction is canceled.');
            case InvoiceStatus::Giro:
                return _('The payment has already been received via giro.');
            case InvoiceStatus::Cash:
                return _('The payment has already been received in cash.');
            case InvoiceStatus::Accepted:
                return _('The payment was received and the package is sent.');
        }

        throw new Exception(_('Unknown status.'));
    }

    /**
     * Set the order payment status.
     */
    public function callback(Request $request, int $id, string $checkId): Response
    {
        $invoice = app(OrmService::class)->getOne(Invoice::class, $id);
        if (!$invoice
            || $checkId !== $invoice->getCheckId()
            || !$this->isHashValid($request)
        ) {
            throw new InvalidInput('', Response::HTTP_BAD_REQUEST);
        }

        $this->setPaymentStatus($request, $invoice);

        return new Response();
    }

    /**
     * Validate payment hash.
     */
    private function isHashValid(Request $request): bool
    {
        $params = $request->query->all();
        unset($params['hash']);

        $eKey = md5(implode('', $params) . ConfigService::getString('pbspassword'));

        return $eKey === $request->get('hash');
    }

    /**
     * Set the order payment status.
     */
    private function setPaymentStatus(Request $request, Invoice $invoice): void
    {
        if ($invoice->isHandled() || InvoiceStatus::PbsOk === $invoice->getStatus()) {
            return;
        }

        $cardType = EpaymentService::getPaymentName(intval($request->get('paymenttype')));
        $internalNote = $this->generateInternalPaymentNote($request);

        if (!app(EmailService::class)->valideMail($invoice->getDepartment())) {
            $invoice->setDepartment(ConfigService::getDefaultEmail());
        }

        $invoice->setCardtype($cardType)
            ->setInternalNote(trim($invoice->getInternalNote() . "\n" . $internalNote))
            ->setStatus(InvoiceStatus::PbsOk)
            ->setTimeStampPay(time())
            ->save();

        $this->sendCustomerEmail($invoice);
        $this->sendAdminEmail($invoice);
    }

    /**
     * Send recipt to the customer.
     */
    private function sendCustomerEmail(Invoice $invoice): void
    {
        $data = [
            'invoice'    => $invoice,
            'siteName'   => ConfigService::getString('site_name'),
            'address'    => ConfigService::getString('address'),
            'postcode'   => ConfigService::getString('postcode'),
            'city'       => ConfigService::getString('city'),
            'phone'      => ConfigService::getString('phone'),
        ];
        $email = new Email([
            'subject'          => sprintf(_('Order #%d - payment completed'), $invoice->getId()),
            'body'             => app(RenderService::class)->render('email/payment-confirmation', $data),
            'senderName'       => ConfigService::getString('site_name'),
            'senderAddress'    => $invoice->getDepartment(),
            'recipientName'    => $invoice->getName(),
            'recipientAddress' => $invoice->getEmail(),
        ]);

        try {
            app(EmailService::class)->send($email);
        } catch (Throwable $exception) {
            app(ExceptionHandler::class)->report($exception);
            $email->save();
        }
    }

    /**
     * Send status email to the admin.
     */
    private function sendAdminEmail(Invoice $invoice): void
    {
        $subject = sprintf(
            _('Attn.: %s - Payment received for invoice #%d'),
            $invoice->getClerk(),
            $invoice->getId()
        );

        $emailBody = app(RenderService::class)->render(
            'admin/email/payment-confirmation',
            ['invoice' => $invoice]
        );

        $email = new Email([
            'subject'          => $subject,
            'body'             => $emailBody,
            'senderName'       => ConfigService::getString('site_name'),
            'senderAddress'    => $invoice->getDepartment(),
            'recipientName'    => ConfigService::getString('site_name'),
            'recipientAddress' => $invoice->getDepartment(),
        ]);

        try {
            app(EmailService::class)->send($email);
        } catch (Throwable $exception) {
            app(ExceptionHandler::class)->report($exception);
            $email->save();
        }
    }

    /**
     * Generate message for the internal note about fraud status.
     */
    private function generateInternalPaymentNote(Request $request): string
    {
        $internalNote = '';
        if ($request->get('fraud')) {
            $internalNote .= _('Possible payment fraud.') . "\n";
        }
        if ($request->get('cardno')) {
            $internalNote .= _('Credit card no.: ') . $request->get('cardno') . "\n";
        }

        $countries = Countries::getOrdered();
        if ($request->get('issuercountry')) {
            $internalNote .= _('Card is from: ') . $countries[$request->get('issuercountry')] . "\n";
        }
        if ($request->get('payercountry')) {
            $internalNote .= _('Payment was made from: ') . $countries[$request->get('payercountry')] . "\n";
        }

        return trim($internalNote);
    }

    /**
     * Check if request should be redirected to a different page in the process.
     */
    private function checkStatus(int $id, string $checkId, ?Invoice $invoice): ?RedirectResponse
    {
        if (!$invoice || $checkId !== $invoice->getCheckId()) {
            return redirect('/betaling/?id=' . $id . '&checkid=' . rawurlencode($checkId), Response::HTTP_SEE_OTHER);
        }
        if ($invoice->isHandled() || InvoiceStatus::PbsOk === $invoice->getStatus()) {
            return redirect($invoice->getLink() . 'status/', Response::HTTP_SEE_OTHER);
        }

        return null;
    }
}