CaiWuXiTong/tests/Feature/Banking/DocumentTransactionsTest.php
WuYongJie 9595267ee7
Some checks failed
Tests / PHP 8.1 (push) Has been cancelled
Tests / PHP 8.2 (push) Has been cancelled
Tests / PHP 8.3 (push) Has been cancelled
chore: 导入 Akaunting 初始源码与团队协作规则
2026-07-20 18:20:25 +08:00

169 lines
6.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Banking;
use App\Jobs\Banking\CreateBankingDocumentTransaction;
use App\Jobs\Document\CreateDocument;
use App\Models\Document\Document;
use Tests\Feature\FeatureTestCase;
class DocumentTransactionsTest extends FeatureTestCase
{
/**
* Regression test for https://github.com/akaunting/akaunting/issues/3350
*
* When a bill is in one currency (e.g. USD) and the payment is submitted
* in a different currency (e.g. TRY), the amount check must still throw
* an exception if the converted amount exceeds the remaining bill balance.
*
* Before the fix the `else` branch silently marked the bill as "paid"
* instead of throwing – this test would have failed against the buggy code.
*/
public function testItShouldPreventOverPaymentInDifferentCurrency(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessageMatches('/over_payment|passes the total/i');
$bill = $this->createBill('USD', 1);
// Pay in TRY with the same rate (1:1) so the converted USD value is
// identical to the TRY amount – clearly over the bill total.
$this->dispatch(new CreateBankingDocumentTransaction($bill, [
'amount' => $bill->amount + 1,
'currency_code' => 'TRY',
'currency_rate' => 1,
'type' => 'expense',
]));
}
/**
* A partial payment in a different currency should succeed and leave
* the bill in "partial" status.
*/
public function testItShouldAllowPartialPaymentInDifferentCurrency(): void
{
$bill = $this->createBill('USD', 1);
// Pay roughly half the bill amount in TRY (rate 1:1 → same USD value).
$half = (int) floor($bill->amount / 2);
// Skip the test when the bill amount rounds down to 0 (shouldn't happen
// in practice, but guards against factory edge-cases in CI).
if ($half <= 0) {
$this->markTestSkipped('Bill amount too small for a meaningful partial-payment test.');
}
$transaction = $this->dispatch(new CreateBankingDocumentTransaction($bill, [
'amount' => $half,
'currency_code' => 'TRY',
'currency_rate' => 1,
'type' => 'expense',
]));
$bill->refresh();
$this->assertSame('partial', $bill->status);
$this->assertNotNull($transaction->id);
}
/**
* A full payment in a different currency should succeed and mark
* the bill as "paid".
*/
public function testItShouldAllowFullPaymentInDifferentCurrency(): void
{
$bill = $this->createBill('USD', 1);
$transaction = $this->dispatch(new CreateBankingDocumentTransaction($bill, [
'amount' => $bill->amount,
'currency_code' => 'TRY',
'currency_rate' => 1,
'type' => 'expense',
]));
$bill->refresh();
$this->assertSame('paid', $bill->status);
$this->assertNotNull($transaction->id);
}
/**
* Same-currency over-payment must also throw (pre-existing behaviour).
*/
public function testItShouldPreventOverPaymentInSameCurrency(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessageMatches('/over_payment|passes the total/i');
$bill = $this->createBill('USD', 1);
$this->dispatch(new CreateBankingDocumentTransaction($bill, [
'amount' => $bill->amount + 1,
'currency_code' => 'USD',
'currency_rate' => 1,
'type' => 'expense',
]));
}
/**
* Regression test for M-1: convertBetween() sentinel bug.
*
* When $from_code == $to_code but both differ from the default currency,
* the old code passed $from_code as the $default sentinel to convertFromDefault().
* This triggered an early-return shortcut inside convert() that skipped the
* rate multiplication and returned the intermediate default-currency amount
* as if it were already in the destination currency.
*
* Scenario: default=TRY, bill=USD rate=2.0, payment=USD rate=1.0
* Bill amount: 100 USD = 200 TRY (at rate 2.0)
* Payment: 50 USD at rate 1.0 = 50 TRY
* Converted to USD (bill currency): 50 TRY × 1.0 = 50 USD → partial payment
*
* Buggy behaviour: convertBetween returned 50 TRY treated as 50 USD, which
* was compared against 100 USD → partial (correct result by accident here).
* The real breakage: when paying the full amount at a worse rate the converted
* value exceeded the balance, or at a better rate it fell short due to wrong math.
*/
public function testItShouldCorrectlyConvertSameCurrencyWithDifferentRates(): void
{
// Bill: 100 USD created at rate 2.0 (1 USD = 2 TRY)
$bill = $this->createBill('USD', 2.0);
// Payment: same currency (USD) but at rate 1.0 (1 USD = 1 TRY).
// Converted: 100 USD at rate 1.0 → 100 TRY → 100 TRY ÷ 1.0 = 100 USD.
// This should fully pay the bill.
$transaction = $this->dispatch(new CreateBankingDocumentTransaction($bill, [
'amount' => $bill->amount,
'currency_code' => 'USD',
'currency_rate' => 1.0,
'type' => 'expense',
]));
$bill->refresh();
// Before the fix, convertBetween returned the TRY intermediate amount
// (200) instead of the correct USD value (100), which broke the comparison.
$this->assertSame('paid', $bill->status);
$this->assertNotNull($transaction->id);
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
/**
* Create a bill in the given currency with a known status of "received"
* (i.e. no payments yet) so the full amount is still outstanding.
*/
private function createBill(string $currency_code, float $currency_rate): Document
{
$request = Document::factory()->bill()->items()->raw([
'currency_code' => $currency_code,
'currency_rate' => $currency_rate,
'status' => 'received',
]);
return $this->dispatch(new CreateDocument($request));
}
}