bavix/laravel-wallet

View on GitHub
src/Traits/CartPay.php

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
<?php

declare(strict_types=1);

namespace Bavix\Wallet\Traits;

use Bavix\Wallet\Exceptions\BalanceIsEmpty;
use Bavix\Wallet\Exceptions\InsufficientFunds;
use Bavix\Wallet\Exceptions\ProductEnded;
use Bavix\Wallet\Interfaces\CartInterface;
use Bavix\Wallet\Interfaces\ProductInterface;
use Bavix\Wallet\Internal\Assembler\AvailabilityDtoAssemblerInterface;
use Bavix\Wallet\Internal\Exceptions\ExceptionInterface;
use Bavix\Wallet\Internal\Exceptions\ModelNotFoundException;
use Bavix\Wallet\Internal\Exceptions\RecordNotFoundException;
use Bavix\Wallet\Internal\Exceptions\TransactionFailedException;
use Bavix\Wallet\Internal\Service\TranslatorServiceInterface;
use Bavix\Wallet\Models\Transfer;
use Bavix\Wallet\Objects\Cart;
use Bavix\Wallet\Services\AssistantServiceInterface;
use Bavix\Wallet\Services\AtomicServiceInterface;
use Bavix\Wallet\Services\BasketServiceInterface;
use Bavix\Wallet\Services\CastServiceInterface;
use Bavix\Wallet\Services\ConsistencyServiceInterface;
use Bavix\Wallet\Services\EagerLoaderServiceInterface;
use Bavix\Wallet\Services\PrepareServiceInterface;
use Bavix\Wallet\Services\PurchaseServiceInterface;
use Bavix\Wallet\Services\TransferServiceInterface;
use function count;
use Illuminate\Database\RecordsNotFoundException;

/**
 * @psalm-require-extends \Illuminate\Database\Eloquent\Model
 *
 * @psalm-require-implements \Bavix\Wallet\Interfaces\Customer
 */
trait CartPay
{
    use HasWallet;

    /**
     * @return non-empty-array<Transfer>
     *
     * @throws ProductEnded
     * @throws BalanceIsEmpty
     * @throws InsufficientFunds
     * @throws RecordNotFoundException
     * @throws RecordsNotFoundException
     * @throws TransactionFailedException
     * @throws ExceptionInterface
     */
    public function payFreeCart(CartInterface $cart): array
    {
        return app(AtomicServiceInterface::class)->block($this, function () use ($cart) {
            $basketDto = $cart->getBasketDto();
            $basketService = app(BasketServiceInterface::class);
            $availabilityAssembler = app(AvailabilityDtoAssemblerInterface::class);
            app(EagerLoaderServiceInterface::class)->loadWalletsByBasket($this, $basketDto);
            if (! $basketService->availability($availabilityAssembler->create($this, $basketDto, false))) {
                throw new ProductEnded(
                    app(TranslatorServiceInterface::class)->get('wallet::errors.product_stock'),
                    ExceptionInterface::PRODUCT_ENDED
                );
            }

            app(ConsistencyServiceInterface::class)->checkPotential($this, 0, true);

            $transfers = [];
            $castService = app(CastServiceInterface::class);
            $prepareService = app(PrepareServiceInterface::class);
            $assistantService = app(AssistantServiceInterface::class);
            foreach ($basketDto->items() as $item) {
                foreach ($item->getItems() as $product) {
                    $transfers[] = $prepareService->transferExtraLazy(
                        $this,
                        $castService->getWallet($this),
                        $product,
                        $castService->getWallet($item->getReceiving() ?? $product),
                        Transfer::STATUS_PAID,
                        0,
                        $assistantService->getMeta($basketDto, $product)
                    );
                }
            }

            assert($transfers !== []);

            return app(TransferServiceInterface::class)->apply($transfers);
        });
    }

    /**
     * @return Transfer[]
     */
    public function safePayCart(CartInterface $cart, bool $force = false): array
    {
        try {
            return $this->payCart($cart, $force);
        } catch (ExceptionInterface) {
            return [];
        }
    }

    /**
     * @return non-empty-array<Transfer>
     *
     * @throws ProductEnded
     * @throws BalanceIsEmpty
     * @throws InsufficientFunds
     * @throws RecordNotFoundException
     * @throws RecordsNotFoundException
     * @throws TransactionFailedException
     * @throws ExceptionInterface
     */
    public function payCart(CartInterface $cart, bool $force = false): array
    {
        return app(AtomicServiceInterface::class)->block($this, function () use ($cart, $force) {
            $basketDto = $cart->getBasketDto();
            $basketService = app(BasketServiceInterface::class);
            $availabilityAssembler = app(AvailabilityDtoAssemblerInterface::class);
            app(EagerLoaderServiceInterface::class)->loadWalletsByBasket($this, $basketDto);
            if (! $basketService->availability($availabilityAssembler->create($this, $basketDto, $force))) {
                throw new ProductEnded(
                    app(TranslatorServiceInterface::class)->get('wallet::errors.product_stock'),
                    ExceptionInterface::PRODUCT_ENDED
                );
            }

            $prices = [];
            $transfers = [];
            $castService = app(CastServiceInterface::class);
            $prepareService = app(PrepareServiceInterface::class);
            $assistantService = app(AssistantServiceInterface::class);
            foreach ($cart->getBasketDto()->items() as $item) {
                foreach ($item->getItems() as $product) {
                    $productId = $product::class.':'.$castService->getModel($product)->getKey();
                    $pricePerItem = $item->getPricePerItem();
                    if ($pricePerItem === null) {
                        $prices[$productId] ??= $product->getAmountProduct($this);
                        $pricePerItem = $prices[$productId];
                    }

                    $transfers[] = $prepareService->transferExtraLazy(
                        $this,
                        $castService->getWallet($this),
                        $product,
                        $castService->getWallet($item->getReceiving() ?? $product),
                        Transfer::STATUS_PAID,
                        $pricePerItem,
                        $assistantService->getMeta($basketDto, $product)
                    );
                }
            }

            if (! $force) {
                app(ConsistencyServiceInterface::class)->checkTransfer($transfers);
            }

            assert($transfers !== []);

            return app(TransferServiceInterface::class)->apply($transfers);
        });
    }

    /**
     * @return non-empty-array<Transfer>
     *
     * @throws ProductEnded
     * @throws RecordNotFoundException
     * @throws RecordsNotFoundException
     * @throws TransactionFailedException
     * @throws ExceptionInterface
     */
    public function forcePayCart(CartInterface $cart): array
    {
        return $this->payCart($cart, true);
    }

    public function safeRefundCart(CartInterface $cart, bool $force = false, bool $gifts = false): bool
    {
        try {
            return $this->refundCart($cart, $force, $gifts);
        } catch (ExceptionInterface) {
            return false;
        }
    }

    /**
     * @throws BalanceIsEmpty
     * @throws InsufficientFunds
     * @throws RecordNotFoundException
     * @throws RecordsNotFoundException
     * @throws TransactionFailedException
     * @throws ModelNotFoundException
     * @throws ExceptionInterface
     */
    public function refundCart(CartInterface $cart, bool $force = false, bool $gifts = false): bool
    {
        return app(AtomicServiceInterface::class)->block($this, function () use ($cart, $force, $gifts) {
            $basketDto = $cart->getBasketDto();
            app(EagerLoaderServiceInterface::class)->loadWalletsByBasket($this, $basketDto);
            $transfers = app(PurchaseServiceInterface::class)->already($this, $basketDto, $gifts);
            if (count($transfers) !== $basketDto->total()) {
                throw new ModelNotFoundException(
                    "No query results for model [{$this->transfers()
                        ->getModel()
                        ->getMorphClass()}]",
                    ExceptionInterface::MODEL_NOT_FOUND
                );
            }

            $index = 0;
            $objects = [];
            $transferIds = [];
            $transfers = array_values($transfers);
            $castService = app(CastServiceInterface::class);
            $prepareService = app(PrepareServiceInterface::class);
            $assistantService = app(AssistantServiceInterface::class);
            foreach ($basketDto->items() as $itemDto) {
                foreach ($itemDto->getItems() as $product) {
                    $transferIds[] = $transfers[$index]->getKey();
                    $objects[] = $prepareService->transferExtraLazy(
                        $product,
                        $castService->getWallet($itemDto->getReceiving() ?? $product),
                        $transfers[$index]->withdraw->wallet,
                        $transfers[$index]->withdraw->wallet,
                        Transfer::STATUS_TRANSFER,
                        $transfers[$index]->deposit->amount,
                        $assistantService->getMeta($basketDto, $product)
                    );

                    $index++;
                }
            }

            if (! $force) {
                app(ConsistencyServiceInterface::class)->checkTransfer($objects);
            }

            assert($objects !== []);

            $transferService = app(TransferServiceInterface::class);

            $transferService->apply($objects);

            return $transferService
                ->updateStatusByIds(Transfer::STATUS_REFUND, $transferIds);
        });
    }

    /**
     * @throws RecordNotFoundException
     * @throws RecordsNotFoundException
     * @throws TransactionFailedException
     * @throws ModelNotFoundException
     * @throws ExceptionInterface
     */
    public function forceRefundCart(CartInterface $cart, bool $gifts = false): bool
    {
        return $this->refundCart($cart, true, $gifts);
    }

    public function safeRefundGiftCart(CartInterface $cart, bool $force = false): bool
    {
        try {
            return $this->refundGiftCart($cart, $force);
        } catch (ExceptionInterface) {
            return false;
        }
    }

    /**
     * @throws BalanceIsEmpty
     * @throws InsufficientFunds
     * @throws RecordNotFoundException
     * @throws RecordsNotFoundException
     * @throws TransactionFailedException
     * @throws ModelNotFoundException
     * @throws ExceptionInterface
     */
    public function refundGiftCart(CartInterface $cart, bool $force = false): bool
    {
        return $this->refundCart($cart, $force, true);
    }

    /**
     * @throws RecordNotFoundException
     * @throws RecordsNotFoundException
     * @throws TransactionFailedException
     * @throws ModelNotFoundException
     * @throws ExceptionInterface
     */
    public function forceRefundGiftCart(CartInterface $cart): bool
    {
        return $this->refundGiftCart($cart, true);
    }

    /**
     * Checks acquired product your wallet.
     *
     * @deprecated The method is slow and will be removed in the future
     * @see PurchaseServiceInterface
     */
    public function paid(ProductInterface $product, bool $gifts = false): ?Transfer
    {
        $cart = app(Cart::class)->withItem($product);
        $purchases = app(PurchaseServiceInterface::class)
            ->already($this, $cart->getBasketDto(), $gifts);

        return current($purchases) ?: null;
    }
}