diff --git a/app/Http/Controllers/Settings/AccountController.php b/app/Http/Controllers/Settings/AccountController.php index b0d04306..dea20f62 100644 --- a/app/Http/Controllers/Settings/AccountController.php +++ b/app/Http/Controllers/Settings/AccountController.php @@ -38,12 +38,23 @@ class AccountController extends Controller public function store(StoreAccountRequest $request): RedirectResponse|JsonResponse { $user = auth()->user(); + $validated = $request->validated(); + $balance = $validated['balance'] ?? null; + unset($validated['balance']); + $account = $user->accounts()->create([ - ...$request->validated(), + ...$validated, 'encrypted' => false, 'name_iv' => null, ]); + if ($balance !== null) { + $account->balances()->create([ + 'balance_date' => now()->toDateString(), + 'balance' => $balance, + ]); + } + // Set user's currency_code from first account if ($user->accounts()->count() === 1) { $user->update(['currency_code' => $account->currency_code]); @@ -53,7 +64,7 @@ class AccountController extends Controller return response()->json($account, 201); } - return to_route('accounts.index'); + return back(); } /** diff --git a/app/Http/Requests/Settings/StoreAccountRequest.php b/app/Http/Requests/Settings/StoreAccountRequest.php index 44fa8cd3..4b529b0e 100644 --- a/app/Http/Requests/Settings/StoreAccountRequest.php +++ b/app/Http/Requests/Settings/StoreAccountRequest.php @@ -37,6 +37,7 @@ class StoreAccountRequest extends FormRequest 'string', Rule::in(array_map(fn ($type) => $type->value, AccountType::cases())), ], + 'balance' => ['nullable', 'integer'], ]; } } diff --git a/lang/es.json b/lang/es.json index 43413393..4a4183aa 100644 --- a/lang/es.json +++ b/lang/es.json @@ -838,6 +838,7 @@ "Only you know this password. It never leaves your device.": "Solo t\u00fa conoces esta contrase\u00f1a. Nunca sale de tu dispositivo.", "Open menu": "Abrir men\u00fa", "Open source": "C\u00f3digo abierto", + "Optional. Set the current balance for this account.": "Opcional. Establece el balance actual de esta cuenta.", "Or, return to": "O, regresar a", "Organize financial data with categories and custom labels": "Organiza datos financieros con categor\u00edas y etiquetas personalizadas", "Organize financial data with categories and\\n custom labels": "Organiza los datos financieros con categor\u00edas y etiquetas personalizadas", diff --git a/resources/js/components/accounts/account-form.tsx b/resources/js/components/accounts/account-form.tsx index e9912dff..53ca58c8 100644 --- a/resources/js/components/accounts/account-form.tsx +++ b/resources/js/components/accounts/account-form.tsx @@ -1,3 +1,4 @@ +import { AmountInput } from '@/components/ui/amount-input'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { @@ -10,6 +11,7 @@ import { import { ACCOUNT_TYPES, CURRENCY_OPTIONS, + balanceTermCapitalized, formatAccountType, type AccountType, type Bank, @@ -20,12 +22,20 @@ import { useCallback, useEffect, useState } from 'react'; import { BankCombobox } from './bank-combobox'; import { CustomBankData, CustomBankForm } from './custom-bank-form'; +const BALANCE_ACCOUNT_TYPES: AccountType[] = [ + 'investment', + 'loan', + 'retirement', + 'savings', +]; + export interface AccountFormData { displayName: string; bankId: number | null; type: AccountType | null; currencyCode: CurrencyCode | null; customBank: CustomBankData | null; + balance: number | null; } interface AccountFormProps { @@ -65,6 +75,10 @@ export function AccountForm({ const [customBankData, setCustomBankData] = useState( initialCustomBankData, ); + const [balance, setBalance] = useState(null); + + const showBalanceField = + selectedType !== null && BALANCE_ACCOUNT_TYPES.includes(selectedType); useEffect(() => { onChange({ @@ -73,6 +87,7 @@ export function AccountForm({ type: selectedType, currencyCode: selectedCurrency, customBank: isCreatingCustomBank ? customBankData : null, + balance: showBalanceField ? balance : null, }); }, [ displayName, @@ -81,6 +96,8 @@ export function AccountForm({ selectedCurrency, isCreatingCustomBank, customBankData, + balance, + showBalanceField, onChange, ]); @@ -215,6 +232,27 @@ export function AccountForm({ + + {showBalanceField && selectedCurrency && ( +
+ +
+ +
+

+ {__( + 'Optional. Set the current balance for this account.', + )} +

+
+ )} ); } diff --git a/resources/js/components/accounts/create-account-dialog.tsx b/resources/js/components/accounts/create-account-dialog.tsx index f0f4393a..9227c1f7 100644 --- a/resources/js/components/accounts/create-account-dialog.tsx +++ b/resources/js/components/accounts/create-account-dialog.tsx @@ -46,6 +46,7 @@ export function CreateAccountDialog({ type: null, currencyCode: null, customBank: null, + balance: null, }); const handleFormChange = useCallback((data: AccountFormData) => { @@ -140,6 +141,9 @@ export function CreateAccountDialog({ bank_id: finalBankId, type: type, currency_code: currencyCode, + ...(formDataRef.current.balance + ? { balance: formDataRef.current.balance } + : {}), }, { onSuccess: () => { diff --git a/tests/Feature/Settings/AccountTest.php b/tests/Feature/Settings/AccountTest.php index 605d6972..5a2d9e48 100644 --- a/tests/Feature/Settings/AccountTest.php +++ b/tests/Feature/Settings/AccountTest.php @@ -8,6 +8,7 @@ use App\Models\User; use function Pest\Laravel\actingAs; use function Pest\Laravel\assertDatabaseHas; +use function Pest\Laravel\assertDatabaseMissing; beforeEach(function () { $this->user = User::factory()->create(); @@ -42,7 +43,7 @@ it('can create a new account with plaintext name', function () { $response = $this->post(route('accounts.store'), $data); - $response->assertRedirect(route('accounts.index')); + $response->assertRedirect(); assertDatabaseHas('accounts', [ 'user_id' => $this->user->id, 'bank_id' => $this->bank->id, @@ -195,3 +196,70 @@ it('prevents deleting another users account', function () { $response->assertForbidden(); assertDatabaseHas('accounts', ['id' => $account->id]); }); + +it('can create an account with an initial balance', function () { + actingAs($this->user); + + $data = [ + 'name' => 'My Savings Account', + 'bank_id' => $this->bank->id, + 'currency_code' => 'USD', + 'type' => AccountType::Savings->value, + 'balance' => 150000, + ]; + + $response = $this->post(route('accounts.store'), $data); + + $response->assertRedirect(); + + $account = Account::where('user_id', $this->user->id) + ->where('name', 'My Savings Account') + ->first(); + + expect($account)->not->toBeNull(); + + assertDatabaseHas('account_balances', [ + 'account_id' => $account->id, + 'balance_date' => now()->toDateString(), + 'balance' => 150000, + ]); +}); + +it('creates account without balance record when balance is not provided', function () { + actingAs($this->user); + + $data = [ + 'name' => 'My Investment Account', + 'bank_id' => $this->bank->id, + 'currency_code' => 'USD', + 'type' => AccountType::Investment->value, + ]; + + $response = $this->post(route('accounts.store'), $data); + + $response->assertRedirect(); + + $account = Account::where('user_id', $this->user->id) + ->where('name', 'My Investment Account') + ->first(); + + expect($account)->not->toBeNull(); + + assertDatabaseMissing('account_balances', [ + 'account_id' => $account->id, + ]); +}); + +it('validates balance must be an integer when provided', function () { + actingAs($this->user); + + $response = $this->post(route('accounts.store'), [ + 'name' => 'My Account', + 'bank_id' => $this->bank->id, + 'currency_code' => 'USD', + 'type' => AccountType::Savings->value, + 'balance' => 'not-a-number', + ]); + + $response->assertSessionHasErrors(['balance']); +});