Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions budget/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Import History lists your imports.** The tab was always empty. It now shows each import's date, account, number of transactions and the dates they cover. The Download and Rollback buttons have gone, as there was nothing to download and rolling back never removed anything.

### Fixed
- **A category can be added to an existing monthly budget adjustment.** If a category was excluded when the adjustment was created, turning it back on left that month without a saved row for it, so changing its budget failed. Saving the budget now adds only the missing category and leaves every other amount in the month unchanged.
- **Bills, transfers and income show the right paid state and dates.** A bill's paid state was judged by calendar month or by its last payment date, and each page worked out dates its own way, so the Bills page, the Transfers page, the Bills Calendar and the dashboard could disagree. A custom bill could jump a year ahead, a one-time bill with no month landed on 1 January next year with an empty Due Date, income read Received beside a date still to come, and editing a bill after paying it put it back on the payment it had just made. Bills, transfers and income now follow one schedule: the next date is the first payment not yet made, received or skipped, paying settles exactly that payment, and an edit that leaves the schedule alone leaves the date alone. Reported by @TerjeTM ([#399](https://github.com/otherworld-dev/Budget/issues/399))
- **A bill is paid once.** An imported statement now replaces a payment you had already marked instead of adding a second one, a double click creates one bill, transfer or income, and paying from a page left open on an old date is refused with "Reload the page" rather than paying the next one. Linking a bank transaction to a bill pays it properly or not at all, and a deleted bill's old payments can be matched to a new bill.
- **A bill's pre-booked transaction stays pending until the bill is paid.** The background job booked it on its due date while the bill was still unpaid, so the money left the balance once then and again when the bill was paid. The update puts those rows back to pending.
Expand Down
17 changes: 16 additions & 1 deletion budget/lib/Service/CategoryService.php
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,22 @@ public function updateSnapshotBudget(string $userId, int $categoryId, string $mo
}
}

throw new \Exception($this->l->t('No budget snapshot found for this category and month'));
if ($snapshots === []) {
throw new \Exception($this->l->t('No budget snapshot found for this category and month'));
}

$category = $this->find($categoryId, $userId);
$effective = $this->budgetSnapshotMapper->findEffective($categoryId, $userId, $month);

$snapshot = new BudgetSnapshot();
$snapshot->setUserId($userId);
$snapshot->setCategoryId($categoryId);
$snapshot->setEffectiveFrom($month);
$snapshot->setAmount($amount ?? $effective?->getAmount() ?? $category->getBudgetAmount());
$snapshot->setPeriod($period ?? $effective?->getPeriod() ?? $category->getBudgetPeriod() ?? 'monthly');
$snapshot->setCreatedAt((new \DateTime())->format('Y-m-d H:i:s'));

return $this->budgetSnapshotMapper->insert($snapshot);
}

/**
Expand Down
36 changes: 34 additions & 2 deletions budget/tests/Unit/Service/CategoryServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace OCA\Budget\Tests\Unit\Service;

use OCA\Budget\Db\BudgetSnapshot;
use OCA\Budget\Db\BudgetSnapshotMapper;
use OCA\Budget\Db\Category;
use OCA\Budget\Db\CategoryMapper;
Expand Down Expand Up @@ -32,6 +33,7 @@ class CategoryServiceTest extends TestCase {
private TagMapper $tagMapper;
private TransactionTagMapper $transactionTagMapper;
private TransactionSplitMapper $splitMapper;
private BudgetSnapshotMapper $budgetSnapshotMapper;
/** The budget month the mocked carryover service reports as current */
private string $currentBudgetMonth;
/** @var array<int, float> recurring budgets the mock returns */
Expand All @@ -58,7 +60,7 @@ protected function setUp(): void {
}
return $text;
});
$budgetSnapshotMapper = $this->createMock(BudgetSnapshotMapper::class);
$this->budgetSnapshotMapper = $this->createMock(BudgetSnapshotMapper::class);

$this->currentBudgetMonth = date('Y-m');
$carryoverService = $this->createMock(\OCA\Budget\Service\BudgetCarryoverService::class);
Expand All @@ -79,7 +81,7 @@ protected function setUp(): void {
$this->service = new CategoryService(
$this->categoryMapper,
$this->transactionMapper,
$budgetSnapshotMapper,
$this->budgetSnapshotMapper,
$this->tagSetMapper,
$this->tagMapper,
$this->transactionTagMapper,
Expand Down Expand Up @@ -1425,4 +1427,34 @@ public function testDeleteGoesAheadWhenNoProjectUsesTheCategory(): void {

$this->serviceWithProjects($projects, $allocations)->delete(1, 'user1');
}

public function testUpdateSnapshotBudgetInsertsCategoryMissingFromExistingMonth(): void {
$existing = new BudgetSnapshot();
$existing->setCategoryId(1);
$this->budgetSnapshotMapper->method('findByMonth')
->with('user1', '2026-10')
->willReturn([$existing]);
$this->categoryMapper->expects($this->once())
->method('find')
->with(140, 'user1')
->willReturn($this->makeCategory(['id' => 140]));
$this->budgetSnapshotMapper->method('findEffective')
->with(140, 'user1', '2026-10')
->willReturn(null);
$this->budgetSnapshotMapper->expects($this->once())
->method('insert')
->willReturnCallback(function (BudgetSnapshot $snapshot): BudgetSnapshot {
$this->assertSame('user1', $snapshot->getUserId());
$this->assertSame(140, $snapshot->getCategoryId());
$this->assertSame('2026-10', $snapshot->getEffectiveFrom());
$this->assertSame(119.0, $snapshot->getAmount());
$this->assertSame('monthly', $snapshot->getPeriod());
return $snapshot;
});

$result = $this->service->updateSnapshotBudget('user1', 140, '2026-10', 119.0, 'monthly');

$this->assertSame(140, $result->getCategoryId());
}

}
Loading