mirror of
https://github.com/acedanger/finance.git
synced 2025-12-05 22:50:12 -08:00
Add VSCode task for automatic build on project open and update dependencies
Remove .vscode from .gitignore to allow for project-specific settings. Introduce a VSCode task to run `npm run dev` automatically when the project is opened. Update environment configuration files and dependencies to enhance support for the Node adapter. Fixes #13
This commit is contained in:
@@ -3,7 +3,7 @@ import { formatCurrency } from '../utils';
|
||||
import type { Account } from '../types';
|
||||
|
||||
interface Props {
|
||||
account: Account;
|
||||
account: Account;
|
||||
}
|
||||
const { account } = Astro.props;
|
||||
---
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import type { Account } from "../types";
|
||||
import { formatCurrency } from "../utils";
|
||||
import {
|
||||
currentAccountId as currentAccountIdStore,
|
||||
refreshKey,
|
||||
} from "../stores/transactionStore";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type { Account } from '../types';
|
||||
import { formatCurrency } from '../utils';
|
||||
import { currentAccountId as currentAccountIdStore, refreshKey } from '../stores/transactionStore';
|
||||
|
||||
interface AccountSummaryProps {
|
||||
// No props needed, data comes from store and fetch
|
||||
@@ -32,14 +29,12 @@ export default function AccountSummary({}: AccountSummaryProps) {
|
||||
try {
|
||||
const response = await fetch(`/api/accounts/${currentAccountId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch account details");
|
||||
throw new Error('Failed to fetch account details');
|
||||
}
|
||||
const data: Account = await response.json();
|
||||
setAccount(data);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "An unknown error occurred"
|
||||
);
|
||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
||||
setAccount(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -58,7 +53,7 @@ export default function AccountSummary({}: AccountSummaryProps) {
|
||||
} else if (account) {
|
||||
balanceContent = formatCurrency(account.balance);
|
||||
} else {
|
||||
balanceContent = "N/A"; // Or some placeholder
|
||||
balanceContent = 'N/A'; // Or some placeholder
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import type { Transaction } from "../types";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type { Transaction } from '../types';
|
||||
// Import store atoms and actions
|
||||
import {
|
||||
currentAccountId as currentAccountIdStore,
|
||||
transactionToEdit as transactionToEditStore,
|
||||
cancelEditingTransaction,
|
||||
transactionSaved,
|
||||
} from "../stores/transactionStore";
|
||||
} from '../stores/transactionStore';
|
||||
|
||||
// Remove props that now come from the store
|
||||
interface AddTransactionFormProps {}
|
||||
@@ -18,9 +18,9 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
|
||||
const transactionToEdit = useStore(transactionToEditStore);
|
||||
|
||||
// --- State Variables ---
|
||||
const [date, setDate] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [amount, setAmount] = useState("");
|
||||
const [date, setDate] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -32,7 +32,7 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
|
||||
useEffect(() => {
|
||||
// Only set default date if not editing
|
||||
if (!transactionToEdit) {
|
||||
setDate(new Date().toISOString().split("T")[0]);
|
||||
setDate(new Date().toISOString().split('T')[0]);
|
||||
}
|
||||
}, [transactionToEdit]); // Rerun if edit mode changes
|
||||
|
||||
@@ -48,17 +48,14 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
|
||||
// Directly format the date object (usually interpreted as UTC midnight)
|
||||
// into the YYYY-MM-DD format required by the input.
|
||||
// No timezone adjustment needed here.
|
||||
setDate(dateObj.toISOString().split("T")[0]);
|
||||
setDate(dateObj.toISOString().split('T')[0]);
|
||||
} else {
|
||||
console.warn(
|
||||
"Invalid date received for editing:",
|
||||
transactionToEdit.date
|
||||
);
|
||||
setDate(""); // Set to empty if invalid
|
||||
console.warn('Invalid date received for editing:', transactionToEdit.date);
|
||||
setDate(''); // Set to empty if invalid
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing date for editing:", e);
|
||||
setDate(""); // Set to empty on error
|
||||
console.error('Error parsing date for editing:', e);
|
||||
setDate(''); // Set to empty on error
|
||||
}
|
||||
setDescription(transactionToEdit.description);
|
||||
setAmount(transactionToEdit.amount.toString());
|
||||
@@ -75,9 +72,9 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
|
||||
// --- Helper Functions ---
|
||||
const resetForm = () => {
|
||||
setEditingId(null);
|
||||
setDate(new Date().toISOString().split("T")[0]);
|
||||
setDescription("");
|
||||
setAmount("");
|
||||
setDate(new Date().toISOString().split('T')[0]);
|
||||
setDescription('');
|
||||
setAmount('');
|
||||
setError(null);
|
||||
// Don't reset isLoading here, it's handled in submit/cancel
|
||||
};
|
||||
@@ -85,28 +82,28 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
|
||||
const validateForm = (): string[] => {
|
||||
const errors: string[] = [];
|
||||
if (!description || description.trim().length < 2) {
|
||||
errors.push("Description must be at least 2 characters long");
|
||||
errors.push('Description must be at least 2 characters long');
|
||||
}
|
||||
if (!amount) {
|
||||
errors.push("Amount is required");
|
||||
errors.push('Amount is required');
|
||||
} else {
|
||||
const amountNum = parseFloat(amount);
|
||||
if (isNaN(amountNum)) {
|
||||
errors.push("Amount must be a valid number");
|
||||
errors.push('Amount must be a valid number');
|
||||
} else if (amountNum === 0) {
|
||||
errors.push("Amount cannot be zero");
|
||||
errors.push('Amount cannot be zero');
|
||||
}
|
||||
}
|
||||
if (!date) {
|
||||
errors.push("Date is required");
|
||||
errors.push('Date is required');
|
||||
} else {
|
||||
try {
|
||||
const dateObj = new Date(date + "T00:00:00"); // Treat input as local date
|
||||
const dateObj = new Date(date + 'T00:00:00'); // Treat input as local date
|
||||
if (isNaN(dateObj.getTime())) {
|
||||
errors.push("Invalid date format");
|
||||
errors.push('Invalid date format');
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push("Invalid date format");
|
||||
errors.push('Invalid date format');
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
@@ -118,13 +115,13 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
|
||||
setError(null);
|
||||
|
||||
if (isLoading || !currentAccountId) {
|
||||
if (!currentAccountId) setError("No account selected.");
|
||||
if (!currentAccountId) setError('No account selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
const validationErrors = validateForm();
|
||||
if (validationErrors.length > 0) {
|
||||
setError(validationErrors.join(". "));
|
||||
setError(validationErrors.join('. '));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -140,21 +137,17 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
|
||||
amount: parseFloat(amount),
|
||||
};
|
||||
|
||||
const method = editingId ? "PUT" : "POST";
|
||||
const url = editingId
|
||||
? `/api/transactions/${editingId}`
|
||||
: "/api/transactions";
|
||||
const method = editingId ? 'PUT' : 'POST';
|
||||
const url = editingId ? `/api/transactions/${editingId}` : '/api/transactions';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(transactionData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMsg = `Failed to ${
|
||||
isEditMode ? "update" : "create"
|
||||
} transaction`;
|
||||
let errorMsg = `Failed to ${isEditMode ? 'update' : 'create'} transaction`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMsg = errorData.error || errorMsg;
|
||||
@@ -169,9 +162,7 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
|
||||
transactionSaved(savedTransaction); // Call store action instead of prop callback
|
||||
resetForm(); // Reset form on success
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "An unexpected error occurred"
|
||||
);
|
||||
setError(err instanceof Error ? err.message : 'An unexpected error occurred');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -185,7 +176,7 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
|
||||
// --- JSX ---
|
||||
return (
|
||||
<form id="add-transaction-form-react" onSubmit={handleSubmit} noValidate>
|
||||
<h4>{isEditMode ? "Edit Transaction" : "New Transaction"}</h4>
|
||||
<h4>{isEditMode ? 'Edit Transaction' : 'New Transaction'}</h4>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<div className="form-group">
|
||||
@@ -228,29 +219,18 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
|
||||
placeholder="e.g. -25.50 or 1200.00"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<small className="help-text">
|
||||
Use negative numbers for expenses (e.g., -50.00)
|
||||
</small>
|
||||
<small className="help-text">Use negative numbers for expenses (e.g., -50.00)</small>
|
||||
</div>
|
||||
<div className="button-group">
|
||||
<button
|
||||
type="submit"
|
||||
className={`form-submit-btn ${isLoading ? "loading" : ""}`}
|
||||
className={`form-submit-btn ${isLoading ? 'loading' : ''}`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading
|
||||
? "Saving..."
|
||||
: isEditMode
|
||||
? "Update Transaction"
|
||||
: "Save Transaction"}
|
||||
{isLoading ? 'Saving...' : isEditMode ? 'Update Transaction' : 'Save Transaction'}
|
||||
</button>
|
||||
{isEditMode && (
|
||||
<button
|
||||
type="button"
|
||||
className="cancel-btn"
|
||||
onClick={handleCancel}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<button type="button" className="cancel-btn" onClick={handleCancel} disabled={isLoading}>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import TransactionTable from './TransactionTable.tsx';
|
||||
import type { Account } from '../types';
|
||||
|
||||
interface Props {
|
||||
account: Account;
|
||||
account: Account;
|
||||
}
|
||||
|
||||
const { account } = Astro.props;
|
||||
|
||||
@@ -4,8 +4,8 @@ import AddTransactionForm from './AddTransactionForm.tsx';
|
||||
import type { Account } from '../types';
|
||||
|
||||
interface Props {
|
||||
accounts: Account[];
|
||||
initialAccount: Account;
|
||||
accounts: Account[];
|
||||
initialAccount: Account;
|
||||
}
|
||||
|
||||
const { accounts, initialAccount } = Astro.props;
|
||||
|
||||
@@ -3,13 +3,15 @@ import { formatCurrency, formatDate } from '../utils';
|
||||
import type { Transaction } from '../types';
|
||||
|
||||
interface Props {
|
||||
transactions: Transaction[];
|
||||
transactions: Transaction[];
|
||||
}
|
||||
|
||||
const { transactions } = Astro.props;
|
||||
|
||||
// Sort transactions by date descending for display
|
||||
const sortedTransactions = [...transactions].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
const sortedTransactions = [...transactions].sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
);
|
||||
|
||||
// TODO: UI/UX Improvements
|
||||
// - Add sorting functionality for all columns
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import type { Transaction } from "../types";
|
||||
import { formatCurrency, formatDate } from "../utils";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type { Transaction } from '../types';
|
||||
import { formatCurrency, formatDate } from '../utils';
|
||||
import {
|
||||
startEditingTransaction,
|
||||
currentAccountId as currentAccountIdStore,
|
||||
triggerRefresh,
|
||||
refreshKey,
|
||||
} from "../stores/transactionStore";
|
||||
} from '../stores/transactionStore';
|
||||
|
||||
interface TransactionTableProps {}
|
||||
|
||||
@@ -29,18 +29,14 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/accounts/${currentAccountId}/transactions`
|
||||
);
|
||||
const response = await fetch(`/api/accounts/${currentAccountId}/transactions`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch transactions");
|
||||
throw new Error('Failed to fetch transactions');
|
||||
}
|
||||
const data: Transaction[] = await response.json();
|
||||
setTransactions(data);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "An unknown error occurred"
|
||||
);
|
||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
||||
setTransactions([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -55,7 +51,7 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
);
|
||||
|
||||
const handleDelete = async (txnId: string) => {
|
||||
if (!confirm("Are you sure you want to delete this transaction?")) {
|
||||
if (!confirm('Are you sure you want to delete this transaction?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,11 +59,11 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/transactions/${txnId}`, {
|
||||
method: "DELETE",
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMsg = "Failed to delete transaction";
|
||||
let errorMsg = 'Failed to delete transaction';
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMsg = errorData.error || errorMsg;
|
||||
@@ -85,10 +81,8 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
|
||||
triggerRefresh();
|
||||
} catch (error) {
|
||||
alert(
|
||||
error instanceof Error ? error.message : "Failed to delete transaction"
|
||||
);
|
||||
console.error("Delete error:", error);
|
||||
alert(error instanceof Error ? error.message : 'Failed to delete transaction');
|
||||
console.error('Delete error:', error);
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
@@ -97,16 +91,14 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
console.log(`Attempting to edit transaction: ${transaction.id}`);
|
||||
startEditingTransaction(transaction);
|
||||
|
||||
const addTransactionSection = document.getElementById(
|
||||
"add-transaction-section"
|
||||
);
|
||||
const toggleAddTxnBtn = document.getElementById("toggle-add-txn-btn");
|
||||
if (addTransactionSection?.classList.contains("collapsed")) {
|
||||
addTransactionSection.classList.replace("collapsed", "expanded");
|
||||
toggleAddTxnBtn?.setAttribute("aria-expanded", "true");
|
||||
const addTransactionSection = document.getElementById('add-transaction-section');
|
||||
const toggleAddTxnBtn = document.getElementById('toggle-add-txn-btn');
|
||||
if (addTransactionSection?.classList.contains('collapsed')) {
|
||||
addTransactionSection.classList.replace('collapsed', 'expanded');
|
||||
toggleAddTxnBtn?.setAttribute('aria-expanded', 'true');
|
||||
addTransactionSection.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -114,7 +106,7 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
// Helper function to render loading state
|
||||
const renderLoading = () => (
|
||||
<tr>
|
||||
<td colSpan={4} style={{ textAlign: "center", padding: "2rem" }}>
|
||||
<td colSpan={4} style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
Loading transactions...
|
||||
</td>
|
||||
</tr>
|
||||
@@ -126,9 +118,9 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
<td
|
||||
colSpan={4}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontStyle: "italic",
|
||||
color: "#777",
|
||||
textAlign: 'center',
|
||||
fontStyle: 'italic',
|
||||
color: '#777',
|
||||
}}
|
||||
>
|
||||
No transactions found for this account.
|
||||
@@ -142,11 +134,7 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
<tr key={txn.id} data-txn-id={txn.id}>
|
||||
<td>{formatDate(txn.date)}</td>
|
||||
<td>{txn.description}</td>
|
||||
<td
|
||||
className={`amount-col ${
|
||||
txn.amount >= 0 ? "amount-positive" : "amount-negative"
|
||||
}`}
|
||||
>
|
||||
<td className={`amount-col ${txn.amount >= 0 ? 'amount-positive' : 'amount-negative'}`}>
|
||||
{formatCurrency(txn.amount)}
|
||||
</td>
|
||||
<td>
|
||||
@@ -169,9 +157,9 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
));
|
||||
|
||||
return (
|
||||
<div id="transaction-section" className={isLoading ? "loading" : ""}>
|
||||
<div id="transaction-section" className={isLoading ? 'loading' : ''}>
|
||||
{error && (
|
||||
<div className="error-message" style={{ padding: "1rem" }}>
|
||||
<div className="error-message" style={{ padding: '1rem' }}>
|
||||
Error loading transactions: {error}
|
||||
</div>
|
||||
)}
|
||||
@@ -188,10 +176,10 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
{isLoading
|
||||
? renderLoading()
|
||||
: error
|
||||
? null // Error message is shown above the table
|
||||
: sortedTransactions.length === 0
|
||||
? renderEmpty()
|
||||
: renderRows()}
|
||||
? null // Error message is shown above the table
|
||||
: sortedTransactions.length === 0
|
||||
? renderEmpty()
|
||||
: renderRows()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// - Implement audit trail
|
||||
// - Add data archival strategy
|
||||
|
||||
import type { Account, Transaction } from "../types";
|
||||
import type { Account, Transaction } from '../types';
|
||||
|
||||
// TODO: Replace in-memory store with persistent database
|
||||
// - Implement database connection and configuration
|
||||
@@ -19,39 +19,39 @@ import type { Account, Transaction } from "../types";
|
||||
// Temporary in-memory store for development
|
||||
export const accounts: Account[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Checking Account",
|
||||
last4: "4321",
|
||||
id: '1',
|
||||
name: 'Checking Account',
|
||||
last4: '4321',
|
||||
balance: 2500.0,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Savings Account",
|
||||
last4: "8765",
|
||||
id: '2',
|
||||
name: 'Savings Account',
|
||||
last4: '8765',
|
||||
balance: 10000.0,
|
||||
},
|
||||
];
|
||||
|
||||
export const transactions: Transaction[] = [
|
||||
{
|
||||
id: "1",
|
||||
accountId: "1",
|
||||
date: "2025-04-20",
|
||||
description: "Grocery Store",
|
||||
id: '1',
|
||||
accountId: '1',
|
||||
date: '2025-04-20',
|
||||
description: 'Grocery Store',
|
||||
amount: -75.5,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
accountId: "1",
|
||||
date: "2025-04-21",
|
||||
description: "Salary Deposit",
|
||||
id: '2',
|
||||
accountId: '1',
|
||||
date: '2025-04-21',
|
||||
description: 'Salary Deposit',
|
||||
amount: 3000.0,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
accountId: "2",
|
||||
date: "2025-04-22",
|
||||
description: "Transfer to Savings",
|
||||
id: '3',
|
||||
accountId: '2',
|
||||
date: '2025-04-22',
|
||||
description: 'Transfer to Savings',
|
||||
amount: 500.0,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
// TODO: Accessibility Improvements
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { accounts } from "../../../../data/store";
|
||||
import type { APIRoute } from 'astro';
|
||||
import { accounts } from '../../../../data/store';
|
||||
|
||||
export const GET: APIRoute = async ({ params }) => {
|
||||
const account = accounts.find((a) => a.id === params.id);
|
||||
|
||||
if (!account) {
|
||||
return new Response(JSON.stringify({ error: "Account not found" }), {
|
||||
return new Response(JSON.stringify({ error: 'Account not found' }), {
|
||||
status: 404,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export const GET: APIRoute = async ({ params }) => {
|
||||
return new Response(JSON.stringify(account), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { transactions } from "../../../../../data/store";
|
||||
import type { APIRoute } from 'astro';
|
||||
import { transactions } from '../../../../../data/store';
|
||||
|
||||
export const GET: APIRoute = async ({ params }) => {
|
||||
const accountTransactions = transactions.filter(
|
||||
(t) => t.accountId === params.id
|
||||
);
|
||||
const accountTransactions = transactions.filter((t) => t.accountId === params.id);
|
||||
|
||||
return new Response(JSON.stringify(accountTransactions), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { accounts } from "../../../data/store";
|
||||
import type { APIRoute } from 'astro';
|
||||
import { accounts } from '../../../data/store';
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
return new Response(JSON.stringify(accounts), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { transactions, accounts } from "../../../../data/store";
|
||||
import type { Transaction } from "../../../../types";
|
||||
import type { APIRoute } from 'astro';
|
||||
import { transactions, accounts } from '../../../../data/store';
|
||||
import type { Transaction } from '../../../../types';
|
||||
|
||||
export const PUT: APIRoute = async ({ request, params }) => {
|
||||
const { id } = params;
|
||||
|
||||
if (!id) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Transaction ID is required" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
return new Response(JSON.stringify({ error: 'Transaction ID is required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -20,9 +17,9 @@ export const PUT: APIRoute = async ({ request, params }) => {
|
||||
const transactionIndex = transactions.findIndex((t) => t.id === id);
|
||||
|
||||
if (transactionIndex === -1) {
|
||||
return new Response(JSON.stringify({ error: "Transaction not found" }), {
|
||||
return new Response(JSON.stringify({ error: 'Transaction not found' }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,9 +28,9 @@ export const PUT: APIRoute = async ({ request, params }) => {
|
||||
// Get the old account first
|
||||
const oldAccount = accounts.find((a) => a.id === oldTransaction.accountId);
|
||||
if (!oldAccount) {
|
||||
return new Response(JSON.stringify({ error: "Account not found" }), {
|
||||
return new Response(JSON.stringify({ error: 'Account not found' }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,9 +39,9 @@ export const PUT: APIRoute = async ({ request, params }) => {
|
||||
if (updates.accountId && updates.accountId !== oldTransaction.accountId) {
|
||||
const foundAccount = accounts.find((a) => a.id === updates.accountId);
|
||||
if (!foundAccount) {
|
||||
return new Response(JSON.stringify({ error: "Account not found" }), {
|
||||
return new Response(JSON.stringify({ error: 'Account not found' }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
newAccount = foundAccount;
|
||||
@@ -74,12 +71,12 @@ export const PUT: APIRoute = async ({ request, params }) => {
|
||||
|
||||
return new Response(JSON.stringify(updatedTransaction), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: "Invalid request body" }), {
|
||||
return new Response(JSON.stringify({ error: 'Invalid request body' }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -88,21 +85,18 @@ export const DELETE: APIRoute = async ({ params }) => {
|
||||
const { id } = params;
|
||||
|
||||
if (!id) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Transaction ID is required" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
return new Response(JSON.stringify({ error: 'Transaction ID is required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const transactionIndex = transactions.findIndex((t) => t.id === id);
|
||||
|
||||
if (transactionIndex === -1) {
|
||||
return new Response(JSON.stringify({ error: "Transaction not found" }), {
|
||||
return new Response(JSON.stringify({ error: 'Transaction not found' }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,9 +104,9 @@ export const DELETE: APIRoute = async ({ params }) => {
|
||||
const account = accounts.find((a) => a.id === transaction.accountId);
|
||||
|
||||
if (!account) {
|
||||
return new Response(JSON.stringify({ error: "Account not found" }), {
|
||||
return new Response(JSON.stringify({ error: 'Account not found' }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
* - Set up proper CORS configuration
|
||||
*/
|
||||
|
||||
import type { APIRoute } from "astro";
|
||||
import { transactions, accounts } from "../../../data/store";
|
||||
import type { Transaction } from "../../../types";
|
||||
import type { APIRoute } from 'astro';
|
||||
import { transactions, accounts } from '../../../data/store';
|
||||
import type { Transaction } from '../../../types';
|
||||
|
||||
/**
|
||||
* TODO: API Improvements
|
||||
@@ -27,7 +27,7 @@ import type { Transaction } from "../../../types";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const transaction = (await request.json()) as Omit<Transaction, "id">;
|
||||
const transaction = (await request.json()) as Omit<Transaction, 'id'>;
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
@@ -36,21 +36,18 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
!transaction.description ||
|
||||
transaction.amount === undefined
|
||||
) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Missing required fields" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
return new Response(JSON.stringify({ error: 'Missing required fields' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Validate account exists
|
||||
const account = accounts.find((a) => a.id === transaction.accountId);
|
||||
if (!account) {
|
||||
return new Response(JSON.stringify({ error: "Account not found" }), {
|
||||
return new Response(JSON.stringify({ error: 'Account not found' }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,12 +65,12 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
|
||||
return new Response(JSON.stringify(newTransaction), {
|
||||
status: 201,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: "Invalid request body" }), {
|
||||
return new Response(JSON.stringify({ error: 'Invalid request body' }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@ import MainContent from '../components/MainContent.astro';
|
||||
import type { Account, Transaction } from '../types';
|
||||
|
||||
export interface Props {
|
||||
account: Account;
|
||||
transactions: Transaction[];
|
||||
account: Account;
|
||||
transactions: Transaction[];
|
||||
}
|
||||
|
||||
// Get the base URL from the incoming request
|
||||
@@ -18,17 +18,19 @@ const accounts: Account[] = await accountsResponse.json();
|
||||
|
||||
// Initialize with first account or empty account if none exist
|
||||
const initialAccount: Account = accounts[0] || {
|
||||
id: '',
|
||||
name: 'No accounts available',
|
||||
last4: '0000',
|
||||
balance: 0
|
||||
id: '',
|
||||
name: 'No accounts available',
|
||||
last4: '0000',
|
||||
balance: 0,
|
||||
};
|
||||
|
||||
// Fetch initial transactions if we have an account, using absolute URL
|
||||
let initialTransactions: Transaction[] = [];
|
||||
if (initialAccount.id) {
|
||||
const transactionsResponse = await fetch(`${baseUrl}/api/accounts/${initialAccount.id}/transactions`);
|
||||
initialTransactions = await transactionsResponse.json();
|
||||
const transactionsResponse = await fetch(
|
||||
`${baseUrl}/api/accounts/${initialAccount.id}/transactions`
|
||||
);
|
||||
initialTransactions = await transactionsResponse.json();
|
||||
}
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { atom } from "nanostores";
|
||||
import type { Transaction } from "../types";
|
||||
import { atom } from 'nanostores';
|
||||
import type { Transaction } from '../types';
|
||||
|
||||
// Atom to hold the ID of the currently selected account
|
||||
export const currentAccountId = atom<string | null>(null);
|
||||
@@ -39,9 +39,7 @@ export function transactionSaved(transaction: Transaction) {
|
||||
}
|
||||
// Potentially trigger UI updates or refreshes here
|
||||
// This might involve dispatching a custom event or calling a refresh function
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("transactionSaved", { detail: { transaction } })
|
||||
);
|
||||
document.dispatchEvent(new CustomEvent('transactionSaved', { detail: { transaction } }));
|
||||
|
||||
// Trigger a general refresh after saving too, to update balance
|
||||
triggerRefresh();
|
||||
|
||||
@@ -69,8 +69,8 @@ body {
|
||||
.collapsible-form {
|
||||
padding: 15px;
|
||||
background-color: #fdfdfd;
|
||||
transition: max-height 0.3s ease-out, opacity 0.3s ease-out,
|
||||
padding 0.3s ease-out, border 0.3s ease-out;
|
||||
transition: max-height 0.3s ease-out, opacity 0.3s ease-out, padding 0.3s ease-out, border 0.3s
|
||||
ease-out;
|
||||
overflow: hidden;
|
||||
max-height: 500px;
|
||||
opacity: 1;
|
||||
|
||||
@@ -1,60 +1,54 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { GET as listAccounts } from "../pages/api/accounts/index";
|
||||
import { GET as getAccount } from "../pages/api/accounts/[id]/index";
|
||||
import { GET as listTransactions } from "../pages/api/accounts/[id]/transactions/index";
|
||||
import { createMockAPIContext } from "./setup";
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { GET as listAccounts } from '../pages/api/accounts/index';
|
||||
import { GET as getAccount } from '../pages/api/accounts/[id]/index';
|
||||
import { GET as listTransactions } from '../pages/api/accounts/[id]/transactions/index';
|
||||
import { createMockAPIContext } from './setup';
|
||||
|
||||
describe("Accounts API", () => {
|
||||
describe("GET /api/accounts", () => {
|
||||
it("should return all accounts", async () => {
|
||||
describe('Accounts API', () => {
|
||||
describe('GET /api/accounts', () => {
|
||||
it('should return all accounts', async () => {
|
||||
const response = await listAccounts(createMockAPIContext() as any);
|
||||
const accounts = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(accounts).toHaveLength(2);
|
||||
expect(accounts[0]).toHaveProperty("id", "1");
|
||||
expect(accounts[1]).toHaveProperty("id", "2");
|
||||
expect(accounts[0]).toHaveProperty('id', '1');
|
||||
expect(accounts[1]).toHaveProperty('id', '2');
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/accounts/:id", () => {
|
||||
it("should return a specific account", async () => {
|
||||
const response = await getAccount(
|
||||
createMockAPIContext({ params: { id: "1" } }) as any
|
||||
);
|
||||
describe('GET /api/accounts/:id', () => {
|
||||
it('should return a specific account', async () => {
|
||||
const response = await getAccount(createMockAPIContext({ params: { id: '1' } }) as any);
|
||||
const account = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(account).toHaveProperty("id", "1");
|
||||
expect(account).toHaveProperty("name", "Test Checking");
|
||||
expect(account).toHaveProperty('id', '1');
|
||||
expect(account).toHaveProperty('name', 'Test Checking');
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent account", async () => {
|
||||
const response = await getAccount(
|
||||
createMockAPIContext({ params: { id: "999" } }) as any
|
||||
);
|
||||
it('should return 404 for non-existent account', async () => {
|
||||
const response = await getAccount(createMockAPIContext({ params: { id: '999' } }) as any);
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(error).toHaveProperty("error", "Account not found");
|
||||
expect(error).toHaveProperty('error', 'Account not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/accounts/:id/transactions", () => {
|
||||
it("should return transactions for an account", async () => {
|
||||
const response = await listTransactions(
|
||||
createMockAPIContext({ params: { id: "1" } }) as any
|
||||
);
|
||||
describe('GET /api/accounts/:id/transactions', () => {
|
||||
it('should return transactions for an account', async () => {
|
||||
const response = await listTransactions(createMockAPIContext({ params: { id: '1' } }) as any);
|
||||
const transactions = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(transactions).toHaveLength(1);
|
||||
expect(transactions[0]).toHaveProperty("accountId", "1");
|
||||
expect(transactions[0]).toHaveProperty('accountId', '1');
|
||||
});
|
||||
|
||||
it("should return empty array for account with no transactions", async () => {
|
||||
it('should return empty array for account with no transactions', async () => {
|
||||
const response = await listTransactions(
|
||||
createMockAPIContext({ params: { id: "999" } }) as any
|
||||
createMockAPIContext({ params: { id: '999' } }) as any
|
||||
);
|
||||
const transactions = await response.json();
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { beforeEach } from "vitest";
|
||||
import { accounts, transactions } from "../data/store";
|
||||
import type { APIContext } from "astro";
|
||||
import { beforeEach } from 'vitest';
|
||||
import { accounts, transactions } from '../data/store';
|
||||
import type { APIContext } from 'astro';
|
||||
|
||||
// Create a mock APIContext factory
|
||||
export function createMockAPIContext<
|
||||
T extends Record<string, string> = Record<string, string>
|
||||
>({ params = {} as T } = {}): Partial<APIContext> {
|
||||
export function createMockAPIContext<T extends Record<string, string> = Record<string, string>>({
|
||||
params = {} as T,
|
||||
} = {}): Partial<APIContext> {
|
||||
return {
|
||||
params,
|
||||
props: {},
|
||||
request: new Request("http://localhost:4321"),
|
||||
site: new URL("http://localhost:4321"),
|
||||
generator: "test",
|
||||
url: new URL("http://localhost:4321"),
|
||||
clientAddress: "127.0.0.1",
|
||||
request: new Request('http://localhost:4321'),
|
||||
site: new URL('http://localhost:4321'),
|
||||
generator: 'test',
|
||||
url: new URL('http://localhost:4321'),
|
||||
clientAddress: '127.0.0.1',
|
||||
cookies: new Headers() as any, // Cast Headers to cookies as we don't need cookie functionality in tests
|
||||
redirect: () => new Response(),
|
||||
locals: {},
|
||||
preferredLocale: undefined,
|
||||
preferredLocaleList: [],
|
||||
currentLocale: undefined,
|
||||
routePattern: "/api/[...path]",
|
||||
originPathname: "/api",
|
||||
routePattern: '/api/[...path]',
|
||||
originPathname: '/api',
|
||||
getActionResult: () => undefined,
|
||||
isPrerendered: false,
|
||||
};
|
||||
@@ -33,15 +33,15 @@ beforeEach(() => {
|
||||
accounts.length = 0;
|
||||
accounts.push(
|
||||
{
|
||||
id: "1",
|
||||
name: "Test Checking",
|
||||
last4: "1234",
|
||||
id: '1',
|
||||
name: 'Test Checking',
|
||||
last4: '1234',
|
||||
balance: 1000.0,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Test Savings",
|
||||
last4: "5678",
|
||||
id: '2',
|
||||
name: 'Test Savings',
|
||||
last4: '5678',
|
||||
balance: 5000.0,
|
||||
}
|
||||
);
|
||||
@@ -50,17 +50,17 @@ beforeEach(() => {
|
||||
transactions.length = 0;
|
||||
transactions.push(
|
||||
{
|
||||
id: "1",
|
||||
accountId: "1",
|
||||
date: "2025-04-24",
|
||||
description: "Test Transaction 1",
|
||||
id: '1',
|
||||
accountId: '1',
|
||||
date: '2025-04-24',
|
||||
description: 'Test Transaction 1',
|
||||
amount: -50.0,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
accountId: "2",
|
||||
date: "2025-04-24",
|
||||
description: "Test Transaction 2",
|
||||
id: '2',
|
||||
accountId: '2',
|
||||
date: '2025-04-24',
|
||||
description: 'Test Transaction 2',
|
||||
amount: 100.0,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -7,31 +7,31 @@
|
||||
// - Add load testing for API endpoints
|
||||
// - Implement test data factories
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { POST as createTransaction } from "../pages/api/transactions/index";
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { POST as createTransaction } from '../pages/api/transactions/index';
|
||||
import {
|
||||
PUT as updateTransaction,
|
||||
DELETE as deleteTransaction,
|
||||
} from "../pages/api/transactions/[id]/index";
|
||||
import { accounts, transactions } from "../data/store";
|
||||
import type { Transaction } from "../types";
|
||||
import { createMockAPIContext } from "./setup";
|
||||
} from '../pages/api/transactions/[id]/index';
|
||||
import { accounts, transactions } from '../data/store';
|
||||
import type { Transaction } from '../types';
|
||||
import { createMockAPIContext } from './setup';
|
||||
|
||||
describe("Transactions API", () => {
|
||||
describe("POST /api/transactions", () => {
|
||||
it("should create a new transaction", async () => {
|
||||
describe('Transactions API', () => {
|
||||
describe('POST /api/transactions', () => {
|
||||
it('should create a new transaction', async () => {
|
||||
const initialBalance = accounts[0].balance;
|
||||
const newTransaction = {
|
||||
accountId: "1",
|
||||
date: "2025-04-24",
|
||||
description: "Test New Transaction",
|
||||
accountId: '1',
|
||||
date: '2025-04-24',
|
||||
description: 'Test New Transaction',
|
||||
amount: -25.0,
|
||||
};
|
||||
|
||||
const ctx = createMockAPIContext() as any;
|
||||
ctx.request = new Request("http://localhost:4321/api/transactions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
ctx.request = new Request('http://localhost:4321/api/transactions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newTransaction),
|
||||
});
|
||||
|
||||
@@ -39,21 +39,21 @@ describe("Transactions API", () => {
|
||||
const result = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(result).toHaveProperty("id");
|
||||
expect(result).toHaveProperty('id');
|
||||
expect(result.description).toBe(newTransaction.description);
|
||||
expect(accounts[0].balance).toBe(initialBalance + newTransaction.amount);
|
||||
});
|
||||
|
||||
it("should reject transaction with missing fields", async () => {
|
||||
it('should reject transaction with missing fields', async () => {
|
||||
const invalidTransaction = {
|
||||
accountId: "1",
|
||||
accountId: '1',
|
||||
// Missing required fields
|
||||
};
|
||||
|
||||
const ctx = createMockAPIContext() as any;
|
||||
ctx.request = new Request("http://localhost:4321/api/transactions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
ctx.request = new Request('http://localhost:4321/api/transactions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(invalidTransaction),
|
||||
});
|
||||
|
||||
@@ -61,21 +61,21 @@ describe("Transactions API", () => {
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(error).toHaveProperty("error", "Missing required fields");
|
||||
expect(error).toHaveProperty('error', 'Missing required fields');
|
||||
});
|
||||
|
||||
it("should reject transaction with invalid account", async () => {
|
||||
it('should reject transaction with invalid account', async () => {
|
||||
const invalidTransaction = {
|
||||
accountId: "999",
|
||||
date: "2025-04-24",
|
||||
description: "Invalid Account Test",
|
||||
accountId: '999',
|
||||
date: '2025-04-24',
|
||||
description: 'Invalid Account Test',
|
||||
amount: 100,
|
||||
};
|
||||
|
||||
const ctx = createMockAPIContext() as any;
|
||||
ctx.request = new Request("http://localhost:4321/api/transactions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
ctx.request = new Request('http://localhost:4321/api/transactions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(invalidTransaction),
|
||||
});
|
||||
|
||||
@@ -83,38 +83,38 @@ describe("Transactions API", () => {
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(error).toHaveProperty("error", "Account not found");
|
||||
expect(error).toHaveProperty('error', 'Account not found');
|
||||
});
|
||||
|
||||
it("should reject invalid request body", async () => {
|
||||
it('should reject invalid request body', async () => {
|
||||
const ctx = createMockAPIContext() as any;
|
||||
ctx.request = new Request("http://localhost:4321/api/transactions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "invalid json",
|
||||
ctx.request = new Request('http://localhost:4321/api/transactions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: 'invalid json',
|
||||
});
|
||||
|
||||
const response = await createTransaction(ctx);
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(error).toHaveProperty("error", "Invalid request body");
|
||||
expect(error).toHaveProperty('error', 'Invalid request body');
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/transactions/:id", () => {
|
||||
it("should update an existing transaction", async () => {
|
||||
describe('PUT /api/transactions/:id', () => {
|
||||
it('should update an existing transaction', async () => {
|
||||
const initialBalance = accounts[0].balance;
|
||||
const originalAmount = transactions[0].amount;
|
||||
const updates = {
|
||||
description: "Updated Description",
|
||||
description: 'Updated Description',
|
||||
amount: -75.0,
|
||||
};
|
||||
|
||||
const ctx = createMockAPIContext({ params: { id: "1" } }) as any;
|
||||
ctx.request = new Request("http://localhost:4321/api/transactions/1", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const ctx = createMockAPIContext({ params: { id: '1' } }) as any;
|
||||
ctx.request = new Request('http://localhost:4321/api/transactions/1', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
|
||||
@@ -124,49 +124,47 @@ describe("Transactions API", () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(result.description).toBe(updates.description);
|
||||
expect(result.amount).toBe(updates.amount);
|
||||
expect(accounts[0].balance).toBe(
|
||||
initialBalance - originalAmount + updates.amount
|
||||
);
|
||||
expect(accounts[0].balance).toBe(initialBalance - originalAmount + updates.amount);
|
||||
});
|
||||
|
||||
it("should reject update with invalid request body", async () => {
|
||||
const ctx = createMockAPIContext({ params: { id: "1" } }) as any;
|
||||
ctx.request = new Request("http://localhost:4321/api/transactions/1", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "invalid json",
|
||||
it('should reject update with invalid request body', async () => {
|
||||
const ctx = createMockAPIContext({ params: { id: '1' } }) as any;
|
||||
ctx.request = new Request('http://localhost:4321/api/transactions/1', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: 'invalid json',
|
||||
});
|
||||
|
||||
const response = await updateTransaction(ctx);
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(error).toHaveProperty("error", "Invalid request body");
|
||||
expect(error).toHaveProperty('error', 'Invalid request body');
|
||||
});
|
||||
|
||||
it("should reject update for non-existent transaction", async () => {
|
||||
const ctx = createMockAPIContext({ params: { id: "999" } }) as any;
|
||||
ctx.request = new Request("http://localhost:4321/api/transactions/999", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ description: "Test" }),
|
||||
it('should reject update for non-existent transaction', async () => {
|
||||
const ctx = createMockAPIContext({ params: { id: '999' } }) as any;
|
||||
ctx.request = new Request('http://localhost:4321/api/transactions/999', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ description: 'Test' }),
|
||||
});
|
||||
|
||||
const response = await updateTransaction(ctx);
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(error).toHaveProperty("error", "Transaction not found");
|
||||
expect(error).toHaveProperty('error', 'Transaction not found');
|
||||
});
|
||||
|
||||
it("should reject update for non-existent account", async () => {
|
||||
it('should reject update for non-existent account', async () => {
|
||||
// First update the transaction to point to a non-existent account
|
||||
transactions[0].accountId = "999";
|
||||
transactions[0].accountId = '999';
|
||||
|
||||
const ctx = createMockAPIContext({ params: { id: "1" } }) as any;
|
||||
ctx.request = new Request("http://localhost:4321/api/transactions/1", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const ctx = createMockAPIContext({ params: { id: '1' } }) as any;
|
||||
ctx.request = new Request('http://localhost:4321/api/transactions/1', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ amount: -100 }),
|
||||
});
|
||||
|
||||
@@ -174,25 +172,25 @@ describe("Transactions API", () => {
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(error).toHaveProperty("error", "Account not found");
|
||||
expect(error).toHaveProperty('error', 'Account not found');
|
||||
|
||||
// Reset account ID for other tests
|
||||
transactions[0].accountId = "1";
|
||||
transactions[0].accountId = '1';
|
||||
});
|
||||
|
||||
it("should handle account balance updates correctly when switching accounts", async () => {
|
||||
it('should handle account balance updates correctly when switching accounts', async () => {
|
||||
// Create initial state
|
||||
const oldAccount = accounts[0];
|
||||
const newAccount = accounts[1];
|
||||
const initialOldBalance = oldAccount.balance;
|
||||
const initialNewBalance = newAccount.balance;
|
||||
const oldTransaction = transactions.find((t) => t.id === "1");
|
||||
if (!oldTransaction) throw new Error("Test transaction not found");
|
||||
const oldTransaction = transactions.find((t) => t.id === '1');
|
||||
if (!oldTransaction) throw new Error('Test transaction not found');
|
||||
|
||||
const ctx = createMockAPIContext({ params: { id: "1" } }) as any;
|
||||
ctx.request = new Request("http://localhost:4321/api/transactions/1", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const ctx = createMockAPIContext({ params: { id: '1' } }) as any;
|
||||
ctx.request = new Request('http://localhost:4321/api/transactions/1', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
accountId: newAccount.id,
|
||||
amount: -100,
|
||||
@@ -206,41 +204,36 @@ describe("Transactions API", () => {
|
||||
expect(result.accountId).toBe(newAccount.id);
|
||||
|
||||
// Old account should have the old amount removed
|
||||
expect(oldAccount.balance).toBe(
|
||||
initialOldBalance + Math.abs(oldTransaction.amount)
|
||||
);
|
||||
expect(oldAccount.balance).toBe(initialOldBalance + Math.abs(oldTransaction.amount));
|
||||
|
||||
// New account should have the new amount added
|
||||
expect(newAccount.balance).toBe(initialNewBalance - 100);
|
||||
});
|
||||
|
||||
it("should reject update without transaction ID", async () => {
|
||||
it('should reject update without transaction ID', async () => {
|
||||
const ctx = createMockAPIContext() as any;
|
||||
ctx.request = new Request(
|
||||
"http://localhost:4321/api/transactions/undefined",
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ description: "Test" }),
|
||||
}
|
||||
);
|
||||
ctx.request = new Request('http://localhost:4321/api/transactions/undefined', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ description: 'Test' }),
|
||||
});
|
||||
|
||||
const response = await updateTransaction(ctx);
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(error).toHaveProperty("error", "Transaction ID is required");
|
||||
expect(error).toHaveProperty('error', 'Transaction ID is required');
|
||||
});
|
||||
|
||||
it("should reject update when old account is missing", async () => {
|
||||
it('should reject update when old account is missing', async () => {
|
||||
// Store current accounts and clear the array
|
||||
const savedAccounts = [...accounts];
|
||||
accounts.length = 0;
|
||||
|
||||
const ctx = createMockAPIContext({ params: { id: "1" } }) as any;
|
||||
ctx.request = new Request("http://localhost:4321/api/transactions/1", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const ctx = createMockAPIContext({ params: { id: '1' } }) as any;
|
||||
ctx.request = new Request('http://localhost:4321/api/transactions/1', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ amount: -100 }),
|
||||
});
|
||||
|
||||
@@ -248,19 +241,19 @@ describe("Transactions API", () => {
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(error).toHaveProperty("error", "Account not found");
|
||||
expect(error).toHaveProperty('error', 'Account not found');
|
||||
|
||||
// Restore accounts
|
||||
accounts.push(...savedAccounts);
|
||||
});
|
||||
|
||||
it("should reject update when new account doesn't exist", async () => {
|
||||
const ctx = createMockAPIContext({ params: { id: "1" } }) as any;
|
||||
ctx.request = new Request("http://localhost:4321/api/transactions/1", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const ctx = createMockAPIContext({ params: { id: '1' } }) as any;
|
||||
ctx.request = new Request('http://localhost:4321/api/transactions/1', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
accountId: "999", // Non-existent account
|
||||
accountId: '999', // Non-existent account
|
||||
amount: -100,
|
||||
}),
|
||||
});
|
||||
@@ -269,18 +262,18 @@ describe("Transactions API", () => {
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(error).toHaveProperty("error", "Account not found");
|
||||
expect(error).toHaveProperty('error', 'Account not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/transactions/:id", () => {
|
||||
it("should delete a transaction", async () => {
|
||||
describe('DELETE /api/transactions/:id', () => {
|
||||
it('should delete a transaction', async () => {
|
||||
const initialBalance = accounts[0].balance;
|
||||
const transactionAmount = transactions[0].amount;
|
||||
const initialCount = transactions.length;
|
||||
|
||||
const response = await deleteTransaction(
|
||||
createMockAPIContext({ params: { id: "1" } }) as any
|
||||
createMockAPIContext({ params: { id: '1' } }) as any
|
||||
);
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
@@ -288,45 +281,45 @@ describe("Transactions API", () => {
|
||||
expect(accounts[0].balance).toBe(initialBalance - transactionAmount);
|
||||
});
|
||||
|
||||
it("should reject delete without transaction ID", async () => {
|
||||
it('should reject delete without transaction ID', async () => {
|
||||
const response = await deleteTransaction(createMockAPIContext() as any);
|
||||
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(error).toHaveProperty("error", "Transaction ID is required");
|
||||
expect(error).toHaveProperty('error', 'Transaction ID is required');
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent transaction", async () => {
|
||||
it('should return 404 for non-existent transaction', async () => {
|
||||
const response = await deleteTransaction(
|
||||
createMockAPIContext({ params: { id: "999" } }) as any
|
||||
createMockAPIContext({ params: { id: '999' } }) as any
|
||||
);
|
||||
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(error).toHaveProperty("error", "Transaction not found");
|
||||
expect(error).toHaveProperty('error', 'Transaction not found');
|
||||
});
|
||||
|
||||
it("should handle deletion with non-existent account", async () => {
|
||||
it('should handle deletion with non-existent account', async () => {
|
||||
// Create a transaction then remove its account
|
||||
const testTransaction: Transaction = {
|
||||
id: "test-delete",
|
||||
accountId: "test-account",
|
||||
date: "2025-04-24",
|
||||
description: "Test Delete",
|
||||
id: 'test-delete',
|
||||
accountId: 'test-account',
|
||||
date: '2025-04-24',
|
||||
description: 'Test Delete',
|
||||
amount: 100,
|
||||
};
|
||||
transactions.push(testTransaction);
|
||||
|
||||
const response = await deleteTransaction(
|
||||
createMockAPIContext({ params: { id: "test-delete" } }) as any
|
||||
createMockAPIContext({ params: { id: 'test-delete' } }) as any
|
||||
);
|
||||
|
||||
const error = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(error).toHaveProperty("error", "Account not found");
|
||||
expect(error).toHaveProperty('error', 'Account not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Transaction } from "../types";
|
||||
import type { Transaction } from '../types';
|
||||
|
||||
export interface TransactionEventDetail {
|
||||
transaction: Transaction;
|
||||
|
||||
16
src/utils.ts
16
src/utils.ts
@@ -1,17 +1,17 @@
|
||||
// Basic currency formatting (USD example)
|
||||
export function formatCurrency(amount: number): string {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
// Basic date formatting
|
||||
export function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString + "T00:00:00"); // Ensure correct parsing as local date
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
const date = new Date(dateString + 'T00:00:00'); // Ensure correct parsing as local date
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// Increase timeout for slower CI environments
|
||||
testTimeout: 10000,
|
||||
// Use the setup file we created
|
||||
setupFiles: ["./src/test/setup.ts"],
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
// Ensure we're using the right environment
|
||||
environment: "node",
|
||||
environment: 'node',
|
||||
// Only include test files
|
||||
include: ["src/test/**/*.{test,spec}.{ts,js}"],
|
||||
include: ['src/test/**/*.{test,spec}.{ts,js}'],
|
||||
// Configure coverage collection
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
exclude: ["node_modules/", "src/test/**/*", "**/*.d.ts"],
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['node_modules/', 'src/test/**/*', '**/*.d.ts'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user