feat(AccountBalanceSync): Update existing balances and add new ones efficiently

This commit is contained in:
Víctor Falcón 2025-12-01 11:04:50 +01:00
parent a826182d47
commit c2c6894cb8
13 changed files with 77 additions and 49 deletions

View File

@ -35,22 +35,33 @@ class AccountBalanceSyncController extends Controller
{
$data = $request->validated();
$balance = new AccountBalance([
'account_id' => $data['account_id'],
'balance_date' => $data['balance_date'],
'balance' => $data['balance'],
]);
$existing = AccountBalance::where('account_id', $data['account_id'])
->where('balance_date', $data['balance_date'])
->first();
if (isset($data['id'])) {
$balance->id = $data['id'];
$balance->exists = false;
if ($existing) {
$existing->update(['balance' => $data['balance']]);
$balance = $existing;
$wasRecentlyCreated = false;
} else {
$balance = new AccountBalance([
'account_id' => $data['account_id'],
'balance_date' => $data['balance_date'],
'balance' => $data['balance'],
]);
if (isset($data['id'])) {
$balance->id = $data['id'];
$balance->exists = false;
}
$balance->save();
$wasRecentlyCreated = true;
}
$balance->save();
return response()->json([
'data' => $balance,
], 201);
'data' => $balance->fresh(),
], $wasRecentlyCreated ? 201 : 200);
}
public function update(StoreAccountBalanceRequest $request, AccountBalance $accountBalance): JsonResponse

View File

@ -22,6 +22,8 @@ import { evaluateRulesForNewTransaction } from '@/lib/rule-engine';
import { accountBalanceSyncService } from '@/services/account-balance-sync';
import { accountSyncService } from '@/services/account-sync';
import { automationRuleSyncService } from '@/services/automation-rule-sync';
import { bankSyncService } from '@/services/bank-sync';
import { categorySyncService } from '@/services/category-sync';
import { transactionSyncService } from '@/services/transaction-sync';
import { type Account } from '@/types/account';
import {
@ -331,6 +333,10 @@ export function ImportTransactionsDrawer({
const key = keyString ? await importKey(keyString) : null;
const rules = key ? await automationRuleSyncService.getAll() : [];
const freshAccounts = await accountSyncService.getAll();
const freshBanks = await bankSyncService.getAll();
const freshCategories = await categorySyncService.getAll();
const BATCH_SIZE = 20;
let processedCount = 0;
@ -359,9 +365,9 @@ export function ImportTransactionsDrawer({
account_id: selectedAccount.id,
},
rules,
categories,
accounts,
banks,
freshCategories,
freshAccounts,
freshBanks,
);
if (ruleMatch) {

View File

@ -188,7 +188,16 @@ export function evaluateRulesForNewTransaction(
banks: Bank[],
): RuleEvaluationResult | null {
if (!rules || !categories || !accounts || !banks) {
consoleDebug('[Rule Engine] Missing required data for rule evaluation');
consoleDebug('[Rule Engine] Missing required data for rule evaluation', {
hasRules: !!rules,
rulesLength: rules?.length,
hasCategories: !!categories,
categoriesLength: categories?.length,
hasAccounts: !!accounts,
accountsLength: accounts?.length,
hasBanks: !!banks,
banksLength: banks?.length,
});
return null;
}

View File

@ -38,45 +38,27 @@ class AccountBalanceSyncService {
async create(
data: Omit<AccountBalance, 'id' | 'created_at' | 'updated_at'>,
): Promise<AccountBalance> {
return await this.syncManager.create<
AccountBalance,
Omit<AccountBalance, 'id' | 'created_at' | 'updated_at'> & {
id?: number;
created_at?: string;
updated_at?: string;
}
>(data);
return await this.syncManager.createLocal<AccountBalance>(data);
}
async createMany(
balances: Omit<AccountBalance, 'id' | 'created_at' | 'updated_at'>[],
): Promise<AccountBalance[]> {
try {
const timestamp = new Date().toISOString();
const created: AccountBalance[] = [];
for (const data of balances) {
const record = {
...data,
id: uuidv7(),
created_at: timestamp,
updated_at: timestamp,
} as AccountBalance;
await db.account_balances.put(record);
await db.pending_changes.add({
store: 'account_balances',
operation: 'create',
data: record,
timestamp,
});
created.push(record);
const result = await this.updateOrCreate(
data.account_id,
data.balance_date,
data.balance,
);
created.push(result);
}
return created;
} catch (error) {
console.error('Failed to create balances in IndexedDB:', error);
console.error('Failed to create balances:', error);
throw new Error(
'Failed to save balances locally. Please refresh the page and try again.',
);
@ -103,8 +85,22 @@ class AccountBalanceSyncService {
.first();
if (existing) {
await this.update(existing.id, { balance });
return (await this.getById(existing.id))!;
const timestamp = new Date().toISOString();
const updated = {
...existing,
balance,
updated_at: timestamp,
};
await db.account_balances.put(updated);
await db.pending_changes.add({
store: 'account_balances',
operation: 'update',
data: { id: existing.id, balance },
timestamp,
});
return updated;
} else {
return await this.create({
account_id: accountId,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -165,12 +165,12 @@ it('cannot update another user account balance', function () {
$response->assertForbidden();
});
it('enforces unique constraint on account_id and balance_date', function () {
it('updates existing balance when creating with duplicate account_id and balance_date', function () {
$user = User::factory()->create();
$account = Account::factory()->for($user)->create();
$date = now()->toDateString();
$date = '2025-01-15';
AccountBalance::factory()->for($account)->create([
$initialBalance = AccountBalance::factory()->for($account)->create([
'balance_date' => $date,
'balance' => 100000,
]);
@ -178,10 +178,16 @@ it('enforces unique constraint on account_id and balance_date', function () {
$balanceData = [
'account_id' => $account->id,
'balance_date' => $date,
'balance' => 200000,
'balance' => 250000,
];
$response = $this->actingAs($user)->postJson('/api/sync/account-balances', $balanceData);
$response->assertStatus(500);
$response->assertOk()
->assertJsonPath('data.id', $initialBalance->id)
->assertJsonPath('data.balance', 250000);
expect(AccountBalance::where('account_id', $account->id)
->where('balance_date', $date)
->count())->toBe(1);
});