use App\Enum\DiscountTypeEnum;
use App\Enum\InvoiceStatusEnum;
use App\Enum\OrganizationStatusPayedEnum;
use App\Enum\PaymentRecurrenceEnum;
use App\Enum\PaymentStatusEnum;
use App\Enum\PaymentTypeEnum;
use App\Enum\ProductKeyEnum;
use App\Enum\RefundStatusEnum;
use App\Enum\VATRateEnum;
use App\Service\GocardlessAPI;
use App\Service\InvoiceFilesService;
use App\Traits\SentryNotifyTrait;
use Doctrine\ORM\EntityManagerInterface;
use Evo\Infrastructure\MappingORM\DiscountCode;
use Evo\Infrastructure\MappingORM\Invoice;
use Evo\Infrastructure\MappingORM\Item;
use Evo\Infrastructure\MappingORM\Organization;
use Evo\Infrastructure\MappingORM\Payment;
use Evo\Infrastructure\MappingORM\Product;
use Evo\Infrastructure\Messenger\Message\UpdateInvoiceReferenceStripeMessage;
use Evo\Infrastructure\Repository\InvoiceRepository;
use Knp\Snappy\Pdf;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsFifoStamp;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use SymfonyBundles\RedisBundle\Redis\ClientInterface;
use Twig\Environment;
class InvoiceUtils
{
use SentryNotifyTrait;
private EntityManagerInterface $em;
private GocardlessAPI $gocardlessAPI;
private MessageBusInterface $bus;
private KernelInterface $kernel;
private InvoiceRepository $invoiceRepository;
private Environment $twig;
private Pdf $pdf;
private InvoiceFilesService $invoiceFilesService;
private ClientInterface $client;
public function __construct(
EntityManagerInterface $em,
GocardlessAPI $gocardlessAPI,
KernelInterface $kernel,
InvoiceRepository $invoiceRepository,
Environment $twig,
Pdf $pdf,
InvoiceFilesService $invoiceFilesService,
MessageBusInterface $bus,
ClientInterface $client
) {
$this->em = $em;
$this->gocardlessAPI = $gocardlessAPI;
$this->bus = $bus;
$this->kernel = $kernel;
$this->invoiceRepository = $invoiceRepository;
$this->twig = $twig;
$this->pdf = $pdf;
$this->invoiceFilesService = $invoiceFilesService;
$this->client = $client;
}
public function isHavingWaitingPayment(Invoice $invoice): bool
{
foreach ($invoice->getPayments() as $payment) {
if (PaymentStatusEnum::WAITING === $payment->getStatus()) {
return true;
}
}
return false;
}
public function setTotalWaitingPayment(Invoice $invoice): float
{
$totalWaitingPayment = 0;
foreach ($invoice->getPayments() as $payment) {
if (PaymentStatusEnum::WAITING === $payment->getStatus()) {
$totalWaitingPayment += $payment->getAmount();
}
}
return $totalWaitingPayment;
}
public function generateInvoice(Invoice $invoice): ?string
{
if (null === $invoice->getOrganization()) {
return null;
}
$publicDir = $this->kernel->getProjectDir().'/public/';
$vat = VATRateEnum::VAT_20 === $invoice->getVATRate() ? $invoice->getPriceWithoutTax() * 0.2 : 0;
$this->generateNumberForInvoice($invoice);
$isDisplayDiscount = $this->checkItemDiscount($invoice);
$companyName = '';
/** @var Organization $organization */
$organization = $invoice->getOrganization();
if ($organization && !$organization->getLegalName()) {
$user = $organization->getUsers()[0];
$companyName = $user ? $user->getFullName() : '';
}
$discountCode = 0;
$discountName = '';
$discountLabel = '';
$isDisplayDiscountInvoice = false;
$isDisplayDiscountItem = false;
$discountCodeType = '';
if (null !== $invoice->getDiscountCode()) {
$discountCode = $invoice->getDiscountCode()->getFirstDiscountConfiguration()->getAmount();
$discountName = $invoice->getDiscountCode()->getPromoCode();
$discountLabel = $invoice->getDiscountCode()->getName();
$discountCodeType = $invoice->getDiscountCode()->getType();
$isDisplayDiscountInvoice = DiscountCodeTypeEnum::INVOICE_DISCOUNT === $invoice->getDiscountCode()->getType();
$isDisplayDiscountItem = in_array($invoice->getDiscountCode()->getType(), [DiscountCodeTypeEnum::PRODUCT_DISCOUNT, DiscountCodeTypeEnum::SUBSCRIPTION_DISCOUNT], true);
}
$aDomPack = [
ProductKeyEnum::PACK_DOMISIMPLE,
ProductKeyEnum::PACK_DIGIPACK,
ProductKeyEnum::PACK_SCANPACK,
ProductKeyEnum::PACK_SCANPACK_WITHOUT_REEXPEDITION,
ProductKeyEnum::PACK_COMPTAPACK,
ProductKeyEnum::PACK_DOMISCAN,
];
$twigData = [
'invoice' => $invoice,
'items' => $invoice->getItems(),
'publicDir' => $publicDir,
'companyName' => $companyName,
'subtotal_without_tax' => $invoice->getPriceWithoutTax(),
'discount_without_tax' => $invoice->getDiscountWithoutTax(),
'totalRefundedPayment' => $invoice->getTotalRefundedPayment(),
'subtotal' => $invoice->getPrice(),
'subtotal_vat' => $vat,
'isDisplayDiscount' => $isDisplayDiscount,
'discountCode' => $discountCode,
'discountName' => $discountName,
'discountLabel' => $discountLabel,
'isDisplayDiscountInvoice' => $isDisplayDiscountInvoice,
'aDomPack' => $aDomPack,
'discountCodeType' => $discountCodeType,
'isDisplayDiscountItem' => $isDisplayDiscountItem,
];
$output = $this->pdf->getOutputFromHtml(
$this->twig->render(
'billing/invoice.html.twig',
$twigData
),
[
'margin-bottom' => '30',
'margin-top' => '10',
'margin-right' => '10',
'margin-left' => '10',
'footer-center' => '',
'footer-right' => 'Page [page]/[toPage]',
'footer-font-name' => 'sans-serif',
'footer-font-size' => '7',
'footer-html' => "
<div style='text-align: center; font-size: 9px; font-family: sans-serif; color: #353535;'>
KOAH - DIGIDOM - GROUPE PARAJURIS - 10 rue de Penthièvre - 75008 Paris 08 - France
<br/>
Siret : 797 978 996 00016 - Naf : 7022Z - TVA : FR 86 797978996 - RCS 797 978 996 PARIS
<br>
SAS au capital de 262 600,00 €
<br>
Tel : 01 85 53 38 24 - Email : support@digidom.pro - Site internet : www.digidom.pro
</div>",
]
);
return $this->invoiceFilesService->sendToStorage($invoice, $output);
}
public function checkItemDiscount(Invoice $invoice)
{
/** @var Item $item */
foreach ($invoice->getItems() as $item) {
if ($item->getDiscountWithoutTax() > 0) {
return true;
}
}
return false;
}
public function discountCodeCalculation(float $amount, DiscountCode $discountCode): float
{
$discountAmount = 0;
switch ($discountCode->getDiscountType()) {
case DiscountTypeEnum::REDUCTION_IN_EURO:
$discountAmount = (float) $discountCode->getFirstDiscountConfiguration()->getAmount();
break;
case DiscountTypeEnum::FIXED_PRICE:
$discountAmount = $amount - $discountCode->getFirstDiscountConfiguration()->getAmount();
break;
}
return $discountAmount;
}
public function generatePaymentGocardless(float $amount, Invoice $invoice, bool $ignoreDomiciliation = false): ?\GoCardlessPro\Resources\Payment
{
if (!$ignoreDomiciliation && !$invoice->getIsDomiciliation()) {
$payment = $this->getPayment($invoice);
if (!$payment instanceof Payment) {
return null;
}
}
if (!$invoice->getOrganization() || .0 === $amount) {
return null;
}
$organization = $invoice->getOrganization();
$mandateID = $organization->getMandatID();
if (false === $organization->getIsAuthorizedDebit()) {
return null;
}
if ($mandateID && $invoice->getPrice() && !$this->getPayment($invoice)) {
$paymentGC = $this->gocardlessAPI->createPayment(
$mandateID,
$amount,
'Payment '.$invoice->getName()
);
if (null !== $paymentGC) {
$payment = $this->getPayment($invoice);
/*
* Create payment
*/
if (null === $payment) {
$payment = new Payment();
$payment->setType(PaymentTypeEnum::GOCARDLESS);
$payment->setPaidOutAt(new \DateTime($invoice->getCreatedAt()->format('Y-m-d H:s:i')));
$payment->setAmount($invoice->getPrice());
$payment->setInvoice($invoice);
$payment->setStatus(PaymentStatusEnum::WAITING);
}
$payment->setReference($paymentGC->id);
$this->em->persist($payment);
$this->em->flush();
return $paymentGC;
}
$this->sendSentryMessage('No payment Gocardless has created for mandate '.$mandateID);
}
return null;
}
public function generatePaymentGocardlessFromPayment(Payment $entity)
{
$invoice = $entity->getInvoice();
if (null === $invoice->getOrganization()) {
return null;
}
/** @var Organization $organization */
$organization = $invoice->getOrganization();
$mandateID = $organization->getMandatID();
if (false === $organization->getIsAuthorizedDebit()) {
return null;
}
$amount = (float) $entity->getAmount();
if ($mandateID && $amount > 0) {
$paymentGC = $this->gocardlessAPI->createPayment(
$mandateID,
$entity->getAmount(),
'Payment '.$invoice->getName()
);
if (null !== $paymentGC) {
$entity->setReference($paymentGC->id);
$this->em->persist($entity);
$this->em->flush();
return $paymentGC;
}
}
$this->sendSentryMessage('No payment Gocardless has created for mandate '.$mandateID);
return null;
}
/**
* @return Payment|null
*/
public function getPayment(Invoice $invoice, $type = PaymentTypeEnum::GOCARDLESS)
{
if (empty($invoice->getPayments()->toArray())) {
return null;
}
/** @var Payment $payment */
foreach ($invoice->getPayments()->toArray() as $payment) {
if ($payment->getType() === $type) {
return $payment;
}
}
return null;
}
/**
* @return Payment|null
*/
public function getPaymentSuccess(Invoice $invoice)
{
/** @var Payment $payment */
foreach ($invoice->getPayments()->toArray() as $payment) {
if (PaymentStatusEnum::SUCCESS === $payment->getStatus()) {
return $payment;
}
}
return null;
}
/**
* @param string $type
*/
public function fixDuplicatePayment(Invoice $invoice, $type = PaymentTypeEnum::GOCARDLESS)
{
$currentePayment = null;
/** @var Payment $payment */
foreach ($invoice->getPayments()->toArray() as $payment) {
if ($payment->getType() === $type) {
$currentePayment = $payment;
break;
}
}
foreach ($invoice->getPayments()->toArray() as $payment) {
if ($payment->getType() === $type && $payment->getId() !== $currentePayment->getId()) {
$this->em->remove($payment);
$this->em->flush();
}
}
}
public function countPaymentType(Invoice $invoice, $type = PaymentTypeEnum::GOCARDLESS)
{
$count = 0;
/** @var Payment $payment */
foreach ($invoice->getPayments()->toArray() as $payment) {
if ($payment->getType() === $type) {
++$count;
}
}
return $count;
}
public function checkInvoice(Invoice $invoice, bool $flush = false): Invoice
{
$totalCreditNote = $this->getTotalCreditNote($invoice);
$totalRefund = $this->getTotalRefund($invoice);
$invoice->setTotalRefund($totalCreditNote);
$invoice->setTotalRefundedPayment($totalRefund);
if ($totalCreditNote > 0 && $totalCreditNote === $invoice->getPrice()) {
$invoice->setIsCanceled(true);
}
$total_paid = $this->getTotalPaid($invoice);
$invoice->setTotalPaid($total_paid - $totalRefund);
$invoice->setRemainingToPay(round(
($invoice->getPrice() - $totalCreditNote) - ($total_paid - $totalRefund),
2
));
$limitReached = false;
$now = date('Y-m-d');
$limitDate = $this->getLimitedDate($invoice);
if ($limitDate < $now) {
$limitReached = true;
}
if (!$invoice->isModel()) {
if (0.0 === $invoice->getRemainingToPay()) {
$invoice->setStatus(InvoiceStatusEnum::PAID);
} elseif (
$invoice->getRemainingToPay() > 0 &&
$limitReached && !$invoice->getIsImported()
) {
$invoice->setStatus(InvoiceStatusEnum::LATE);
} elseif ($invoice->getTotalPaid() > 0 && $invoice->getRemainingToPay() > 0) {
$invoice->setStatus(InvoiceStatusEnum::PARTIALLY_PAID);
} elseif ($invoice->getRemainingToPay() < 0) {
$invoice->setStatus(InvoiceStatusEnum::OVER_PAID);
} elseif (0.0 === $invoice->getTotalPaid()) {
$invoice->setStatus(InvoiceStatusEnum::SENT);
}
}
if ($flush) {
$this->em->persist($invoice);
$this->em->flush();
}
return $invoice;
}
public function getTotalCreditNote(?Invoice $invoice)
{
if (!$invoice instanceof Invoice) {
return 0;
}
$total_refund = 0;
if ($invoice->getCreditNotes()->count() > 0) {
foreach ($invoice->getCreditNotes() as $creditNote) {
if (CreditNoteTypeEnum::REFUND == $creditNote->getType()) {
$total_refund += $creditNote->getAmount();
}
}
}
return round($total_refund, 2);
}
public function getTotalRefund(?Invoice $invoice)
{
if (!$invoice instanceof Invoice) {
return 0;
}
$totalRefund = 0;
if ($invoice->getRefunds()->count() > 0) {
foreach ($invoice->getRefunds() as $refund) {
if (RefundStatusEnum::SUCCESS === $refund->getStatus()) {
$totalRefund += $refund->getAmount();
}
}
}
return round($totalRefund, 2);
}
public function getTotalPaid(?Invoice $invoice)
{
if (!$invoice instanceof Invoice) {
return 0;
}
$total_paid = 0;
$waiting = 0;
if ($invoice->getPayments()->count() > 0) {
foreach ($invoice->getPayments() as $payment) {
if (PaymentStatusEnum::SUCCESS === $payment->getStatus()) {
$total_paid += $payment->getAmount();
} else {
$waiting += $payment->getAmount();
}
}
}
// si besoin on pourra distinguer waiting et total paid plus tard
return round($total_paid, 2);
}
public function getLimitedDate(Invoice $invoice)
{
$paymentDeadline = $invoice->getPaymentDeadline();
if (null !== $paymentDeadline) {
$limitDate = $paymentDeadline->format('Y-m-d');
} else {
$limitDate = date('Y-m-d', strtotime('+30 days', $invoice->getCreatedAt()->getTimestamp()));
}
return $limitDate;
}
/**
* @return bool
*/
public function isFormalityInvoice(Invoice $invoice)
{
/** @var Item $item */
foreach ($invoice->getItems() as $item) {
/** @var Product $product */
$product = $item->getProduct();
if ($product) {
$category = $product->getCategory();
if (null !== $category && CategoryTypeEnum::FORMALITES === $category->getType()) {
return true;
}
}
}
return false;
}
public function paid(Invoice $invoice, $type = PaymentTypeEnum::STRIPE, $strypeRefence = null)
{
if (PaymentTypeEnum::STRIPE === $type && null !== $strypeRefence) {
try {
$payment = null;
/** @var Payment $item */
foreach ($invoice->getPayments()->toArray() as $item) {
if ($item->getType() === $type) {
$payment = $item;
break;
}
}
if (null !== $payment) {
$payment->setReference($strypeRefence);
$payment->setStatus(PaymentStatusEnum::SUCCESS);
$this->em->persist($payment);
}
$this->em->persist($invoice);
$this->em->flush();
return true;
} catch (\Exception $e) {
$this->captureSentryException($e);
$this->bus->dispatch(
(new Envelope(new UpdateInvoiceReferenceStripeMessage($invoice->getId(), $strypeRefence)))->with(
new AmazonSqsFifoStamp('Invoice', uniqid('', true))
)
);
}
}
return false;
}
public function unpaid(Invoice $invoice, $type = PaymentTypeEnum::STRIPE, $strypeRefence = null)
{
if (PaymentTypeEnum::STRIPE === $type) {
$payment = null;
/** @var Payment $item */
foreach ($invoice->getPayments()->toArray() as $item) {
if ($item->getType() === $type) {
$payment = $item;
break;
}
}
if (null !== $payment) {
if ($strypeRefence) {
$payment->setReference($strypeRefence);
}
$payment->setStatus(PaymentStatusEnum::ERROR);
$this->em->persist($payment);
$this->em->flush();
return true;
}
}
return false;
}
/**
* @return bool
*/
public function isInvoiceOfInscription(?Invoice $invoice)
{
if (!$invoice instanceof Invoice) {
return false;
}
$response = false;
/** @var Item $item */
foreach ($invoice->getItems() as $item) {
/** @var Product $product */
$product = $item->getProduct();
if (ProductKeyEnum::SECURITY_DEPOSIT === $product->getUniqueKey()) {
$response = true;
break;
}
}
return $response;
}
public function isWaiting(Invoice $invoice): bool
{
$payments = $invoice->getPayments()->toArray();
$isWaiting = false;
/** @var Payment $payment */
foreach ($payments as $payment) {
if (PaymentTypeEnum::GOCARDLESS === $payment->getType() &&
PaymentStatusEnum::WAITING === $payment->getStatus() &&
in_array($invoice->getStatus(), [InvoiceStatusEnum::SENT, InvoiceStatusEnum::LATE], true)) {
$isWaiting = true;
break;
}
if (PaymentTypeEnum::STRIPE === $payment->getType() &&
in_array($payment->getStatus(), [PaymentStatusEnum::WAITING, PaymentStatusEnum::ERROR], true) &&
in_array($invoice->getStatus(), [InvoiceStatusEnum::SENT, InvoiceStatusEnum::LATE], true)) {
$isWaiting = true;
break;
}
}
return $isWaiting;
}
/**
* Check status of invoice to return true or false
* If status of invoice is unpaid the fields IsStatusInvoicePayed passed to true and function return false
* After the function checkt if payment is late or not.
*
* @return bool
*/
public function checkUnpaid(Organization $organization)
{
$haveUnpaidInvoice = false;
/** @var Invoice $invoice */
foreach ($organization->getInvoices() as $invoice) {
if (InvoiceStatusEnum::LATE === $invoice->getStatus() && !$invoice->getIsImported()) {
$haveUnpaidInvoice = true;
break;
}
}
if (!$haveUnpaidInvoice) {
$organization->setStatusInvoicePayed(OrganizationStatusPayedEnum::PAID);
$this->em->flush();
return true;
}
$organization->setStatusInvoicePayed(OrganizationStatusPayedEnum::UNPAID);
$this->em->flush();
return false;
}
/**
* @return Invoice|bool|mixed
*/
public function checkRegisterInvoice(Organization $organization)
{
foreach ($organization->getInvoices() as $invoice) {
foreach ($invoice->getItems() as $item) {
if (ProductKeyEnum::SECURITY_DEPOSIT === $item->getProduct()->getUniqueKey()) {
return $invoice;
}
}
}
return false;
}
/**
* @return bool
*/
public function isFullRepaid(Invoice $invoice)
{
$amoutCreditNote = 0;
foreach ($invoice->getCreditNotes() as $creditNote) {
$amoutCreditNote += $creditNote->getAmount();
}
return $amoutCreditNote >= $invoice->getPrice();
}
public function sortByCreatedDate(&$array)
{
usort(
$array,
fn ($a, $b) => $a->getCreatedAt() <=> $b->getCreatedAt()
);
$array = array_reverse($array);
}
public function updateStatus(array $data): array
{
$invoiceRepo = $this->em->getRepository(Invoice::class);
foreach ($data['invoices'] as $id) {
/** @var Invoice $invoice */
$invoice = $this->em->getReference(Invoice::class, (int) $id);
if (!$invoice) {
continue;
}
$payment = $this->setNewStatusPayment($invoice, $data);
$this->em->persist($payment);
}
$this->em->flush();
$invoices = [];
/* get invoices updated * */
foreach ($data['invoices'] as $id) {
/** @var Invoice $invoice */
$invoice = $invoiceRepo->find((int) $id);
if (!$invoice) {
continue;
}
$invoices[] = $invoice;
}
return $invoices;
}
private function setNewStatusPayment(Invoice $invoice, array $data): Payment
{
$payment = $this->getPayment($invoice, PaymentTypeEnum::STRIPE);
if (is_null($payment)) {
$payment = (new Payment())
->setType(PaymentTypeEnum::STRIPE)
->setInvoice($invoice)
->setAmount($invoice->getRemainingToPay());
}
if ('succeeded' === $data['stripe_status']) {
$payment->setStatus(PaymentStatusEnum::SUCCESS);
}
if ('pending' === $data['stripe_status']) {
$payment->setStatus(PaymentStatusEnum::WAITING);
}
return $payment;
}
public static function generateInvoiceUniquekey(int $subscriptionId, string $subscriptionType, string $yearMonth): string
{
$periodeKey = PaymentRecurrenceEnum::MONTHLY === $subscriptionType ? 'M' : 'A';
$periodeDate = PaymentRecurrenceEnum::MONTHLY === $subscriptionType ? $yearMonth : date('Y', strtotime($yearMonth));
return $subscriptionId.'-'.$periodeKey.'-'.$periodeDate;
}
public function createInvoice(
Organization $organization,
string $object,
float $amount,
float $globalDiscountWithoutTax,
array $items,
Payment $payment,
?DiscountCode $promoCode
): invoice {
$invoice = new Invoice();
if (null !== $promoCode) {
$invoice->setDiscountCode($promoCode);
}
$invoice->setObject($object);
if ($organization->isAttachedToPrescriber() && $organization->getPrescriber()) {
$invoice->setOrganization($organization->getPrescriber());
} else {
$invoice->setOrganization($organization);
}
$invoice->setIsSent(true);
$invoice->setStatus(InvoiceStatusEnum::PAID);
$invoice->setPrice($amount);
$invoice->setDiscountWithoutTax($globalDiscountWithoutTax);
foreach ($items as $item) {
$invoice->addItem($item);
}
$invoice->addPayment($payment);
$this->em->persist($invoice);
return $invoice;
}
public static function setPrices(Invoice $invoice): void
{
if (0 === count($invoice->getItems())) {
throw new \LogicException('This invoice must have minimum one item !');
}
if (null !== $invoice->getVATRate()) {
$totalPriceWithoutTax = 0;
foreach ($invoice->getItems() as $item) {
if (!$item->getIsOffer()) {
$sum = (($item->getUnitPriceWithoutTax() * $item->getQuantity()) - $item->getDiscountWithoutTax());
if ($sum < 0) {
$sum = 0;
}
$totalPriceWithoutTax += $sum;
}
}
if (!$invoice->getOrganization() && !$invoice->getIsTemplate() && !$invoice->isModel()) {
throw new \LogicException('This invoice must have a organization !');
}
if (null !== $invoice->getOrganization()) {
$isExemptTVA = $invoice->getOrganization()->getIsExemptTVA();
if (true === $isExemptTVA) {
$invoice->setVATRate(VATRateEnum::VAT_0);
}
}
$VATRate = $invoice->getVATRate();
$totalPriceWithoutTax -= $invoice->getDiscountWithoutTax();
$price = round($totalPriceWithoutTax * (1 + $VATRate / 100), 2);
if ($price < 0) {
$price = 0;
}
$priceWithoutTax = round($price / (1 + $VATRate / 100), 2);
$invoice->setPrice($price);
$invoice->setPriceWithoutTax($priceWithoutTax);
}
}
public function generateNumberForInvoice(Invoice $invoice): void
{
$this->client->select(0);
$lockKey = 'invoice_lock';
if (!$invoice->getNumber() && !$invoice->getIsImported()) {
$lockAcquired = $this->client->set($lockKey, 1, 'EX', 5, 'NX');
if ($lockAcquired) {
try {
$this->em->beginTransaction();
$lastInvoice = $this->invoiceRepository->getLastNumber();
$currentInvoiceNumber = $this->client->get('invoice_number');
if (!$currentInvoiceNumber || $lastInvoice > $currentInvoiceNumber) {
$this->client->set('invoice_number', $lastInvoice);
}
$newInvoiceNumber = $this->client->incr('invoice_number');
$invoice->setNumber($newInvoiceNumber);
$this->em->persist($invoice);
$this->em->flush();
$this->em->commit();
} catch (\Exception $e) {
$this->em->rollback();
$this->client->decr('invoice_number');
throw $e;
} finally {
$this->client->del($lockKey);
}
} else {
throw new \RuntimeException('Cannot acquire lock for invoice number generation, please try again.');
}
}
}
}