feat(AccountBalanceSync): Update existing balances and add new ones efficiently
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||