diff --git a/composer.json b/composer.json index 3e1e3e426..8337c3970 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "ext-xsl": "*", "api-platform/core": "^2.6", "async-aws/ses": "^1.3", - "brick/math": "^0.9 || ^0.10.2", + "brick/math": "^0.11.0", "composer/package-versions-deprecated": "^1.8", "defuse/php-encryption": "^2.2", "doctrine/cache": "^2.2", diff --git a/composer.lock b/composer.lock index 8856b41ed..54a8ed87b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b21c0b94ad4e95820fb15593f2690efb", + "content-hash": "f75b0c71c9112ec9e7a04c4d65b2fdcd", "packages": [ { "name": "alcohol/iso4217", @@ -456,26 +456,25 @@ }, { "name": "brick/math", - "version": "0.10.2", + "version": "0.11.0", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "459f2781e1a08d52ee56b0b1444086e038561e3f" + "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/459f2781e1a08d52ee56b0b1444086e038561e3f", - "reference": "459f2781e1a08d52ee56b0b1444086e038561e3f", + "url": "https://api.github.com/repos/brick/math/zipball/0ad82ce168c82ba30d1c01ec86116ab52f589478", + "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478", "shasum": "" }, "require": { - "ext-json": "*", - "php": "^7.4 || ^8.0" + "php": "^8.0" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", "phpunit/phpunit": "^9.0", - "vimeo/psalm": "4.25.0" + "vimeo/psalm": "5.0.0" }, "type": "library", "autoload": { @@ -500,7 +499,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.10.2" + "source": "https://github.com/brick/math/tree/0.11.0" }, "funding": [ { @@ -508,7 +507,7 @@ "type": "github" } ], - "time": "2022-08-10T22:54:19+00:00" + "time": "2023-01-15T23:15:59+00:00" }, { "name": "clue/stream-filter", diff --git a/config/packages/doctrine.php b/config/packages/doctrine.php index 291d0d82a..2c5d9d9e9 100644 --- a/config/packages/doctrine.php +++ b/config/packages/doctrine.php @@ -6,6 +6,7 @@ use Ramsey\Uuid\Doctrine\UuidType; use SolidInvoice\CoreBundle\Doctrine\Filter\ArchivableFilter; use SolidInvoice\CoreBundle\Doctrine\Filter\CompanyFilter; +use SolidInvoice\CoreBundle\Doctrine\Type\BigIntegerType; use SolidInvoice\MoneyBundle\Doctrine\Hydrator\MoneyHydrator; use Symfony\Config\DoctrineConfig; use function Symfony\Component\DependencyInjection\Loader\Configurator\env; @@ -36,6 +37,10 @@ ->type(UuidBinaryOrderedTimeType::NAME) ->class(UuidBinaryOrderedTimeType::class); + $dbalConfig + ->type(BigIntegerType::NAME) + ->class(BigIntegerType::class); + $ormConfig->autoGenerateProxyClasses(param('kernel.debug')); $entityManagerConfig = $ormConfig->entityManager('default'); diff --git a/src/ApiBundle/Serializer/Normalizer/BigIntegerNormalizer.php b/src/ApiBundle/Serializer/Normalizer/BigIntegerNormalizer.php new file mode 100644 index 000000000..68eb2e8de --- /dev/null +++ b/src/ApiBundle/Serializer/Normalizer/BigIntegerNormalizer.php @@ -0,0 +1,55 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace SolidInvoice\ApiBundle\Serializer\Normalizer; + +use Brick\Math\BigNumber; +use Brick\Math\Exception\MathException; +use Brick\Math\RoundingMode; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use function is_a; + +/** + * @see \SolidInvoice\ApiBundle\Tests\Serializer\Normalizer\BigIntegerNormalizerTest + */ +class BigIntegerNormalizer implements NormalizerInterface, DenormalizerInterface +{ + /** + * @throws MathException + */ + public function denormalize($data, string $type, string $format = null, array $context = []): BigNumber + { + return BigNumber::of($data)->toBigDecimal()->multipliedBy(100)->toBigInteger(); + } + + public function supportsDenormalization($data, $type, $format = null): bool + { + return is_a($type, BigNumber::class, true); + } + + /** + * @param BigNumber $object + * @param array $context + * @throws MathException + */ + public function normalize($object, string $format = null, array $context = []): float + { + return $object->toBigDecimal()->dividedBy(100, 2, RoundingMode::HALF_EVEN)->toFloat(); + } + + public function supportsNormalization($data, $format = null): bool + { + return $data instanceof BigNumber; + } +} diff --git a/src/ApiBundle/Serializer/Normalizer/DiscountNormalizer.php b/src/ApiBundle/Serializer/Normalizer/DiscountNormalizer.php index d050c3d55..08126178c 100644 --- a/src/ApiBundle/Serializer/Normalizer/DiscountNormalizer.php +++ b/src/ApiBundle/Serializer/Normalizer/DiscountNormalizer.php @@ -13,7 +13,7 @@ namespace SolidInvoice\ApiBundle\Serializer\Normalizer; -use Money\Money; +use Brick\Math\BigInteger; use SolidInvoice\CoreBundle\Entity\Discount; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -43,7 +43,7 @@ public function supportsDenormalization($data, $type, $format = null): bool /** * @param Discount $object * @param array $context - * @return array{type: string, value: float|Money|null} + * @return array{type: string, value: BigInteger|int|float|string|null} */ public function normalize($object, string $format = null, array $context = []): array { diff --git a/src/ApiBundle/Serializer/Normalizer/MoneyNormalizer.php b/src/ApiBundle/Serializer/Normalizer/MoneyNormalizer.php deleted file mode 100644 index b6b230447..000000000 --- a/src/ApiBundle/Serializer/Normalizer/MoneyNormalizer.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace SolidInvoice\ApiBundle\Serializer\Normalizer; - -use Money\Currency; -use Money\Money; -use SolidInvoice\MoneyBundle\Entity\Money as MoneyEntity; -use SolidInvoice\MoneyBundle\Formatter\MoneyFormatterInterface; -use SolidInvoice\SettingsBundle\SystemConfig; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * @see \SolidInvoice\ApiBundle\Tests\Serializer\Normalizer\MoneyNormalizerTest - */ -class MoneyNormalizer implements NormalizerInterface, DenormalizerInterface -{ - public function __construct( - private readonly MoneyFormatterInterface $formatter, - private readonly SystemConfig $systemConfig - ) { - } - - /** - * @param string|int $data - * @param array $context - */ - public function denormalize($data, string $type, string $format = null, array $context = []): Money - { - // @TODO: Currency should be determined if there is a client added to the context - return new Money($data * 100, $this->systemConfig->getCurrency()); - } - - /** - * @param mixed $data - */ - public function supportsDenormalization($data, string $type, string $format = null): bool - { - return Money::class === $type || MoneyEntity::class === $type; - } - - /** - * @param Money $object - * @param array $context - */ - public function normalize($object, string $format = null, array $context = []): string - { - return $this->formatter->format($object); - } - - /** - * @param mixed $data - */ - public function supportsNormalization($data, string $format = null): bool - { - return $data instanceof Money; - } -} diff --git a/src/ApiBundle/Tests/Serializer/Normalizer/BigIntegerNormalizerTest.php b/src/ApiBundle/Tests/Serializer/Normalizer/BigIntegerNormalizerTest.php new file mode 100644 index 000000000..cec6d9b79 --- /dev/null +++ b/src/ApiBundle/Tests/Serializer/Normalizer/BigIntegerNormalizerTest.php @@ -0,0 +1,68 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace SolidInvoice\ApiBundle\Tests\Serializer\Normalizer; + +use Brick\Math\BigDecimal; +use Brick\Math\BigInteger; +use Brick\Math\BigNumber; +use Brick\Math\Exception\MathException; +use PHPUnit\Framework\TestCase; +use SolidInvoice\ApiBundle\Serializer\Normalizer\BigIntegerNormalizer; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +final class BigIntegerNormalizerTest extends TestCase +{ + private BigIntegerNormalizer $normalizer; + + protected function setUp(): void + { + $this->normalizer = new BigIntegerNormalizer(); + } + + /** + * @throws MathException + */ + public function testSupportsNormalization(): void + { + self::assertTrue($this->normalizer->supportsNormalization(BigInteger::of(1))); + self::assertTrue($this->normalizer->supportsNormalization(BigDecimal::of(1.1))); + self::assertTrue($this->normalizer->supportsNormalization(BigNumber::of(1.1))); + self::assertFalse($this->normalizer->supportsNormalization(BigInteger::class)); + } + + public function testSupportsDenormalization(): void + { + self::assertTrue($this->normalizer->supportsDenormalization(null, BigInteger::class)); + self::assertTrue($this->normalizer->supportsDenormalization(null, BigDecimal::class)); + self::assertTrue($this->normalizer->supportsDenormalization(null, BigNumber::class)); + self::assertFalse($this->normalizer->supportsDenormalization([], NormalizerInterface::class)); + } + + /** + * @throws MathException + */ + public function testNormalization(): void + { + self::assertEquals(1, $this->normalizer->normalize(BigInteger::of(100))); + } + + /** + * @throws MathException + */ + public function testDenormalization(): void + { + self::assertEquals(BigInteger::of(1000000), $this->normalizer->denormalize(10000, BigNumber::class)); + self::assertEquals(BigInteger::of(1000010), $this->normalizer->denormalize(10000.1, BigNumber::class)); + } +} diff --git a/src/ApiBundle/Tests/Serializer/Normalizer/CreditNormalizerTest.php b/src/ApiBundle/Tests/Serializer/Normalizer/CreditNormalizerTest.php index 221834406..1badaef2a 100644 --- a/src/ApiBundle/Tests/Serializer/Normalizer/CreditNormalizerTest.php +++ b/src/ApiBundle/Tests/Serializer/Normalizer/CreditNormalizerTest.php @@ -13,11 +13,12 @@ namespace SolidInvoice\ApiBundle\Tests\Serializer\Normalizer; -use Money\Currency; -use Money\Money; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use PHPUnit\Framework\TestCase; use SolidInvoice\ApiBundle\Serializer\Normalizer\CreditNormalizer; use SolidInvoice\ClientBundle\Entity\Credit; +use Symfony\Component\Serializer\Exception\ExceptionInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -83,6 +84,10 @@ public function denormalize($data, $class, $format = null, array $context = []) self::assertFalse($normalizer->supportsDenormalization([], NormalizerInterface::class)); } + /** + * @throws MathException + * @throws ExceptionInterface + */ public function testNormalization(): void { $parentNormalizer = new class() implements NormalizerInterface, DenormalizerInterface { @@ -110,10 +115,9 @@ public function denormalize($data, $class, $format = null, array $context = []) $normalizer = new CreditNormalizer($parentNormalizer); $credit = new Credit(); - $money = new Money(10000, new Currency('USD')); - $credit->setValue($money); + $credit->setValue(10000); - self::assertEquals($money, $normalizer->normalize($credit)); + self::assertEquals(BigInteger::of(10000), $normalizer->normalize($credit)); } public function testDenormalization(): void diff --git a/src/ApiBundle/Tests/Serializer/Normalizer/DiscountNormalizerTest.php b/src/ApiBundle/Tests/Serializer/Normalizer/DiscountNormalizerTest.php index 45be4559a..03ac05378 100644 --- a/src/ApiBundle/Tests/Serializer/Normalizer/DiscountNormalizerTest.php +++ b/src/ApiBundle/Tests/Serializer/Normalizer/DiscountNormalizerTest.php @@ -13,8 +13,8 @@ namespace SolidInvoice\ApiBundle\Tests\Serializer\Normalizer; -use Money\Currency; -use Money\Money; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use PHPUnit\Framework\TestCase; use SolidInvoice\ApiBundle\Serializer\Normalizer\DiscountNormalizer; use SolidInvoice\CoreBundle\Entity\Discount; @@ -41,13 +41,16 @@ public function testSupportsDenormalization(): void self::assertFalse($this->normalizer->supportsDenormalization([], NormalizerInterface::class)); } + /** + * @throws MathException + */ public function testNormalization(): void { $discount = new Discount(); $discount->setType(Discount::TYPE_MONEY); $discount->setValue(100); - self::assertEquals(['type' => 'money', 'value' => new Money(10000, new Currency('USD'))], $this->normalizer->normalize($discount)); + self::assertEquals(['type' => 'money', 'value' => BigInteger::of(100)], $this->normalizer->normalize($discount)); } public function testDenormalization(): void diff --git a/src/ApiBundle/Tests/Serializer/Normalizer/MoneyNormalizerTest.php b/src/ApiBundle/Tests/Serializer/Normalizer/MoneyNormalizerTest.php deleted file mode 100644 index 555abe4df..000000000 --- a/src/ApiBundle/Tests/Serializer/Normalizer/MoneyNormalizerTest.php +++ /dev/null @@ -1,68 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace SolidInvoice\ApiBundle\Tests\Serializer\Normalizer; - -use Mockery as M; -use Money\Currency; -use Money\Money; -use PHPUnit\Framework\TestCase; -use SolidInvoice\ApiBundle\Serializer\Normalizer\MoneyNormalizer; -use SolidInvoice\MoneyBundle\Formatter\MoneyFormatter; -use SolidInvoice\SettingsBundle\SystemConfig; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -class MoneyNormalizerTest extends TestCase -{ - use M\Adapter\Phpunit\MockeryPHPUnitIntegration; - - private MoneyNormalizer $normalizer; - - protected function setUp(): void - { - $systemConfig = M::mock(SystemConfig::class); - - $systemConfig - ->shouldReceive('getCurrency') - ->zeroOrMoreTimes() - ->andReturn(new Currency('USD')); - - $this->normalizer = new MoneyNormalizer(new MoneyFormatter('en', $systemConfig), $systemConfig); - } - - public function testSupportsNormalization(): void - { - self::assertTrue($this->normalizer->supportsNormalization(new Money(100, new Currency('USD')))); - self::assertFalse($this->normalizer->supportsNormalization(Money::class)); - } - - public function testSupportsDenormalization(): void - { - self::assertTrue($this->normalizer->supportsDenormalization(null, Money::class)); - self::assertFalse($this->normalizer->supportsDenormalization([], NormalizerInterface::class)); - } - - public function testNormalization(): void - { - $money = new Money(10000, new Currency('USD')); - - self::assertSame('$100.00', $this->normalizer->normalize($money)); - } - - public function testDenormalization(): void - { - $money = new Money(10000, new Currency('USD')); - - self::assertEquals($money, $this->normalizer->denormalize(100, Money::class)); - } -} diff --git a/src/ClientBundle/Action/Ajax/Credit.php b/src/ClientBundle/Action/Ajax/Credit.php index 2c22b56f6..a45b4cfa2 100644 --- a/src/ClientBundle/Action/Ajax/Credit.php +++ b/src/ClientBundle/Action/Ajax/Credit.php @@ -13,7 +13,9 @@ namespace SolidInvoice\ClientBundle\Action\Ajax; -use Money\Money; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; +use JsonException; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\ClientBundle\Repository\CreditRepository; use SolidInvoice\CoreBundle\Response\AjaxResponse; @@ -36,18 +38,22 @@ public function get(Client $client): JsonResponse return $this->toJson($client); } + /** + * @throws MathException + * @throws JsonException + */ public function put(Request $request, Client $client): JsonResponse { - $value = new Money( - ((json_decode($request->getContent() ?: '[]', true, 512, JSON_THROW_ON_ERROR)['credit'] ?? 0) * 100), - $client->getCurrency() - ); + $value = BigInteger::of(((json_decode($request->getContent() ?: '[]', true, 512, JSON_THROW_ON_ERROR)['credit'] ?? 0) * 100)); $this->repository->addCredit($client, $value); return $this->toJson($client); } + /** + * @throws MathException + */ private function toJson(Client $client): JsonResponse { return $this->json( diff --git a/src/ClientBundle/Entity/Client.php b/src/ClientBundle/Entity/Client.php index 3ca10d5b7..76bd69074 100644 --- a/src/ClientBundle/Entity/Client.php +++ b/src/ClientBundle/Entity/Client.php @@ -327,7 +327,7 @@ public function getAddresses(): Collection return $this->addresses; } - public function getCredit(): ?Credit + public function getCredit(): Credit { return $this->credit; } diff --git a/src/ClientBundle/Entity/Credit.php b/src/ClientBundle/Entity/Credit.php index c166b82a4..467fa7493 100644 --- a/src/ClientBundle/Entity/Credit.php +++ b/src/ClientBundle/Entity/Credit.php @@ -13,15 +13,16 @@ namespace SolidInvoice\ClientBundle\Entity; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Doctrine\ORM\Mapping as ORM; -use Money\Money; use Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType; use Ramsey\Uuid\Doctrine\UuidOrderedTimeGenerator; use Ramsey\Uuid\UuidInterface; use SolidInvoice\ClientBundle\Repository\CreditRepository; +use SolidInvoice\CoreBundle\Doctrine\Type\BigIntegerType; use SolidInvoice\CoreBundle\Traits\Entity\CompanyAware; use SolidInvoice\CoreBundle\Traits\Entity\TimeStampable; -use SolidInvoice\MoneyBundle\Entity\Money as MoneyEntity; use Stringable; #[ORM\Table(name: Credit::TABLE_NAME)] @@ -39,15 +40,15 @@ class Credit implements Stringable #[ORM\CustomIdGenerator(class: UuidOrderedTimeGenerator::class)] private ?UuidInterface $id = null; - #[ORM\Embedded(class: MoneyEntity::class)] - private MoneyEntity $value; + #[ORM\Column(name: 'value_amount', type: BigIntegerType::NAME)] + private BigInteger $value; #[ORM\OneToOne(inversedBy: 'credit', targetEntity: Client::class)] private ?Client $client = null; public function __construct() { - $this->value = new MoneyEntity(); + $this->value = BigInteger::zero(); } public function getId(): UuidInterface @@ -67,24 +68,23 @@ public function setClient(Client $client): self return $this; } - public function getValue(): Money + public function getValue(): BigInteger { - if ($this->value->getCurrency() === null) { - $this->value->setCurrency($this->client->getCurrency()->getCode()); - } - - return $this->value->getMoney(); + return $this->value; } - public function setValue(Money $value): self + /** + * @throws MathException + */ + public function setValue(BigInteger|float|int|string $value): self { - $this->value = new MoneyEntity($value); + $this->value = BigInteger::of($value); return $this; } public function __toString(): string { - return $this->value->getMoney()->getAmount(); + return (string) $this->value->toInt(); } } diff --git a/src/ClientBundle/Listener/ClientListener.php b/src/ClientBundle/Listener/ClientListener.php index ca3dedad6..a527f5adb 100644 --- a/src/ClientBundle/Listener/ClientListener.php +++ b/src/ClientBundle/Listener/ClientListener.php @@ -14,21 +14,16 @@ namespace SolidInvoice\ClientBundle\Listener; use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface; -use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Events; use Doctrine\Persistence\Event\LifecycleEventArgs; use Doctrine\Persistence\ObjectManager; use JsonException; use Money\Currency; -use Money\Money; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\ClientBundle\Entity\Credit; use SolidInvoice\ClientBundle\Model\Status; use SolidInvoice\ClientBundle\Notification\ClientCreateNotification; -use SolidInvoice\InvoiceBundle\Entity\Invoice; use SolidInvoice\NotificationBundle\Notification\NotificationManager; -use SolidInvoice\PaymentBundle\Entity\Payment; -use SolidInvoice\QuoteBundle\Entity\Quote; use SolidInvoice\SettingsBundle\SystemConfig; class ClientListener implements EventSubscriberInterface @@ -47,7 +42,6 @@ public function getSubscribedEvents(): array return [ Events::prePersist, Events::postPersist, - Events::postUpdate, Events::postLoad, ]; } @@ -72,7 +66,6 @@ public function prePersist(LifecycleEventArgs $event): void $credit = new Credit(); $credit->setClient($entity); - $credit->setValue(new Money(0, $entity->getCurrency())); $entity->setCredit($credit); } } @@ -112,32 +105,4 @@ public function postPersist(LifecycleEventArgs $event): void $this->notification->sendNotification('client_create', $notification); } - - /** - * @param LifecycleEventArgs $event - */ - public function postUpdate(LifecycleEventArgs $event): void - { - $entity = $event->getObject(); - - if (! $entity instanceof Client) { - return; - } - - $objectManager = $event->getObjectManager(); - - assert($objectManager instanceof EntityManagerInterface); - - $entityChangeSet = $objectManager->getUnitOfWork()->getEntityChangeSet($entity); - - // Only update the currencies when the client currency changed - if (array_key_exists('currency', $entityChangeSet)) { - $em = $objectManager; - - $em->getRepository(Invoice::class)->updateCurrency($entity); - $em->getRepository(Quote::class)->updateCurrency($entity); - $em->getRepository(Payment::class)->updateCurrency($entity); - $em->getRepository(Credit::class)->updateCurrency($entity); - } - } } diff --git a/src/ClientBundle/Repository/CreditRepository.php b/src/ClientBundle/Repository/CreditRepository.php index 5126ee88d..1f2806ed9 100644 --- a/src/ClientBundle/Repository/CreditRepository.php +++ b/src/ClientBundle/Repository/CreditRepository.php @@ -13,9 +13,10 @@ namespace SolidInvoice\ClientBundle\Repository; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; -use Money\Money; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\ClientBundle\Entity\Credit; @@ -26,20 +27,26 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, Credit::class); } - public function addCredit(Client $client, Money $amount): Credit + /** + * @throws MathException + */ + public function addCredit(Client $client, BigInteger|float|int|string $amount): Credit { $credit = $client->getCredit(); - $credit->setValue($credit->getValue()->add($amount)); + $credit->setValue($credit->getValue()->plus($amount)); return $this->save($credit); } - public function deductCredit(Client $client, Money $amount): Credit + /** + * @throws MathException + */ + public function deductCredit(Client $client, BigInteger|float|int|string $amount): Credit { $credit = $client->getCredit(); - $credit->setValue($credit->getValue()->subtract($amount)); + $credit->setValue($credit->getValue()->minus($amount)); return $this->save($credit); } @@ -52,22 +59,4 @@ private function save(Credit $credit): Credit return $credit; } - - public function updateCurrency(Client $client): void - { - $filters = $this->getEntityManager()->getFilters(); - $filters->disable('archivable'); - - $qb = $this->createQueryBuilder('c'); - - $qb->update() - ->set('c.value.currency', ':currency') - ->where('c.client = :client') - ->setParameter('currency', $client->getCurrency()) - ->setParameter('client', $client); - - $qb->getQuery()->execute(); - - $filters->enable('archivable'); - } } diff --git a/src/ClientBundle/Tests/Form/Type/CreditTypeTest.php b/src/ClientBundle/Tests/Form/Type/CreditTypeTest.php index 8712fd3d5..11d542d33 100644 --- a/src/ClientBundle/Tests/Form/Type/CreditTypeTest.php +++ b/src/ClientBundle/Tests/Form/Type/CreditTypeTest.php @@ -13,13 +13,16 @@ namespace SolidInvoice\ClientBundle\Tests\Form\Type; -use Money\Currency; -use Money\Money; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use SolidInvoice\ClientBundle\Form\Type\CreditType; use SolidInvoice\CoreBundle\Tests\FormTestCase; class CreditTypeTest extends FormTestCase { + /** + * @throws MathException + */ public function testSubmit(): void { $amount = $this->faker->numberBetween(0, 10000); @@ -29,7 +32,7 @@ public function testSubmit(): void ]; $object = [ - 'amount' => new Money($amount * 100, new Currency('USD')), + 'amount' => BigInteger::of($amount * 100), ]; $this->assertFormData(CreditType::class, $formData, $object); diff --git a/src/ClientBundle/Tests/Functional/Api/ClientTest.php b/src/ClientBundle/Tests/Functional/Api/ClientTest.php index 5b00bf768..135ad4727 100644 --- a/src/ClientBundle/Tests/Functional/Api/ClientTest.php +++ b/src/ClientBundle/Tests/Functional/Api/ClientTest.php @@ -75,7 +75,7 @@ public function testCreate(): void ], ], 'addresses' => [], - 'credit' => '$0.00', + 'credit' => 0, ], $result); } @@ -111,7 +111,7 @@ public function testGet(): void ], ], 'addresses' => [], - 'credit' => '$0.00', + 'credit' => 0, ], $data); } @@ -139,7 +139,7 @@ public function testEdit(): void ], ], 'addresses' => [], - 'credit' => '$0.00', + 'credit' => 0, ], $data); } } diff --git a/src/CoreBundle/Billing/TotalCalculator.php b/src/CoreBundle/Billing/TotalCalculator.php index 399d6cdf4..e5b155823 100644 --- a/src/CoreBundle/Billing/TotalCalculator.php +++ b/src/CoreBundle/Billing/TotalCalculator.php @@ -13,7 +13,8 @@ namespace SolidInvoice\CoreBundle\Billing; -use Money\Money; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use SolidInvoice\CoreBundle\Entity\Discount; use SolidInvoice\CoreBundle\Exception\UnexpectedTypeException; use SolidInvoice\InvoiceBundle\Entity\BaseInvoice; @@ -32,6 +33,9 @@ public function __construct( ) { } + /** + * @throws MathException + */ public function calculateTotals($entity): void { if (! $entity instanceof BaseInvoice && ! $entity instanceof Quote) { @@ -42,28 +46,39 @@ public function calculateTotals($entity): void if ($entity instanceof Invoice) { $totalPaid = $this->paymentRepository->getTotalPaidForInvoice($entity); - $entity->setBalance($entity->getTotal()->subtract(new Money($totalPaid, $entity->getClient()->getCurrency()))); + $entity->setBalance($entity->getTotal()->minus($totalPaid)); } } + /** + * @throws MathException + */ private function updateTotal($entity): void { /** @var BaseInvoice|Quote $entity */ - $total = new Money(0, $entity->getClient()->getCurrency()); - $subTotal = new Money(0, $entity->getClient()->getCurrency()); - $tax = new Money(0, $entity->getClient()->getCurrency()); + $total = BigInteger::zero(); + $subTotal = BigInteger::zero(); + $tax = BigInteger::zero(); foreach ($entity->getItems() as $item) { - $item->setTotal($item->getPrice()->multiply($item->getQty())); + $item->setTotal($item->getPrice()->multipliedBy($item->getQty())); $rowTotal = $item->getTotal(); - $total = $total->add($item->getTotal()); - $subTotal = $subTotal->add($item->getTotal()); + $total = $total->plus($item->getTotal()); + $subTotal = $subTotal->plus($item->getTotal()); if (($rowTax = $item->getTax()) instanceof Tax) { - $this->setTax($rowTax, $rowTotal, $subTotal, $total, $tax); + if (Tax::TYPE_INCLUSIVE === $rowTax->getType()) { + $taxAmount = $rowTotal->toBigDecimal()->dividedBy(($rowTax->getRate() / 100) + 1)->minus($rowTotal)->negated(); + $subTotal = $subTotal->minus($taxAmount); + } else { + $taxAmount = $rowTotal->toBigDecimal()->multipliedBy($rowTax->getRate() / 100); + $total = $total->plus($taxAmount); + } + + $tax = $tax->plus($taxAmount); } } @@ -77,30 +92,19 @@ private function updateTotal($entity): void $entity->setTax($tax); } - private function setDiscount(BaseInvoice|Quote $entity, Money $total): Money + /** + * @throws MathException + */ + private function setDiscount(BaseInvoice|Quote $entity, BigInteger $total): BigInteger { $discount = $entity->getDiscount(); - $discountValue = null; if (Discount::TYPE_PERCENTAGE === $discount->getType()) { - $discountValue = $total->multiply(((float) $discount->getValuePercentage()) / 100); + $discountValue = $total->toBigDecimal()->multipliedBy(((float) $discount->getValuePercentage()) / 100); } else { - $discountValue = $discount->getValueMoney()->getMoney(); + $discountValue = $discount->getValueMoney(); } - return $total->subtract($discountValue); - } - - private function setTax(Tax $rowTax, Money $rowTotal, Money &$subTotal, Money &$total, Money &$tax): void - { - if (Tax::TYPE_INCLUSIVE === $rowTax->getType()) { - $taxAmount = $rowTotal->divide(($rowTax->getRate() / 100) + 1)->subtract($rowTotal)->multiply(-1); - $subTotal = $subTotal->subtract($taxAmount); - } else { - $taxAmount = $rowTotal->multiply($rowTax->getRate() / 100); - $total = $total->add($taxAmount); - } - - $tax = $tax->add($taxAmount); + return $total->minus($discountValue); } } diff --git a/src/CoreBundle/Company/CompanySelector.php b/src/CoreBundle/Company/CompanySelector.php index 52e0b8c74..5b88303a7 100644 --- a/src/CoreBundle/Company/CompanySelector.php +++ b/src/CoreBundle/Company/CompanySelector.php @@ -20,8 +20,6 @@ use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidFactory; use Ramsey\Uuid\UuidInterface; -use RuntimeException; -use SolidInvoice\SettingsBundle\SystemConfig; use function assert; final class CompanySelector @@ -31,8 +29,7 @@ final class CompanySelector private readonly OrderedTimeCodec $codec; public function __construct( - private readonly ManagerRegistry $registry, - private readonly SystemConfig $config, + private readonly ManagerRegistry $registry ) { $factory = clone Uuid::getFactory(); assert($factory instanceof UuidFactory); @@ -59,11 +56,5 @@ public function switchCompany(UuidInterface $companyId): void ->setParameter('companyId', $companyIdBytes, Types::STRING); $this->companyId = $companyId; - - try { - Currency::set($this->config->getCurrency()); - } catch (RuntimeException) { - // Currency is not set, so we can't set it here - } } } diff --git a/src/CoreBundle/Company/Currency.php b/src/CoreBundle/Company/Currency.php deleted file mode 100644 index ea2496def..000000000 --- a/src/CoreBundle/Company/Currency.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace SolidInvoice\CoreBundle\Company; - -use Money\Currency as MoneyCurrency; - -final class Currency -{ - private static MoneyCurrency $currency; - - public static function set(MoneyCurrency $currency): void - { - self::$currency = $currency; - } - - public static function get(): MoneyCurrency - { - if (! isset(self::$currency)) { - throw new \RuntimeException('Currency has not been set'); - } - - return self::$currency; - } -} diff --git a/src/CoreBundle/Doctrine/Type/BigIntegerType.php b/src/CoreBundle/Doctrine/Type/BigIntegerType.php new file mode 100644 index 000000000..fae37a61f --- /dev/null +++ b/src/CoreBundle/Doctrine/Type/BigIntegerType.php @@ -0,0 +1,69 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace SolidInvoice\CoreBundle\Doctrine\Type; + +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\Type; +use function get_class; + +final class BigIntegerType extends Type +{ + public const NAME = 'BigInteger'; + + public function getName(): string + { + return self::NAME; + } + + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string + { + return $platform->getBigIntTypeDeclarationSQL($column); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + try { + return BigInteger::of($value); + } catch (MathException $e) { + throw ConversionException::conversionFailedSerialization($value, $this->getName(), $e::class, $e); + } + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?int + { + if ($value === null) { + return null; + } + + if ($value instanceof BigInteger) { + try { + return $value->toInt(); + } catch (MathException $e) { + throw ConversionException::conversionFailedSerialization($value, $this->getName(), $e::class, $e); + } + } + + throw ConversionException::conversionFailedFormat($value, $this->getName(), get_class($value)); + } + + public function requiresSQLCommentHint(AbstractPlatform $platform): bool + { + return true; + } +} diff --git a/src/CoreBundle/Entity/Discount.php b/src/CoreBundle/Entity/Discount.php index 962b9a2ff..a5d3870bc 100644 --- a/src/CoreBundle/Entity/Discount.php +++ b/src/CoreBundle/Entity/Discount.php @@ -13,11 +13,11 @@ namespace SolidInvoice\CoreBundle\Entity; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Money\Currency; -use Money\Money; -use SolidInvoice\MoneyBundle\Entity\Money as MoneyEntity; +use SolidInvoice\CoreBundle\Doctrine\Type\BigIntegerType; use Symfony\Component\Serializer\Annotation as Serialize; #[ORM\Embeddable] @@ -27,9 +27,9 @@ class Discount final public const TYPE_MONEY = 'money'; - #[ORM\Embedded(class: MoneyEntity::class)] + #[ORM\Column(name: 'valueMoney_amount', type: BigIntegerType::NAME)] #[Serialize\Groups(['invoice_api', 'quote_api', 'client_api'])] - private MoneyEntity $valueMoney; + private BigInteger $valueMoney; #[ORM\Column(name: 'value_percentage', type: Types::FLOAT, nullable: true)] #[Serialize\Groups(['invoice_api', 'quote_api', 'client_api'])] @@ -41,7 +41,7 @@ class Discount public function __construct() { - $this->valueMoney = new MoneyEntity(); + $this->valueMoney = BigInteger::zero(); } public function getType(): ?string @@ -56,14 +56,17 @@ public function setType(?string $type): self return $this; } - public function getValueMoney(): ?MoneyEntity + public function getValueMoney(): BigInteger { return $this->valueMoney; } - public function setValueMoney(MoneyEntity $valueMoney): self + /** + * @throws MathException + */ + public function setValueMoney(BigInteger|float|int|string $valueMoney): self { - $this->valueMoney = $valueMoney; + $this->valueMoney = BigInteger::of($valueMoney); return $this; } @@ -80,27 +83,27 @@ public function setValuePercentage(float $valuePercentage): self return $this; } - public function getValue(): float| Money |null + public function getValue(): float | BigInteger { return match ($this->getType()) { - self::TYPE_PERCENTAGE => $this->getValuePercentage(), - self::TYPE_MONEY => $this->getValueMoney() instanceof MoneyEntity ? $this->getValueMoney()->getMoney() : null, - default => null, + self::TYPE_PERCENTAGE => $this->getValuePercentage() ?? 0.0, + self::TYPE_MONEY => $this->getValueMoney(), + default => BigInteger::zero(), }; } - public function setValue(float| Money |null $value): void + /** + * @throws MathException + */ + public function setValue(float | BigInteger $value): void { switch ($this->getType()) { case self::TYPE_PERCENTAGE: $this->setValuePercentage((float) $value); - break; case self::TYPE_MONEY: - // @TODO: USD should not be hard-coded - $this->setValueMoney(new MoneyEntity(new Money(((int) $value) * 100, new Currency('USD')))); - + $this->setValueMoney($value); break; } } diff --git a/src/CoreBundle/Entity/ItemInterface.php b/src/CoreBundle/Entity/ItemInterface.php index 779d201c4..aabfffaea 100644 --- a/src/CoreBundle/Entity/ItemInterface.php +++ b/src/CoreBundle/Entity/ItemInterface.php @@ -13,7 +13,7 @@ namespace SolidInvoice\CoreBundle\Entity; -use Money\Money; +use Brick\Math\BigInteger; use Ramsey\Uuid\UuidInterface; use SolidInvoice\TaxBundle\Entity\Tax; @@ -25,17 +25,17 @@ public function setDescription(string $description): self; public function getDescription(): ?string; - public function setPrice(Money $price): self; + public function setPrice(BigInteger|float|int|string $price): self; - public function getPrice(): ?Money; + public function getPrice(): BigInteger; public function setQty(float $qty): self; public function getQty(): ?float; - public function setTotal(Money $total): self; + public function setTotal(BigInteger|float|int|string $total): self; - public function getTotal(): ?Money; + public function getTotal(): BigInteger; public function getTax(): ?Tax; diff --git a/src/CoreBundle/Form/Transformer/DiscountTransformer.php b/src/CoreBundle/Form/Transformer/DiscountTransformer.php index 535d9659b..041f87024 100644 --- a/src/CoreBundle/Form/Transformer/DiscountTransformer.php +++ b/src/CoreBundle/Form/Transformer/DiscountTransformer.php @@ -13,13 +13,22 @@ namespace SolidInvoice\CoreBundle\Form\Transformer; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Money\Money; use Symfony\Component\Form\DataTransformerInterface; class DiscountTransformer implements DataTransformerInterface { + /** + * @throws MathException + */ public function transform($value): ?int { + if ($value instanceof BigInteger) { + return $value->toInt(); + } + if (! $value instanceof Money) { return null !== $value ? (int) $value : $value; } diff --git a/src/CoreBundle/Tests/Billing/TotalCalculatorTest.php b/src/CoreBundle/Tests/Billing/TotalCalculatorTest.php index 9d659d006..a535e717b 100644 --- a/src/CoreBundle/Tests/Billing/TotalCalculatorTest.php +++ b/src/CoreBundle/Tests/Billing/TotalCalculatorTest.php @@ -13,9 +13,12 @@ namespace SolidInvoice\CoreBundle\Tests\Billing; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; +use Doctrine\ORM\Exception\NotSupported; +use Doctrine\ORM\Exception\ORMException; +use Doctrine\ORM\OptimisticLockException; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; -use Money\Currency; -use Money\Money; use PHPUnit\Framework\TestCase; use SolidInvoice\ClientBundle\Test\Factory\ClientFactory; use SolidInvoice\CoreBundle\Billing\TotalCalculator; @@ -44,54 +47,63 @@ public function testOnlyAcceptsQuotesOrInvoices(): void $updater->calculateTotals(new stdClass()); } + /** + * @throws MathException + * @throws NotSupported + */ public function testUpdateWithSingleItem(): void { $updater = new TotalCalculator($this->em->getRepository(Payment::class)); $invoice = new Invoice(); $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD', 'company' => $this->company])->object()); - $invoice->setTotal(new Money(0, new Currency('USD'))); $item = new Item(); $item->setQty(1) - ->setPrice(new Money(15000, new Currency('USD'))); + ->setPrice(15000); $invoice->addItem($item); $updater->calculateTotals($invoice); - self::assertEquals(new Money(15000, new Currency('USD')), $invoice->getTotal()); - self::assertEquals(new Money(15000, new Currency('USD')), $invoice->getBalance()); - self::assertEquals(new Money(15000, new Currency('USD')), $invoice->getBaseTotal()); + self::assertEquals(BigInteger::of(15000), $invoice->getTotal()); + self::assertEquals(BigInteger::of(15000), $invoice->getBalance()); + self::assertEquals(BigInteger::of(15000), $invoice->getBaseTotal()); } + /** + * @throws MathException + * @throws NotSupported + */ public function testUpdateWithSingleItemAndMultipleQtys(): void { $updater = new TotalCalculator($this->em->getRepository(Payment::class)); $invoice = new Invoice(); $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD', 'company' => $this->company])->object()); - $invoice->setTotal(new Money(0, new Currency('USD'))); $item = new Item(); $item->setQty(2) - ->setPrice(new Money(15000, new Currency('USD'))); + ->setPrice(15000); $invoice->addItem($item); $updater->calculateTotals($invoice); - self::assertEquals(new Money(30000, new Currency('USD')), $invoice->getTotal()); - self::assertEquals(new Money(30000, new Currency('USD')), $invoice->getBalance()); - self::assertEquals(new Money(30000, new Currency('USD')), $invoice->getBaseTotal()); + self::assertEquals(BigInteger::of(30000), $invoice->getTotal()); + self::assertEquals(BigInteger::of(30000), $invoice->getBalance()); + self::assertEquals(BigInteger::of(30000), $invoice->getBaseTotal()); } + /** + * @throws MathException + * @throws NotSupported + */ public function testUpdateWithPercentageDiscount(): void { $updater = new TotalCalculator($this->em->getRepository(Payment::class)); $invoice = new Invoice(); $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD', 'company' => $this->company])->object()); - $invoice->setTotal(new Money(0, new Currency('USD'))); $item = new Item(); $item->setQty(2) - ->setPrice(new Money(15000, new Currency('USD'))); + ->setPrice(15000); $invoice->addItem($item); $discount = new Discount(); $discount->setType(Discount::TYPE_PERCENTAGE); @@ -100,21 +112,24 @@ public function testUpdateWithPercentageDiscount(): void $updater->calculateTotals($invoice); - self::assertEquals(new Money(25500, new Currency('USD')), $invoice->getTotal()); - self::assertEquals(new Money(25500, new Currency('USD')), $invoice->getBalance()); - self::assertEquals(new Money(30000, new Currency('USD')), $invoice->getBaseTotal()); + self::assertEquals(BigInteger::of(25500), $invoice->getTotal()); + self::assertEquals(BigInteger::of(25500), $invoice->getBalance()); + self::assertEquals(BigInteger::of(30000), $invoice->getBaseTotal()); } + /** + * @throws MathException + * @throws NotSupported + */ public function testUpdateWithMonetaryDiscount(): void { $updater = new TotalCalculator($this->em->getRepository(Payment::class)); $invoice = new Invoice(); - $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD', 'company' => $this->company])->object()); - $invoice->setTotal(new Money(0, new Currency('USD'))); + $invoice->setClient(ClientFactory::createOne(['company' => $this->company])->object()); $item = new Item(); $item->setQty(2) - ->setPrice(new Money(15000, new Currency('USD'))); + ->setPrice(15000); $invoice->addItem($item); $discount = new Discount(); $discount->setType(Discount::TYPE_MONEY); @@ -123,11 +138,15 @@ public function testUpdateWithMonetaryDiscount(): void $updater->calculateTotals($invoice); - self::assertEquals(new Money(22000, new Currency('USD')), $invoice->getTotal()); - self::assertEquals(new Money(22000, new Currency('USD')), $invoice->getBalance()); - self::assertEquals(new Money(30000, new Currency('USD')), $invoice->getBaseTotal()); + self::assertEquals(BigInteger::of(29920), $invoice->getTotal()); + self::assertEquals(BigInteger::of(29920), $invoice->getBalance()); + self::assertEquals(BigInteger::of(30000), $invoice->getBaseTotal()); } + /** + * @throws MathException + * @throws NotSupported + */ public function testUpdateWithTaxIncl(): void { $updater = new TotalCalculator($this->em->getRepository(Payment::class)); @@ -138,22 +157,25 @@ public function testUpdateWithTaxIncl(): void $invoice = new Invoice(); $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD', 'company' => $this->company])->object()); - $invoice->setTotal(new Money(0, new Currency('USD'))); $item = new Item(); $item->setQty(2) - ->setPrice(new Money(15000, new Currency('USD'))) + ->setPrice(15000) ->setTax($tax); $invoice->addItem($item); $updater->calculateTotals($invoice); - self::assertEquals(new Money(30000, new Currency('USD')), $invoice->getTotal()); - self::assertEquals(new Money(30000, new Currency('USD')), $invoice->getBalance()); - self::assertEquals(new Money(25000, new Currency('USD')), $invoice->getBaseTotal()); - self::assertEquals(new Money(5000, new Currency('USD')), $invoice->getTax()); + self::assertEquals(BigInteger::of(30000), $invoice->getTotal()); + self::assertEquals(BigInteger::of(30000), $invoice->getBalance()); + self::assertEquals(BigInteger::of(25000), $invoice->getBaseTotal()); + self::assertEquals(BigInteger::of(5000), $invoice->getTax()); } + /** + * @throws MathException + * @throws NotSupported + */ public function testUpdateWithTaxExcl(): void { $updater = new TotalCalculator($this->em->getRepository(Payment::class)); @@ -164,22 +186,25 @@ public function testUpdateWithTaxExcl(): void $invoice = new Invoice(); $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD', 'company' => $this->company])->object()); - $invoice->setTotal(new Money(0, new Currency('USD'))); $item = new Item(); $item->setQty(2) - ->setPrice(new Money(15000, new Currency('USD'))) + ->setPrice(15000) ->setTax($tax); $invoice->addItem($item); $updater->calculateTotals($invoice); - self::assertEquals(new Money(36000, new Currency('USD')), $invoice->getTotal()); - self::assertEquals(new Money(36000, new Currency('USD')), $invoice->getBalance()); - self::assertEquals(new Money(30000, new Currency('USD')), $invoice->getBaseTotal()); - self::assertEquals(new Money(6000, new Currency('USD')), $invoice->getTax()); + self::assertEquals(BigInteger::of(36000), $invoice->getTotal()); + self::assertEquals(BigInteger::of(36000), $invoice->getBalance()); + self::assertEquals(BigInteger::of(30000), $invoice->getBaseTotal()); + self::assertEquals(BigInteger::of(6000), $invoice->getTax()); } + /** + * @throws MathException + * @throws NotSupported + */ public function testUpdateWithTaxInclAndPercentageDiscount(): void { $updater = new TotalCalculator($this->em->getRepository(Payment::class)); @@ -190,10 +215,9 @@ public function testUpdateWithTaxInclAndPercentageDiscount(): void $invoice = new Invoice(); $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD', 'company' => $this->company])->object()); - $invoice->setTotal(new Money(0, new Currency('USD'))); $item = new Item(); $item->setQty(2) - ->setPrice(new Money(15000, new Currency('USD'))) + ->setPrice(15000) ->setTax($tax); $invoice->addItem($item); $discount = new Discount(); @@ -203,12 +227,16 @@ public function testUpdateWithTaxInclAndPercentageDiscount(): void $updater->calculateTotals($invoice); - self::assertEquals(new Money(25500, new Currency('USD')), $invoice->getTotal()); - self::assertEquals(new Money(25500, new Currency('USD')), $invoice->getBalance()); - self::assertEquals(new Money(25000, new Currency('USD')), $invoice->getBaseTotal()); - self::assertEquals(new Money(5000, new Currency('USD')), $invoice->getTax()); + self::assertEquals(BigInteger::of(25500), $invoice->getTotal()); + self::assertEquals(BigInteger::of(25500), $invoice->getBalance()); + self::assertEquals(BigInteger::of(25000), $invoice->getBaseTotal()); + self::assertEquals(BigInteger::of(5000), $invoice->getTax()); } + /** + * @throws MathException + * @throws NotSupported + */ public function testUpdateWithTaxExclAndMonetaryDiscount(): void { $updater = new TotalCalculator($this->em->getRepository(Payment::class)); @@ -219,10 +247,9 @@ public function testUpdateWithTaxExclAndMonetaryDiscount(): void $invoice = new Invoice(); $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD', 'company' => $this->company])->object()); - $invoice->setTotal(new Money(0, new Currency('USD'))); $item = new Item(); $item->setQty(2) - ->setPrice(new Money(15000, new Currency('USD'))) + ->setPrice(15000) ->setTax($tax); $invoice->addItem($item); $discount = new Discount(); @@ -232,23 +259,29 @@ public function testUpdateWithTaxExclAndMonetaryDiscount(): void $updater->calculateTotals($invoice); - self::assertEquals(new Money(28000, new Currency('USD')), $invoice->getTotal()); - self::assertEquals(new Money(28000, new Currency('USD')), $invoice->getBalance()); - self::assertEquals(new Money(30000, new Currency('USD')), $invoice->getBaseTotal()); - self::assertEquals(new Money(6000, new Currency('USD')), $invoice->getTax()); + self::assertEquals(BigInteger::of(35920), $invoice->getTotal()); + self::assertEquals(BigInteger::of(35920), $invoice->getBalance()); + self::assertEquals(BigInteger::of(30000), $invoice->getBaseTotal()); + self::assertEquals(BigInteger::of(6000), $invoice->getTax()); } + /** + * @throws ORMException + * @throws OptimisticLockException + * @throws MathException + * @throws NotSupported + */ public function testUpdateTotalsWithPayments(): void { $invoice = new Invoice(); $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD', 'company' => $this->company])->object()); - $invoice->setTotal(new Money(30000, new Currency('USD'))); - $invoice->setBaseTotal(new Money(30000, new Currency('USD'))); - $invoice->setBalance(new Money(30000, new Currency('USD'))); + $invoice->setTotal(30000); + $invoice->setBaseTotal(30000); + $invoice->setBalance(30000); $invoice->setStatus(Graph::STATUS_PENDING); $item = new Item(); $item->setQty(2) - ->setPrice(new Money(15000, new Currency('USD'))) + ->setPrice(15000) ->setDescription('foobar'); $invoice->addItem($item); @@ -264,8 +297,8 @@ public function testUpdateTotalsWithPayments(): void $updater->calculateTotals($invoice); - self::assertEquals(new Money(30000, new Currency('USD')), $invoice->getTotal()); - self::assertEquals(new Money(29000, new Currency('USD')), $invoice->getBalance()); - self::assertEquals(new Money(30000, new Currency('USD')), $invoice->getBaseTotal()); + self::assertEquals(BigInteger::of(30000), $invoice->getTotal()); + self::assertEquals(BigInteger::of(29000), $invoice->getBalance()); + self::assertEquals(BigInteger::of(30000), $invoice->getBaseTotal()); } } diff --git a/src/CoreBundle/Tests/Listener/CompanyEventSubscriberTest.php b/src/CoreBundle/Tests/Listener/CompanyEventSubscriberTest.php index 65f71cd35..b57d0f6d8 100644 --- a/src/CoreBundle/Tests/Listener/CompanyEventSubscriberTest.php +++ b/src/CoreBundle/Tests/Listener/CompanyEventSubscriberTest.php @@ -20,7 +20,6 @@ use Doctrine\ORM\Query\FilterCollection; use Doctrine\Persistence\ManagerRegistry; use Mockery as M; -use Money\Currency; use PHPUnit\Framework\TestCase; use Ramsey\Uuid\Codec\OrderedTimeCodec; use Ramsey\Uuid\Uuid; @@ -30,7 +29,6 @@ use SolidInvoice\CoreBundle\Company\CompanySelector; use SolidInvoice\CoreBundle\Entity\Company; use SolidInvoice\CoreBundle\Listener\CompanyEventSubscriber; -use SolidInvoice\SettingsBundle\SystemConfig; use SolidInvoice\UserBundle\Entity\User; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -54,7 +52,7 @@ public function testItRedirectsToCompanySelectPageIfACompanyIsNotSetAndUserHasMu // Test that it redirects to the company select page if a company is not set and the user has multiple companies $router = M::mock(RouterInterface::class); - $companySelector = new CompanySelector(M::mock(ManagerRegistry::class), M::mock(SystemConfig::class)); + $companySelector = new CompanySelector(M::mock(ManagerRegistry::class)); $security = M::mock(Security::class); $user = new User(); @@ -92,14 +90,8 @@ public function testItSetsTheCompanyWhenNoCompanyIsSetAndTheUserOnlyHasOneCompan $router = M::mock(RouterInterface::class); $registry = M::mock(ManagerRegistry::class); $security = M::mock(Security::class); - $config = M::mock(SystemConfig::class); - $config - ->shouldReceive('getCurrency') - ->once() - ->andReturn(new Currency('USD')); - - $companySelector = new CompanySelector($registry, $config); + $companySelector = new CompanySelector($registry); $user = new User(); $company = new Company(); @@ -138,7 +130,7 @@ public function testItContinueTheRequestWhenACompanyIsNotSetAndTheUserIsOnACompa // Test that it continues the request when a company is not set and the user is on a company select page $router = M::mock(RouterInterface::class); - $companySelector = new CompanySelector(M::mock(ManagerRegistry::class), M::mock(SystemConfig::class)); + $companySelector = new CompanySelector(M::mock(ManagerRegistry::class)); $security = M::mock(Security::class); $security->shouldNotReceive('getUser'); @@ -164,7 +156,7 @@ public function testItContinueExecutionWhenNoCompanyIsSetAndNoUserIsLoggedIn(): // Test that it continues execution when no company is set and no user is logged in $router = M::mock(RouterInterface::class); - $companySelector = new CompanySelector(M::mock(ManagerRegistry::class), M::mock(SystemConfig::class)); + $companySelector = new CompanySelector(M::mock(ManagerRegistry::class)); $security = M::mock(Security::class); $security @@ -193,13 +185,7 @@ public function testItSetsTheCompanyWhenItIsAvailableInTheSession(): void $registry = M::mock(ManagerRegistry::class); $security = M::mock(Security::class); - $config = M::mock(SystemConfig::class); - $config - ->shouldReceive('getCurrency') - ->once() - ->andReturn(new Currency('USD')); - - $companySelector = new CompanySelector($registry, $config); + $companySelector = new CompanySelector($registry); $security->shouldNotReceive('getUser'); diff --git a/src/CoreBundle/Twig/Extension/BillingExtension.php b/src/CoreBundle/Twig/Extension/BillingExtension.php index e18104df0..4ab91fbc3 100644 --- a/src/CoreBundle/Twig/Extension/BillingExtension.php +++ b/src/CoreBundle/Twig/Extension/BillingExtension.php @@ -13,7 +13,7 @@ namespace SolidInvoice\CoreBundle\Twig\Extension; -use Money\Money; +use Brick\Math\BigDecimal; use SolidInvoice\CoreBundle\Form\FieldRenderer; use SolidInvoice\MoneyBundle\Calculator; use Symfony\Component\Form\FormView; @@ -32,7 +32,7 @@ public function getFunctions(): array { return [ new TwigFunction('billing_fields', fn (FormView $form) => $this->fieldRenderer->render($form, 'children[items].vars[prototype]'), ['is_safe' => ['html']]), - new TwigFunction('discount', fn ($entity): Money => $this->calculator->calculateDiscount($entity)), + new TwigFunction('discount', fn ($entity): BigDecimal => $this->calculator->calculateDiscount($entity)), ]; } } diff --git a/src/InvoiceBundle/DataFixtures/ORM/LoadData.php b/src/InvoiceBundle/DataFixtures/ORM/LoadData.php index 3469201fc..1e25e9705 100644 --- a/src/InvoiceBundle/DataFixtures/ORM/LoadData.php +++ b/src/InvoiceBundle/DataFixtures/ORM/LoadData.php @@ -13,13 +13,12 @@ namespace SolidInvoice\InvoiceBundle\DataFixtures\ORM; +use Brick\Math\BigInteger; use DateTimeImmutable; use DateTimeZone; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; use Exception; -use Money\Currency; -use Money\Money; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\ClientBundle\Entity\Contact; use SolidInvoice\InvoiceBundle\Entity\Invoice; @@ -58,13 +57,13 @@ public function load(ObjectManager $manager): void $item = new Item(); $item->setQty(1); - $item->setPrice(new Money(10000, new Currency('USD'))); + $item->setPrice(BigInteger::of(10000)); $item->setDescription('Test Item'); $invoice->addItem($item); $recurringItem = new Item(); $recurringItem->setQty(1); - $recurringItem->setPrice(new Money(10000, new Currency('USD'))); + $recurringItem->setPrice(BigInteger::of(10000)); $recurringItem->setDescription('Test Item'); $recurringInvoice->addItem($recurringItem); diff --git a/src/InvoiceBundle/Entity/BaseInvoice.php b/src/InvoiceBundle/Entity/BaseInvoice.php index d593c504f..20986a4cc 100644 --- a/src/InvoiceBundle/Entity/BaseInvoice.php +++ b/src/InvoiceBundle/Entity/BaseInvoice.php @@ -13,12 +13,14 @@ namespace SolidInvoice\InvoiceBundle\Entity; +use Brick\Math\BigInteger; +use Brick\Math\BigNumber; +use Brick\Math\Exception\MathException; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Money\Money; +use SolidInvoice\CoreBundle\Doctrine\Type\BigIntegerType; use SolidInvoice\CoreBundle\Entity\Discount; use SolidInvoice\CoreBundle\Traits\Entity\CompanyAware; -use SolidInvoice\MoneyBundle\Entity\Money as MoneyEntity; use Symfony\Component\Serializer\Annotation as Serialize; #[ORM\MappedSuperclass] @@ -30,17 +32,17 @@ abstract class BaseInvoice #[Serialize\Groups(['invoice_api', 'recurring_invoice_api', 'client_api'])] protected ?string $status = null; - #[ORM\Embedded(class: MoneyEntity::class)] + #[ORM\Column(name: 'total_amount', type: BigIntegerType::NAME)] #[Serialize\Groups(['invoice_api', 'recurring_invoice_api', 'client_api'])] - protected MoneyEntity $total; + protected BigInteger $total; - #[ORM\Embedded(class: MoneyEntity::class)] + #[ORM\Column(name: 'baseTotal_amount', type: BigIntegerType::NAME)] #[Serialize\Groups(['invoice_api', 'recurring_invoice_api', 'client_api'])] - protected MoneyEntity $baseTotal; + protected BigInteger $baseTotal; - #[ORM\Embedded(class: MoneyEntity::class)] + #[ORM\Column(name: 'tax_amount', type: BigIntegerType::NAME)] #[Serialize\Groups(['invoice_api', 'recurring_invoice_api', 'client_api'])] - protected MoneyEntity $tax; + protected BigInteger $tax; #[ORM\Embedded(class: Discount::class)] #[Serialize\Groups(['invoice_api', 'recurring_invoice_api', 'client_api', 'create_invoice_api', 'create_recurring_invoice_api'])] @@ -57,9 +59,9 @@ abstract class BaseInvoice public function __construct() { $this->discount = new Discount(); - $this->baseTotal = new MoneyEntity(); - $this->tax = new MoneyEntity(); - $this->total = new MoneyEntity(); + $this->baseTotal = BigInteger::zero(); + $this->tax = BigInteger::zero(); + $this->total = BigInteger::zero(); } public function getStatus(): ?string @@ -74,26 +76,32 @@ public function setStatus(string $status): self return $this; } - public function getTotal(): Money + public function getTotal(): BigInteger { - return $this->total->getMoney(); + return $this->total; } - public function setTotal(Money $total): self + /** + * @throws MathException + */ + public function setTotal(BigInteger|float|int|string $total): self { - $this->total = new MoneyEntity($total); + $this->total = BigInteger::of($total); return $this; } - public function getBaseTotal(): Money + public function getBaseTotal(): BigInteger { - return $this->baseTotal->getMoney(); + return $this->baseTotal; } - public function setBaseTotal(Money $baseTotal): self + /** + * @throws MathException + */ + public function setBaseTotal(BigInteger|float|int|string $baseTotal): self { - $this->baseTotal = new MoneyEntity($baseTotal); + $this->baseTotal = BigInteger::of($baseTotal); return $this; } @@ -110,6 +118,14 @@ public function setDiscount(Discount $discount): self return $this; } + /** + * @throws MathException + */ + public function hasDiscount(): bool + { + return BigNumber::of($this->discount->getValue())->isPositive(); + } + public function getTerms(): ?string { return $this->terms; @@ -134,14 +150,17 @@ public function setNotes(?string $notes): self return $this; } - public function getTax(): ?Money + public function getTax(): BigInteger { - return $this->tax->getMoney(); + return $this->tax; } - public function setTax(?Money $tax = null): self + /** + * @throws MathException + */ + public function setTax(BigInteger|float|int|string $tax): self { - $this->tax = $tax ? new MoneyEntity($tax) : null; + $this->tax = BigInteger::of($tax); return $this; } diff --git a/src/InvoiceBundle/Entity/Invoice.php b/src/InvoiceBundle/Entity/Invoice.php index 58bcb3873..1fa6e3b04 100644 --- a/src/InvoiceBundle/Entity/Invoice.php +++ b/src/InvoiceBundle/Entity/Invoice.php @@ -15,6 +15,8 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use DateTime; use DateTimeInterface; use Doctrine\Common\Collections\ArrayCollection; @@ -22,7 +24,6 @@ use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Exception; -use Money\Money; use Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType; use Ramsey\Uuid\Doctrine\UuidOrderedTimeGenerator; use Ramsey\Uuid\Doctrine\UuidType; @@ -30,12 +31,12 @@ use Ramsey\Uuid\UuidInterface; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\ClientBundle\Entity\Contact; +use SolidInvoice\CoreBundle\Doctrine\Type\BigIntegerType; use SolidInvoice\CoreBundle\Entity\ItemInterface; use SolidInvoice\CoreBundle\Traits\Entity\Archivable; use SolidInvoice\CoreBundle\Traits\Entity\TimeStampable; use SolidInvoice\InvoiceBundle\Repository\InvoiceRepository; use SolidInvoice\InvoiceBundle\Traits\InvoiceStatusTrait; -use SolidInvoice\MoneyBundle\Entity\Money as MoneyEntity; use SolidInvoice\PaymentBundle\Entity\Payment; use SolidInvoice\QuoteBundle\Entity\Quote; use Symfony\Component\Serializer\Annotation as Serialize; @@ -81,9 +82,9 @@ class Invoice extends BaseInvoice #[Serialize\Groups(['invoice_api', 'recurring_invoice_api', 'client_api', 'create_invoice_api', 'create_recurring_invoice_api'])] protected ?Client $client = null; - #[ORM\Embedded(class: MoneyEntity::class)] + #[ORM\Column(name: 'balance_amount', type: BigIntegerType::NAME)] #[Serialize\Groups(['invoice_api', 'client_api'])] - private ?MoneyEntity $balance = null; + private BigInteger $balance; #[ORM\Column(name: 'due', type: Types::DATE_MUTABLE, nullable: true)] #[Assert\DateTime] @@ -126,9 +127,12 @@ class Invoice extends BaseInvoice public function __construct() { parent::__construct(); + $this->payments = new ArrayCollection(); $this->items = new ArrayCollection(); $this->users = new ArrayCollection(); + $this->balance = BigInteger::zero(); + try { $this->setUuid(Uuid::uuid1()); } catch (Exception) { @@ -151,30 +155,30 @@ public function setUuid(UuidInterface $uuid): self return $this; } - public function getClient(): ?Client + public function getClient(): Client { return $this->client; } - public function setClient(?Client $client): self + public function setClient(Client $client): self { $this->client = $client; - if ($client instanceof Client && null !== $client->getCurrencyCode()) { - $this->total->setCurrency($client->getCurrency()->getCode()); - $this->baseTotal->setCurrency($client->getCurrency()->getCode()); - $this->tax->setCurrency($client->getCurrency()->getCode()); - } + return $this; } - public function getBalance(): Money + public function getBalance(): BigInteger { - return $this->balance->getMoney(); + return $this->balance; } - public function setBalance(Money $balance): self + /** + * @throws MathException + */ + public function setBalance(BigInteger|float|int|string $balance): self { - $this->balance = new MoneyEntity($balance); + $this->balance = BigInteger::of($balance); + return $this; } diff --git a/src/InvoiceBundle/Entity/Item.php b/src/InvoiceBundle/Entity/Item.php index 1617766f0..560878843 100644 --- a/src/InvoiceBundle/Entity/Item.php +++ b/src/InvoiceBundle/Entity/Item.php @@ -13,17 +13,18 @@ namespace SolidInvoice\InvoiceBundle\Entity; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Money\Money; use Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType; use Ramsey\Uuid\Doctrine\UuidOrderedTimeGenerator; use Ramsey\Uuid\UuidInterface; +use SolidInvoice\CoreBundle\Doctrine\Type\BigIntegerType; use SolidInvoice\CoreBundle\Entity\ItemInterface; use SolidInvoice\CoreBundle\Traits\Entity\CompanyAware; use SolidInvoice\CoreBundle\Traits\Entity\TimeStampable; use SolidInvoice\InvoiceBundle\Repository\ItemRepository; -use SolidInvoice\MoneyBundle\Entity\Money as MoneyEntity; use SolidInvoice\TaxBundle\Entity\Tax; use Stringable; use Symfony\Component\Serializer\Annotation as Serialize; @@ -51,10 +52,10 @@ class Item implements ItemInterface, Stringable #[Serialize\Groups(['invoice_api', 'recurring_invoice_api', 'client_api', 'create_invoice_api', 'create_recurring_invoice_api'])] private ?string $description = null; - #[ORM\Embedded(class: MoneyEntity::class)] + #[ORM\Column(name: 'price_amount', type: BigIntegerType::NAME)] #[Assert\NotBlank] #[Serialize\Groups(['invoice_api', 'recurring_invoice_api', 'client_api', 'create_invoice_api', 'create_recurring_invoice_api'])] - private MoneyEntity $price; + private BigInteger $price; #[ORM\Column(name: 'qty', type: Types::FLOAT)] #[Assert\NotBlank] @@ -71,14 +72,14 @@ class Item implements ItemInterface, Stringable #[Serialize\Groups(['invoice_api', 'recurring_invoice_api', 'client_api', 'create_invoice_api', 'create_recurring_invoice_api'])] private ?Tax $tax = null; - #[ORM\Embedded(class: MoneyEntity::class)] + #[ORM\Column(name: 'total_amount', type: BigIntegerType::NAME)] #[Serialize\Groups(['invoice_api', 'recurring_invoice_api', 'client_api'])] - private MoneyEntity $total; + private BigInteger $total; public function __construct() { - $this->total = new MoneyEntity(); - $this->price = new MoneyEntity(); + $this->total = BigInteger::zero(); + $this->price = BigInteger::zero(); } public function getId(): UuidInterface @@ -98,16 +99,19 @@ public function getDescription(): ?string return $this->description; } - public function setPrice(Money $price): ItemInterface + /** + * @throws MathException + */ + public function setPrice(BigInteger|float|int|string $price): ItemInterface { - $this->price = new MoneyEntity($price); + $this->price = BigInteger::of($price); return $this; } - public function getPrice(): ?Money + public function getPrice(): BigInteger { - return $this->price->getMoney(); + return $this->price; } public function setQty(float $qty): ItemInterface @@ -138,16 +142,19 @@ public function getInvoice(): BaseInvoice return $this->invoice ?? $this->recurringInvoice; } - public function setTotal(Money $total): ItemInterface + /** + * @throws MathException + */ + public function setTotal(BigInteger|float|int|string $total): ItemInterface { - $this->total = new MoneyEntity($total); + $this->total = BigInteger::of($total); return $this; } - public function getTotal(): Money + public function getTotal(): BigInteger { - return $this->total->getMoney(); + return $this->total; } public function getTax(): ?Tax @@ -162,10 +169,13 @@ public function setTax(?Tax $tax): ItemInterface return $this; } + /** + * @throws MathException + */ #[ORM\PrePersist] public function updateTotal(): void { - $this->total = new MoneyEntity($this->getPrice()->multiply($this->qty)); + $this->total = $this->getPrice()->multipliedBy($this->qty); } public function __toString(): string diff --git a/src/InvoiceBundle/Listener/InvoiceCancelListener.php b/src/InvoiceBundle/Listener/InvoiceCancelListener.php index 51067ce1e..1276ab109 100644 --- a/src/InvoiceBundle/Listener/InvoiceCancelListener.php +++ b/src/InvoiceBundle/Listener/InvoiceCancelListener.php @@ -13,8 +13,8 @@ namespace SolidInvoice\InvoiceBundle\Listener; +use Brick\Math\Exception\MathException; use Doctrine\Persistence\ManagerRegistry; -use Money\Money; use SolidInvoice\ClientBundle\Entity\Credit; use SolidInvoice\ClientBundle\Repository\CreditRepository; use SolidInvoice\InvoiceBundle\Entity\Invoice; @@ -43,6 +43,9 @@ public function __construct( ) { } + /** + * @throws MathException + */ public function onInvoiceCancelled(InvoiceEvent $event): void { $invoice = $event->getInvoice(); @@ -57,7 +60,7 @@ public function onInvoiceCancelled(InvoiceEvent $event): void $invoice->setBalance($invoice->getTotal()); $em->persist($invoice); - $totalPaid = new Money($paymentRepository->getTotalPaidForInvoice($invoice), $invoice->getClient()->getCurrency()); + $totalPaid = $paymentRepository->getTotalPaidForInvoice($invoice); if ($totalPaid->isPositive()) { $paymentRepository->updatePaymentStatus($invoice->getPayments(), Status::STATUS_CREDIT); diff --git a/src/InvoiceBundle/Listener/InvoicePaidListener.php b/src/InvoiceBundle/Listener/InvoicePaidListener.php index 3dfefdf35..4c0569d4e 100644 --- a/src/InvoiceBundle/Listener/InvoicePaidListener.php +++ b/src/InvoiceBundle/Listener/InvoicePaidListener.php @@ -13,8 +13,8 @@ namespace SolidInvoice\InvoiceBundle\Listener; +use Brick\Math\Exception\MathException; use Doctrine\Persistence\ManagerRegistry; -use Money\Money; use SolidInvoice\ClientBundle\Entity\Credit; use SolidInvoice\ClientBundle\Repository\CreditRepository; use SolidInvoice\InvoiceBundle\Entity\Invoice; @@ -40,6 +40,9 @@ public function __construct( ) { } + /** + * @throws MathException + */ public function onInvoicePaid(Event $event): void { /** @var Invoice $invoice */ @@ -50,19 +53,16 @@ public function onInvoicePaid(Event $event): void /** @var PaymentRepository $paymentRepository */ $paymentRepository = $em->getRepository(Payment::class); - $currency = $invoice->getClient()->getCurrency(); - - $invoice->setBalance(new Money(0, $currency)); $em->persist($invoice); - $totalPaid = new Money($paymentRepository->getTotalPaidForInvoice($invoice), $currency); + $totalPaid = $paymentRepository->getTotalPaidForInvoice($invoice); - if ($totalPaid->greaterThan($invoice->getTotal())) { + if ($totalPaid->isGreaterThan($invoice->getTotal())) { $client = $invoice->getClient(); /** @var CreditRepository $creditRepository */ $creditRepository = $em->getRepository(Credit::class); - $creditRepository->addCredit($client, $totalPaid->subtract($invoice->getTotal())); + $creditRepository->addCredit($client, $totalPaid->minus($invoice->getTotal())); } $em->flush(); diff --git a/src/InvoiceBundle/Manager/InvoiceManager.php b/src/InvoiceBundle/Manager/InvoiceManager.php index d554df5f9..baa72ba49 100644 --- a/src/InvoiceBundle/Manager/InvoiceManager.php +++ b/src/InvoiceBundle/Manager/InvoiceManager.php @@ -13,10 +13,12 @@ namespace SolidInvoice\InvoiceBundle\Manager; +use Brick\Math\Exception\MathException; use Carbon\Carbon; use Carbon\CarbonImmutable; use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ObjectManager; +use JsonException; use SolidInvoice\InvoiceBundle\Entity\BaseInvoice; use SolidInvoice\InvoiceBundle\Entity\Invoice; use SolidInvoice\InvoiceBundle\Entity\Item; @@ -52,11 +54,17 @@ public function __construct( $this->dispatcher = $dispatcher; } + /** + * @throws MathException + */ public function createFromQuote(Quote $quote): Invoice { return $this->createFromObject($quote); } + /** + * @throws MathException + */ public function createFromRecurring(RecurringInvoice $recurringInvoice): Invoice { $invoice = $this->createFromObject($recurringInvoice); @@ -89,6 +97,9 @@ public function createFromRecurring(RecurringInvoice $recurringInvoice): Invoice return $invoice; } + /** + * @throws MathException + */ private function createFromObject($object): Invoice { /** @var RecurringInvoice|Quote $object */ @@ -136,6 +147,7 @@ private function createFromObject($object): Invoice /** * @throws InvalidTransitionException + * @throws JsonException */ public function create(BaseInvoice $invoice): BaseInvoice { @@ -157,9 +169,9 @@ public function create(BaseInvoice $invoice): BaseInvoice } /** - * @throws InvalidTransitionException + * @throws InvalidTransitionException|\JsonException */ - private function applyTransition(BaseInvoice $invoice, string $transition): bool + private function applyTransition(BaseInvoice $invoice, string $transition): void { if ($this->invoiceStateMachine->can($invoice, $transition)) { $oldStatus = $invoice->getStatus(); @@ -182,7 +194,7 @@ private function applyTransition(BaseInvoice $invoice, string $transition): bool $this->notification->sendNotification('invoice_status_update', $notification); } - return true; + return; } throw new InvalidTransitionException($transition); diff --git a/src/InvoiceBundle/Repository/InvoiceRepository.php b/src/InvoiceBundle/Repository/InvoiceRepository.php index b3a9017da..fccdb7d99 100644 --- a/src/InvoiceBundle/Repository/InvoiceRepository.php +++ b/src/InvoiceBundle/Repository/InvoiceRepository.php @@ -13,19 +13,17 @@ namespace SolidInvoice\InvoiceBundle\Repository; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Common\Collections\Criteria; -use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; -use Money\Currency; -use Money\Money; use Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\InvoiceBundle\Entity\Invoice; -use SolidInvoice\InvoiceBundle\Entity\Item; use SolidInvoice\InvoiceBundle\Model\Graph; use SolidInvoice\PaymentBundle\Entity\Payment; @@ -42,26 +40,25 @@ public function __construct(ManagerRegistry $registry) /** * Get the total amount for paid invoices. * + * @throws MathException * @deprecated This function is deprecated, and the one in PaymentRepository should be used instead - * - * @throws NoResultException|NonUniqueResultException */ - public function getTotalIncome(Client $client = null): int + public function getTotalIncome(Client $client = null): BigInteger { @trigger_error( 'This function is deprecated, and the one in PaymentRepository should be used instead', E_USER_DEPRECATED ); - return $this->getTotalByStatus(Graph::STATUS_PAID, $client, 'money'); + return $this->getTotalByStatus(Graph::STATUS_PAID, $client); } /** * Get the total amount for a specific invoice status. * - * @throws NoResultException|NonUniqueResultException + * @throws MathException */ - public function getTotalByStatus(string $status, Client $client = null, int|string $hydrate = AbstractQuery::HYDRATE_SINGLE_SCALAR): int + public function getTotalByStatus(string $status, Client $client = null): BigInteger { $qb = $this->createQueryBuilder('i'); @@ -74,7 +71,11 @@ public function getTotalByStatus(string $status, Client $client = null, int|stri ->setParameter('client', $client->getId(), UuidBinaryOrderedTimeType::NAME); } - return $qb->getQuery()->getSingleResult($hydrate); + try { + return BigInteger::of($qb->getQuery()->getSingleResult()); + } catch (NoResultException | NonUniqueResultException) { + return BigInteger::zero(); + } } /** @@ -84,7 +85,7 @@ public function getTotalOutstanding(Client $client = null): int { $qb = $this->createQueryBuilder('i'); - $qb->select('SUM(i.balance.value)') + $qb->select('SUM(i.balance)') ->where('i.status = :status') ->setParameter('status', Graph::STATUS_PENDING); @@ -107,7 +108,7 @@ public function getTotalOutstanding(Client $client = null): int * * @param string|string[] $status */ - public function getCountByStatus(string|array $status, Client $client = null): int + public function getCountByStatus(string | array $status, Client $client = null): int { $qb = $this->createQueryBuilder('i'); @@ -183,49 +184,6 @@ public function getArchivedGridQuery(): QueryBuilder return $qb; } - public function updateCurrency(Client $client): void - { - $filters = $this->getEntityManager()->getFilters(); - $filters->disable('archivable'); - - $currency = $client->getCurrency(); - - $qb = $this->createQueryBuilder('i'); - - $qb->update() - ->set('i.total.currency', ':currency') - ->set('i.baseTotal.currency', ':currency') - ->set('i.balance.currency', ':currency') - ->set('i.tax.currency', ':currency') - ->where('i.client = :client') - ->setParameter('client', $client->getId(), UuidBinaryOrderedTimeType::NAME) - ->setParameter('currency', $currency); - - if ($qb->getQuery()->execute()) { - $qbi = $this->getEntityManager()->createQueryBuilder(); - - $qbi->update() - ->from(Item::class, 'it') - ->set('it.price.currency', ':currency') - ->set('it.total.currency', ':currency') - ->where( - $qbi->expr()->in( - 'it.invoice', - $this->createQueryBuilder('i') - ->select('i.id') - ->where('i.client = :client') - ->getDQL() - ) - ) - ->setParameter('client', $client->getId(), UuidBinaryOrderedTimeType::NAME) - ->setParameter('currency', $currency); - - $qbi->getQuery()->execute(); - } - - $filters->enable('archivable'); - } - /** * @param list $ids */ @@ -253,35 +211,29 @@ public function isFullyPaid(Invoice $invoice): bool { $invoiceTotal = $invoice->getTotal(); - $totalPaid = new Money( - $this->getEntityManager() - ->getRepository(Payment::class) - ->getTotalPaidForInvoice($invoice), - $invoiceTotal->getCurrency() - ); + $totalPaid = $this->getEntityManager() + ->getRepository(Payment::class) + ->getTotalPaidForInvoice($invoice); - return $totalPaid->equals($invoiceTotal) || $totalPaid->greaterThan($invoiceTotal); + return $totalPaid->isEqualTo($invoiceTotal) || $totalPaid->isGreaterThan($invoiceTotal); } - public function getTotalOutstandingForClient(Client $client): ?Money + public function getTotalOutstandingForClient(Client $client): BigInteger { $qb = $this->createQueryBuilder('i'); - $qb->select('SUM(i.balance.value) as total, i.balance.currency as currency') + $qb->select('SUM(i.balance) as total') ->where('i.status = :status') ->andWhere('i.client = :client') - ->groupBy('i.balance.currency') ->setParameter('client', $client->getId(), UuidBinaryOrderedTimeType::NAME) ->setParameter('status', Graph::STATUS_PENDING); $query = $qb->getQuery(); - $result = $query->getArrayResult(); - - if ([] === $result) { - return null; + try { + return BigInteger::of($query->getSingleScalarResult()); + } catch (MathException | NoResultException | NonUniqueResultException) { + return BigInteger::zero(); } - - return new Money($result[0]['total'], new Currency($result[0]['currency'])); } } diff --git a/src/InvoiceBundle/Repository/ItemRepository.php b/src/InvoiceBundle/Repository/ItemRepository.php index 3f0770496..6ae4e098f 100644 --- a/src/InvoiceBundle/Repository/ItemRepository.php +++ b/src/InvoiceBundle/Repository/ItemRepository.php @@ -13,6 +13,8 @@ namespace SolidInvoice\InvoiceBundle\Repository; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; use SolidInvoice\InvoiceBundle\Entity\Invoice; @@ -28,6 +30,7 @@ public function __construct(ManagerRegistry $registry) /** * Removes all tax rates from invoices. + * @throws MathException */ public function removeTax(Tax $tax): void { @@ -41,8 +44,8 @@ public function removeTax(Tax $tax): void /** @var Invoice $invoice */ foreach ($query->execute() as $invoice) { - $invoice->setTotal($invoice->getBaseTotal()->add($invoice->getTax())); - $invoice->setTax(null); + $invoice->setTotal($invoice->getBaseTotal()->plus($invoice->getTax())); + $invoice->setTax(BigInteger::zero()); $this->getEntityManager()->persist($invoice); } diff --git a/src/InvoiceBundle/Resources/views/invoice_template.html.twig b/src/InvoiceBundle/Resources/views/invoice_template.html.twig index 0d1f54360..4416333a6 100644 --- a/src/InvoiceBundle/Resources/views/invoice_template.html.twig +++ b/src/InvoiceBundle/Resources/views/invoice_template.html.twig @@ -165,7 +165,7 @@ {% endif %} - {% if invoice.discount.type is not empty %} + {% if invoice.hasDiscount %} diff --git a/src/InvoiceBundle/Test/Factory/InvoiceFactory.php b/src/InvoiceBundle/Test/Factory/InvoiceFactory.php index 8af63d313..4e7c30c02 100644 --- a/src/InvoiceBundle/Test/Factory/InvoiceFactory.php +++ b/src/InvoiceBundle/Test/Factory/InvoiceFactory.php @@ -11,8 +11,8 @@ namespace SolidInvoice\InvoiceBundle\Test\Factory; -use Money\Currency; -use Money\Money; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Ramsey\Uuid\Uuid; use SolidInvoice\ClientBundle\Test\Factory\ClientFactory; use SolidInvoice\CoreBundle\Entity\Discount; @@ -45,6 +45,7 @@ final class InvoiceFactory extends ModelFactory { /** * @return array + * @throws MathException */ protected function getDefaults(): array { @@ -59,13 +60,13 @@ protected function getDefaults(): array 'archived' => self::faker()->boolean(), 'created' => self::faker()->dateTime(), 'updated' => self::faker()->dateTime(), - 'balance' => new Money(self::faker()->randomNumber(), new Currency(self::faker()->currencyCode())), - 'total' => new Money(self::faker()->randomNumber(), new Currency(self::faker()->currencyCode())), - 'baseTotal' => new Money(self::faker()->randomNumber(), new Currency(self::faker()->currencyCode())), - 'tax' => new Money(self::faker()->randomNumber(), new Currency(self::faker()->currencyCode())), + 'balance' => BigInteger::of(self::faker()->randomNumber()), + 'total' => BigInteger::of(self::faker()->randomNumber()), + 'baseTotal' => BigInteger::of(self::faker()->randomNumber()), + 'tax' => BigInteger::of(self::faker()->randomNumber()), 'discount' => (new Discount()) ->setType(self::faker()->text()) - ->setValueMoney(new \SolidInvoice\MoneyBundle\Entity\Money(new Money(self::faker()->randomNumber(), new Currency(self::faker()->currencyCode())))) + ->setValueMoney(BigInteger::of(self::faker()->randomNumber())) ->setValuePercentage(self::faker()->randomFloat()), ]; } diff --git a/src/InvoiceBundle/Tests/Cloner/InvoiceClonerTest.php b/src/InvoiceBundle/Tests/Cloner/InvoiceClonerTest.php index fbd6d7121..6d7fc492e 100644 --- a/src/InvoiceBundle/Tests/Cloner/InvoiceClonerTest.php +++ b/src/InvoiceBundle/Tests/Cloner/InvoiceClonerTest.php @@ -13,11 +13,10 @@ namespace SolidInvoice\InvoiceBundle\Tests\Cloner; +use Brick\Math\Exception\MathException; use DateTime; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mockery as M; -use Money\Currency; -use Money\Money; use PHPUnit\Framework\TestCase; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\CoreBundle\Entity\Discount; @@ -32,13 +31,14 @@ class InvoiceClonerTest extends TestCase { use MockeryPHPUnitIntegration; + /** + * @throws MathException + */ public function testClone(): void { - $currency = new Currency('USD'); - $client = new Client(); $client->setName('Test Client'); - $client->setWebsite('http://example.com'); + $client->setWebsite('https://example.com'); $client->setCreated(new DateTime('NOW')); $tax = new Tax(); @@ -50,20 +50,20 @@ public function testClone(): void $item->setTax($tax); $item->setDescription('Item Description'); $item->setCreated(new DateTime('now')); - $item->setPrice(new Money(120, $currency)); + $item->setPrice(120); $item->setQty(10); - $item->setTotal(new Money((12 * 10), $currency)); + $item->setTotal(120 * 10); $invoice = new Invoice(); - $invoice->setBaseTotal(new Money(123, $currency)); + $invoice->setBaseTotal(123); $discount = new Discount(); $discount->setType(Discount::TYPE_PERCENTAGE); $discount->setValue(12); $invoice->setDiscount($discount); $invoice->setNotes('Notes'); - $invoice->setTax(new Money(432, $currency)); + $invoice->setTax(432); $invoice->setTerms('Terms'); - $invoice->setTotal(new Money(987, $currency)); + $invoice->setTotal(987); $invoice->setClient($client); $invoice->addItem($item); @@ -100,7 +100,6 @@ public function testClone(): void public function testCloneWithRecurring(): void { - $currency = new Currency('USD'); $date = new DateTime('now'); $client = new Client(); @@ -117,20 +116,20 @@ public function testCloneWithRecurring(): void $item->setTax($tax); $item->setDescription('Item Description'); $item->setCreated(new DateTime('now')); - $item->setPrice(new Money(120, $currency)); + $item->setPrice(120); $item->setQty(10); - $item->setTotal(new Money((12 * 10), $currency)); + $item->setTotal(120 * 10); $invoice = new RecurringInvoice(); - $invoice->setBaseTotal(new Money(123, $currency)); + $invoice->setBaseTotal(123); $discount = new Discount(); $discount->setType(Discount::TYPE_PERCENTAGE); $discount->setValue(12); $invoice->setDiscount($discount); $invoice->setNotes('Notes'); - $invoice->setTax(new Money(432, $currency)); + $invoice->setTax(432); $invoice->setTerms('Terms'); - $invoice->setTotal(new Money(987, $currency)); + $invoice->setTotal(987); $invoice->setClient($client); $invoice->addItem($item); $invoice->setFrequency('* * * * *'); diff --git a/src/InvoiceBundle/Tests/Form/Handler/InvoiceEditHandlerTest.php b/src/InvoiceBundle/Tests/Form/Handler/InvoiceEditHandlerTest.php index 4379458f7..62688e110 100644 --- a/src/InvoiceBundle/Tests/Form/Handler/InvoiceEditHandlerTest.php +++ b/src/InvoiceBundle/Tests/Form/Handler/InvoiceEditHandlerTest.php @@ -13,9 +13,12 @@ namespace SolidInvoice\InvoiceBundle\Tests\Form\Handler; +use Brick\Math\Exception\MathException; +use Doctrine\ORM\Exception\NotSupported; +use Doctrine\ORM\Exception\ORMException; +use Doctrine\ORM\OptimisticLockException; use Mockery as M; use Money\Currency; -use Money\Money; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\CoreBundle\Entity\Discount; use SolidInvoice\CoreBundle\Response\FlashResponse; @@ -48,6 +51,11 @@ final class InvoiceEditHandlerTest extends FormHandlerTestCase private Client $client; + /** + * @throws OptimisticLockException + * @throws ORMException + * @throws MathException + */ protected function setUp(): void { parent::setUp(); @@ -60,7 +68,7 @@ protected function setUp(): void $discount->setType(Discount::TYPE_PERCENTAGE); $discount->setValue(10); $this->invoice->setDiscount($discount); - $this->invoice->setBalance(new Money(1000, new Currency('USD'))); + $this->invoice->setBalance(1000); $this->em->persist($this->invoice); $this->em->flush(); @@ -112,6 +120,7 @@ public function getHandler(): InvoiceEditHandler /** * @param Invoice $invoice + * @throws NotSupported */ protected function assertOnSuccess(?Response $response, FormRequest $form, $invoice): void { diff --git a/src/InvoiceBundle/Tests/Form/Type/InvoiceTypeTest.php b/src/InvoiceBundle/Tests/Form/Type/InvoiceTypeTest.php index e90ef107b..8f7ce2d1c 100644 --- a/src/InvoiceBundle/Tests/Form/Type/InvoiceTypeTest.php +++ b/src/InvoiceBundle/Tests/Form/Type/InvoiceTypeTest.php @@ -15,7 +15,7 @@ use Mockery as M; use Money\Currency; -use Money\Money; +use SolidInvoice\ClientBundle\Test\Factory\ClientFactory; use SolidInvoice\CoreBundle\Entity\Discount; use SolidInvoice\CoreBundle\Form\Type\DiscountType; use SolidInvoice\CoreBundle\Tests\FormTestCase; @@ -33,8 +33,9 @@ public function testSubmit(): void $notes = $this->faker->text; $terms = $this->faker->text; $discountValue = $this->faker->numberBetween(0, 100); + $client = ClientFactory::createOne()->object(); $formData = [ - 'client' => null, + 'client' => $client->getId()->toString(), 'discount' => [ 'value' => $discountValue, 'type' => Discount::TYPE_PERCENTAGE, @@ -48,6 +49,7 @@ public function testSubmit(): void ]; $object = new Invoice(); + $object->setClient($client); $data = clone $object; $data->setUuid($object->getUuid()); @@ -57,9 +59,6 @@ public function testSubmit(): void $discount->setType(Discount::TYPE_PERCENTAGE); $discount->setValue($discountValue); $object->setDiscount($discount); - $object->setTotal(new Money(0, new Currency('USD'))); - $object->setTax(new Money(0, new Currency('USD'))); - $object->setBaseTotal(new Money(0, new Currency('USD'))); $this->assertFormData($this->factory->create(InvoiceType::class, $data), $formData, $object); } diff --git a/src/InvoiceBundle/Tests/Form/Type/ItemTypeTest.php b/src/InvoiceBundle/Tests/Form/Type/ItemTypeTest.php index ee67102ce..6b980302b 100644 --- a/src/InvoiceBundle/Tests/Form/Type/ItemTypeTest.php +++ b/src/InvoiceBundle/Tests/Form/Type/ItemTypeTest.php @@ -14,7 +14,6 @@ namespace SolidInvoice\InvoiceBundle\Tests\Form\Type; use Money\Currency; -use Money\Money; use SolidInvoice\CoreBundle\Tests\FormTestCase; use SolidInvoice\InvoiceBundle\Entity\Item; use SolidInvoice\InvoiceBundle\Form\Type\ItemType; @@ -40,7 +39,7 @@ public function testSubmit(): void $object = new Item(); $object->setDescription($description); $object->setQty($qty); - $object->setPrice(new Money($price * 100, $currency)); + $object->setPrice($price * 100); $this->assertFormData($this->factory->create(ItemType::class, null, ['currency' => $currency]), $formData, $object); } diff --git a/src/InvoiceBundle/Tests/Form/Type/RecurringInvoiceTypeTest.php b/src/InvoiceBundle/Tests/Form/Type/RecurringInvoiceTypeTest.php index 957b4183d..eb43abada 100644 --- a/src/InvoiceBundle/Tests/Form/Type/RecurringInvoiceTypeTest.php +++ b/src/InvoiceBundle/Tests/Form/Type/RecurringInvoiceTypeTest.php @@ -16,7 +16,6 @@ use Cron\CronExpression; use Mockery as M; use Money\Currency; -use Money\Money; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\CoreBundle\Entity\Discount; use SolidInvoice\CoreBundle\Form\Type\DiscountType; @@ -51,7 +50,7 @@ public function testSubmit(): void 'total' => 0, 'baseTotal' => 0, 'tax' => 0, - 'frequency' => (string) CronExpression::factory('@weekly'), + 'frequency' => (string) new CronExpression('@weekly'), 'date_start' => $this->faker->dateTime, 'date_end' => $this->faker->dateTime, ]; @@ -68,9 +67,6 @@ public function testSubmit(): void $discount->setType(Discount::TYPE_PERCENTAGE); $discount->setValue($discountValue); $object->setDiscount($discount); - $object->setTotal(new Money(0, new Currency('USD'))); - $object->setTax(new Money(0, new Currency('USD'))); - $object->setBaseTotal(new Money(0, new Currency('USD'))); $this->assertFormData($this->factory->create(RecurringInvoiceType::class, $data), $formData, $object); } diff --git a/src/InvoiceBundle/Tests/Functional/Api/InvoiceTest.php b/src/InvoiceBundle/Tests/Functional/Api/InvoiceTest.php index f97da2bbb..f822dbfc5 100644 --- a/src/InvoiceBundle/Tests/Functional/Api/InvoiceTest.php +++ b/src/InvoiceBundle/Tests/Functional/Api/InvoiceTest.php @@ -75,25 +75,25 @@ public function testCreate(): void self::assertSame([ 'client' => '/api/clients/' . $contact->getClient()->getId(), - 'balance' => '$90.00', + 'balance' => 90, 'due' => null, 'paidDate' => null, 'items' => [ [ 'description' => 'Foo Item', - 'price' => '$100.00', + 'price' => 100, 'qty' => 1, 'tax' => null, - 'total' => '$100.00', + 'total' => 100, ], ], 'users' => [ '/api/contacts/' . $contact->getId(), ], 'status' => 'draft', - 'total' => '$90.00', - 'baseTotal' => '$100.00', - 'tax' => '$0.00', + 'total' => 90, + 'baseTotal' => 100, + 'tax' => 0, 'discount' => [ 'type' => 'percentage', 'value' => 10, @@ -122,29 +122,29 @@ public function testGet(): void 'id' => $invoice->getId()->toString(), 'uuid' => $invoice->getUuid()->toString(), 'client' => '/api/clients/' . $invoice->getClient()->getId(), - 'balance' => '$100.00', + 'balance' => 100, 'due' => null, 'paidDate' => null, 'items' => [ [ 'id' => $invoice->getItems()->first()->getId()->toString(), 'description' => 'Test Item', - 'price' => '$100.00', + 'price' => 100, 'qty' => 1, 'tax' => null, - 'total' => '$100.00', + 'total' => 100, ], ], 'users' => [ '/api/contacts/' . $invoice->getUsers()->first()->getId(), ], 'status' => 'draft', - 'total' => '$100.00', - 'baseTotal' => '$100.00', - 'tax' => '$0.00', + 'total' => 100, + 'baseTotal' => 100, + 'tax' => 0, 'discount' => [ 'type' => null, - 'value' => null, + 'value' => 0, ], 'terms' => null, 'notes' => null, @@ -177,26 +177,26 @@ public function testEdit(): void 'id' => $invoice->getId()->toString(), 'uuid' => $invoice->getUuid()->toString(), 'client' => '/api/clients/' . $invoice->getClient()->getId(), - 'balance' => '$90.00', + 'balance' => 90, 'due' => null, 'paidDate' => null, 'items' => [ [ 'id' => $invoice->getItems()->first()->getId()->toString(), 'description' => 'Foo Item', - 'price' => '$100.00', + 'price' => 100, 'qty' => 1, 'tax' => null, - 'total' => '$100.00', + 'total' => 100, ], ], 'users' => [ '/api/contacts/' . $invoice->getUsers()->first()->getId(), ], 'status' => 'draft', - 'total' => '$90.00', - 'baseTotal' => '$100.00', - 'tax' => '$0.00', + 'total' => 90, + 'baseTotal' => 100, + 'tax' => 0, 'discount' => [ 'type' => 'percentage', 'value' => 10, diff --git a/src/InvoiceBundle/Tests/Functional/Api/RecurringInvoiceTest.php b/src/InvoiceBundle/Tests/Functional/Api/RecurringInvoiceTest.php index 916de2dbd..3046d6251 100644 --- a/src/InvoiceBundle/Tests/Functional/Api/RecurringInvoiceTest.php +++ b/src/InvoiceBundle/Tests/Functional/Api/RecurringInvoiceTest.php @@ -64,7 +64,7 @@ public function testCreate(): void ], 'items' => [ [ - 'price' => 100, + 'price' => 100.10, 'qty' => 1, 'description' => 'Foo Item', ], @@ -88,19 +88,19 @@ public function testCreate(): void 'items' => [ [ 'description' => 'Foo Item', - 'price' => '$100.00', + 'price' => 100.1, 'qty' => 1, 'tax' => null, - 'total' => '$100.00', + 'total' => 100.1, ], ], 'users' => [ '/api/contacts/' . $contact->getId(), ], 'status' => 'draft', - 'total' => '$90.00', - 'baseTotal' => '$100.00', - 'tax' => '$0.00', + 'total' => 90.09, + 'baseTotal' => 100.1, + 'tax' => 0, 'discount' => [ 'type' => 'percentage', 'value' => 10, @@ -135,22 +135,22 @@ public function testGet(): void [ 'id' => $recurringInvoice->getItems()->first()->getId()->toString(), 'description' => 'Test Item', - 'price' => '$100.00', + 'price' => 100, 'qty' => 1, 'tax' => null, - 'total' => '$100.00', + 'total' => 100, ], ], 'users' => [ '/api/contacts/' . $recurringInvoice->getUsers()->first()->getId()->toString(), ], 'status' => 'draft', - 'total' => '$100.00', - 'baseTotal' => '$100.00', - 'tax' => '$0.00', + 'total' => 100, + 'baseTotal' => 100, + 'tax' => 0, 'discount' => [ 'type' => null, - 'value' => null, + 'value' => 0, ], 'terms' => null, 'notes' => null, @@ -190,19 +190,19 @@ public function testEdit(): void [ 'id' => $recurringInvoice->getItems()->first()->getId()->toString(), 'description' => 'Foo Item', - 'price' => '$100.00', + 'price' => 100, 'qty' => 1, 'tax' => null, - 'total' => '$100.00', + 'total' => 100, ], ], 'users' => [ '/api/contacts/' . $recurringInvoice->getUsers()->first()->getId()->toString(), ], 'status' => 'draft', - 'total' => '$90.00', - 'baseTotal' => '$100.00', - 'tax' => '$0.00', + 'total' => 90, + 'baseTotal' => 100, + 'tax' => 0, 'discount' => [ 'type' => 'percentage', 'value' => 10, diff --git a/src/InvoiceBundle/Tests/Listener/WorkFlowSubscriberTest.php b/src/InvoiceBundle/Tests/Listener/WorkFlowSubscriberTest.php index 31fd18e31..9ac4753b6 100644 --- a/src/InvoiceBundle/Tests/Listener/WorkFlowSubscriberTest.php +++ b/src/InvoiceBundle/Tests/Listener/WorkFlowSubscriberTest.php @@ -15,8 +15,6 @@ use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mockery as M; -use Money\Currency; -use Money\Money; use PHPUnit\Framework\TestCase; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\CoreBundle\Test\Traits\DoctrineTestTrait; @@ -42,7 +40,7 @@ public function testInvoicePaid(): void $subscriber = new WorkFlowSubscriber($this->registry, $notification); $invoice = new Invoice(); - $invoice->setBalance(new Money(1200, new Currency('USD'))); + $invoice->setBalance(1200); $invoice->setClient((new Client())->setName('Test')->setCurrencyCode('USD')); $invoice->setStatus('pending'); @@ -60,7 +58,7 @@ public function testInvoiceArchive(): void $subscriber = new WorkFlowSubscriber($this->registry, $notification); $invoice = new Invoice(); - $invoice->setBalance(new Money(1200, new Currency('USD'))); + $invoice->setBalance(1200); $invoice->setClient((new Client())->setName('Test')->setCurrencyCode('USD')); $invoice->setStatus('pending'); diff --git a/src/InvoiceBundle/Tests/Manager/InvoiceManagerTest.php b/src/InvoiceBundle/Tests/Manager/InvoiceManagerTest.php index 8e25d6cca..a67e2bcf8 100644 --- a/src/InvoiceBundle/Tests/Manager/InvoiceManagerTest.php +++ b/src/InvoiceBundle/Tests/Manager/InvoiceManagerTest.php @@ -19,7 +19,6 @@ use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mockery as M; use Money\Currency; -use Money\Money; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\CoreBundle\Entity\Company; use SolidInvoice\CoreBundle\Entity\Discount; @@ -90,20 +89,20 @@ public function testCreateFromQuote(): void $item->setTax($tax); $item->setDescription('Item Description'); $item->setCreated(new DateTime('now')); - $item->setPrice(new Money(120, $currency)); + $item->setPrice(120); $item->setQty(10); - $item->setTotal(new Money((12 * 10), $currency)); + $item->setTotal(120 * 10); $quote = new Quote(); - $quote->setBaseTotal(new Money(123, $currency)); + $quote->setBaseTotal(123); $discount = new Discount(); $discount->setType(Discount::TYPE_PERCENTAGE); $discount->setValue(12); $quote->setDiscount($discount); $quote->setNotes('Notes'); - $quote->setTax(new Money(432, $currency)); + $quote->setTax(432); $quote->setTerms('Terms'); - $quote->setTotal(new Money(987, $currency)); + $quote->setTotal(987); $quote->setClient($client); $quote->addItem($item); $quote->setCompany(new Company()); @@ -152,20 +151,20 @@ public function testCreateFromRecurring(): void $item->setTax($tax); $item->setDescription('Item Description {day} {day_name} {month} {year}'); $item->setCreated(new DateTime('now')); - $item->setPrice(new Money(120, $currency)); + $item->setPrice(120); $item->setQty(10); - $item->setTotal(new Money((12 * 10), $currency)); + $item->setTotal(120 * 10); $recurringInvoice = new RecurringInvoice(); - $recurringInvoice->setBaseTotal(new Money(123, $currency)); + $recurringInvoice->setBaseTotal(123); $discount = new Discount(); $discount->setType(Discount::TYPE_PERCENTAGE); $discount->setValue(12); $recurringInvoice->setDiscount($discount); $recurringInvoice->setNotes('Notes'); - $recurringInvoice->setTax(new Money(432, $currency)); + $recurringInvoice->setTax(432); $recurringInvoice->setTerms('Terms'); - $recurringInvoice->setTotal(new Money(987, $currency)); + $recurringInvoice->setTotal(987); $recurringInvoice->setClient($client); $recurringInvoice->addItem($item); $recurringInvoice->setFrequency('* 0 0 * *'); diff --git a/src/MoneyBundle/Calculator.php b/src/MoneyBundle/Calculator.php index 70ae22b00..c6903dfe0 100644 --- a/src/MoneyBundle/Calculator.php +++ b/src/MoneyBundle/Calculator.php @@ -13,8 +13,11 @@ namespace SolidInvoice\MoneyBundle; +use Brick\Math\BigDecimal; +use Brick\Math\BigInteger; +use Brick\Math\BigNumber; +use Brick\Math\Exception\MathException; use InvalidArgumentException; -use Money\Money; use SolidInvoice\CoreBundle\Entity\Discount; use SolidInvoice\InvoiceBundle\Entity\Invoice; use SolidInvoice\MoneyBundle\Formatter\MoneyFormatter; @@ -25,7 +28,10 @@ */ final class Calculator { - public function calculateDiscount($entity): Money + /** + * @throws MathException + */ + public function calculateDiscount($entity): BigDecimal { if (! $entity instanceof Quote && ! $entity instanceof Invoice) { throw new InvalidArgumentException(sprintf('"%s" expects instance of Quote or Invoice, "%s" given.', __METHOD__, get_debug_type($entity))); @@ -33,29 +39,24 @@ public function calculateDiscount($entity): Money $discount = $entity->getDiscount(); - if (null !== $entity->getTax()) { - $invoiceTotal = $entity->getBaseTotal()->add($entity->getTax()); - } else { - $invoiceTotal = $entity->getBaseTotal(); - } + $invoiceTotal = $entity->getBaseTotal()->plus($entity->getTax()); if (Discount::TYPE_PERCENTAGE === $discount->getType()) { - return new Money($this->calculatePercentage($invoiceTotal, $discount->getValue()), $invoiceTotal->getCurrency()); + return BigDecimal::of($this->calculatePercentage($invoiceTotal, $discount->getValue())); } - return $discount->getValue(); + return $discount->getValue()->toBigDecimal(); } - public function calculatePercentage($amount, float $percentage = 0.0): float + /** + * @throws MathException + */ + public function calculatePercentage(BigInteger|int|float|string $amount, float $percentage = 0.0): float { if ($percentage < 0) { $percentage *= 100; } - if ($amount instanceof Money) { - return MoneyFormatter::toFloat($amount->multiply($percentage)); - } - - return ($amount * $percentage) / 100; + return MoneyFormatter::toFloat(BigNumber::of($amount)->multipliedBy($percentage)); } } diff --git a/src/MoneyBundle/Entity/Money.php b/src/MoneyBundle/Entity/Money.php deleted file mode 100644 index d149ac178..000000000 --- a/src/MoneyBundle/Entity/Money.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace SolidInvoice\MoneyBundle\Entity; - -use Doctrine\DBAL\Types\Types; -use Doctrine\ORM\Mapping as ORM; -use Money\Currency; -use Money\Money as BaseMoney; - -#[ORM\Embeddable] -class Money -{ - #[ORM\Column(name: 'amount', type: Types::STRING, nullable: true)] - private ?string $value = null; - - #[ORM\Column(name: 'currency', type: Types::STRING, length: 3, nullable: true)] - private ?string $currency = null; - - // @TODO: Ensure that a money object is always passed in - public function __construct(?BaseMoney $money = null) - { - if ($money instanceof BaseMoney) { - $this->value = $money->getAmount(); - $this->currency = $money->getCurrency()->getCode(); - } - } - - public function getMoney(): BaseMoney - { - // @TODO: USD should not be hard-coded - return new BaseMoney((int) $this->value, $this->currency ? new Currency($this->currency) : \SolidInvoice\CoreBundle\Company\Currency::get()); - } - - public function getCurrency(): ?string - { - return $this->currency; - } - - public function setCurrency(string $currency): void - { - $this->currency = $currency; - } -} diff --git a/src/MoneyBundle/Form/DataTransformer/ModelTransformer.php b/src/MoneyBundle/Form/DataTransformer/ModelTransformer.php index 6c71a61df..977b14ac0 100644 --- a/src/MoneyBundle/Form/DataTransformer/ModelTransformer.php +++ b/src/MoneyBundle/Form/DataTransformer/ModelTransformer.php @@ -13,6 +13,8 @@ namespace SolidInvoice\MoneyBundle\Form\DataTransformer; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use InvalidArgumentException; use Money\Currency; use Money\Money; @@ -38,6 +40,9 @@ public function __construct($currency) } } + /** + * @throws MathException + */ public function transform($value) { if (null === $value) { @@ -48,19 +53,18 @@ public function transform($value) return $value; } - return new Money((int) ($value * 100), $this->currency); + return new Money(BigInteger::of($value)->toInt(), $this->currency); } + /** + * @throws MathException + */ public function reverseTransform($value) { if ($value instanceof Money) { - return $value; - } - - if (is_int($value)) { - return new Money($value, $this->currency); + return BigInteger::of($value->getAmount()); } - return 0; + return BigInteger::of($value->getAmount()); } } diff --git a/src/MoneyBundle/Form/DataTransformer/ViewTransformer.php b/src/MoneyBundle/Form/DataTransformer/ViewTransformer.php index 080f5439e..2d58977fb 100644 --- a/src/MoneyBundle/Form/DataTransformer/ViewTransformer.php +++ b/src/MoneyBundle/Form/DataTransformer/ViewTransformer.php @@ -13,6 +13,9 @@ namespace SolidInvoice\MoneyBundle\Form\DataTransformer; +use Brick\Math\BigNumber; +use Brick\Math\Exception\DivisionByZeroException; +use Brick\Math\Exception\NumberFormatException; use InvalidArgumentException; use Money\Currency; use Money\Money; @@ -37,12 +40,16 @@ public function transform($value): float return 0.0; } + /** + * @throws NumberFormatException + * @throws DivisionByZeroException + */ public function reverseTransform($value): Money { if (! is_numeric($value)) { $value = 0; } - return new Money($value * 100, $this->currency); + return new Money(BigNumber::of($value)->multipliedBy(100)->toInt(), $this->currency); } } diff --git a/src/MoneyBundle/Formatter/MoneyFormatter.php b/src/MoneyBundle/Formatter/MoneyFormatter.php index 1dfa75d8c..a8cb97f83 100644 --- a/src/MoneyBundle/Formatter/MoneyFormatter.php +++ b/src/MoneyBundle/Formatter/MoneyFormatter.php @@ -13,6 +13,9 @@ namespace SolidInvoice\MoneyBundle\Formatter; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; +use Brick\Math\RoundingMode; use Money\Currencies\ISOCurrencies; use Money\Currency; use Money\Formatter\IntlMoneyFormatter; @@ -60,10 +63,9 @@ public function format(Money $money): string } /** - * @param Currency|string|null $currency * @throws Throwable */ - public function getCurrencySymbol($currency = null, bool $catch = false): string + public function getCurrencySymbol(Currency|string|null $currency = null, bool $catch = false): string { try { return Currencies::getSymbol($this->getCurrency($currency), $this->locale); @@ -97,9 +99,15 @@ public function getPattern(): string return '%s%v'; } - public static function toFloat(Money $amount): float + /** + * @throws MathException + */ + public static function toFloat(BigInteger $amount): float { - return ((float) $amount->getAmount()) / (10 ** 2); + return $amount + ->toBigDecimal() + ->dividedBy(100, 2, RoundingMode::HALF_EVEN) + ->toFloat(); } private function getCurrency(Currency|string|null $currency): string diff --git a/src/MoneyBundle/Formatter/MoneyFormatterInterface.php b/src/MoneyBundle/Formatter/MoneyFormatterInterface.php index f42789b34..a8b46eb4c 100644 --- a/src/MoneyBundle/Formatter/MoneyFormatterInterface.php +++ b/src/MoneyBundle/Formatter/MoneyFormatterInterface.php @@ -13,16 +13,13 @@ namespace SolidInvoice\MoneyBundle\Formatter; +use Brick\Math\BigInteger; use Money\Currency; -use Money\Money; use Money\MoneyFormatter; interface MoneyFormatterInterface extends MoneyFormatter { - /** - * @param Currency|string $currency - */ - public function getCurrencySymbol($currency = null): string; + public function getCurrencySymbol(Currency|string $currency = null): string; public function getThousandSeparator(): string; @@ -30,5 +27,5 @@ public function getDecimalSeparator(): string; public function getPattern(): string; - public static function toFloat(Money $amount): float; + public static function toFloat(BigInteger $amount): float; } diff --git a/src/MoneyBundle/Tests/CalculatorTest.php b/src/MoneyBundle/Tests/CalculatorTest.php index 9d871458c..d81a727a1 100644 --- a/src/MoneyBundle/Tests/CalculatorTest.php +++ b/src/MoneyBundle/Tests/CalculatorTest.php @@ -13,9 +13,9 @@ namespace SolidInvoice\MoneyBundle\Tests; +use Brick\Math\BigDecimal; +use Brick\Math\Exception\MathException; use InvalidArgumentException; -use Money\Currency; -use Money\Money; use PHPUnit\Framework\TestCase; use SolidInvoice\CoreBundle\Entity\Discount; use SolidInvoice\InvoiceBundle\Entity\Invoice; @@ -23,6 +23,9 @@ class CalculatorTest extends TestCase { + /** + * @throws MathException + */ public function testCalculateDiscountWithInvalidEntity(): void { $this->expectException(InvalidArgumentException::class); @@ -31,6 +34,9 @@ public function testCalculateDiscountWithInvalidEntity(): void $calculator->calculateDiscount(''); } + /** + * @throws MathException + */ public function testCalculateDiscount(): void { $calculator = new Calculator(); @@ -39,11 +45,14 @@ public function testCalculateDiscount(): void $discount->setType(Discount::TYPE_PERCENTAGE); $discount->setValue(10); $entity->setDiscount($discount); - $entity->setBaseTotal(new Money(20000, new Currency('USD'))); + $entity->setBaseTotal(20000); - self::assertEquals(new Money(2000, new Currency('USD')), $calculator->calculateDiscount($entity)); + self::assertEquals(BigDecimal::of(2000.0), $calculator->calculateDiscount($entity)); } + /** + * @throws MathException + */ public function testCalculateDiscountPercentage(): void { $calculator = new Calculator(); @@ -52,16 +61,19 @@ public function testCalculateDiscountPercentage(): void $discount->setType(Discount::TYPE_MONEY); $discount->setValue(35); $entity->setDiscount($discount); - $entity->setBaseTotal(new Money(200, new Currency('USD'))); + $entity->setBaseTotal(200); - self::assertEquals(new Money(3500, new Currency('USD')), $calculator->calculateDiscount($entity)); + self::assertEquals(BigDecimal::of(35), $calculator->calculateDiscount($entity)); } + /** + * @throws MathException + */ public function testCalculatePercentage(): void { $calculator = new Calculator(); self::assertSame(0.0, $calculator->calculatePercentage(100)); self::assertSame(24.0, $calculator->calculatePercentage(200, 12)); - self::assertSame(40.0, $calculator->calculatePercentage(new Money(200, new Currency('USD')), 20)); + self::assertSame(40.0, $calculator->calculatePercentage(200, 20)); } } diff --git a/src/MoneyBundle/Tests/Twig/Extension/MoneyFormatterExtensionTest.php b/src/MoneyBundle/Tests/Twig/Extension/MoneyFormatterExtensionTest.php index 76eeed084..c1a0810ee 100644 --- a/src/MoneyBundle/Tests/Twig/Extension/MoneyFormatterExtensionTest.php +++ b/src/MoneyBundle/Tests/Twig/Extension/MoneyFormatterExtensionTest.php @@ -14,12 +14,9 @@ namespace SolidInvoice\MoneyBundle\Tests\Twig\Extension; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; -use Mockery as M; use Money\Currency; -use Money\Money; use PHPUnit\Framework\TestCase; use SolidInvoice\MoneyBundle\Formatter\MoneyFormatter; -use SolidInvoice\MoneyBundle\Formatter\MoneyFormatterInterface; use SolidInvoice\MoneyBundle\Twig\Extension\MoneyFormatterExtension; use SolidInvoice\SettingsBundle\SystemConfig; use Twig\TwigFilter; @@ -31,7 +28,7 @@ class MoneyFormatterExtensionTest extends TestCase public function testGetFunctions(): void { - $systemConfig = M::mock(SystemConfig::class); + $systemConfig = $this->createMock(SystemConfig::class); $moneyFormatter = new MoneyFormatter('en_US', $systemConfig); $extension = new MoneyFormatterExtension($moneyFormatter, $systemConfig); @@ -47,16 +44,15 @@ public function testGetFunctions(): void public function testGetFilters(): void { - $money = new Money(1200, new Currency('USD')); + $systemConfig = $this->createMock(SystemConfig::class); - $moneyFormatter = M::mock(MoneyFormatterInterface::class); - $moneyFormatter - ->shouldReceive('format') - ->once() - ->with($money) - ->andReturn('$12,00'); + $systemConfig + ->expects(self::once()) + ->method('getCurrency') + ->willReturn(new Currency('USD')); - $extension = new MoneyFormatterExtension($moneyFormatter, M::mock(SystemConfig::class)); + $moneyFormatter = new MoneyFormatter('en_US', $systemConfig); + $extension = new MoneyFormatterExtension($moneyFormatter, $systemConfig); $filters = $extension->getFilters(); @@ -64,6 +60,6 @@ public function testGetFilters(): void self::assertInstanceOf(TwigFilter::class, $filters[0]); self::assertSame('formatCurrency', $filters[0]->getName()); - self::assertSame('$12,00', call_user_func($filters[0]->getCallable(), $money)); + self::assertSame('$12.00', call_user_func($filters[0]->getCallable(), 1200)); } } diff --git a/src/MoneyBundle/Twig/Extension/MoneyFormatterExtension.php b/src/MoneyBundle/Twig/Extension/MoneyFormatterExtension.php index c3afe20f7..832755ba0 100644 --- a/src/MoneyBundle/Twig/Extension/MoneyFormatterExtension.php +++ b/src/MoneyBundle/Twig/Extension/MoneyFormatterExtension.php @@ -13,6 +13,7 @@ namespace SolidInvoice\MoneyBundle\Twig\Extension; +use Brick\Math\BigNumber; use Money\Currency; use Money\Money; use SolidInvoice\MoneyBundle\Formatter\MoneyFormatterInterface; @@ -20,7 +21,6 @@ use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; -use function is_int; /** * @see \SolidInvoice\MoneyBundle\Tests\Twig\Extension\MoneyFormatterExtensionTest @@ -43,20 +43,8 @@ public function getFunctions(): array public function getFilters(): array { return [ - new TwigFilter('formatCurrency', function ($money, ?Currency $currency = null): string { - if (null === $money) { - if ($currency instanceof Currency) { - return $this->formatter->format(new Money(0, $currency)); - } - - return $this->formatter->format(new Money(0, $this->systemConfig->getCurrency())); - } - - if (is_int($money)) { - return $this->formatter->format(new Money($money, $this->systemConfig->getCurrency())); - } - - return $this->formatter->format($money); + new TwigFilter('formatCurrency', function (BigNumber|int|float|string $value, ?Currency $currency = null): string { + return $this->formatter->format(new Money(BigNumber::of($value)->toBigInteger()->toInt(), $currency ?? $this->systemConfig->getCurrency())); }), ]; } diff --git a/src/PaymentBundle/Action/Prepare.php b/src/PaymentBundle/Action/Prepare.php index c0e891046..80411dffe 100644 --- a/src/PaymentBundle/Action/Prepare.php +++ b/src/PaymentBundle/Action/Prepare.php @@ -14,6 +14,7 @@ namespace SolidInvoice\PaymentBundle\Action; use const FILTER_VALIDATE_BOOLEAN; +use Brick\Math\BigInteger; use DateTime; use Exception; use Money\Money; @@ -112,8 +113,7 @@ public function __invoke(Request $request, ?Invoice $invoice): Template | Respon if ($form->isSubmitted() && $form->isValid()) { $paymentFactories = $this->paymentFactories->getFactories($form->getData()['payment_method']->getFactoryName()); $data = $form->getData(); - /** @var Money $amount */ - $amount = $data['amount']; + $amount = BigInteger::of($data['amount']); /** @var PaymentMethod $paymentMethod */ $paymentMethod = $data['payment_method']; @@ -128,9 +128,9 @@ public function __invoke(Request $request, ?Invoice $invoice): Template | Respon $clientCredit = $invoice->getClient()->getCredit()->getValue(); $invalid = ''; - if ($amount->greaterThan($clientCredit)) { + if ($amount->isGreaterThan($clientCredit)) { $invalid = 'payment.create.exception.not_enough_credit'; - } elseif ($amount->greaterThan($invoice->getBalance())) { + } elseif ($amount->isGreaterThan($invoice->getBalance())) { $invalid = 'payment.create.exception.amount_exceeds_balance'; } diff --git a/src/PaymentBundle/Listener/PaymentCompleteListener.php b/src/PaymentBundle/Listener/PaymentCompleteListener.php index 0752d16ee..a03506494 100644 --- a/src/PaymentBundle/Listener/PaymentCompleteListener.php +++ b/src/PaymentBundle/Listener/PaymentCompleteListener.php @@ -13,10 +13,9 @@ namespace SolidInvoice\PaymentBundle\Listener; +use Brick\Math\Exception\MathException; use Doctrine\Persistence\ManagerRegistry; use Generator; -use Money\Currency; -use Money\Money; use SolidInvoice\ClientBundle\Entity\Credit; use SolidInvoice\CoreBundle\Response\FlashResponse; use SolidInvoice\InvoiceBundle\Entity\Invoice; @@ -49,16 +48,19 @@ public function __construct( ) { } + /** + * @throws MathException + */ public function onPaymentComplete(PaymentCompleteEvent $event): void { $payment = $event->getPayment(); $status = (string) $payment->getStatus(); - if ('credit' === $payment->getMethod()->getGatewayName()) { + if ('credit' === $payment->getMethod()?->getGatewayName()) { $creditRepository = $this->registry->getRepository(Credit::class); $creditRepository->deductCredit( $payment->getClient(), - new Money($payment->getTotalAmount(), new Currency($payment->getCurrencyCode())) + $payment->getTotalAmount(), ); } @@ -70,8 +72,8 @@ public function onPaymentComplete(PaymentCompleteEvent $event): void } else { $paymentRepository = $this->registry->getRepository(Payment::class); $invoiceTotal = $invoice->getTotal(); - $totalPaid = new Money($paymentRepository->getTotalPaidForInvoice($invoice), $invoiceTotal->getCurrency()); - $invoice->setBalance($invoiceTotal->subtract($totalPaid)); + $totalPaid = $paymentRepository->getTotalPaidForInvoice($invoice); + $invoice->setBalance($invoiceTotal->minus($totalPaid)); $em = $this->registry->getManager(); $em->persist($invoice); diff --git a/src/PaymentBundle/PaymentAction/PaypalExpress/CapturePaymentAction.php b/src/PaymentBundle/PaymentAction/PaypalExpress/CapturePaymentAction.php index 4967eed5e..561fef080 100644 --- a/src/PaymentBundle/PaymentAction/PaypalExpress/CapturePaymentAction.php +++ b/src/PaymentBundle/PaymentAction/PaypalExpress/CapturePaymentAction.php @@ -13,6 +13,7 @@ namespace SolidInvoice\PaymentBundle\PaymentAction\PaypalExpress; +use Brick\Math\Exception\MathException; use Exception; use Payum\Core\Action\ActionInterface; use Payum\Core\Bridge\Spl\ArrayObject; @@ -21,9 +22,11 @@ use Payum\Core\GatewayAwareTrait; use Payum\Core\Request\Capture; use Payum\Core\Security\GenericTokenFactoryInterface; +use SolidInvoice\InvoiceBundle\Entity\Invoice; use SolidInvoice\InvoiceBundle\Entity\Item; -use SolidInvoice\MoneyBundle\Formatter\MoneyFormatterInterface; +use SolidInvoice\MoneyBundle\Formatter\MoneyFormatter; use SolidInvoice\PaymentBundle\Entity\Payment; +use function assert; /** * @deprecated This action is not used anymore and will be removed in a future version @@ -39,11 +42,14 @@ class CapturePaymentAction implements ActionInterface, GatewayAwareInterface public function __construct( GenericTokenFactoryInterface $tokenFactory, - private readonly MoneyFormatterInterface $formatter ) { $this->tokenFactory = $tokenFactory; } + /** + * @throws MathException + * @throws Exception + */ public function execute($request): void { RequestNotSupportedException::assertSupports($this, $request); @@ -56,34 +62,35 @@ public function execute($request): void } $invoice = $payment->getInvoice(); + assert($invoice instanceof Invoice); $details = []; $details['PAYMENTREQUEST_0_INVNUM'] = $invoice->getId() . '-' . $payment->getId(); $details['PAYMENTREQUEST_0_CURRENCYCODE'] = $payment->getCurrencyCode(); - $details['PAYMENTREQUEST_0_AMT'] = number_format($this->formatter->toFloat($invoice->getTotal()), 2); - $details['PAYMENTREQUEST_0_ITEMAMT'] = number_format($this->formatter->toFloat($invoice->getTotal()), 2); + $details['PAYMENTREQUEST_0_AMT'] = number_format(MoneyFormatter::toFloat($invoice->getTotal()), 2); + $details['PAYMENTREQUEST_0_ITEMAMT'] = number_format(MoneyFormatter::toFloat($invoice->getTotal()), 2); $counter = 0; foreach ($invoice->getItems() as $item) { /** @var Item $item */ $details['L_PAYMENTREQUEST_0_NAME' . $counter] = $item->getDescription(); - $details['L_PAYMENTREQUEST_0_AMT' . $counter] = number_format($this->formatter->toFloat($item->getPrice()), 2); + $details['L_PAYMENTREQUEST_0_AMT' . $counter] = number_format(MoneyFormatter::toFloat($item->getPrice()), 2); $details['L_PAYMENTREQUEST_0_QTY' . $counter] = $item->getQty(); ++$counter; } if ($invoice->getDiscount()->getValue()) { - $discount = $invoice->getBaseTotal()->multiply($invoice->getDiscount()); + $discount = $invoice->getBaseTotal()->multipliedBy($invoice->getDiscount()->getValue()); $details['L_PAYMENTREQUEST_0_NAME' . $counter] = 'Discount'; - $details['L_PAYMENTREQUEST_0_AMT' . $counter] = '-' . number_format($this->formatter->toFloat($discount), 2); + $details['L_PAYMENTREQUEST_0_AMT' . $counter] = '-' . number_format(MoneyFormatter::toFloat($discount), 2); $details['L_PAYMENTREQUEST_0_QTY' . $counter] = 1; } if (null !== $tax = $invoice->getTax()) { $details['L_PAYMENTREQUEST_0_NAME' . $counter] = 'Tax Total'; - $details['L_PAYMENTREQUEST_0_AMT' . $counter] = number_format($this->formatter->toFloat($tax), 2); + $details['L_PAYMENTREQUEST_0_AMT' . $counter] = number_format(MoneyFormatter::toFloat($tax), 2); $details['L_PAYMENTREQUEST_0_QTY' . $counter] = 1; } diff --git a/src/PaymentBundle/Repository/PaymentRepository.php b/src/PaymentBundle/Repository/PaymentRepository.php index cf5378251..dfe7b8e1a 100644 --- a/src/PaymentBundle/Repository/PaymentRepository.php +++ b/src/PaymentBundle/Repository/PaymentRepository.php @@ -13,6 +13,8 @@ namespace SolidInvoice\PaymentBundle\Repository; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use DateTime; use DateTimeInterface; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; @@ -23,8 +25,6 @@ use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; -use Money\Currency; -use Money\Money; use Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType; use Ramsey\Uuid\UuidInterface; use SolidInvoice\ClientBundle\Entity\Client; @@ -47,7 +47,8 @@ public function __construct(ManagerRegistry $registry) /** * Gets the total income that was received. * - * @return Money[] + * @return BigInteger[] + * @throws MathException */ public function getTotalIncome(): array { @@ -63,7 +64,7 @@ public function getTotalIncome(): array $results = []; foreach ($query->getArrayResult() as $result) { - $results[] = new Money($result['total'], new Currency($result['currencyCode'])); + $results[$result['currencyCode']] = BigInteger::of($result['total']); } return $results; @@ -116,10 +117,10 @@ protected function getPaymentQueryBuilder(string $orderField = null, string $sor /** * Returns an array of all the payments for an invoice. */ - public function getTotalPaidForInvoice(Invoice $invoice): int + public function getTotalPaidForInvoice(Invoice $invoice): BigInteger { if (! $invoice->getId() instanceof UuidInterface) { - return 0; + return BigInteger::zero(); } $queryBuilder = $this->createQueryBuilder('p'); @@ -134,9 +135,9 @@ public function getTotalPaidForInvoice(Invoice $invoice): int $query = $queryBuilder->getQuery(); try { - return (int) $query->getSingleScalarResult(); - } catch (NoResultException | NonUniqueResultException) { - return 0; + return BigInteger::of((int) $query->getSingleScalarResult()); + } catch (NoResultException | NonUniqueResultException | MathException) { + return BigInteger::zero(); } } @@ -159,7 +160,8 @@ public function getPaymentsForClient(Client $client, string $orderField = null, /** * Gets the most recent created payments. * - * @return array> + * @return array> + * @throws MathException */ public function getRecentPayments(int $limit = 5): array { @@ -175,7 +177,7 @@ public function getRecentPayments(int $limit = 5): array ->setMaxResults($limit); return array_map(static function (array $payment): array { - $payment['amount'] = new Money($payment['totalAmount'], new Currency($payment['currencyCode'])); + $payment['amount'] = BigInteger::of($payment['totalAmount']); return $payment; }, $qb->getQuery()->getArrayResult()); @@ -297,27 +299,10 @@ public function getGridQuery(array $parameters = []): QueryBuilder return $qb; } - public function updateCurrency(Client $client): void - { - $filters = $this->getEntityManager()->getFilters(); - $filters->disable('archivable'); - - $currency = $client->getCurrency(); - - $qb = $this->createQueryBuilder('p'); - - $qb->update() - ->set('p.currencyCode', ':currency') - ->where('p.client = :client') - ->setParameter('client', $client->getId(), UuidBinaryOrderedTimeType::NAME) - ->setParameter('currency', $currency->getCode()); - - $qb->getQuery()->execute(); - - $filters->enable('archivable'); - } - - public function getTotalIncomeForClient(Client $client): ?Money + /** + * @throws MathException + */ + public function getTotalIncomeForClient(Client $client): BigInteger { $qb = $this->createQueryBuilder('p'); @@ -333,9 +318,9 @@ public function getTotalIncomeForClient(Client $client): ?Money $result = $query->getResult(); if ([] === $result) { - return null; + return BigInteger::zero(); } - return new Money($result[0]['total'], new Currency($result[0]['currencyCode'])); + return BigInteger::of($result[0]['total']); } } diff --git a/src/PaymentBundle/Tests/Form/Type/PaymentTypeTest.php b/src/PaymentBundle/Tests/Form/Type/PaymentTypeTest.php index e1e1b93d3..46f54b765 100644 --- a/src/PaymentBundle/Tests/Form/Type/PaymentTypeTest.php +++ b/src/PaymentBundle/Tests/Form/Type/PaymentTypeTest.php @@ -13,13 +13,17 @@ namespace SolidInvoice\PaymentBundle\Tests\Form\Type; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Money\Currency; -use Money\Money; use SolidInvoice\CoreBundle\Tests\FormTestCase; use SolidInvoice\PaymentBundle\Form\Type\PaymentType; class PaymentTypeTest extends FormTestCase { + /** + * @throws MathException + */ public function testSubmit(): void { $paymentMethod = $this->faker->name; @@ -31,7 +35,7 @@ public function testSubmit(): void ]; $object = [ - 'amount' => new Money($amount * 100, new Currency('USD')), + 'amount' => BigInteger::of($amount * 100), ]; $this->assertFormData($this->factory->create(PaymentType::class, [], ['currency' => new Currency('USD'), 'preferred_choices' => [], 'user' => null]), $formData, $object); diff --git a/src/PaymentBundle/Tests/Repository/PaymentRepositoryTest.php b/src/PaymentBundle/Tests/Repository/PaymentRepositoryTest.php index 814c295ee..34935c947 100644 --- a/src/PaymentBundle/Tests/Repository/PaymentRepositoryTest.php +++ b/src/PaymentBundle/Tests/Repository/PaymentRepositoryTest.php @@ -13,11 +13,13 @@ namespace SolidInvoice\PaymentBundle\Tests\Repository; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use DateTime; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; -use Money\Currency; -use Money\Money; +use Doctrine\ORM\Exception\NotSupported; +use Doctrine\Persistence\Mapping\MappingException; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\ClientBundle\Test\Factory\ClientFactory; use SolidInvoice\CoreBundle\Test\Traits\DoctrineTestTrait; @@ -38,6 +40,9 @@ final class PaymentRepositoryTest extends KernelTestCase use DoctrineTestTrait; use Factories; + /** + * @throws NotSupported + */ public function testGetTotalPaidForInvoice(): void { $client = ClientFactory::createOne(); @@ -52,15 +57,18 @@ public function testGetTotalPaidForInvoice(): void PaymentFactory::assert() ->count(1); - self::assertSame( - 500123, + self::assertTrue( $this ->em ->getRepository(Payment::class) ->getTotalPaidForInvoice($invoice->object()) + ->isEqualTo(500123) ); } + /** + * @throws NotSupported + */ public function testGetTotalPaidForInvoiceWithNoCapturedPayments(): void { $client = ClientFactory::createOne(); @@ -75,15 +83,18 @@ public function testGetTotalPaidForInvoiceWithNoCapturedPayments(): void PaymentFactory::assert() ->count(1); - self::assertSame( - 0, + self::assertTrue( $this ->em ->getRepository(Payment::class) ->getTotalPaidForInvoice($invoice->object()) + ->isEqualTo(0) ); } + /** + * @throws NotSupported + */ public function testGetTotalPaidForInvoiceWithDifferentInvoice(): void { $client = ClientFactory::createOne(); @@ -98,15 +109,18 @@ public function testGetTotalPaidForInvoiceWithDifferentInvoice(): void PaymentFactory::assert() ->count(1); - self::assertSame( - 0, + self::assertTrue( $this ->em ->getRepository(Payment::class) ->getTotalPaidForInvoice($invoice[1]->object()) + ->isEqualTo(0) ); } + /** + * @throws NotSupported + */ public function testGetTotalIncomeForClient(): void { $client = ClientFactory::createOne(['currencyCode' => 'USD']); @@ -122,18 +136,18 @@ public function testGetTotalIncomeForClient(): void PaymentFactory::assert() ->count(1); - self::assertEquals( - new Money( - 500123, - new Currency('USD') - ), + self::assertTrue( $this ->em ->getRepository(Payment::class) ->getTotalIncomeForClient($client->object()) + ->isEqualTo(500123) ); } + /** + * @throws NotSupported + */ public function testGetTotalIncomeForClientWithNoPayments(): void { $client = ClientFactory::createOne(['currencyCode' => 'USD']); @@ -149,14 +163,18 @@ public function testGetTotalIncomeForClientWithNoPayments(): void PaymentFactory::assert() ->count(1); - self::assertNull( + self::assertTrue( $this ->em ->getRepository(Payment::class) ->getTotalIncomeForClient($client->object()) + ->isZero() ); } + /** + * @throws NotSupported + */ public function testGetGridQuery(): void { $queryBuilder = $this @@ -227,6 +245,9 @@ public function testGetGridQuery(): void ); } + /** + * @throws NotSupported + */ public function testGetPaymentsForClient(): void { $client = ClientFactory::createOne(['currencyCode' => 'USD']); @@ -268,6 +289,10 @@ public function testGetPaymentsForClient(): void ); } + /** + * @throws NotSupported + * @throws MathException + */ public function testGetTotalIncome(): void { $client = ClientFactory::createOne(['currencyCode' => 'USD']); @@ -280,15 +305,21 @@ public function testGetTotalIncome(): void 'status' => Status::STATUS_CAPTURED ]); + PaymentFactory::createMany(2, [ + 'invoice' => InvoiceFactory::new(['client' => $client]), + 'client' => ClientFactory::createOne(['currencyCode' => 'EUR']), + 'currencyCode' => 'EUR', + 'totalAmount' => 500123, + 'status' => Status::STATUS_CAPTURED + ]); + PaymentFactory::assert() - ->count(3); + ->count(5); self::assertEquals( [ - new Money( - 500123 * 3, - new Currency('USD') - ), + 'USD' => BigInteger::of(500123 * 3), + 'EUR' => BigInteger::of(500123 * 2), ], $this ->em @@ -297,6 +328,10 @@ public function testGetTotalIncome(): void ); } + /** + * @throws MathException + * @throws NotSupported + */ public function testGetTotalIncomeWithMultipleCurrencies(): void { $client = ClientFactory::createOne(['currencyCode' => 'USD']); @@ -324,14 +359,8 @@ public function testGetTotalIncomeWithMultipleCurrencies(): void self::assertEquals( [ - new Money( - 500123 * 3, - new Currency('USD') - ), - new Money( - 500123, - new Currency('EUR') - ), + 'USD' => BigInteger::of(500123 * 3), + 'EUR' => BigInteger::of(500123), ], $this ->em @@ -340,6 +369,9 @@ public function testGetTotalIncomeWithMultipleCurrencies(): void ); } + /** + * @throws NotSupported + */ public function testGetPaymentsForInvoice(): void { $client = ClientFactory::createOne(['currencyCode' => 'USD']); @@ -407,30 +439,9 @@ public function testGetPaymentsList(): void ); } - public function testUpdateCurrency(): void - { - $client = ClientFactory::createOne()->object(); - - PaymentFactory::createMany(3, [ - 'client' => $client, - ]); - - $client->setCurrencyCode('EUR'); - - $this - ->em - ->getRepository(Payment::class) - ->updateCurrency($client); - - $this->em->clear(); - - $payments = $this->em->getRepository(Payment::class)->findAll(); - - foreach ($payments as $payment) { - self::assertSame('EUR', $payment->getCurrencyCode()); - } - } - + /** + * @throws NotSupported + */ public function testGetPaymentsByMonth(): void { $created = new DateTimeImmutable(); @@ -451,6 +462,10 @@ public function testGetPaymentsByMonth(): void ); } + /** + * @throws MappingException + * @throws NotSupported + */ public function testUpdatePaymentStatus(): void { /** @var Payment $payment */ @@ -470,6 +485,10 @@ public function testUpdatePaymentStatus(): void self::assertSame(Status::STATUS_CAPTURED, $payment->getStatus()); } + /** + * @throws NotSupported + * @throws MathException + */ public function testGetRecentPayments(): void { $client = ClientFactory::createOne(['currencyCode' => 'USD', 'archived' => null]); @@ -506,7 +525,7 @@ public function testGetRecentPayments(): void 'client_id' => $client->getId(), 'client' => $client->getName(), 'message' => 'test', - 'amount' => new Money(500123, new Currency('USD')) + 'amount' => BigInteger::of(500123) ] ], $this diff --git a/src/PaymentBundle/Twig/PaymentExtension.php b/src/PaymentBundle/Twig/PaymentExtension.php index 9e5c210eb..de040e683 100644 --- a/src/PaymentBundle/Twig/PaymentExtension.php +++ b/src/PaymentBundle/Twig/PaymentExtension.php @@ -13,8 +13,8 @@ namespace SolidInvoice\PaymentBundle\Twig; +use Brick\Math\BigInteger; use Doctrine\Persistence\ManagerRegistry; -use Money\Money; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\InvoiceBundle\Entity\Invoice; use SolidInvoice\PaymentBundle\Entity\Payment; @@ -48,11 +48,11 @@ public function getFunctions(): array ->registry ->getRepository(PaymentMethod::class) ->getTotalMethodsConfigured($includeInternal)), - new TwigFunction('total_income', fn (Client $client): ?Money => $this + new TwigFunction('total_income', fn (Client $client): BigInteger => $this ->registry ->getRepository(Payment::class) ->getTotalIncomeForClient($client)), - new TwigFunction('total_outstanding', fn (Client $client): ?Money => $this + new TwigFunction('total_outstanding', fn (Client $client): BigInteger => $this ->registry ->getRepository(Invoice::class) ->getTotalOutstandingForClient($client)), diff --git a/src/QuoteBundle/Cloner/QuoteCloner.php b/src/QuoteBundle/Cloner/QuoteCloner.php index f098a26ed..949a374f7 100644 --- a/src/QuoteBundle/Cloner/QuoteCloner.php +++ b/src/QuoteBundle/Cloner/QuoteCloner.php @@ -13,6 +13,7 @@ namespace SolidInvoice\QuoteBundle\Cloner; +use Brick\Math\Exception\MathException; use Carbon\Carbon; use SolidInvoice\QuoteBundle\Entity\Item; use SolidInvoice\QuoteBundle\Entity\Quote; @@ -31,6 +32,9 @@ public function __construct( ) { } + /** + * @throws MathException + */ public function clone(Quote $quote): Quote { // We don't use 'clone', since cloning a quote will clone all the item id's and nested values. @@ -59,6 +63,9 @@ public function clone(Quote $quote): Quote return $newQuote; } + /** + * @throws MathException + */ private function addItems(Quote $quote, Carbon $now): Traversable { foreach ($quote->getItems() as $item) { diff --git a/src/QuoteBundle/DataFixtures/ORM/LoadData.php b/src/QuoteBundle/DataFixtures/ORM/LoadData.php index b0f8c367c..938b027dc 100644 --- a/src/QuoteBundle/DataFixtures/ORM/LoadData.php +++ b/src/QuoteBundle/DataFixtures/ORM/LoadData.php @@ -13,10 +13,9 @@ namespace SolidInvoice\QuoteBundle\DataFixtures\ORM; +use Brick\Math\BigInteger; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use Money\Currency; -use Money\Money; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\ClientBundle\Entity\Contact; use SolidInvoice\QuoteBundle\Entity\Item; @@ -44,7 +43,7 @@ public function load(ObjectManager $manager): void $item = new Item(); $item->setQty(1); - $item->setPrice(new Money(10000, new Currency('USD'))); + $item->setPrice(BigInteger::of(10000)); $item->setDescription('Test Item'); $quote->addItem($item); diff --git a/src/QuoteBundle/Entity/Item.php b/src/QuoteBundle/Entity/Item.php index a530abc09..85ab355d8 100644 --- a/src/QuoteBundle/Entity/Item.php +++ b/src/QuoteBundle/Entity/Item.php @@ -13,16 +13,17 @@ namespace SolidInvoice\QuoteBundle\Entity; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Money\Money; use Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType; use Ramsey\Uuid\Doctrine\UuidOrderedTimeGenerator; use Ramsey\Uuid\UuidInterface; +use SolidInvoice\CoreBundle\Doctrine\Type\BigIntegerType; use SolidInvoice\CoreBundle\Entity\ItemInterface; use SolidInvoice\CoreBundle\Traits\Entity\CompanyAware; use SolidInvoice\CoreBundle\Traits\Entity\TimeStampable; -use SolidInvoice\MoneyBundle\Entity\Money as MoneyEntity; use SolidInvoice\QuoteBundle\Repository\ItemRepository; use SolidInvoice\TaxBundle\Entity\Tax; use Stringable; @@ -51,10 +52,10 @@ class Item implements ItemInterface, Stringable #[Serialize\Groups(['quote_api', 'client_api', 'create_quote_api'])] private ?string $description = null; - #[ORM\Embedded(class: MoneyEntity::class)] + #[ORM\Column(name: 'price_amount', type: BigIntegerType::NAME)] #[Assert\NotBlank] #[Serialize\Groups(['quote_api', 'client_api', 'create_quote_api'])] - private MoneyEntity $price; + private BigInteger $price; #[ORM\Column(name: 'qty', type: Types::FLOAT)] #[Assert\NotBlank] @@ -68,14 +69,14 @@ class Item implements ItemInterface, Stringable #[Serialize\Groups(['quote_api', 'client_api', 'create_quote_api'])] private ?Tax $tax = null; - #[ORM\Embedded(class: MoneyEntity::class)] + #[ORM\Column(name: 'total_amount', type: BigIntegerType::NAME)] #[Serialize\Groups(['quote_api', 'client_api'])] - private MoneyEntity $total; + private BigInteger $total; public function __construct() { - $this->total = new MoneyEntity(); - $this->price = new MoneyEntity(); + $this->total = BigInteger::zero(); + $this->price = BigInteger::zero(); } public function getId(): UuidInterface @@ -95,16 +96,19 @@ public function getDescription(): ?string return $this->description; } - public function setPrice(Money $price): ItemInterface + /** + * @throws MathException + */ + public function setPrice(BigInteger|float|int|string $price): ItemInterface { - $this->price = new MoneyEntity($price); + $this->price = BigInteger::of($price); return $this; } - public function getPrice(): ?Money + public function getPrice(): BigInteger { - return $this->price->getMoney(); + return $this->price; } public function setQty(float $qty): ItemInterface @@ -131,16 +135,19 @@ public function getQuote(): ?Quote return $this->quote; } - public function setTotal(Money $total): ItemInterface + /** + * @throws MathException + */ + public function setTotal(BigInteger|float|int|string $total): ItemInterface { - $this->total = new MoneyEntity($total); + $this->total = BigInteger::of($total); return $this; } - public function getTotal(): Money + public function getTotal(): BigInteger { - return $this->total->getMoney(); + return $this->total; } public function getTax(): ?Tax @@ -158,7 +165,7 @@ public function setTax(?Tax $tax): ItemInterface #[ORM\PrePersist] public function updateTotal(): void { - $this->total = new MoneyEntity($this->getPrice()->multiply($this->qty)); + $this->total = $this->getPrice()->multipliedBy($this->qty); } public function __toString(): string diff --git a/src/QuoteBundle/Entity/Quote.php b/src/QuoteBundle/Entity/Quote.php index e85a2db80..f5bef91ea 100644 --- a/src/QuoteBundle/Entity/Quote.php +++ b/src/QuoteBundle/Entity/Quote.php @@ -15,13 +15,14 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use DateTimeInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Exception; -use Money\Money; use Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType; use Ramsey\Uuid\Doctrine\UuidOrderedTimeGenerator; use Ramsey\Uuid\Doctrine\UuidType; @@ -29,13 +30,13 @@ use Ramsey\Uuid\UuidInterface; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\ClientBundle\Entity\Contact; +use SolidInvoice\CoreBundle\Doctrine\Type\BigIntegerType; use SolidInvoice\CoreBundle\Entity\Discount; use SolidInvoice\CoreBundle\Entity\ItemInterface; use SolidInvoice\CoreBundle\Traits\Entity\Archivable; use SolidInvoice\CoreBundle\Traits\Entity\CompanyAware; use SolidInvoice\CoreBundle\Traits\Entity\TimeStampable; use SolidInvoice\InvoiceBundle\Entity\Invoice; -use SolidInvoice\MoneyBundle\Entity\Money as MoneyEntity; use SolidInvoice\QuoteBundle\Repository\QuoteRepository; use SolidInvoice\QuoteBundle\Traits\QuoteStatusTrait; use Symfony\Component\Serializer\Annotation as Serialize; @@ -86,17 +87,17 @@ class Quote #[Serialize\Groups(['quote_api', 'create_quote_api'])] private ?Client $client = null; - #[ORM\Embedded(class: MoneyEntity::class)] + #[ORM\Column(name: 'total_amount', type: BigIntegerType::NAME)] #[Serialize\Groups(['quote_api', 'client_api'])] - private MoneyEntity $total; + private BigInteger $total; - #[ORM\Embedded(class: MoneyEntity::class)] + #[ORM\Column(name: 'baseTotal_amount', type: BigIntegerType::NAME)] #[Serialize\Groups(['quote_api', 'client_api'])] - private MoneyEntity $baseTotal; + private BigInteger $baseTotal; - #[ORM\Embedded(class: MoneyEntity::class)] + #[ORM\Column(name: 'tax_amount', type: BigIntegerType::NAME)] #[Serialize\Groups(['quote_api', 'client_api'])] - private MoneyEntity $tax; + private BigInteger $tax; #[ORM\Embedded(class: Discount::class)] #[Serialize\Groups(['quote_api', 'client_api', 'create_quote_api'])] @@ -145,9 +146,9 @@ public function __construct() $this->setUuid(Uuid::uuid1()); } catch (Exception) { } - $this->baseTotal = new MoneyEntity(); - $this->tax = new MoneyEntity(); - $this->total = new MoneyEntity(); + $this->baseTotal = BigInteger::zero(); + $this->tax = BigInteger::zero(); + $this->total = BigInteger::zero(); } public function getId(): ?UuidInterface @@ -222,33 +223,37 @@ public function getClient(): ?Client public function setClient(?Client $client): self { $this->client = $client; - if ($client instanceof Client && null !== $client->getCurrencyCode()) { - $this->total->setCurrency($client->getCurrency()->getCode()); - $this->baseTotal->setCurrency($client->getCurrency()->getCode()); - $this->tax->setCurrency($client->getCurrency()->getCode()); - } + return $this; } - public function getTotal(): ?Money + public function getTotal(): BigInteger { - return $this->total->getMoney(); + return $this->total; } - public function setTotal(Money $total): self + /** + * @throws MathException + */ + public function setTotal(BigInteger|float|int|string $total): self { - $this->total = new MoneyEntity($total); + $this->total = BigInteger::of($total); + return $this; } - public function getBaseTotal(): ?Money + public function getBaseTotal(): BigInteger { - return $this->baseTotal->getMoney(); + return $this->baseTotal; } - public function setBaseTotal(Money $baseTotal): self + /** + * @throws MathException + */ + public function setBaseTotal(BigInteger|float|int|string $baseTotal): self { - $this->baseTotal = new MoneyEntity($baseTotal); + $this->baseTotal = BigInteger::of($baseTotal); + return $this; } @@ -319,14 +324,18 @@ public function setNotes(?string $notes): self return $this; } - public function getTax(): ?Money + public function getTax(): BigInteger { - return $this->tax->getMoney(); + return $this->tax; } - public function setTax(Money $tax): self + /** + * @throws MathException + */ + public function setTax(BigInteger|float|int|string $tax): self { - $this->tax = new MoneyEntity($tax); + $this->tax = BigInteger::of($tax); + return $this; } diff --git a/src/QuoteBundle/Repository/ItemRepository.php b/src/QuoteBundle/Repository/ItemRepository.php index 237ca55e3..3a93f756c 100644 --- a/src/QuoteBundle/Repository/ItemRepository.php +++ b/src/QuoteBundle/Repository/ItemRepository.php @@ -13,9 +13,9 @@ namespace SolidInvoice\QuoteBundle\Repository; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\ORM\OptimisticLockException; -use Doctrine\ORM\ORMException; use Doctrine\Persistence\ManagerRegistry; use SolidInvoice\QuoteBundle\Entity\Item; use SolidInvoice\QuoteBundle\Entity\Quote; @@ -31,7 +31,7 @@ public function __construct(ManagerRegistry $registry) /** * Removes all tax rates from invoices. * - * @throws ORMException|OptimisticLockException + * @throws MathException */ public function removeTax(Tax $tax): void { @@ -45,8 +45,8 @@ public function removeTax(Tax $tax): void /** @var Quote $quote */ foreach ($query->execute() as $quote) { - $quote->setTotal($quote->getBaseTotal()->add($quote->getTax())); - $quote->setTax(null); + $quote->setTotal($quote->getBaseTotal()->plus($quote->getTax())); + $quote->setTax(BigInteger::zero()); $this->getEntityManager()->persist($quote); } diff --git a/src/QuoteBundle/Repository/QuoteRepository.php b/src/QuoteBundle/Repository/QuoteRepository.php index 3332992db..fdda2617b 100644 --- a/src/QuoteBundle/Repository/QuoteRepository.php +++ b/src/QuoteBundle/Repository/QuoteRepository.php @@ -20,7 +20,6 @@ use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use SolidInvoice\ClientBundle\Entity\Client; -use SolidInvoice\QuoteBundle\Entity\Item; use SolidInvoice\QuoteBundle\Entity\Quote; use function array_walk; @@ -99,48 +98,6 @@ public function getArchivedGridQuery(): QueryBuilder return $qb; } - public function updateCurrency(Client $client): void - { - $filters = $this->getEntityManager()->getFilters(); - $filters->disable('archivable'); - - $currency = $client->getCurrency(); - - $qb = $this->createQueryBuilder('q'); - - $qb->update() - ->set('q.total.currency', ':currency') - ->set('q.baseTotal.currency', ':currency') - ->set('q.tax.currency', ':currency') - ->where('q.client = :client') - ->setParameter('client', $client) - ->setParameter('currency', $currency); - - if ($qb->getQuery()->execute()) { - $qbi = $this->getEntityManager()->createQueryBuilder(); - - $qbi->update() - ->from(Item::class, 'qt') - ->set('qt.price.currency', ':currency') - ->set('qt.total.currency', ':currency') - ->where( - $qbi->expr()->in( - 'qt.quote', - $this->createQueryBuilder('q') - ->select('q.id') - ->where('q.client = :client') - ->getDQL() - ) - ) - ->setParameter('client', $client) - ->setParameter('currency', $currency); - - $qbi->getQuery()->execute(); - } - - $filters->enable('archivable'); - } - /** * @param list $ids */ diff --git a/src/QuoteBundle/Test/Factory/QuoteFactory.php b/src/QuoteBundle/Test/Factory/QuoteFactory.php index 28b0bc9df..f3ccbdf56 100644 --- a/src/QuoteBundle/Test/Factory/QuoteFactory.php +++ b/src/QuoteBundle/Test/Factory/QuoteFactory.php @@ -11,8 +11,8 @@ namespace SolidInvoice\QuoteBundle\Test\Factory; -use Money\Currency; -use Money\Money; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Ramsey\Uuid\Uuid; use SolidInvoice\ClientBundle\Test\Factory\ClientFactory; use SolidInvoice\CoreBundle\Entity\Discount; @@ -46,6 +46,7 @@ final class QuoteFactory extends ModelFactory { /** * @return array + * @throws MathException */ protected function getDefaults(): array { @@ -60,12 +61,12 @@ protected function getDefaults(): array 'archived' => self::faker()->boolean(), 'created' => self::faker()->dateTime(), 'updated' => self::faker()->dateTime(), - 'total' => new Money(self::faker()->randomNumber(), new Currency(self::faker()->currencyCode())), - 'baseTotal' => new Money(self::faker()->randomNumber(), new Currency(self::faker()->currencyCode())), - 'tax' => new Money(self::faker()->randomNumber(), new Currency(self::faker()->currencyCode())), + 'total' => BigInteger::of(self::faker()->randomNumber()), + 'baseTotal' => BigInteger::of(self::faker()->randomNumber()), + 'tax' => BigInteger::of(self::faker()->randomNumber()), 'discount' => (new Discount()) ->setType(self::faker()->text()) - ->setValueMoney(new \SolidInvoice\MoneyBundle\Entity\Money(new Money(self::faker()->randomNumber(), new Currency(self::faker()->currencyCode())))) + ->setValueMoney(self::faker()->randomNumber()) ->setValuePercentage(self::faker()->randomFloat()), ]; } diff --git a/src/QuoteBundle/Tests/Cloner/QuoteClonerTest.php b/src/QuoteBundle/Tests/Cloner/QuoteClonerTest.php index e0ddc6a80..4aa40b86f 100644 --- a/src/QuoteBundle/Tests/Cloner/QuoteClonerTest.php +++ b/src/QuoteBundle/Tests/Cloner/QuoteClonerTest.php @@ -13,9 +13,10 @@ namespace SolidInvoice\QuoteBundle\Tests\Cloner; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use DateTime; use Money\Currency; -use Money\Money; use PHPUnit\Framework\TestCase; use SolidInvoice\ClientBundle\Entity\Client; use SolidInvoice\CoreBundle\Entity\Discount; @@ -32,6 +33,9 @@ class QuoteClonerTest extends TestCase { + /** + * @throws MathException + */ public function testClone(): void { $currency = new Currency('USD'); @@ -50,20 +54,20 @@ public function testClone(): void $item->setTax($tax); $item->setDescription('Item Description'); $item->setCreated(new DateTime('now')); - $item->setPrice(new Money(120, $currency)); + $item->setPrice(BigInteger::of(120)); $item->setQty(10); - $item->setTotal(new Money((12 * 10), $currency)); + $item->setTotal(BigInteger::of(120 * 10)); $quote = new Quote(); - $quote->setBaseTotal(new Money(123, $currency)); + $quote->setBaseTotal(BigInteger::of(123)); $discount = new Discount(); $discount->setType(Discount::TYPE_PERCENTAGE); $discount->setValue(12); $quote->setDiscount($discount); $quote->setNotes('Notes'); - $quote->setTax(new Money(432, $currency)); + $quote->setTax(BigInteger::of(432)); $quote->setTerms('Terms'); - $quote->setTotal(new Money(987, $currency)); + $quote->setTotal(BigInteger::of(987)); $quote->setClient($client); $quote->addItem($item); diff --git a/src/QuoteBundle/Tests/Form/Type/ItemTypeTest.php b/src/QuoteBundle/Tests/Form/Type/ItemTypeTest.php index d4834a3c0..f3b5df976 100644 --- a/src/QuoteBundle/Tests/Form/Type/ItemTypeTest.php +++ b/src/QuoteBundle/Tests/Form/Type/ItemTypeTest.php @@ -11,8 +11,9 @@ namespace SolidInvoice\QuoteBundle\Tests\Form\Type; +use Brick\Math\BigInteger; +use Brick\Math\Exception\MathException; use Money\Currency; -use Money\Money; use SolidInvoice\CoreBundle\Tests\FormTestCase; use SolidInvoice\QuoteBundle\Entity\Item; use SolidInvoice\QuoteBundle\Form\Type\ItemType; @@ -21,6 +22,9 @@ final class ItemTypeTest extends FormTestCase { + /** + * @throws MathException + */ public function testSubmit(): void { $description = $this->faker->text; @@ -38,7 +42,7 @@ public function testSubmit(): void $object = new Item(); $object->setDescription($description); $object->setQty($qty); - $object->setPrice(new Money($price * 100, $currency)); + $object->setPrice(BigInteger::of($price)->multipliedBy(100)); $this->assertFormData($this->factory->create(ItemType::class, null, ['currency' => $currency]), $formData, $object); } diff --git a/src/QuoteBundle/Tests/Functional/Api/QuoteTest.php b/src/QuoteBundle/Tests/Functional/Api/QuoteTest.php index 40e54345c..5572a191a 100644 --- a/src/QuoteBundle/Tests/Functional/Api/QuoteTest.php +++ b/src/QuoteBundle/Tests/Functional/Api/QuoteTest.php @@ -76,9 +76,9 @@ public function testCreate(): void self::assertSame([ 'status' => 'draft', 'client' => '/api/clients/' . $contact->getClient()->getId(), - 'total' => '$90.00', - 'baseTotal' => '$100.00', - 'tax' => '$0.00', + 'total' => 90, + 'baseTotal' => 100, + 'tax' => 0, 'discount' => [ 'type' => 'percentage', 'value' => 10, @@ -89,10 +89,10 @@ public function testCreate(): void 'items' => [ [ 'description' => 'Foo Item', - 'price' => '$100.00', + 'price' => 100, 'qty' => 1, 'tax' => null, - 'total' => '$100.00', + 'total' => 100, ], ], 'users' => [ @@ -121,12 +121,12 @@ public function testGet(): void 'uuid' => $quote->getUuid()->toString(), 'status' => 'draft', 'client' => '/api/clients/' . $quote->getClient()->getId(), - 'total' => '$100.00', - 'baseTotal' => '$100.00', - 'tax' => '$0.00', + 'total' => 100, + 'baseTotal' => 100, + 'tax' => 0, 'discount' => [ 'type' => null, - 'value' => null, + 'value' => 0, ], 'terms' => null, 'notes' => null, @@ -135,10 +135,10 @@ public function testGet(): void [ 'id' => $quote->getItems()->first()->getId()->toString(), 'description' => 'Test Item', - 'price' => '$100.00', + 'price' => 100, 'qty' => 1, 'tax' => null, - 'total' => '$100.00', + 'total' => 100, ], ], 'users' => [ @@ -174,9 +174,9 @@ public function testEdit(): void 'uuid' => $quote->getUuid()->toString(), 'status' => 'draft', 'client' => '/api/clients/' . $quote->getClient()->getId(), - 'total' => '$90.00', - 'baseTotal' => '$100.00', - 'tax' => '$0.00', + 'total' => 90, + 'baseTotal' => 100, + 'tax' => 0, 'discount' => [ 'type' => 'percentage', 'value' => 10, @@ -188,10 +188,10 @@ public function testEdit(): void [ 'id' => $quote->getItems()->first()->getId()->toString(), 'description' => 'Foo Item', - 'price' => '$100.00', + 'price' => 100, 'qty' => 1, 'tax' => null, - 'total' => '$100.00', + 'total' => 100, ], ], 'users' => [ diff --git a/src/UserBundle/Tests/Form/Handler/UserInviteFormHandlerTest.php b/src/UserBundle/Tests/Form/Handler/UserInviteFormHandlerTest.php index ece862139..48f415a5a 100644 --- a/src/UserBundle/Tests/Form/Handler/UserInviteFormHandlerTest.php +++ b/src/UserBundle/Tests/Form/Handler/UserInviteFormHandlerTest.php @@ -20,7 +20,6 @@ use SolidInvoice\CoreBundle\Response\FlashResponse; use SolidInvoice\CoreBundle\Templating\Template; use SolidInvoice\FormBundle\Test\FormHandlerTestCase; -use SolidInvoice\SettingsBundle\SystemConfig; use SolidInvoice\UserBundle\Entity\User; use SolidInvoice\UserBundle\Form\Handler\UserInviteFormHandler; use SolidInvoice\UserBundle\UserInvitation\UserInvitation; @@ -56,7 +55,7 @@ public function getHandler(): UserInviteFormHandler { $handler = new UserInviteFormHandler( $this->router, - new CompanySelector($this->registry, M::mock(SystemConfig::class)), + new CompanySelector($this->registry), $this->registry->getRepository(Company::class), $this->registry->getRepository(User::class), M::mock(Security::class),