1
0
Fork 0
mirror of https://github.com/archtechx/money.git synced 2025-12-13 03:34:04 +00:00

Initial commit

This commit is contained in:
Samuel Štancl 2021-11-16 19:06:57 +01:00
commit 8847454577
33 changed files with 2435 additions and 0 deletions

18
tests/Currencies/CZK.php Normal file
View file

@ -0,0 +1,18 @@
<?php
namespace ArchTech\Money\Tests\Currencies;
use ArchTech\Money\Currency;
class CZK extends Currency
{
protected string $code = 'CZK';
protected string $name = 'Czech Crown';
protected float $rate = 25;
protected int $mathDecimals = 2;
protected int $displayDecimals = 0;
protected string $decimalSeparator = ',';
protected string $thousandsSeparator = '.';
protected int $rounding = 2;
protected string $suffix = ' Kč';
}

16
tests/Currencies/EUR.php Normal file
View file

@ -0,0 +1,16 @@
<?php
namespace ArchTech\Money\Tests\Currencies;
use ArchTech\Money\Currency;
class EUR extends Currency
{
protected string $code = 'EUR';
protected string $name = 'Euro';
protected float $rate = 0.9;
protected int $mathDecimals = 4;
protected int $displayDecimals = 2;
protected int $rounding = 0;
protected string $suffix = ' €';
}

11
tests/Pest.php Normal file
View file

@ -0,0 +1,11 @@
<?php
use ArchTech\Money\Tests\TestCase;
use Pest\TestSuite;
uses(ArchTech\Money\Tests\TestCase::class)->in('Pest');
function pest(): TestCase
{
return TestSuite::getInstance()->test;
}

View file

@ -0,0 +1,192 @@
<?php
use ArchTech\Money\Currencies\USD;
use ArchTech\Money\Currency;
use ArchTech\Money\Tests\Currencies\CZK;
use ArchTech\Money\Tests\Currencies\EUR;
beforeEach(fn () => currencies()->reset());
test('only USD is loaded by default', function () {
expect(currencies()->all())
->toHaveCount(1)
->toHaveKey('USD');
});
test('USD is the default currency', function () {
expect(currencies()->getDefault())->toBeInstanceOf(USD::class);
});
test('USD is the current currency', function () {
expect(currencies()->getCurrent())->toBeInstanceOf(USD::class);
});
test('the default currency can be changed', function () {
currencies()->add([CZK::class, EUR::class]);
expect(currencies()->getDefault())->toBeInstanceOf(USD::class);
// Using class
currencies()->setDefault(CZK::class);
expect(currencies()->getDefault())->toBeInstanceOf(CZK::class);
// Using code
currencies()->setDefault('EUR');
expect(currencies()->getDefault())->toBeInstanceOf(EUR::class);
});
test('the current currency can be changed', function () {
currencies()->add([CZK::class, EUR::class]);
expect(currencies()->getCurrent())->toBeInstanceOf(USD::class);
// Using class
currencies()->setCurrent(CZK::class);
expect(currencies()->getCurrent())->toBeInstanceOf(CZK::class);
// Using code
currencies()->setCurrent('EUR');
expect(currencies()->getCurrent())->toBeInstanceOf(EUR::class);
});
test('the current currency can be persisted', function () {
currencies()->add([CZK::class, EUR::class]);
$store = [];
$resolverCalled = false;
currencies()->storeCurrentUsing(function ($currency) use (&$store) {
$store[] = $currency;
});
currencies()->resolveCurrentUsing(function () use (&$resolverCalled, &$store) {
$resolverCalled = true;
return last($store);
});
currencies()->setCurrent('CZK');
expect($store)->toBe(['CZK']);
expect(currencies()->getCurrent()->code())->toBe('CZK');
expect($resolverCalled)->toBeFalse();
currencies()->setCurrent('EUR');
expect($store)->toBe(['CZK', 'EUR']);
expect(currencies()->getCurrent()->code())->toBe('EUR');
expect($resolverCalled)->toBeFalse();
currencies()->forgetCurrent();
expect(currencies()->getCurrent()->code())->toBe('EUR');
expect($resolverCalled)->toBeTrue(); // got called
});
test('individual currencies can be removed', function () {
currencies()->add(CZK::class);
expect(currencies()->all())->toHaveCount(2);
currencies()->remove('USD');
expect(currencies()->all())->toHaveCount(1);
});
test('currencies can be cleared', function () {
currencies()->clear();
expect(currencies()->all())->toHaveCount(0);
});
test('more currencies can be provided', function () {
currencies()->add([CZK::class, EUR::class]);
expect(currencies()->all())->toHaveCount(3);
});
test('duplicate currencies get overriden', function () {
$customUSD = new USD(rate: 20);
currencies()->add($customUSD);
expect(currencies()->all())->toHaveCount(1);
expect(currencies()->all())->toHaveKey('USD', $customUSD);
});
test('the default currency can have any rate', function () {
// This tests that the default currency doesn't have to have a rate of 1
currencies()->add([CZK::class, EUR::class]);
expect(
money(1500, 'CZK')->convertTo(EUR::class)->formatted()
)->toBe('0.54 €');
currencies()->setDefault(CZK::class);
expect(
money(1500, 'CZK')->convertTo(EUR::class)->formatted()
)->toBe('0.54 €');
});
test('the getCode method accepts any currency format', function () {
expect(currencies()->getCode(USD::class))->toBe('USD');
expect(currencies()->getCode(new USD))->toBe('USD');
expect(currencies()->getCode('USD'))->toBe('USD');
});
test('array currencies get converted to anonymous Currency objects', function () {
currencies()->add((new CZK)->toArray());
expect(currencies()->all())->toHaveKey('CZK');
});
test('add accepts any currency format', function () {
currencies()->clear();
// Class instances
currencies()->add($instance = new CZK);
expect(currencies()->all())->toHaveKey('CZK', $instance);
// Anonymous class instances
currencies()->add($gbp = new Currency(code: 'GBP', rate: 0.8, name: 'British Pound'));
expect(currencies()->all())->toHaveKey('GBP', $gbp);
// Class names
currencies()->add(USD::class);
expect(currencies()->all())->toHaveKey('USD');
// Arrays
currencies()->add((new CZK)->toArray());
expect(currencies()->all())->toHaveKey('CZK');
});
test('the add method accepts an array of named Currency instances', function () {
currencies()->add([
new CZK,
new EUR,
]);
expect(currencies()->all())->toHaveKeys(['CZK', 'EUR']);
});
test('the add method accepts an array of anonymous Currency instances', function () {
currencies()->add([
new Currency(code: 'USD', rate: 1, name: 'United States Dollar'),
new Currency(code: 'EUR', rate: 0.8, name: 'Euro'),
]);
expect(currencies()->all())->toHaveKeys(['USD', 'EUR']);
});
test('the add method accepts an array of Currency class strings', function () {
currencies()->add([USD::class, EUR::class]);
expect(currencies()->all())->toHaveKeys(['USD', 'EUR']);
});
test('the add method accepts an array of currency arrays', function () {
currencies()->add([
(new CZK)->toArray(),
(new EUR)->toArray(),
]);
expect(currencies()->all())->toHaveKeys(['USD', 'CZK', 'EUR']);
});

View file

@ -0,0 +1,42 @@
<?php
use ArchTech\Money\Currency;
use ArchTech\Money\Exceptions\InvalidCurrencyException;
use ArchTech\Money\Tests\Currencies\CZK;
test("a currency is invalid if it doesn't have a name", function () {
pest()->expectException(InvalidCurrencyException::class);
new Currency(rate: 2.0, code: 'CZK');
});
test("a currency is invalid if it doesn't have a code", function () {
pest()->expectException(InvalidCurrencyException::class);
new Currency(rate: 2.0, name: 'Czech Crown');
});
test('currencies can be serialized to JSON', function () {
expect(json_encode(new CZK))->json()->toBe([
'code' => 'CZK',
'name' => 'Czech Crown',
'rate' => 25,
'prefix' => '',
'suffix' => ' Kč',
'mathDecimals' => 2,
'displayDecimals' => 0,
'rounding' => 2,
'decimalSeparator' => ',',
'thousandsSeparator' => '.',
]);
});
test('currencies can be created from JSON', function () {
$original = new Currency(code: 'GBP', rate: 0.8, name: 'British Pound');
$json = json_encode($original);
$new = Currency::fromJson($json);
expect($original->toArray())->toBe($new->toArray());
});

View file

@ -0,0 +1,48 @@
<?php
use ArchTech\Money\CurrencyManager;
use ArchTech\Money\Currencies\USD;
use ArchTech\Money\Currency;
use ArchTech\Money\Money;
use ArchTech\Money\Tests\Currencies\CZK;
use ArchTech\Money\Tests\Currencies\EUR;
beforeEach(fn () => currencies()->add([CZK::class, EUR::class]));
test('prefixes are applied', function () {
expect(Money::fromDecimal(10.00, USD::class)->formatted())->toBe('$10.00');
});
test('suffixes are applied', function () {
expect(Money::fromDecimal(10.00, CZK::class)->formatted())->toBe('10 Kč');
});
test('decimals can be applied even if the decimal points are zero', function () {
expect(Money::fromDecimal(10.00, CZK::class)->formatted())->toBe('10 Kč');
expect(Money::fromDecimal(10.00, EUR::class)->formatted())->toBe('10.00 €');
});
test('decimals have a separator', function () {
expect(Money::fromDecimal(10.34, EUR::class)->formatted())->toBe('10.34 €');
expect(Money::fromDecimal(10.34, CZK::class)->rawFormatted())->toBe('10,34 Kč');
});
test('thousands have a separator', function () {
currencies()->add(new Currency(
code: 'FOO',
name: 'Foo Currency',
thousandsSeparator: ' ',
));
expect(Money::fromDecimal(1234567.89, 'USD')->formatted())->toBe('$1,234,567.89');
expect(Money::fromDecimal(1234567.89, 'EUR')->formatted())->toBe('1,234,567.89 €');
expect(Money::fromDecimal(1234567.89, 'CZK')->formatted())->toBe('1.234.568 Kč');
expect(Money::fromDecimal(1234567.89, 'FOO')->formatted())->toBe('1 234 567.89');
});
test('the format method accepts overrides', function () {
expect(Money::fromDecimal(10.45)->formatted(['decimalSeparator' => ',', 'prefix' => '$$$']))->toBe('$$$10,45');
expect(Money::fromDecimal(10.45)->formatted(decimalSeparator: ',', suffix: ' USD'))->toBe('$10,45 USD');
});

53
tests/Pest/HelperTest.php Normal file
View file

@ -0,0 +1,53 @@
<?php
use ArchTech\Money\Currencies\USD;
use ArchTech\Money\Money;
use ArchTech\Money\Tests\Currencies\CZK;
use ArchTech\Money\Tests\Currencies\EUR;
test('the currency helper can be used to fetch the current currency', function () {
expect(currency())->toBe(currencies()->getCurrent());
});
test('the currency helper can be used to fetch a specific currency using its code', function () {
currencies()->add(EUR::class);
expect(currency('EUR'))->toBeInstanceOf(EUR::class);
});
test('the currency helper can be used to fetch a specific currency using its class', function () {
currencies()->add(EUR::class);
expect(currency(EUR::class))->toBeInstanceOf(EUR::class);
});
test('the money helper creates a new Money instance', function () {
expect(money(200))->toBeInstanceOf(Money::class);
});
test('the money helper accepts a string currency', function () {
currencies()->add([EUR::class, CZK::class]);
expect(money(200, 'EUR')->currency())->toBeInstanceOf(EUR::class);
expect(money(200, 'CZK')->currency())->toBeInstanceOf(CZK::class);
});
test('the money helper accepts a class currency', function () {
currencies()->add([EUR::class, CZK::class]);
expect(money(200, EUR::class)->currency())->toBeInstanceOf(EUR::class);
expect(money(200, CZK::class)->currency())->toBeInstanceOf(CZK::class);
});
test('the money helper accepts a currency object', function () {
currencies()->add([EUR::class, CZK::class]);
expect(money(200, new EUR)->currency())->toBeInstanceOf(EUR::class);
expect(money(200, new CZK)->currency())->toBeInstanceOf(CZK::class);
});
test('the money helper falls back to the default currency', function () {
currencies()->add(EUR::class);
expect(money(200)->currency())->toBeInstanceOf(USD::class);
});

44
tests/Pest/MathTest.php Normal file
View file

@ -0,0 +1,44 @@
<?php
use ArchTech\Money\CurrencyManager;
use ArchTech\Money\Tests\Currencies\CZK;
test('money is rounded to its math decimals after each operation', function () {
expect(
money(33)->times(3.03)->value() // 0.9999
)->toBe(100);
});
test('operations use the math decimals even if the display decimals are different', function () {
currencies()->add(CZK::class); // uses 0 display decimals
$money = money(33, 'CZK')->times(3); // 0.99
expect($money->value())->toBe(99);
expect($money->formatted())->toBe('1 Kč');
$money = money(33, 'CZK')->times(3.03); // 0.9999
expect($money->value())->toBe(100);
expect($money->formatted())->toBe('1 Kč');
});
test('rounding is not applied between operations', function () {
currencies()->add(CZK::class);
// 99
$money = money(33, 'CZK')->times(3);
expect($money->value())->toBe(99);
expect($money->rounded()->value())->toBe(100);
expect($money->rounding())->toBe(1);
// 139
$money = $money->add(40);
expect($money->value())->toBe(139);
expect($money->rounded()->value())->toBe(100);
expect($money->rounding())->toBe(-39);
// 151
$money = $money->add(12);
expect($money->value())->toBe(151);
expect($money->rounded()->value())->toBe(200);
expect($money->rounding())->toBe(49);
});

232
tests/Pest/MoneyTest.php Normal file
View file

@ -0,0 +1,232 @@
<?php
use ArchTech\Money\CurrencyManager;
use ArchTech\Money\Currencies\USD;
use ArchTech\Money\Currency;
use ArchTech\Money\Money;
use ArchTech\Money\Tests\Currencies\CZK;
use ArchTech\Money\Tests\Currencies\EUR;
test('Money value is immutable', function () {
pest()->expectError();
$money = money(100);
$money->value = 200;
});
test('Money currency is immutable', function () {
pest()->expectError();
$money = money(100);
$money->currency = 'EUR';
});
test('money can be created from a decimal value', function () {
$money = Money::fromDecimal(10.0, 'USD');
expect($money->value())->toBe(1000);
});
test('money can be converted to decimals', function () {
currencies()->add(CZK::class);
$money = Money::fromDecimal(10.0, 'USD');
expect($money->value())->toBe(1000);
expect($money->decimal())->toBe(10.0);
$money = Money::fromDecimal(15.0, 'CZK');
expect($money->value())->toBe(1500);
expect($money->decimal())->toBe(15.0);
});
test('money can be added in base value', function () {
$money = money(100);
$money = $money->add(200);
expect($money->value())->toBe(300);
});
test('money can be added from another Money instance', function () {
$money = money(100);
$money = $money->addMoney(money(500));
expect($money->value())->toBe(600);
});
test('money can be added from a Money instance with a different currency', function () {
currencies()->add(CZK::class);
$usd = Money::fromDecimal(10.0, 'USD');
$czk = Money::fromDecimal(100.0, 'CZK'); // 4 USD
$usd = $usd->addMoney($czk);
expect($usd->decimal())->toBe(14.0);
});
test('money can be subtracted in base value', function () {
$money = money(300);
$money = $money->subtract(200);
expect($money->value())->toBe(100);
});
test('money can be subtracted by another Money instance', function () {
$money = money(500);
$money = $money->subtractMoney(money(100));
expect($money->value())->toBe(400);
});
test('money can be subtracted by a Money instance with a different currency', function () {
currencies()->add(CZK::class);
$usd = Money::fromDecimal(10.0, 'USD');
$czk = Money::fromDecimal(100.0, 'CZK'); // 4 USD
$usd = $usd->subtractMoney($czk);
expect($usd->decimal())->toBe(6.0);
});
test('money can be multiplied', function () {
expect(money(100)->multiplyBy(2.5)->value())->toBe(250);
expect(money(100)->times(2.5)->value())->toBe(250);
});
test('money can be divided', function () {
expect(money(100)->divideBy(10)->value())->toBe(10);
});
test('fees can be added to and subtracted from money', function () {
$money = Money::fromDecimal(10.0);
expect($money->addFee(0.1)->decimal())->toBe(11.0);
expect($money->subtractFee(0.1)->decimal())->toBe(9.09); // 10/1.1
});
test('taxes can be added and subtracted from money', function () {
currencies()->add([CZK::class]);
expect(
Money::fromDecimal(100.0, 'CZK')->addTax(0.21)->decimal()
)->toBe(121.0);
expect(
Money::fromDecimal(121.0, 'CZK')->subtractTax(0.21)->decimal()
)->toBe(100.0);
});
test('money can be converted to a different currency', function () {
currencies()->add([CZK::class]);
$money = Money::fromDecimal(100.0);
expect($money->currency())->toBeInstanceOf(USD::class);
expect($money->currency()->code())->toBe('USD');
$money = $money->convertTo(CZK::class);
expect($money->decimal())->toBe(2500.0);
expect($money->currency())->toBeInstanceOf(CZK::class);
expect($money->currency()->code())->toBe('CZK');
});
test('money can be formatted', function () {
expect(
Money::fromDecimal(10.00, USD::class)->formatted()
)->toBe('$10.00');
});
test('money can be formatted without rounding', function () {
currencies()->add([CZK::class]);
expect(
Money::fromDecimal(10.34, CZK::class)->rawFormatted()
)->toBe('10,34 Kč');
});
test('converting money to a string returns the formatted string', function () {
expect(
(string) Money::fromDecimal(10.00, USD::class)
)->toBe('$10.00');
});
test('money can be converted to default currency value', function () {
currencies()->add(CZK::class);
$money = money(5000, CZK::class);
expect($money->valueInDefaultCurrency())->toBe(
$money->convertTo(currencies()->getDefault())->value()
);
});
test('money can have rounding', function () {
currencies()->add([CZK::class, EUR::class]);
expect(money(12340, 'CZK')->rounding())->toBe(-40);
expect(money(12340, 'EUR')->rounding())->toBe(0);
});
test('money can be rounded with a custom precision', function () {
currencies()->add(CZK::class);
expect(money(12340, 'CZK')->rounding())->toBe(-40);
expect(money(12340, 'CZK')->rounded()->value())->toBe(12300);
expect(money(12340, 'CZK')->rounded(3)->value())->toBe(12000);
});
test('money can be compared', function () {
expect(
money(123)->equals(money(123))
)->toBeTrue();
expect(
money(123)->equals(money(456))
)->toBeFalse();
});
test('money can be compared with different currencies', function () {
currencies()->add(CZK::class);
expect(
money(123, 'USD')->equals(money(123)->convertTo(CZK::class))
)->toBeTrue();
});
test('the is method compares both the value and the currency', function () {
currencies()->add([CZK::class, EUR::class]);
expect(
money(123, 'EUR')->is(money(123)->convertTo(CZK::class))
)->toBeFalse();
expect(
money(123, 'EUR')->is(money(123)->convertTo(CZK::class)->convertTo(EUR::class))
)->toBeFalse();
});
test('money can be serialized to JSON', function () {
currencies()->add(CZK::class);
$money = Money::fromDecimal(22, 'CZK');
expect(json_encode($money))->json()->toBe(['value' => 2200, 'currency' => 'CZK']);
expect($money->toJson())->json()->toBe(['value' => 2200, 'currency' => 'CZK']);
});
test('money can be instantiated from JSON', function () {
currencies()->add(CZK::class);
$original = Money::fromDecimal(22, 'CZK');
$json = json_encode($original);
$new = Money::fromJson($json)->toArray();
expect($new)->toBe($original->toArray());
});

15
tests/TestCase.php Normal file
View file

@ -0,0 +1,15 @@
<?php
namespace ArchTech\Money\Tests;
use Orchestra\Testbench\TestCase as TestbenchTestCase;
use ArchTech\Money\MoneyServiceProvider;
class TestCase extends TestbenchTestCase
{
protected function getPackageProviders($app)
{
return [
MoneyServiceProvider::class,
];
}
}