feat: optionally update manual account balance on transaction delete (#491)
## What When deleting a transaction that belongs to a **manual** (non-bank-connected) account, the user can now opt to update the account's **current balance** for today, so it stays accurate without re-syncing. - Deleting an **expense** (amount < 0) → balance **increases** by the amount. - Deleting **income** (amount > 0) → balance **decreases** by the amount. - Formula: `new_balance = current_balance − transaction.amount`. If today has no balance row, it's seeded from the latest known balance. - Available for **single delete** and **bulk delete** (a checkbox in the confirmation dialog). - Connected accounts are never touched — their balances come from bank sync. - On the account page, the balance display refreshes automatically after the delete (no page reload). ## How - New `ManualBalanceAdjuster` service; `TransactionController@destroy` calls it when the request carries `update_balance` and the account is manual. - Frontend `transactionSyncService.delete/deleteMany` accept an `updateBalance` option; both delete dialogs show the checkbox only when a manual account is affected. - `TransactionList` gains an `onBalanceUpdated` callback; `Accounts/Show` uses it to bump the balance chart's refresh key. - Added `banking_connection_id` to the accounts payload (transactions, budgets, and the shared Inertia props feeding accounts/show) so the frontend can identify manual accounts. <img width="896" height="400" alt="image" src="https://github.com/user-attachments/assets/e7f35ccb-13a1-40a1-bf04-615752bddf1a" />
This commit is contained in:
parent
14b79557d7
commit
45e25e018d
|
|
@ -92,7 +92,7 @@ class BudgetController extends Controller
|
|||
->where('user_id', $user->id)
|
||||
->with('bank:id,name,logo')
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code']);
|
||||
->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id']);
|
||||
|
||||
$banks = Bank::query()
|
||||
->availableForUser($user)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use App\Models\Bank;
|
|||
use App\Models\Category;
|
||||
use App\Models\Label;
|
||||
use App\Models\Transaction;
|
||||
use App\Services\ManualBalanceAdjuster;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -79,7 +80,7 @@ class TransactionController extends Controller
|
|||
->where('user_id', $user->id)
|
||||
->with('bank:id,name,logo')
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code']);
|
||||
->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id']);
|
||||
|
||||
$banks = Bank::query()
|
||||
->availableForUser($user)
|
||||
|
|
@ -121,7 +122,7 @@ class TransactionController extends Controller
|
|||
->where('user_id', $user->id)
|
||||
->with('bank:id,name,logo')
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code']);
|
||||
->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id']);
|
||||
|
||||
$banks = Bank::query()
|
||||
->availableForUser($user)
|
||||
|
|
@ -213,10 +214,14 @@ class TransactionController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Transaction $transaction): JsonResponse
|
||||
public function destroy(Request $request, Transaction $transaction, ManualBalanceAdjuster $balanceAdjuster): JsonResponse
|
||||
{
|
||||
$this->authorize('delete', $transaction);
|
||||
|
||||
if ($request->boolean('update_balance')) {
|
||||
$balanceAdjuster->reverseDeletedTransaction($transaction);
|
||||
}
|
||||
|
||||
$transaction->delete();
|
||||
|
||||
return response()->json([
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class HandleInertiaRequests extends Middleware
|
|||
'accounts' => fn () => $user ? $user->accounts()
|
||||
->with(['bank:id,name,logo', 'realEstateDetail:account_id,linked_loan_account_id'])
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code'])
|
||||
->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id'])
|
||||
->map(function ($account) {
|
||||
$data = $account->only([
|
||||
'id',
|
||||
|
|
@ -134,6 +134,7 @@ class HandleInertiaRequests extends Middleware
|
|||
'bank_id',
|
||||
'type',
|
||||
'currency_code',
|
||||
'banking_connection_id',
|
||||
'bank',
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\AccountBalance;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class ManualBalanceAdjuster
|
||||
{
|
||||
/**
|
||||
* Reverse a deleted transaction's effect on its manual account's current balance.
|
||||
*
|
||||
* Adjusts today's balance by the inverse of the transaction amount: an expense
|
||||
* (negative amount) increases the balance, income (positive amount) decreases it.
|
||||
* Connected accounts are skipped because their balances come from bank sync.
|
||||
*/
|
||||
public function reverseDeletedTransaction(Transaction $transaction): void
|
||||
{
|
||||
$account = $transaction->account;
|
||||
|
||||
if ($account === null || $account->isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$today = Carbon::now()->toDateString();
|
||||
|
||||
$currentBalance = $account->balances()
|
||||
->where('balance_date', '<=', $today)
|
||||
->orderByDesc('balance_date')
|
||||
->value('balance') ?? 0;
|
||||
|
||||
AccountBalance::updateOrCreate(
|
||||
[
|
||||
'account_id' => $account->id,
|
||||
'balance_date' => $today,
|
||||
],
|
||||
[
|
||||
'balance' => $currentBalance - $transaction->amount,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1560,6 +1560,8 @@
|
|||
"Update the balance record.": "Actualiza el registro de balance.",
|
||||
"Update the category and notes for this transaction.": "Actualiza la categoría y notas de esta transacción.",
|
||||
"Update the category information.": "Actualiza la información de la categoría.",
|
||||
"Update the current balance of the affected manual accounts to reflect this change.": "Actualiza el saldo actual de las cuentas manuales afectadas para reflejar este cambio.",
|
||||
"Update the current balance of the manual account to reflect this change.": "Actualiza el saldo actual de la cuenta manual para reflejar este cambio.",
|
||||
"Update the label information.": "Actualiza la información de la etiqueta.",
|
||||
"Update the owed amount record.": "Actualiza el registro de monto adeudado.",
|
||||
"Update the rule to automatically categorize transactions and add labels.": "Actualiza la regla para categorizar automáticamente transacciones y agregar etiquetas.",
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import {
|
|||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
|
|
@ -240,6 +241,7 @@ export interface TransactionListProps {
|
|||
headerActions?: ReactNode;
|
||||
maxHeight?: number;
|
||||
hideColumns?: string[];
|
||||
onBalanceUpdated?: () => void;
|
||||
}
|
||||
|
||||
export function TransactionList({
|
||||
|
|
@ -256,6 +258,7 @@ export function TransactionList({
|
|||
headerActions,
|
||||
maxHeight,
|
||||
hideColumns = [],
|
||||
onBalanceUpdated,
|
||||
}: TransactionListProps) {
|
||||
const { isKeySet } = useEncryptionKey();
|
||||
const locale = useLocale();
|
||||
|
|
@ -308,6 +311,7 @@ export function TransactionList({
|
|||
const [deleteTransaction, setDeleteTransaction] =
|
||||
useState<DecryptedTransaction | null>(null);
|
||||
const [isBulkDeleteMode, setIsBulkDeleteMode] = useState(false);
|
||||
const [updateBalanceOnDelete, setUpdateBalanceOnDelete] = useState(true);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
|
||||
const [isBulkUpdating, setIsBulkUpdating] = useState(false);
|
||||
|
|
@ -766,6 +770,16 @@ export function TransactionList({
|
|||
searchInIndexedDB();
|
||||
}, [filters.searchText, isKeySet, accountId]);
|
||||
|
||||
const manualAccountIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
accounts
|
||||
.filter((account) => account.banking_connection_id === null)
|
||||
.map((account) => account.id),
|
||||
),
|
||||
[accounts],
|
||||
);
|
||||
|
||||
const filteredTransactions = useMemo(() => {
|
||||
return transactions.filter((transaction) => {
|
||||
if (filters.searchText && isKeySet) {
|
||||
|
|
@ -1116,8 +1130,16 @@ export function TransactionList({
|
|||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
const balanceWasUpdated =
|
||||
updateBalanceOnDelete &&
|
||||
manualAccountIds.has(deleteTransaction.account_id);
|
||||
try {
|
||||
await transactionSyncService.delete(deleteTransaction.id);
|
||||
await transactionSyncService.delete(deleteTransaction.id, {
|
||||
updateBalance: balanceWasUpdated,
|
||||
});
|
||||
if (balanceWasUpdated) {
|
||||
onBalanceUpdated?.();
|
||||
}
|
||||
setTransactions((previous) =>
|
||||
previous.filter(
|
||||
(transaction) => transaction.id !== deleteTransaction.id,
|
||||
|
|
@ -1245,8 +1267,20 @@ export function TransactionList({
|
|||
}
|
||||
|
||||
setIsBulkDeleting(true);
|
||||
const balanceMayHaveUpdated =
|
||||
updateBalanceOnDelete &&
|
||||
transactions.some(
|
||||
(transaction) =>
|
||||
selectedIds.includes(transaction.id) &&
|
||||
manualAccountIds.has(transaction.account_id),
|
||||
);
|
||||
try {
|
||||
await transactionSyncService.deleteMany(selectedIds);
|
||||
await transactionSyncService.deleteMany(selectedIds, {
|
||||
updateBalance: updateBalanceOnDelete,
|
||||
});
|
||||
if (balanceMayHaveUpdated) {
|
||||
onBalanceUpdated?.();
|
||||
}
|
||||
setTransactions((previous) =>
|
||||
previous.filter(
|
||||
(transaction) => !selectedIds.includes(transaction.id),
|
||||
|
|
@ -1461,6 +1495,7 @@ export function TransactionList({
|
|||
if (!open) {
|
||||
setDeleteTransaction(null);
|
||||
setIsBulkDeleteMode(false);
|
||||
setUpdateBalanceOnDelete(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -1477,6 +1512,33 @@ export function TransactionList({
|
|||
: 'Are you sure you want to delete this transaction? This action cannot be undone.'}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
{(isBulkDeleteMode
|
||||
? transactions.some(
|
||||
(transaction) =>
|
||||
Object.keys(rowSelection).includes(
|
||||
transaction.id,
|
||||
) &&
|
||||
manualAccountIds.has(transaction.account_id),
|
||||
)
|
||||
: deleteTransaction !== null &&
|
||||
manualAccountIds.has(
|
||||
deleteTransaction.account_id,
|
||||
)) && (
|
||||
<label className="flex cursor-pointer items-start gap-3 rounded-md border border-border bg-muted/40 p-3 text-sm">
|
||||
<Checkbox
|
||||
checked={updateBalanceOnDelete}
|
||||
onCheckedChange={(checked) =>
|
||||
setUpdateBalanceOnDelete(checked === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span className="text-muted-foreground">
|
||||
{__(
|
||||
'Update the current balance of the manual account to reflect this change.',
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
disabled={isDeleting || isBulkDeleting}
|
||||
|
|
|
|||
|
|
@ -403,6 +403,9 @@ export default function AccountShow({
|
|||
showActionsMenu={false}
|
||||
maxHeight={600}
|
||||
hideColumns={['bank', 'account']}
|
||||
onBalanceUpdated={() =>
|
||||
setChartRefreshKey((key) => key + 1)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import {
|
|||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
|
|
@ -457,6 +458,7 @@ export default function Transactions({
|
|||
const [deleteTransaction, setDeleteTransaction] =
|
||||
useState<DecryptedTransaction | null>(null);
|
||||
const [isBulkDeleteMode, setIsBulkDeleteMode] = useState(false);
|
||||
const [updateBalanceOnDelete, setUpdateBalanceOnDelete] = useState(true);
|
||||
const [bulkDeleteConfirmation, setBulkDeleteConfirmation] = useState('');
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
|
||||
|
|
@ -874,7 +876,11 @@ export default function Transactions({
|
|||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await transactionSyncService.delete(deleteTransaction.id);
|
||||
await transactionSyncService.delete(deleteTransaction.id, {
|
||||
updateBalance:
|
||||
updateBalanceOnDelete &&
|
||||
manualAccountIds.has(deleteTransaction.account_id),
|
||||
});
|
||||
setAllTransactions((previous) =>
|
||||
previous.filter(
|
||||
(transaction) => transaction.id !== deleteTransaction.id,
|
||||
|
|
@ -963,6 +969,28 @@ export default function Transactions({
|
|||
|
||||
const selectedCount = useMemo(() => selectedIds.length, [selectedIds]);
|
||||
|
||||
const manualAccountIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
accounts
|
||||
.filter((account) => account.banking_connection_id === null)
|
||||
.map((account) => account.id),
|
||||
),
|
||||
[accounts],
|
||||
);
|
||||
|
||||
const bulkDeleteTouchesManualAccount = useMemo(
|
||||
() =>
|
||||
isSelectingAll
|
||||
? manualAccountIds.size > 0
|
||||
: allTransactions.some(
|
||||
(transaction) =>
|
||||
selectedIds.includes(transaction.id) &&
|
||||
manualAccountIds.has(transaction.account_id),
|
||||
),
|
||||
[isSelectingAll, manualAccountIds, allTransactions, selectedIds],
|
||||
);
|
||||
|
||||
const bulkDeleteConfirmationText = useMemo(
|
||||
() => getBulkDeleteConfirmationText(selectedCount),
|
||||
[selectedCount],
|
||||
|
|
@ -990,7 +1018,9 @@ export default function Transactions({
|
|||
|
||||
setIsBulkDeleting(true);
|
||||
try {
|
||||
await transactionSyncService.deleteMany(selectedIds);
|
||||
await transactionSyncService.deleteMany(selectedIds, {
|
||||
updateBalance: updateBalanceOnDelete,
|
||||
});
|
||||
setAllTransactions((previous) =>
|
||||
previous.filter(
|
||||
(transaction) => !selectedIds.includes(transaction.id),
|
||||
|
|
@ -1280,6 +1310,7 @@ export default function Transactions({
|
|||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDeleteTransaction(null);
|
||||
setUpdateBalanceOnDelete(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -1294,6 +1325,25 @@ export default function Transactions({
|
|||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
{deleteTransaction !== null &&
|
||||
manualAccountIds.has(deleteTransaction.account_id) && (
|
||||
<label className="flex cursor-pointer items-start gap-3 rounded-md border border-border bg-muted/40 p-3 text-sm">
|
||||
<Checkbox
|
||||
checked={updateBalanceOnDelete}
|
||||
onCheckedChange={(checked) =>
|
||||
setUpdateBalanceOnDelete(
|
||||
checked === true,
|
||||
)
|
||||
}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span className="text-muted-foreground">
|
||||
{__(
|
||||
'Update the current balance of the manual account to reflect this change.',
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>
|
||||
{__('Cancel')}
|
||||
|
|
@ -1316,6 +1366,7 @@ export default function Transactions({
|
|||
setDeleteTransaction(null);
|
||||
setIsBulkDeleteMode(false);
|
||||
setBulkDeleteConfirmation('');
|
||||
setUpdateBalanceOnDelete(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -1340,6 +1391,22 @@ export default function Transactions({
|
|||
disabled={isBulkDeleting}
|
||||
autoFocus
|
||||
/>
|
||||
{bulkDeleteTouchesManualAccount && (
|
||||
<label className="flex cursor-pointer items-start gap-3 rounded-md border border-border bg-muted/40 p-3 text-sm">
|
||||
<Checkbox
|
||||
checked={updateBalanceOnDelete}
|
||||
onCheckedChange={(checked) =>
|
||||
setUpdateBalanceOnDelete(checked === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span className="text-muted-foreground">
|
||||
{__(
|
||||
'Update the current balance of the affected manual accounts to reflect this change.',
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
|
|||
|
|
@ -176,8 +176,13 @@ class TransactionSyncService {
|
|||
return response.data.count || 0;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await axios.delete(`/transactions/${id}`);
|
||||
async delete(
|
||||
id: string,
|
||||
options?: { updateBalance?: boolean },
|
||||
): Promise<void> {
|
||||
await axios.delete(`/transactions/${id}`, {
|
||||
data: options?.updateBalance ? { update_balance: true } : undefined,
|
||||
});
|
||||
await db.transactions.delete(id);
|
||||
}
|
||||
|
||||
|
|
@ -189,9 +194,12 @@ class TransactionSyncService {
|
|||
}
|
||||
}
|
||||
|
||||
async deleteMany(ids: string[]): Promise<void> {
|
||||
async deleteMany(
|
||||
ids: string[],
|
||||
options?: { updateBalance?: boolean },
|
||||
): Promise<void> {
|
||||
for (const id of ids) {
|
||||
await this.delete(id);
|
||||
await this.delete(id, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -238,6 +238,136 @@ test('users cannot delete other users transactions', function () {
|
|||
]);
|
||||
});
|
||||
|
||||
test('deleting a manual account expense increases the current balance when requested', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
$account = Account::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
$account->balances()->create([
|
||||
'balance_date' => now()->toDateString(),
|
||||
'balance' => 100000,
|
||||
]);
|
||||
|
||||
$transaction = Transaction::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'account_id' => $account->id,
|
||||
'amount' => -2500,
|
||||
]);
|
||||
|
||||
actingAs($user)
|
||||
->deleteJson(route('transactions.destroy', $transaction), ['update_balance' => true])
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('account_balances', [
|
||||
'account_id' => $account->id,
|
||||
'balance_date' => now()->toDateString(),
|
||||
'balance' => 102500,
|
||||
]);
|
||||
});
|
||||
|
||||
test('deleting a manual account income decreases the current balance when requested', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
$account = Account::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
$account->balances()->create([
|
||||
'balance_date' => now()->toDateString(),
|
||||
'balance' => 100000,
|
||||
]);
|
||||
|
||||
$transaction = Transaction::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'account_id' => $account->id,
|
||||
'amount' => 3000,
|
||||
]);
|
||||
|
||||
actingAs($user)
|
||||
->deleteJson(route('transactions.destroy', $transaction), ['update_balance' => true])
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('account_balances', [
|
||||
'account_id' => $account->id,
|
||||
'balance_date' => now()->toDateString(),
|
||||
'balance' => 97000,
|
||||
]);
|
||||
});
|
||||
|
||||
test('deleting a transaction creates a current balance from the latest known balance', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
$account = Account::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
$account->balances()->create([
|
||||
'balance_date' => now()->subDays(5)->toDateString(),
|
||||
'balance' => 50000,
|
||||
]);
|
||||
|
||||
$transaction = Transaction::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'account_id' => $account->id,
|
||||
'amount' => -1500,
|
||||
]);
|
||||
|
||||
actingAs($user)
|
||||
->deleteJson(route('transactions.destroy', $transaction), ['update_balance' => true])
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('account_balances', [
|
||||
'account_id' => $account->id,
|
||||
'balance_date' => now()->toDateString(),
|
||||
'balance' => 51500,
|
||||
]);
|
||||
});
|
||||
|
||||
test('deleting a transaction does not change the balance when not requested', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
$account = Account::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
$account->balances()->create([
|
||||
'balance_date' => now()->toDateString(),
|
||||
'balance' => 100000,
|
||||
]);
|
||||
|
||||
$transaction = Transaction::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'account_id' => $account->id,
|
||||
'amount' => -2500,
|
||||
]);
|
||||
|
||||
actingAs($user)
|
||||
->deleteJson(route('transactions.destroy', $transaction))
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('account_balances', [
|
||||
'account_id' => $account->id,
|
||||
'balance_date' => now()->toDateString(),
|
||||
'balance' => 100000,
|
||||
]);
|
||||
});
|
||||
|
||||
test('deleting a connected account transaction never changes the balance', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
$account = Account::factory()->connected()->create(['user_id' => $user->id]);
|
||||
|
||||
$account->balances()->create([
|
||||
'balance_date' => now()->toDateString(),
|
||||
'balance' => 100000,
|
||||
]);
|
||||
|
||||
$transaction = Transaction::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'account_id' => $account->id,
|
||||
'amount' => -2500,
|
||||
]);
|
||||
|
||||
actingAs($user)
|
||||
->deleteJson(route('transactions.destroy', $transaction), ['update_balance' => true])
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertDatabaseHas('account_balances', [
|
||||
'account_id' => $account->id,
|
||||
'balance_date' => now()->toDateString(),
|
||||
'balance' => 100000,
|
||||
]);
|
||||
});
|
||||
|
||||
test('transactions index page passes user categories', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
|
|
|
|||
Loading…
Reference in New Issue