Newer
Older
<?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\Subscription as StripeSubscription;
use Stripe\Customer as StripeCustomer;
use Stripe\Invoice as StripeInvoice;
use Stripe\PaymentIntent as StripePaymentIntent;
/**
* 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
*
* @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);
}
// Create Stripe billing plan
$billingPlanId = BillingPlans::createBillingPlanFromOrder($Order);
$Order->addHistory(QUI::getLocale()->get('quiqqer/payment-stripe', 'order.offer_created', [
$Order->setPaymentData(AbstractBasePayment::ATTR_STRIPE_BILLING_PLAN_ID, $billingPlanId);
$Order->update($SystemUser);
$paymentMethodId = $Order->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_PAYMENT_METHOD_ID);
if (empty($paymentMethodId)) {
return false;
$Customer = $Order->getCustomer();
$StripeCustomer = self::createStripeCustomerFromQuiqqerUser($Customer);
Utils::attachPaymentMethodToStripeCustomer($Customer, $paymentMethodId);
// Create Stripe subscription
$StripeSubscription = StripeSubscription::create([
'customer' => $StripeCustomer->id,
'items' => [
'payment_behavior' => 'allow_incomplete',
'default_payment_method' => $paymentMethodId
try {
$Order->addHistory(QUI::getLocale()->get('quiqqer/payment-stripe', 'history.order.subscription_created', [
'subscriptionId' => $StripeSubscription->id
]));
$Order->setPaymentData(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID, $StripeSubscription->id);
$Order->setPaymentData(AbstractBasePayment::ATTR_STRIPE_ORDER_SUCCESSFUL, true);
// Write subscription data to db
QUI::getDataBase()->insert(
Provider::getStripeBillingSubscriptionsTable(),
[
'stripe_id' => $StripeSubscription->id,
'stripe_billing_plan_id' => $billingPlanId,
'stripe_customer_id' => $StripeCustomer->id,
'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);
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
}
$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;
/**
* 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;
}
/**
* 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
*/
public static function billSubscriptionBalance(Invoice $Invoice)
{
$subscriptionId = $Invoice->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID);
throw new QUI\ERP\Payments\Stripe\StripeException(
'exception.Recurring.Subscriptions.subscription_id_not_found',
[
'invoiceId' => $Invoice->getId()
]
),
404
);
}
//
// $data = self::getSubscriptionData($subscriptionId);
//
// if (empty($data)) {
// $data = self::getSubscriptionDetails($subscriptionId);
// }
// Check if a Paymill transaction matches the Invoice
$unprocessedTransactions = self::getUnprocessedTransactions($subscriptionId);
$Invoice->calculatePayments();
/** @var AbstractBaseRecurringPayment $Payment */
$paymentMethodData = \json_decode($Invoice->getAttribute('payment_method_data'), true);
$Payment = Payments::getInstance()->getPayment($paymentMethodData['id'])->getPaymentType();
$amountDue = (int)$transaction['amount_due'];
$currency = \mb_strtoupper($transaction['currency']);
if ($currency !== $invoiceCurrency) {
continue;
}
continue;
}
// Transaction amount equals Invoice amount
try {
$InvoiceTransaction = TransactionFactory::createPaymentTransaction(
$Invoice->getCurrency(),
$Invoice->getHash(),
$Payment->getName(),
[
AbstractBasePayment::ATTR_STRIPE_INVOICE_ID => $transaction['id']
$Invoice->getGlobalProcessId()
);
$Invoice->addTransaction($InvoiceTransaction);
QUI::getDataBase()->update(
Provider::getStripeBillingSubscriptionsTransactionsTable(),
[
'quiqqer_transaction_id' => $InvoiceTransaction->getTxId(),
'quiqqer_transaction_completed' => 1
],
[
QUI::getLocale()->get(
'quiqqer/payment-stripe',
'history.Invoice.add_stripe_transaction',
[
'quiqqerTransactionId' => $InvoiceTransaction->getTxId(),
'stripeTransactionId' => $transaction['id']
]
)
);
} catch (\Exception $Exception) {
QUI\System\Log::writeException($Exception);
}
break;
}
}
/**
* Get data of all Stripe Subscriptions (QUIQQER data only; no Stripe query performed!)
*
* @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 .= " FROM `".Provider::getStripeBillingSubscriptionsTable()."`";
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
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;
}
/**
*/
public static function getSubscriptionDetails($subscriptionId)
{
Provider::setupApi();
return StripeSubscription::retrieve($subscriptionId)->toArray();
}
/**
* 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
*/
public static function cancelSubscription($subscriptionId, $reason = '')
{
$data = self::getSubscriptionData($subscriptionId);
if (empty($data)) {
throw new QUI\ERP\Payments\Stripe\StripeException([
'quiqqer/payment-stripe',
'exception.PaymentMethods.Recurring.Subscriptions.cancelSubscription.no_data',
[
'subscriptionId' => $subscriptionId
]
]);
}
try {
$Locale = new QUI\Locale();
$Locale->setCurrent($data['customer']['lang']);
} catch (\Exception $Exception) {
QUI\System\Log::writeException($Exception);
return;
}
$Subscription = StripeSubscription::retrieve($subscriptionId);
$Subscription->cancel();
// Set status in QUIQQER database to "not active"
self::setSubscriptionAsInactive($subscriptionId);
}
/**
* Set a subscription as inactive
*
* @param string $subscriptionId - Stripe Subscription ID
* @return void
*/
public static function setSubscriptionAsInactive($subscriptionId)
{
try {
QUI::getDataBase()->update(
Provider::getStripeBillingSubscriptionsTable(),
]
);
} catch (\Exception $Exception) {
QUI\System\Log::writeException($Exception);
}
}
/**
* Fetches subscription transactions (invoices) from Stripe
* @param string $subscriptionId - Stripe Subscription ID
* @param string $startAfterInvoiceId (optional) - Only fetch invoices after this Stripe Invoice ID
public static function getSubscriptionTransactions($subscriptionId, $startAfterInvoiceId = null)
{
$searchParams = [
'limit' => 100,
'subscription' => $subscriptionId
];
// if ($startAfterInvoiceId) {
// $searchParams['starting_after'] = $startAfterInvoiceId;
// }
$result = StripeInvoice::all($searchParams);
$transactions = [];
foreach ($result->autoPagingIterator() as $Transaction) {
$transactions[] = $Transaction->toArray();
}
/**
* Process all unpaid Invoices of Subscriptions
*
* @return void
*/
public static function processUnpaidInvoices()
{
$Invoices = InvoiceHandler::getInstance();
// Determine payment type IDs
$payments = Payments::getInstance()->getPayments([
'select' => ['id'],
'where' => [
'payment_type' => [
'type' => 'IN',
'value' => Provider::getRecurringPaymentTypes()
]
]);
$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
]
]);
$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(),
'where' => [
'global_process_id' => [
'type' => 'IN',
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
]
]
]);
} 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
*/
public static function processDeniedTransactions(Invoice $Invoice)
{
$subscriptionId = $Invoice->getPaymentDataEntry(AbstractBasePayment::ATTR_STRIPE_SUBSCRIPTION_ID);
if (empty($subscriptionId)) {
return;
}
$data = self::getSubscriptionData($subscriptionId);
if (empty($data)) {
return;
}
// Get all "uncollectible" Stripe transactions (invoices)
$unprocessedTransactions = self::getUnprocessedTransactions(
$subscriptionId,
StripeInvoice::STATUS_UNCOLLECTIBLE
);
} catch (\Exception $Exception) {
QUI\System\Log::writeException($Exception);
return;
}
try {
$Invoice->calculatePayments();
$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();
$amountDue = (int)$transaction['amount_due'];
$currency = \mb_strtoupper($transaction['currency']);
if ($currency !== $invoiceCurrency) {
continue;
}
continue;
}
// Transaction amount equals Invoice amount
try {
$InvoiceTransaction = TransactionFactory::createPaymentTransaction(
$Invoice->getCurrency(),
$Invoice->getHash(),
$Payment->getName(),
[],
null,
false,
$Invoice->getGlobalProcessId()
);
$InvoiceTransaction->changeStatus(TransactionHandler::STATUS_ERROR);
$Invoice->addTransaction($InvoiceTransaction);
QUI::getDataBase()->update(
Provider::getStripeBillingSubscriptionsTransactionsTable(),
[
'quiqqer_transaction_id' => $InvoiceTransaction->getTxId(),
'quiqqer_transaction_completed' => 1
],
[
QUI::getLocale()->get(
'quiqqer/payment-stripe',
'history.Invoice.add_stripe_transaction',
[
'quiqqerTransactionId' => $InvoiceTransaction->getTxId(),
'stripeTransactionId' => $transaction['id']
]
)
);
} catch (\Exception $Exception) {
QUI\System\Log::writeException($Exception);
}
}
}
/**
* Refreshes transactions for a Stripe Subscription
*/
protected static function refreshTransactionList($subscriptionId)
{
if (isset(self::$transactionsRefreshed[$subscriptionId])) {
return;
}
// Get global process id
$data = self::getSubscriptionData($subscriptionId);
$globalProcessId = $data['global_process_id'];
// Determine existing transactions
$result = QUI::getDataBase()->fetch([
'select' => ['stripe_invoice_id'],
'from' => Provider::getStripeBillingSubscriptionsTransactionsTable(),
]
]);
$existing = [];
foreach ($result as $row) {
$existing[$row['stripe_invoice_id']] = true;
$transactions = self::getSubscriptionTransactions($data['stripe_id']);
$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.'
);
continue;
}
$transactionTime = $TransactionTime->format('Y-m-d H:i:s');
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
]
);
}
}
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"]
* @return array
* @throws QUI\Database\Exception
* @throws \Exception
*/
protected static function getUnprocessedTransactions($subscriptionId, $status = StripeInvoice::STATUS_PAID)
self::refreshTransactionList($subscriptionId);
'select' => ['stripe_invoice_data'],
'from' => Provider::getStripeBillingSubscriptionsTransactionsTable(),
'stripe_subscription_id' => $subscriptionId,
'quiqqer_transaction_id' => null
]
]);
$transactions = [];
foreach ($result as $row) {
$t = json_decode($row['stripe_invoice_data'], true);
if ($t['status'] !== $status) {
continue;
}
$transactions[] = $t;
}
return $transactions;
}
/**
* Get available data by Subscription ID (QUIQQER DATA)
* @param string $subscriptionId - Stripe Subscription ID
* @return array|false
*/
protected static function getSubscriptionData($subscriptionId)
{
try {
$result = QUI::getDataBase()->fetch([
'from' => Provider::getStripeBillingSubscriptionsTable(),
],
'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);
* Create a new Stripe Customer based on a QUIQQER User (customer)
*
* @param QUI\Users\User $User
* @return StripeCustomer
*
* @throws QUI\Exception
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());
// 'payment_method' => $paymentMethodId,
// 'invoice_settings' => [
// 'default_payment_method' => $paymentMethodId
// ],
'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;