Files
InvoiceShelf/tests/Feature/Admin/ExpenseTest.php
2026-08-02 13:01:01 +02:00

341 lines
11 KiB
PHP

<?php
use App\Http\Controllers\Company\Expense\ExpensesController;
use App\Http\Requests\ExpenseRequest;
use App\Models\Company;
use App\Models\Expense;
use App\Models\Tax;
use App\Models\TaxType;
use App\Models\User;
use Illuminate\Support\Facades\Artisan;
use Laravel\Sanctum\Sanctum;
use function Pest\Laravel\getJson;
use function Pest\Laravel\postJson;
use function Pest\Laravel\putJson;
beforeEach(function () {
Artisan::call('db:seed', ['--class' => 'DatabaseSeeder', '--force' => true]);
Artisan::call('db:seed', ['--class' => 'DemoSeeder', '--force' => true]);
$user = User::find(1);
$this->withHeaders([
'company' => $user->companies()->first()->id,
]);
Sanctum::actingAs(
$user,
['*']
);
});
test('get expenses', function () {
getJson('api/v1/expenses?page=1')->assertOk();
});
test('create expense', function () {
$expense = Expense::factory()->raw([
'amount' => 150,
'exchange_rate' => 76.217498,
'base_amount' => 11432.6247,
]);
postJson('api/v1/expenses', $expense)->assertStatus(201);
$this->assertDatabaseHas('expenses', [
'notes' => $expense['notes'],
'expense_category_id' => $expense['expense_category_id'],
'amount' => $expense['amount'],
'exchange_rate' => $expense['exchange_rate'],
'base_amount' => $expense['base_amount'],
]);
});
test('store validates using a form request', function () {
$this->assertActionUsesFormRequest(
ExpensesController::class,
'store',
ExpenseRequest::class
);
});
test('get expense data', function () {
$expense = Expense::factory()->create([
'expense_number' => 'EXP-000001',
'expense_date' => '2019-02-05',
]);
getJson("api/v1/expenses/{$expense->id}")->assertOk();
$this->assertDatabaseHas('expenses', [
'id' => $expense->id,
'expense_number' => $expense['expense_number'],
'notes' => $expense['notes'],
'expense_category_id' => $expense['expense_category_id'],
'amount' => $expense['amount'],
]);
});
test('update expense', function () {
$expense = Expense::factory()->create([
'expense_date' => '2019-02-05',
]);
$expense2 = Expense::factory()->raw();
putJson('api/v1/expenses/'.$expense->id, $expense2)->assertOk();
$this->assertDatabaseHas('expenses', [
'id' => $expense->id,
'notes' => $expense2['notes'],
'expense_category_id' => $expense2['expense_category_id'],
'amount' => $expense2['amount'],
]);
});
test('update validates using a form request', function () {
$this->assertActionUsesFormRequest(
ExpensesController::class,
'update',
ExpenseRequest::class
);
});
test('search expenses', function () {
$filters = [
'page' => 1,
'limit' => 15,
'expense_category_id' => 1,
'search' => 'cate',
'from_date' => '2020-07-18',
'to_date' => '2020-07-20',
];
$queryString = http_build_query($filters, '', '&');
$response = getJson('api/v1/expenses?'.$queryString);
$response->assertOk();
});
test('delete multiple expenses', function () {
$expenses = Expense::factory()->count(3)->create([
'expense_date' => '2019-02-05',
]);
$data = [
'ids' => $expenses->pluck('id'),
];
$response = postJson('api/v1/expenses/delete', $data);
$response
->assertOk()
->assertJson([
'success' => true,
]);
foreach ($expenses as $expense) {
$this->assertModelMissing($expense);
}
});
test('update expense with EUR currency', function () {
$expense = Expense::factory()->create([
'expense_date' => '2019-02-05',
]);
$expense2 = Expense::factory()->raw([
'amount' => 150,
'exchange_rate' => 76.217498,
'base_amount' => 11432.6247,
]);
putJson('api/v1/expenses/'.$expense->id, $expense2)->assertOk();
$this->assertDatabaseHas('expenses', [
'id' => $expense->id,
'expense_category_id' => $expense2['expense_category_id'],
'amount' => $expense2['amount'],
'exchange_rate' => $expense2['exchange_rate'],
'base_amount' => $expense2['base_amount'],
]);
});
test('creates receipt tax snapshots from configured tax types', function () {
$companyId = User::find(1)->companies()->first()->id;
$percentageTax = TaxType::factory()->create([
'company_id' => $companyId,
'name' => 'VAT',
'percent' => 18.5,
'calculation_type' => 'percentage',
'fixed_amount' => null,
'compound_tax' => true,
'transaction_type' => TaxType::TRANSACTION_TYPE_PURCHASES,
]);
$fixedTax = TaxType::factory()->create([
'company_id' => $companyId,
'name' => 'Stamp duty',
'percent' => null,
'calculation_type' => 'fixed',
'fixed_amount' => 25,
'compound_tax' => false,
'transaction_type' => TaxType::TRANSACTION_TYPE_PURCHASES,
]);
$expense = Expense::factory()->raw([
'amount' => 100,
'exchange_rate' => 1.2345,
'currency_id' => 1,
'base_amount' => 0,
]);
$expense['taxes'] = json_encode([
['tax_type_id' => $percentageTax->id, 'amount' => 35],
['tax_type_id' => $fixedTax->id, 'amount' => 10],
]);
$response = postJson('api/v1/expenses', $expense)->assertCreated();
$expenseId = $response->json('data.id');
$this->assertDatabaseHas('taxes', [
'expense_id' => $expenseId,
'tax_type_id' => $percentageTax->id,
'name' => 'VAT',
'amount' => 35,
'base_amount' => 43,
'percent' => 18.5,
'calculation_type' => 'percentage',
'compound_tax' => 1,
'company_id' => $companyId,
'currency_id' => 1,
]);
$this->assertDatabaseHas('taxes', [
'expense_id' => $expenseId,
'tax_type_id' => $fixedTax->id,
'name' => 'Stamp duty',
'amount' => 10,
'base_amount' => 12,
'fixed_amount' => 25,
'calculation_type' => 'fixed',
]);
$this->assertDatabaseHas('expenses', [
'id' => $expenseId,
'amount' => 100,
]);
$response->assertJsonPath('data.taxes.0.expense_id', $expenseId);
});
test('updates receipt taxes only when taxes are submitted', function () {
$companyId = User::find(1)->companies()->first()->id;
$oldTaxType = TaxType::factory()->create([
'company_id' => $companyId,
'transaction_type' => TaxType::TRANSACTION_TYPE_PURCHASES,
]);
$newTaxType = TaxType::factory()->create([
'company_id' => $companyId,
'transaction_type' => TaxType::TRANSACTION_TYPE_PURCHASES,
]);
$expense = Expense::factory()->create(['amount' => 100]);
Tax::factory()->create([
'expense_id' => $expense->id,
'tax_type_id' => $oldTaxType->id,
'company_id' => $companyId,
'amount' => 10,
]);
$payload = Expense::factory()->raw(['amount' => 100]);
putJson("api/v1/expenses/{$expense->id}", $payload)->assertOk();
expect(Tax::where('expense_id', $expense->id)->count())->toBe(1);
$payload['taxes'] = [['tax_type_id' => $newTaxType->id, 'amount' => 20]];
putJson("api/v1/expenses/{$expense->id}", $payload)
->assertOk()
->assertJsonPath('data.taxes.0.tax_type_id', $newTaxType->id);
$this->assertDatabaseMissing('taxes', ['expense_id' => $expense->id, 'tax_type_id' => $oldTaxType->id]);
$this->assertDatabaseHas('taxes', ['expense_id' => $expense->id, 'tax_type_id' => $newTaxType->id, 'amount' => 20]);
$payload['taxes'] = [];
putJson("api/v1/expenses/{$expense->id}", $payload)->assertOk();
expect(Tax::where('expense_id', $expense->id)->count())->toBe(0);
});
test('validates malformed and invalid receipt taxes', function () {
$companyId = User::find(1)->companies()->first()->id;
$taxType = TaxType::factory()->create([
'company_id' => $companyId,
'transaction_type' => TaxType::TRANSACTION_TYPE_PURCHASES,
]);
$salesTaxType = TaxType::factory()->create(['company_id' => $companyId]);
$otherCompany = Company::factory()->create();
$otherCompanyTaxType = TaxType::factory()->create([
'company_id' => $otherCompany->id,
'transaction_type' => TaxType::TRANSACTION_TYPE_PURCHASES,
]);
$expense = Expense::factory()->raw(['amount' => 10]);
postJson('api/v1/expenses', array_merge($expense, ['taxes' => '{not valid json']))
->assertUnprocessable()
->assertJsonValidationErrors('taxes');
postJson('api/v1/expenses', array_merge($expense, ['taxes' => ['not an object']]))
->assertUnprocessable()
->assertJsonValidationErrors('taxes.0');
postJson('api/v1/expenses', array_merge($expense, ['taxes' => [
['tax_type_id' => $taxType->id, 'amount' => 1],
['tax_type_id' => $taxType->id, 'amount' => 1],
]]))->assertUnprocessable()->assertJsonValidationErrors('taxes.1.tax_type_id');
postJson('api/v1/expenses', array_merge($expense, ['taxes' => [
['tax_type_id' => $otherCompanyTaxType->id, 'amount' => 1],
]]))->assertUnprocessable()->assertJsonValidationErrors('taxes.0.tax_type_id');
postJson('api/v1/expenses', array_merge($expense, ['taxes' => [
['tax_type_id' => $salesTaxType->id, 'amount' => 1],
]]))->assertUnprocessable()->assertJsonValidationErrors('taxes.0.tax_type_id');
postJson('api/v1/expenses', array_merge($expense, ['taxes' => [
['tax_type_id' => $taxType->id, 'amount' => -1],
]]))->assertUnprocessable()->assertJsonValidationErrors('taxes.0.amount');
postJson('api/v1/expenses', array_merge($expense, ['taxes' => [
['tax_type_id' => $taxType->id, 'amount' => 11],
]]))->assertUnprocessable()->assertJsonValidationErrors('taxes');
postJson('api/v1/expenses', array_merge($expense, ['taxes' => [
['tax_type_id' => $taxType->id, 'amount' => 1, 'name' => 'untrusted'],
]]))->assertUnprocessable()->assertJsonValidationErrors('taxes.0');
});
test('includes taxes on expense detail responses but not list responses', function () {
$companyId = User::find(1)->companies()->first()->id;
$expense = Expense::factory()->create();
$taxType = TaxType::factory()->create([
'company_id' => $companyId,
'transaction_type' => TaxType::TRANSACTION_TYPE_PURCHASES,
]);
Tax::factory()->create([
'expense_id' => $expense->id,
'tax_type_id' => $taxType->id,
'company_id' => $companyId,
]);
getJson("api/v1/expenses/{$expense->id}")
->assertOk()
->assertJsonPath('data.taxes.0.expense_id', $expense->id);
$list = getJson('api/v1/expenses?page=1')->assertOk()->json('data.0');
expect($list)->not->toHaveKey('taxes');
});
test('deleting expenses removes their receipt taxes', function () {
$companyId = User::find(1)->companies()->first()->id;
$expense = Expense::factory()->create();
$taxType = TaxType::factory()->create([
'company_id' => $companyId,
'transaction_type' => TaxType::TRANSACTION_TYPE_PURCHASES,
]);
$tax = Tax::factory()->create([
'expense_id' => $expense->id,
'company_id' => $companyId,
'tax_type_id' => $taxType->id,
]);
postJson('api/v1/expenses/delete', ['ids' => [$expense->id]])->assertOk();
$this->assertDatabaseMissing('taxes', ['id' => $tax->id]);
});