Skip to content
Code-Schnipsel Gruppen Projekte
Subscriptions.php 29 KiB
Newer Older
Patrick Müller's avatar
Patrick Müller committed
<?php

namespace QUI\ERP\Payments\Stripe\PaymentMethods\Recurring;

use QUI;
use QUI\ERP\Order\AbstractOrder;
use QUI\ERP\Accounting\Invoice\Invoice;
use QUI\Utils\Security\Orthos;
use QUI\ERP\Accounting\Payments\Transactions\Factory as TransactionFactory;
use QUI\ERP\Accounting\Invoice\Handler as InvoiceHandler;
use QUI\ERP\Accounting\Payments\Payments;
use QUI\ERP\Accounting\Payments\Transactions\Handler as TransactionHandler;
use QUI\ERP\Payments\Stripe\AbstractBasePayment;
use Stripe\Exception\ApiErrorException;
Patrick Müller's avatar
Patrick Müller committed
use Stripe\Subscription as StripeSubscription;
use Stripe\Customer as StripeCustomer;
use Stripe\Invoice as StripeInvoice;
use Stripe\PaymentIntent as StripePaymentIntent;
Patrick Müller's avatar
Patrick Müller committed
use QUI\ERP\Payments\Stripe\Provider;
Patrick Müller's avatar
Patrick Müller committed
use QUI\ERP\Payments\Stripe\Utils;
Patrick Müller's avatar
Patrick Müller committed

/**
 * Class Subscriptions
 *
 * Handler for Stripe subscriptions
 */
class Subscriptions
{
    /**
     * Runtime cache that knows then a transaction history
     * for a Subscriptios has been freshly fetched from Paymill.
     *
     * Prevents multiple unnecessary API calls.
     *
     * @var array
     */
    protected static $transactionsRefreshed = [];

    /**
     * Create a Stripe subscription form an order
     *
     * @param AbstractOrder $Order
     * @return int|false - Subscription ID
Patrick Müller's avatar
Patrick Müller committed
     *
     * @throws \Exception
     */
    public static function createSubscription(AbstractOrder $Order)
    {
        $SystemUser = QUI::getUsers()->getSystemUser();

        if ($Order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID)) {
            $confirmData = self::confirmSubscription($Order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID));

            switch ($confirmData['status']) {
                /**
                 * If the Subscription could not be fully finalized and is expired ->
                 * delete it from the Order and create a new Subscription.
                 */
                case StripeSubscription::STATUS_INCOMPLETE_EXPIRED:
                    $Order->setPaymentData(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID, false);
                    $Order->update($SystemUser);
                    break;

                default:
                    return $Order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID);
Patrick Müller's avatar
Patrick Müller committed
        }

        // Create Stripe billing plan
        $billingPlanId = BillingPlans::createBillingPlanFromOrder($Order);

        $Order->addHistory(QUI::getLocale()->get('quiqqer/payment-stripe', 'order.offer_created', [
Patrick Müller's avatar
Patrick Müller committed
            'billingPlanId' => $billingPlanId
        ]));

        $Order->setPaymentData(AbstractBasePayment::ATTR_STRIPE_BILLING_PLAN_ID, $billingPlanId);
        $Order->update($SystemUser);
Patrick Müller's avatar
Patrick Müller committed

        $paymentMethodId = $Order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_PAYMENT_METHOD_ID);
Patrick Müller's avatar
Patrick Müller committed

        if (empty($paymentMethodId)) {
            return false;
Patrick Müller's avatar
Patrick Müller committed
        }

        // Create Stripe customer for subscription
        $Customer       = $Order->getCustomer();
        $StripeCustomer = self::createStripeCustomerFromQuiqqerUser($Customer);
        Utils::attachPaymentMethodToStripeCustomer($Customer, $paymentMethodId);
Patrick Müller's avatar
Patrick Müller committed

        // Create Stripe subscription
        $StripeSubscription = StripeSubscription::create([
            'customer'               => $StripeCustomer->id,
            'items'                  => [
Patrick Müller's avatar
Patrick Müller committed
                [
                    'plan' => $billingPlanId
                ]
            'payment_behavior'       => 'allow_incomplete',
            'default_payment_method' => $paymentMethodId
Patrick Müller's avatar
Patrick Müller committed
        ]);

Patrick Müller's avatar
Patrick Müller committed
        try {
            $Order->addHistory(QUI::getLocale()->get('quiqqer/payment-stripe', 'history.order.subscription_created', [
                'subscriptionId' => $StripeSubscription->id
            ]));
Patrick Müller's avatar
Patrick Müller committed

            $Order->setPaymentData(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID, $StripeSubscription->id);
            $Order->setPaymentData(AbstractBasePayment::ATTR_STRIPE_ORDER_SUCCESSFUL, true);
Patrick Müller's avatar
Patrick Müller committed
            $Order->update(QUI::getUsers()->getSystemUser());
Patrick Müller's avatar
Patrick Müller committed

Patrick Müller's avatar
Patrick Müller committed
            // Write subscription data to db
            QUI::getDataBase()->insert(
                Provider::getStripeBillingSubscriptionsTable(),
                [
                    'stripe_id'              => $StripeSubscription->id,
                    'stripe_billing_plan_id' => $billingPlanId,
                    'stripe_customer_id'     => $StripeCustomer->id,
Patrick Müller's avatar
Patrick Müller committed
                    'customer'               => json_encode($Order->getCustomer()->getAttributes()),
                    'global_process_id'      => $Order->getHash(),
                    'active'                 => 1
                ]
            );
        } catch (\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
        }

        return $StripeSubscription->id;
    }

    /**
     * Confirm subscription status (was creation and payment successful?)
     *
     * @param string $subscriptionId
     * @return array
     */
    public static function confirmSubscription($subscriptionId)
    {
        Provider::setupApi();

        try {
            $StripeSubscription = StripeSubscription::retrieve([
                'id'     => $subscriptionId,
                'expand' => [
                    'latest_invoice.payment_intent'
                ]
            ]);
        } catch (\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
Patrick Müller's avatar
Patrick Müller committed
            return ['status' => 'api_error'];
        }

        $data = [
            'status' => $StripeSubscription->status
        ];

        switch ($StripeSubscription->status) {
            case StripeSubscription::STATUS_INCOMPLETE:
                // check status of latest (first) Subscription Invoice
                switch ($StripeSubscription->latest_invoice->payment_intent->status) {
                    case StripePaymentIntent::STATUS_REQUIRES_ACTION:
                        $data['status']       = 'action_required';
                        $data['clientSecret'] = $StripeSubscription->latest_invoice->payment_intent->client_secret;
                        break;

                    case StripePaymentIntent::STATUS_REQUIRES_PAYMENT_METHOD:
                        $data['status'] = 'retry_payment_method';
                        break;

                    default:
                        $data['status'] = 'error';
                }
                break;
        }

        return $data;
Patrick Müller's avatar
Patrick Müller committed
    }

    /**
     * Get ID of latest Stripe Invoice of a Stripe Subscription
     *
     * @param string $subscriptionId
     * @return string - Stripe Invoice ID
     * @throws ApiErrorException
     */
    public static function getLatestSubscriptionInvoiceId($subscriptionId)
    {
        Provider::setupApi();

        $StripeSubscription = StripeSubscription::retrieve([
            'id'     => $subscriptionId,
            'expand' => [
                'latest_invoice'
            ]
        ]);

        return $StripeSubscription->latest_invoice->id;
    }

Patrick Müller's avatar
Patrick Müller committed
    /**
     * Bills the balance for an agreement based on an Invoice
     *
     * @param Invoice $Invoice
     * @return void
     *
     * @throws QUI\ERP\Payments\Stripe\StripeException
     * @throws QUI\Exception
     * @throws ApiErrorException
Patrick Müller's avatar
Patrick Müller committed
     */
    public static function billSubscriptionBalance(Invoice $Invoice)
    {
        $subscriptionId = $Invoice->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID);
Patrick Müller's avatar
Patrick Müller committed

        if (empty($subscriptionId)) {
            throw new QUI\ERP\Payments\Stripe\StripeException(
Patrick Müller's avatar
Patrick Müller committed
                QUI::getLocale()->get(
                    'quiqqer/payment-stripe',
Patrick Müller's avatar
Patrick Müller committed
                    'exception.Recurring.Subscriptions.subscription_id_not_found',
                    [
                        'invoiceId' => $Invoice->getId()
                    ]
                ),
                404
            );
        }
//
//        $data = self::getSubscriptionData($subscriptionId);
//
//        if (empty($data)) {
//            $data = self::getSubscriptionDetails($subscriptionId);
//        }
Patrick Müller's avatar
Patrick Müller committed

        // Check if a Paymill transaction matches the Invoice
        $unprocessedTransactions = self::getUnprocessedTransactions($subscriptionId);
        $Invoice->calculatePayments();

Patrick Müller's avatar
Patrick Müller committed
        $invoiceAmount   = Utils::getCentAmount($Invoice);
Patrick Müller's avatar
Patrick Müller committed
        $invoiceCurrency = $Invoice->getCurrency()->getCode();

        /** @var AbstractBaseRecurringPayment $Payment */
        $paymentMethodData = \json_decode($Invoice->getAttribute('payment_method_data'), true);
        $Payment           = Payments::getInstance()->getPayment($paymentMethodData['id'])->getPaymentType();
Patrick Müller's avatar
Patrick Müller committed

        foreach ($unprocessedTransactions as $transaction) {
            $amountDue = (int)$transaction['amount_due'];
            $currency  = \mb_strtoupper($transaction['currency']);
Patrick Müller's avatar
Patrick Müller committed

            if ($currency !== $invoiceCurrency) {
                continue;
            }

            if ($amountDue < $invoiceAmount) {
Patrick Müller's avatar
Patrick Müller committed
                continue;
            }

            // Transaction amount equals Invoice amount
            try {
                $InvoiceTransaction = TransactionFactory::createPaymentTransaction(
                    $amountDue,
Patrick Müller's avatar
Patrick Müller committed
                    $Invoice->getCurrency(),
                    $Invoice->getHash(),
                    $Payment->getName(),
                    [
                        AbstractBasePayment::ATTR_STRIPE_INVOICE_ID => $transaction['id']
Patrick Müller's avatar
Patrick Müller committed
                    ],
                    null,
                    false,
Patrick Müller's avatar
Patrick Müller committed
                    $Invoice->getGlobalProcessId()
                );

                $Invoice->addTransaction($InvoiceTransaction);

                QUI::getDataBase()->update(
                    Provider::getStripeBillingSubscriptionsTransactionsTable(),
Patrick Müller's avatar
Patrick Müller committed
                    [
                        'quiqqer_transaction_id'        => $InvoiceTransaction->getTxId(),
                        'quiqqer_transaction_completed' => 1
                    ],
                    [
                        'stripe_invoice_id' => $transaction['id']
Patrick Müller's avatar
Patrick Müller committed
                    ]
                );

                $Invoice->addHistory(
                    QUI::getLocale()->get(
                        'quiqqer/payment-stripe',
                        'history.Invoice.add_stripe_transaction',
                        [
                            'quiqqerTransactionId' => $InvoiceTransaction->getTxId(),
                            'stripeTransactionId'  => $transaction['id']
                        ]
                    )
Patrick Müller's avatar
Patrick Müller committed
                );
            } catch (\Exception $Exception) {
                QUI\System\Log::writeException($Exception);
            }

            break;
        }
    }

    /**
     * Get data of all Stripe Subscriptions (QUIQQER data only; no Stripe query performed!)
Patrick Müller's avatar
Patrick Müller committed
     *
     * @param array $searchParams
     * @param bool $countOnly (optional) - Return count of all results
     * @return array|int
     */
    public static function getSubscriptionList($searchParams, $countOnly = false)
    {
        $Grid       = new QUI\Utils\Grid($searchParams);
        $gridParams = $Grid->parseDBParams($searchParams);

        $binds = [];
        $where = [];

        if ($countOnly) {
            $sql = "SELECT COUNT(id)";
Patrick Müller's avatar
Patrick Müller committed
        } else {
            $sql = "SELECT *";
        }

        $sql .= " FROM `".Provider::getStripeBillingSubscriptionsTable()."`";
Patrick Müller's avatar
Patrick Müller committed

        if (!empty($searchParams['search'])) {
            $where[] = '`global_process_id` LIKE :search';

            $binds['search'] = [
                'value' => '%'.$searchParams['search'].'%',
                'type'  => \PDO::PARAM_STR
            ];
        }

        // build WHERE query string
        if (!empty($where)) {
            $sql .= " WHERE ".implode(" AND ", $where);
        }

        // ORDER
        if (!empty($searchParams['sortOn'])
        ) {
            $sortOn = Orthos::clear($searchParams['sortOn']);
            $order  = "ORDER BY ".$sortOn;

            if (isset($searchParams['sortBy']) &&
                !empty($searchParams['sortBy'])
            ) {
                $order .= " ".Orthos::clear($searchParams['sortBy']);
            } else {
                $order .= " ASC";
            }

            $sql .= " ".$order;
        }

        // LIMIT
        if (!empty($gridParams['limit'])
            && !$countOnly
        ) {
            $sql .= " LIMIT ".$gridParams['limit'];
        } else {
            if (!$countOnly) {
                $sql .= " LIMIT ".(int)20;
            }
        }

        $Stmt = QUI::getPDO()->prepare($sql);

        // bind search values
        foreach ($binds as $var => $bind) {
            $Stmt->bindValue(':'.$var, $bind['value'], $bind['type']);
        }

        try {
            $Stmt->execute();
            $result = $Stmt->fetchAll(\PDO::FETCH_ASSOC);
        } catch (\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
            return [];
        }

        if ($countOnly) {
            return (int)current(current($result));
        }

        return $result;
    }

    /**
     * Get details of a Subscription (Stripe data)
Patrick Müller's avatar
Patrick Müller committed
     *
     * @param string $subscriptionId
     * @return array
     * @throws ApiErrorException
Patrick Müller's avatar
Patrick Müller committed
     */
    public static function getSubscriptionDetails($subscriptionId)
    {
        return StripeSubscription::retrieve($subscriptionId)->toArray();
Patrick Müller's avatar
Patrick Müller committed
    }

    /**
     * Cancel a Subscription
     *
     * @param int|string $subscriptionId
     * @param string $reason (optional) - The reason why the billing agreement is being cancelled
     * @return void
     * @throws QUI\ERP\Payments\Stripe\StripeException
     * @throws ApiErrorException
Patrick Müller's avatar
Patrick Müller committed
     */
    public static function cancelSubscription($subscriptionId, $reason = '')
    {
        $data = self::getSubscriptionData($subscriptionId);

        if (empty($data)) {
Patrick Müller's avatar
Patrick Müller committed
            throw new QUI\ERP\Payments\Stripe\StripeException([
                'quiqqer/payment-stripe',
                'exception.PaymentMethods.Recurring.Subscriptions.cancelSubscription.no_data',
                [
                    'subscriptionId' => $subscriptionId
                ]
            ]);
Patrick Müller's avatar
Patrick Müller committed
        }

        try {
            $Locale = new QUI\Locale();
            $Locale->setCurrent($data['customer']['lang']);
        } catch (\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
            return;
        }

        Provider::setupApi();
Patrick Müller's avatar
Patrick Müller committed

        $Subscription = StripeSubscription::retrieve($subscriptionId);
        $Subscription->cancel();
Patrick Müller's avatar
Patrick Müller committed

        // Set status in QUIQQER database to "not active"
        self::setSubscriptionAsInactive($subscriptionId);
    }

    /**
     * Set a subscription as inactive
     *
     * @param string $subscriptionId - Stripe Subscription ID
Patrick Müller's avatar
Patrick Müller committed
     * @return void
     */
    public static function setSubscriptionAsInactive($subscriptionId)
    {
        try {
            QUI::getDataBase()->update(
                Provider::getStripeBillingSubscriptionsTable(),
Patrick Müller's avatar
Patrick Müller committed
                [
                    'active' => 0
                ],
                [
                    'stripe_id' => $subscriptionId
Patrick Müller's avatar
Patrick Müller committed
                ]
            );
        } catch (\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
        }
    }

    /**
     * Fetches subscription transactions (invoices) from Stripe
Patrick Müller's avatar
Patrick Müller committed
     *
     * @param string $subscriptionId - Stripe Subscription ID
     * @param string $startAfterInvoiceId (optional) - Only fetch invoices after this Stripe Invoice ID
Patrick Müller's avatar
Patrick Müller committed
     * @return array
     *
     * @throws ApiErrorException
Patrick Müller's avatar
Patrick Müller committed
     */
    public static function getSubscriptionTransactions($subscriptionId, $startAfterInvoiceId = null)
    {
        $searchParams = [
            'limit'        => 100,
            'subscription' => $subscriptionId
        ];
Patrick Müller's avatar
Patrick Müller committed

//        if ($startAfterInvoiceId) {
//            $searchParams['starting_after'] = $startAfterInvoiceId;
//        }

        $result       = StripeInvoice::all($searchParams);
        $transactions = [];

        foreach ($result->autoPagingIterator() as $Transaction) {
            $transactions[] = $Transaction->toArray();
Patrick Müller's avatar
Patrick Müller committed
        }

        return $result['data'];
Patrick Müller's avatar
Patrick Müller committed
    }

    /**
     * Process all unpaid Invoices of Subscriptions
     *
     * @return void
     */
    public static function processUnpaidInvoices()
    {
        Provider::setupApi();

Patrick Müller's avatar
Patrick Müller committed
        $Invoices = InvoiceHandler::getInstance();

        // Determine payment type IDs
        $payments = Payments::getInstance()->getPayments([
            'select' => ['id'],
            'where'  => [
                'payment_type' => [
                    'type'  => 'IN',
                    'value' => Provider::getRecurringPaymentTypes()
                ]
Patrick Müller's avatar
Patrick Müller committed
        ]);

        $paymentTypeIds = [];

        /** @var QUI\ERP\Accounting\Payments\Types\Payment $Payment */
        foreach ($payments as $Payment) {
            $paymentTypeIds[] = $Payment->getId();
        }

        if (empty($paymentTypeIds)) {
            return;
        }

        // Get all unpaid Invoices
        $result = $Invoices->search([
            'select' => ['id', 'global_process_id'],
            'where'  => [
                'paid_status'    => 0,
                'type'           => InvoiceHandler::TYPE_INVOICE,
                'payment_method' => [
                    'type'  => 'IN',
                    'value' => $paymentTypeIds
                ]
            ],
            'order'  => 'date ASC'
Patrick Müller's avatar
Patrick Müller committed
        ]);

        $invoiceIds = [];

        foreach ($result as $row) {
            $globalProcessId = $row['global_process_id'];

            if (!isset($invoiceIds[$globalProcessId])) {
                $invoiceIds[$globalProcessId] = [];
            }

            $invoiceIds[$globalProcessId][] = $row['id'];
        }

        if (empty($invoiceIds)) {
            return;
        }

        // Determine relevant Subscriptions
        try {
            $result = QUI::getDataBase()->fetch([
                'select' => ['global_process_id'],
                'from'   => Provider::getStripeBillingSubscriptionsTable(),
Patrick Müller's avatar
Patrick Müller committed
                'where'  => [
                    'global_process_id' => [
                        'type'  => 'IN',
                        'value' => \array_keys($invoiceIds)
Patrick Müller's avatar
Patrick Müller committed
                    ]
                ]
            ]);
        } catch (\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
            return;
        }

        // Refresh Billing Agreement transactions
        foreach ($result as $row) {
            // Handle invoices
            foreach ($invoiceIds as $globalProcessId => $invoices) {
                if ($row['global_process_id'] !== $globalProcessId) {
                    continue;
                }

                foreach ($invoices as $invoiceId) {
                    try {
                        $Invoice = $Invoices->get($invoiceId);

                        // First: Process all failed transactions for Invoice
                        self::processDeniedTransactions($Invoice);

                        // Second: Process all completed transactions for Invoice
                        self::billSubscriptionBalance($Invoice);
                    } catch (\Exception $Exception) {
                        QUI\System\Log::writeException($Exception);
                    }
                }
            }
        }
    }

    /**
     * Processes all denied Paymill transactions for an Invoice and create corresponding ERP Transactions
     *
     * @param Invoice $Invoice
     * @return void
     *
     * @throws \Exception
Patrick Müller's avatar
Patrick Müller committed
     */
    public static function processDeniedTransactions(Invoice $Invoice)
    {
        $subscriptionId = $Invoice->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID);
Patrick Müller's avatar
Patrick Müller committed

        if (empty($subscriptionId)) {
            return;
        }

        $data = self::getSubscriptionData($subscriptionId);

        if (empty($data)) {
            return;
        }

        // Get all "uncollectible" Stripe transactions (invoices)
Patrick Müller's avatar
Patrick Müller committed
        try {
            $unprocessedTransactions = self::getUnprocessedTransactions(
                $subscriptionId,
                StripeInvoice::STATUS_UNCOLLECTIBLE
            );
Patrick Müller's avatar
Patrick Müller committed
        } catch (\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
            return;
        }

        try {
            $Invoice->calculatePayments();

Patrick Müller's avatar
Patrick Müller committed
            $invoiceAmount   = Utils::getCentAmount($Invoice);
Patrick Müller's avatar
Patrick Müller committed
            $invoiceCurrency = $Invoice->getCurrency()->getCode();
        } catch (\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
            return;
        }

        /** @var AbstractBaseRecurringPayment $Payment */
        $paymentMethodData = \json_decode($Invoice->getAttribute('payment_method_data'), true);
        $Payment           = Payments::getInstance()->getPayment($paymentMethodData['id'])->getPaymentType();
Patrick Müller's avatar
Patrick Müller committed

        foreach ($unprocessedTransactions as $transaction) {
            $amountDue = (int)$transaction['amount_due'];
            $currency  = \mb_strtoupper($transaction['currency']);
Patrick Müller's avatar
Patrick Müller committed

            if ($currency !== $invoiceCurrency) {
                continue;
            }

            if ($amountDue < $invoiceAmount) {
Patrick Müller's avatar
Patrick Müller committed
                continue;
            }

            // Transaction amount equals Invoice amount
            try {
                $InvoiceTransaction = TransactionFactory::createPaymentTransaction(
                    $amountDue,
Patrick Müller's avatar
Patrick Müller committed
                    $Invoice->getCurrency(),
                    $Invoice->getHash(),
                    $Payment->getName(),
                    [],
                    null,
                    false,
                    $Invoice->getGlobalProcessId()
                );

                $InvoiceTransaction->changeStatus(TransactionHandler::STATUS_ERROR);

                $Invoice->addTransaction($InvoiceTransaction);

                QUI::getDataBase()->update(
                    Provider::getStripeBillingSubscriptionsTransactionsTable(),
Patrick Müller's avatar
Patrick Müller committed
                    [
                        'quiqqer_transaction_id'        => $InvoiceTransaction->getTxId(),
                        'quiqqer_transaction_completed' => 1
                    ],
                    [
                        'stripe_invoice_id' => $transaction['id']
Patrick Müller's avatar
Patrick Müller committed
                    ]
                );

                $Invoice->addHistory(
                    QUI::getLocale()->get(
                        'quiqqer/payment-stripe',
                        'history.Invoice.add_stripe_transaction',
                        [
                            'quiqqerTransactionId' => $InvoiceTransaction->getTxId(),
                            'stripeTransactionId'  => $transaction['id']
                        ]
                    )
Patrick Müller's avatar
Patrick Müller committed
                );
            } catch (\Exception $Exception) {
                QUI\System\Log::writeException($Exception);
            }
        }
    }

    /**
     * Refreshes transactions for a Stripe Subscription
Patrick Müller's avatar
Patrick Müller committed
     *
     * @param string $subscriptionId
Patrick Müller's avatar
Patrick Müller committed
     * @throws QUI\Database\Exception
     * @throws ApiErrorException
Patrick Müller's avatar
Patrick Müller committed
     */
    protected static function refreshTransactionList($subscriptionId)
    {
        if (isset(self::$transactionsRefreshed[$subscriptionId])) {
            return;
        }

        // Get global process id
        $data            = self::getSubscriptionData($subscriptionId);
        $globalProcessId = $data['global_process_id'];
Patrick Müller's avatar
Patrick Müller committed

        // Determine existing transactions
        $result = QUI::getDataBase()->fetch([
            'select' => ['stripe_invoice_id'],
            'from'   => Provider::getStripeBillingSubscriptionsTransactionsTable(),
Patrick Müller's avatar
Patrick Müller committed
            'where'  => [
                'stripe_subscription_id' => $subscriptionId
Patrick Müller's avatar
Patrick Müller committed
            ]
        ]);

        $existing = [];

        foreach ($result as $row) {
            $existing[$row['stripe_invoice_id']] = true;
Patrick Müller's avatar
Patrick Müller committed
        }

        // Parse NEW transactions
        $transactions = self::getSubscriptionTransactions($data['stripe_id']);
Patrick Müller's avatar
Patrick Müller committed

        foreach ($transactions as $transaction) {
            $TransactionTime = date_create('@'.$transaction['created']);

            if (empty($TransactionTime)) {
                \QUI\System\Log::addWarning(
                    'Transaction date for Stripe Invoice '.$transaction['id'].' could not be parsed.'
                    .' Please check manually.'
                );
Patrick Müller's avatar
Patrick Müller committed

                continue;
            }

            $transactionTime = $TransactionTime->format('Y-m-d H:i:s');

            if (isset($existing[$transaction['id']])) {
                QUI::getDataBase()->update(
                    Provider::getStripeBillingSubscriptionsTransactionsTable(),
                    [
                        'stripe_invoice_data' => \json_encode($transaction),
                    ],
                    [
                        'stripe_invoice_id' => $transaction['id'],
                    ]
                );
            } else {
                QUI::getDataBase()->insert(
                    Provider::getStripeBillingSubscriptionsTransactionsTable(),
                    [
                        'stripe_invoice_id'      => $transaction['id'],
                        'stripe_subscription_id' => $subscriptionId,
                        'stripe_invoice_data'    => \json_encode($transaction),
                        'stripe_invoice_date'    => $transactionTime,
                        'global_process_id'      => $globalProcessId
                    ]
                );
Patrick Müller's avatar
Patrick Müller committed
            }
        }

        self::$transactionsRefreshed[$subscriptionId] = true;
    }

    /**
     * Get all completed Subscription transactions that are unprocessed by QUIQQER ERP
     *
     * @param string $subscriptionId
     * @param string $status (optional) - Get transactions with this status [default: "paid"]
Patrick Müller's avatar
Patrick Müller committed
     * @return array
     * @throws QUI\Database\Exception
     * @throws \Exception
     */
    protected static function getUnprocessedTransactions($subscriptionId, $status = StripeInvoice::STATUS_PAID)
Patrick Müller's avatar
Patrick Müller committed
    {
        self::refreshTransactionList($subscriptionId);

Patrick Müller's avatar
Patrick Müller committed
        $result = QUI::getDataBase()->fetch([
            'select' => ['stripe_invoice_data'],
            'from'   => Provider::getStripeBillingSubscriptionsTransactionsTable(),
Patrick Müller's avatar
Patrick Müller committed
            'where'  => [
                'stripe_subscription_id' => $subscriptionId,
                'quiqqer_transaction_id' => null
Patrick Müller's avatar
Patrick Müller committed
            ]
        ]);

        $transactions = [];

        foreach ($result as $row) {
            $t = json_decode($row['stripe_invoice_data'], true);
Patrick Müller's avatar
Patrick Müller committed

            if ($t['status'] !== $status) {
                continue;
            }

            $transactions[] = $t;
        }

        return $transactions;
    }

    /**
     * Get available data by Subscription ID (QUIQQER DATA)
Patrick Müller's avatar
Patrick Müller committed
     *
     * @param string $subscriptionId - Stripe Subscription ID
Patrick Müller's avatar
Patrick Müller committed
     * @return array|false
     */
    protected static function getSubscriptionData($subscriptionId)
    {
        try {
            $result = QUI::getDataBase()->fetch([
                'from'  => Provider::getStripeBillingSubscriptionsTable(),
Patrick Müller's avatar
Patrick Müller committed
                'where' => [
                    'stripe_id' => $subscriptionId
Patrick Müller's avatar
Patrick Müller committed
                ],
                'limit' => 1
            ]);
        } catch (\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
            return false;
        }

        if (empty($result)) {
            return false;
        }

        $data             = current($result);
        $data['customer'] = \json_decode($data['customer'], true);
Patrick Müller's avatar
Patrick Müller committed

        return $data;
    }

    /**
     * Create a new Stripe Customer based on a QUIQQER User (customer)
     *
     * @param QUI\Users\User $User
     * @return StripeCustomer
     *
     * @throws ApiErrorException
     */
    public static function createStripeCustomerFromQuiqqerUser($User = null)
    {
        if (empty($User)) {
            $User = QUI::getUserBySession();
        }

        // If ERPUser -> get QUIQQER User
        if ($User instanceof QUI\ERP\User) {
            $User = QUI::getUsers()->get($User->getId());
        }

        $stripeCustomerId = $User->getAttribute(AbstractBasePayment::USER_ATTR_STRIPE_CUSTOMER_ID);

        if (!empty($stripeCustomerId)) {
            return StripeCustomer::retrieve($stripeCustomerId);
        }

        $quiqqerCustomerEmail = $User->getAttribute('email');
        $userLocaleList       = $User->getLocale()->getLocalesByLang($User->getLang());

        $Customer = StripeCustomer::create([
//            'payment_method'    => $paymentMethodId,
//            'invoice_settings'  => [
//                'default_payment_method' => $paymentMethodId
//            ],
                'quiqqerUserId' => $User->getId()
            ],
            'email'             => $quiqqerCustomerEmail ?: '',
            'description'       => QUI::conf('globals', 'host'),
            'preferred_locales' => [str_replace('_', '-', $userLocaleList[0])]
            // transform strings like "de_DE" to "de-DE"
            $User->setAttribute(AbstractBasePayment::USER_ATTR_STRIPE_CUSTOMER_ID, $Customer->id);
            $User->save(QUI::getUsers()->getSystemUser());
        } catch (\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
        }

        return $Customer;
Patrick Müller's avatar
Patrick Müller committed
    }
}