Merge branch 'main' of github.com:acedanger/finance

This commit is contained in:
GitHub Copilot
2025-05-05 07:05:00 -04:00
21 changed files with 1728 additions and 353 deletions

View File

@@ -0,0 +1,13 @@
# GitHub MCP Server Configuration
GITHUB_PERSONAL_ACCESS_TOKEN=your_token_here
# PostgreSQL Configuration
POSTGRES_USER=financeuser
POSTGRES_PASSWORD=changeme
POSTGRES_DB=finance
POSTGRES_PORT=5432
# pgAdmin Configuration
PGADMIN_DEFAULT_EMAIL=peter@peterwood.dev
PGADMIN_DEFAULT_PASSWORD=admin
PGADMIN_PORT=5050

12
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye
LABEL org.opencontainers.image.source=https://github.com/acedanger/finance
LABEL org.opencontainers.image.description="Development container for Finance application with Node.js, TypeScript, and dev tools"
# Install additional OS packages
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends git-core \
&& apt-get clean -y && rm -rf /var/lib/apt/lists/*
# Install global npm packages if needed
RUN su node -c "npm install -g typescript"

View File

@@ -0,0 +1,115 @@
# Requires -Version 5.0
param(
[Parameter(Mandatory=$true)]
[string]$GitHubUsername
)
# Stop on first error
$ErrorActionPreference = "Stop"
# Configuration
$ImageName = "finance-devcontainer"
$ImageTag = "latest"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$DockerfilePath = Join-Path $ScriptDir "Dockerfile"
$EnvFile = Join-Path $ScriptDir ".env"
$FullImageName = "ghcr.io/$GitHubUsername/$ImageName`:$ImageTag"
# Function to check required tools
function Test-RequiredTools {
$tools = @("docker", "gh")
foreach ($tool in $tools) {
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) {
Write-Error "Error: $tool is required but not installed"
exit 1
}
}
}
# Function to load environment variables from .env file
function Get-EnvContent {
if (Test-Path $EnvFile) {
Write-Host "Loading environment from $EnvFile"
$envContent = Get-Content $EnvFile
foreach ($line in $envContent) {
if ($line -match '^GITHUB_PERSONAL_ACCESS_TOKEN=(.*)$') {
return $matches[1]
}
}
}
Write-Error "Error: GITHUB_PERSONAL_ACCESS_TOKEN not found in $EnvFile"
exit 1
}
# Verify prerequisites
Write-Host "=== Building Development Container ==="
Write-Host "Username: $GitHubUsername"
Write-Host "Image: $FullImageName"
Write-Host "Dockerfile: $DockerfilePath"
# Check required tools
Test-RequiredTools
# Check GitHub authentication
try {
gh auth status
} catch {
Write-Error "Error: Not authenticated with GitHub. Please run 'gh auth login' first"
exit 1
}
# Get GitHub PAT from .env file
$GitHubPAT = Get-EnvContent
if ([string]::IsNullOrEmpty($GitHubPAT)) {
Write-Error "Error: GITHUB_PERSONAL_ACCESS_TOKEN is empty"
exit 1
}
Write-Host "Using PAT: $($GitHubPAT.Substring(0, 4))... (first 4 chars)"
# Build the image
Write-Host "`n=> Building image..."
try {
docker build -t $FullImageName -f $DockerfilePath $ScriptDir
} catch {
Write-Error "Error: Docker build failed"
exit 1
}
# Log in to GitHub Container Registry
Write-Host "`n=> Logging into GitHub Container Registry..."
$GitHubPAT | docker login ghcr.io -u $GitHubUsername --password-stdin
if ($LASTEXITCODE -ne 0) {
Write-Error "Error: Failed to authenticate with GitHub Container Registry"
exit 1
}
# Push to GitHub Container Registry
Write-Host "`n=> Pushing image to GitHub Container Registry..."
docker push $FullImageName
if ($LASTEXITCODE -ne 0) {
Write-Error @"
Error: Failed to push image
Please check your GitHub PAT has the required permissions:
- read:packages
- write:packages
"@
exit 1
}
Write-Host "`n=== Success! ==="
Write-Host "The development container image has been built and pushed"
Write-Host "`nTo use this image, update your devcontainer.json with:"
Write-Host @"
{
"image": "$FullImageName"
}
"@
Write-Host "`nNext steps:"
Write-Host "1. Update .devcontainer/devcontainer.json with the image reference above"
Write-Host "2. Rebuild your development container in VS Code"
Write-Host " (Command Palette -> 'Dev Containers: Rebuild Container')"

View File

@@ -0,0 +1,92 @@
#!/bin/bash
# Exit on error, undefined variables, and pipe failures
set -euo pipefail
# Configuration
GITHUB_USERNAME=$1
IMAGE_NAME="finance-devcontainer"
IMAGE_TAG="latest"
# Load environment variables from .env file if it exists
ENV_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)/.env"
if [ -f "$ENV_FILE" ]; then
echo "Loading environment from $ENV_FILE"
# Use grep to find the PAT line and extract the value, handling both Unix and Windows line endings
GITHUB_PERSONAL_ACCESS_TOKEN=$(grep -a "^GITHUB_PERSONAL_ACCESS_TOKEN=" "$ENV_FILE" | sed 's/^GITHUB_PERSONAL_ACCESS_TOKEN=//' | tr -d '\r')
export GITHUB_PERSONAL_ACCESS_TOKEN
fi
# Check for required username argument
if [ -z "${GITHUB_USERNAME:-}" ]; then
echo "Error: GitHub username is required"
echo "Usage: $0 <github_username>"
echo "Example: $0 acedanger"
exit 1
fi
# Check for required tools
for cmd in docker gh; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Error: $cmd is required but not installed"
exit 1
fi
done
# Verify PAT is loaded
if [ -z "${GITHUB_PERSONAL_ACCESS_TOKEN:-}" ]; then
echo "Error: GITHUB_PERSONAL_ACCESS_TOKEN is not set"
echo "Please ensure it is defined in $ENV_FILE"
exit 1
fi
# Check GitHub authentication
if ! gh auth status >/dev/null 2>&1; then
echo "Error: Not authenticated with GitHub. Please run 'gh auth login' first"
exit 1
fi
# Get absolute path to Dockerfile
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
DOCKERFILE_PATH="$SCRIPT_DIR/Dockerfile"
FULL_IMAGE_NAME="ghcr.io/$GITHUB_USERNAME/$IMAGE_NAME:$IMAGE_TAG"
echo "=== Building Development Container ==="
echo "Username: $GITHUB_USERNAME"
echo "Image: $FULL_IMAGE_NAME"
echo "Dockerfile: $DOCKERFILE_PATH"
echo "Using PAT: ${GITHUB_PERSONAL_ACCESS_TOKEN:0:4}... (first 4 chars)"
# Build the image
echo -e "\n=> Building image..."
if ! docker build -t "$FULL_IMAGE_NAME" -f "$DOCKERFILE_PATH" "$SCRIPT_DIR"; then
echo "Error: Docker build failed"
exit 1
fi
# Log in to GitHub Container Registry
echo -e "\n=> Logging into GitHub Container Registry..."
echo "$GITHUB_PERSONAL_ACCESS_TOKEN" | docker login ghcr.io -u "$GITHUB_USERNAME" --password-stdin
# Push to GitHub Container Registry
echo -e "\n=> Pushing image to GitHub Container Registry..."
if ! docker push "$FULL_IMAGE_NAME"; then
echo "Error: Failed to push image"
echo "Please check your GitHub PAT has the required permissions:"
echo " - read:packages"
echo " - write:packages"
exit 1
fi
echo -e "\n=== Success! ==="
echo "The development container image has been built and pushed"
echo -e "\nTo use this image, update your devcontainer.json with:"
echo '{
"image": "'$FULL_IMAGE_NAME'"
}'
echo -e "\nNext steps:"
echo "1. Update .devcontainer/devcontainer.json with the image reference above"
echo "2. Rebuild your development container in VS Code"
echo " (Command Palette -> 'Dev Containers: Rebuild Container')"

View File

@@ -1,8 +1,16 @@
{ {
"name": "Finance App Development", "name": "Finance App Development",
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye", "image": "ghcr.io/acedanger/finance-devcontainer:latest",
"workspaceFolder": "/workspaces/finance",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/finance,type=bind,consistency=cached",
"features": { "features": {
"ghcr.io/devcontainers/features/git:1": {} "ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"runArgs": ["--dns", "8.8.8.8", "--dns", "8.8.4.4", "--network=host", "--dns-search=."],
"containerEnv": {
"HOSTALIASES": "/etc/host.aliases"
}, },
"customizations": { "customizations": {
"vscode": { "vscode": {
@@ -11,27 +19,39 @@
"GitHub.copilot", "GitHub.copilot",
"GitHub.copilot-chat", "GitHub.copilot-chat",
"ms-vscode.vscode-typescript-next", "ms-vscode.vscode-typescript-next",
"bradlc.vscode-tailwindcss" "bradlc.vscode-tailwindcss",
"biomejs.biome",
"PKief.material-icon-theme",
"Gruntfuggly.todo-tree",
"humao.rest-client"
], ],
"settings": { "settings": {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"[typescript]": { "[typescript]": {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": null, "editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true "source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
} }
}, },
"[javascript]": { "[javascript]": {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": null, "editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true "source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
} }
}, },
"[astro]": { "[astro]": {
"editor.defaultFormatter": "astro-build.astro-vscode" "editor.defaultFormatter": "astro-build.astro-vscode"
}, },
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[markdown]": {
"editor.defaultFormatter": "biomejs.biome"
},
"typescript.updateImportsOnFileMove.enabled": "always", "typescript.updateImportsOnFileMove.enabled": "always",
"editor.bracketPairColorization.enabled": true, "editor.bracketPairColorization.enabled": true,
"files.exclude": { "files.exclude": {
@@ -40,12 +60,31 @@
"**/node_modules": true, "**/node_modules": true,
"**/.idea": true "**/.idea": true
}, },
"terminal.integrated.defaultProfile.linux": "bash" "terminal.integrated.defaultProfile.linux": "bash",
"mcp.servers.github": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"--env-file",
"${containerWorkspaceFolder}/.devcontainer/.env",
"ghcr.io/github/github-mcp-server"
],
"env": {}
}
} }
} }
}, },
"forwardPorts": [3000], "forwardPorts": [3000],
"postCreateCommand": "npm install && npm run check", "postCreateCommand": "npm install && npm run check",
"remoteUser": "node", "remoteUser": "node",
"mounts": ["type=bind,source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,readonly"] "mounts": [
"type=bind,source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/node/.ssh,readonly"
],
"updateRemoteUserUID": true,
"remoteEnv": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${localEnv:GITHUB_PERSONAL_ACCESS_TOKEN}"
},
"postStartCommand": "gh auth status || gh auth login"
} }

85
.github/APP_GOAL.md vendored Normal file
View File

@@ -0,0 +1,85 @@
# Application Goal
From a high level perspective, I want to build a web application that allows users to manage their financial transactions and institutions. The application should provide a user-friendly interface for users to create, update, delete, and view their financial transactions and institutions. It should also allow users to group their transactions by date or institution for better organization and analysis.
The application should be built using modern web technologies and should be self-hosted. It should also include authentication and authorization features to ensure that users can securely access their data. The application should be designed with scalability in mind, allowing for future enhancements and additional features.
The application should be easy to maintain and extend, with a focus on code quality and best practices. It should also include automated testing and continuous integration/continuous deployment (CI/CD) processes to ensure that changes can be made safely and efficiently.
The application should be well-documented, with clear instructions for installation, configuration, and usage. The documentation should also include information on how to contribute to the project and report issues.
The application should be designed to be user-friendly and accessible, with a focus on providing a positive user experience. It should also include features for data visualization and reporting, allowing users to gain insights into their financial transactions and institutions.
The application should be designed to be responsive and work well on a variety of devices, including desktops, tablets, and smartphones. It should also include features for data import and export, allowing users to easily transfer their data to and from other applications.
The application should be designed to be secure, with a focus on protecting user data and preventing unauthorized access. It should also include features for data backup and recovery, ensuring that users can recover their data in case of loss or corruption.
The application should be designed to be modular and extensible, allowing for the addition of new features and functionality in the future. It should also include features for user feedback and support, allowing users to report issues and request new features.
The application should be designed to be performant, with a focus on minimizing load times and optimizing resource usage. It should also include features for monitoring and logging, allowing developers to track performance and identify issues.
---
## Features
- User authentication and authorization using OAuth2
- User profile management
- Financial institution management (create, update, delete, view)
- Financial transaction management (create, update, delete, view)
- Grouping transactions by financial institution
- Grouping transactions by date
- Responsive design for desktop and mobile devices
- Data visualization and reporting features
- Data import and export features
- Data backup and recovery features
- Modular and extensible architecture
- User feedback and support features
- Monitoring and logging features
- Automated testing and CI/CD processes
- Well-documented codebase and user documentation
- Clear instructions for installation, configuration, and usage
- Code quality and best practices
- User-friendly and accessible design
- Performance optimization and resource usage minimization
- Support for recurring transactions and budgeting
- Allow users to set up recurring transactions for regular expenses or income
- Budgeting features to help users track their spending and savings goals
---
## Technical Requirements
I want to use the following technologies:
- **Frontend**: React, TypeScript, Tailwind CSS, Zod, Cors
- **Backend**: Node.js, Fastify
- **Database**: PostgreSQL, Prisma
- **Deployment**: Docker, Self-hosted
- **Authentication**: OAuth2
- **Testing**: Jest, React Testing Library
- **CI/CD**: GitHub Actions
- **Documentation**: Docusaurus
---
## User Stories
- As a user, I want to be able to create, update, delete, and view - financial institutions.
- As a user, I want to be able to view a list of financial institutions in - the system.
- As a user, I want to be able to create, update, delete, and view - financial transactions.
- As a user, I want to be able to view a list of financial transactions in - the system.
- As a user, I want to be able to view a list of financial transactions - grouped by financial institution.
- As a user, I want to be able to view a list of financial transactions - grouped by date.
- As a user, I want to be able log in using oauth2.
- As a user, I want to be able to log out.
- As a user, I want to be able to view my profile.
- As a user, I want to be able to update my profile.
---
## Out of Scope
---
## Open Questions
- What specific data visualization and reporting features do you want to include?
- What specific data import and export features do you want to include?
- What specific data backup and recovery features do you want to include?
- What specific user feedback and support features do you want to include?
- What specific monitoring and logging features do you want to include?
- What specific performance optimization and resource usage minimization techniques do you want to include?
- What specific security features do you want to include?
- What specific user experience and accessibility features do you want to include?

View File

@@ -10,6 +10,28 @@ This project is a web user interface (UI) for a CRUD (Create, Read, Update, Dele
* **Language:** TypeScript, JavaScript (client-side scripts), HTML, CSS * **Language:** TypeScript, JavaScript (client-side scripts), HTML, CSS
* **Styling:** Plain CSS (`src/styles/global.css`) * **Styling:** Plain CSS (`src/styles/global.css`)
* **Data:** Using Astro's built-in API routes in `src/pages/api/` with a temporary in-memory store (`src/data/store.ts`). **The goal is to eventually replace the in-memory store with a persistent database.** * **Data:** Using Astro's built-in API routes in `src/pages/api/` with a temporary in-memory store (`src/data/store.ts`). **The goal is to eventually replace the in-memory store with a persistent database.**
* **Development Environment:** VS Code Dev Container using private Docker image (`ghcr.io/acedanger/finance-devcontainer:latest`)
## Development Environment
* **Dev Container:** The project uses a VS Code Dev Container for consistent development environments.
* Container configuration in `.devcontainer/devcontainer.json`
* Uses a private container image hosted on GitHub Container Registry
* Image: `ghcr.io/acedanger/finance-devcontainer:latest`
* Includes all necessary development tools and extensions
* Configured with GitHub CLI and authentication
* Features Docker-in-Docker support for additional container needs
* **Container Features:**
* Node.js and npm pre-installed
* Git and GitHub CLI configured
* TypeScript support
* VS Code extensions pre-configured
* Docker-in-Docker capability
* Automatic GitHub authentication
* **Authentication:**
* Requires GitHub Personal Access Token for private container access
* Token should be configured in `.devcontainer/.env`
* GitHub CLI authentication handled in post-start command
## Current State & Key Features ## Current State & Key Features
@@ -36,6 +58,10 @@ This project is a web user interface (UI) for a CRUD (Create, Read, Update, Dele
## File Structure Overview ## File Structure Overview
* `.devcontainer/`: Development container configuration
* `devcontainer.json`: VS Code Dev Container configuration
* `Dockerfile`: Base container definition (for reference)
* `.env.example`: Template for container environment variables
* `src/components/`: Reusable UI components. * `src/components/`: Reusable UI components.
* `src/data/`: Data store and persistence layer. * `src/data/`: Data store and persistence layer.
* `src/layouts/`: Base page layout(s). * `src/layouts/`: Base page layout(s).

3
.gitignore vendored
View File

@@ -27,3 +27,6 @@ pnpm-debug.log*
# Test coverage # Test coverage
coverage/ coverage/
# DevContainer environment files
.devcontainer/.env

155
ENVIRONMENT_SETUP.md Normal file
View File

@@ -0,0 +1,155 @@
# Development Environment Setup Guide
## Prerequisites
- Windows, macOS, or Linux
- Git
- Docker Desktop
- Visual Studio Code with Remote - Containers extension
- GitHub CLI
## Initial Setup
1. **Clone the Repository**
```bash
git clone https://github.com/acedanger/finance.git
cd finance
```
2. **Create Environment Files**
```bash
cp .devcontainer/.env.example .devcontainer/.env
cp .env.example .env
```
## GitHub Personal Access Token (PAT)
1. **Create a new PAT:**
- Go to GitHub Settings > Developer settings > Personal access tokens > Tokens (classic)
- Click "Generate new token (classic)"
- Name: `finance-dev-token`
- Set expiration: 90 days (recommended)
- Required scopes:
- `repo` (Full control of private repositories)
- `read:packages` (Download container images)
- `write:packages` (Upload container images)
- `delete:packages` (Optional: manage container versions)
- `workflow` (GitHub Actions integration)
2. **Configure the PAT:**
- Open `.devcontainer/.env`
- Set `GITHUB_PERSONAL_ACCESS_TOKEN=your_token_here`
## Building the Development Container
### Using VS Code (Recommended)
1. Open the project in VS Code
2. When prompted, click "Reopen in Container"
- Or press F1 and run "Dev Containers: Rebuild and Reopen in Container"
### Using Command Line
1. Install the devcontainer CLI:
```bash
npm install -g @devcontainers/cli
```
2. Build the container:
```bash
devcontainer build .
```
3. Start the container:
```bash
# With post-create command
devcontainer up --workspace-folder .
# Skip post-create command
devcontainer up --workspace-folder . --skip-post-create
```
## Building and Pushing the Container Image
1. Ensure you're in the `.devcontainer` directory
2. Choose your preferred script:
**Using PowerShell:**
```powershell
.\build-and-push.ps1 your_github_username
```
**Using Git Bash/Unix Shell:**
```bash
chmod +x build-and-push.sh
./build-and-push.sh your_github_username
```
Both scripts perform the same functions:
- Validate prerequisites (Docker, GitHub CLI)
- Load GitHub PAT from `.env` file
- Build the container image
- Push to GitHub Container Registry
- Provide next steps for using the image
## Environment Files
The project uses two separate environment files:
1. `.devcontainer/.env`:
- Container-specific configuration
- GitHub PAT
- PostgreSQL settings
- pgAdmin settings
2. `.env`:
- Application-specific configuration
- Development server port
- API base URL
- Node environment
## Troubleshooting
### Container Build Issues
- Ensure Docker Desktop is running
- Check that your PAT has the correct permissions
- Try rebuilding without cache: `devcontainer build . --no-cache`
### GitHub Authentication Issues
- Verify your PAT in `.devcontainer/.env`
- Try logging in manually: `gh auth login`
- Check GitHub CLI status: `gh auth status`
### Network Issues
- If DNS resolution fails, try using different DNS servers in `devcontainer.json`
- Check if GitHub Container Registry (ghcr.io) is accessible
- Verify Docker network settings
### VS Code Issues
- Install/update the Remote - Containers extension
- Clear VS Code's container cache
- Check VS Code's "Dev Containers" output panel for detailed logs
## Common Operations
### Rebuilding the Container
```bash
# Using VS Code
F1 -> "Dev Containers: Rebuild Container"
# Using CLI
devcontainer build . --no-cache
```
### Updating the Container Image
1. Make changes to `Dockerfile` or `devcontainer.json`
2. Run the build script:
```bash
./build-and-push.sh your_github_username
```
3. Update image reference in `devcontainer.json`
4. Rebuild container in VS Code
### Starting Development Server
```bash
npm run dev
```

View File

@@ -11,4 +11,60 @@ A web application for managing financial transactions across multiple bank accou
* **Key Features (Implemented & Planned):** Account switching, transaction listing, adding, editing, and deleting transactions. * **Key Features (Implemented & Planned):** Account switching, transaction listing, adding, editing, and deleting transactions.
## Logs ## Logs
This app is currently deployed using Cloudflare Pages. The logs can be viewed with the `npx wrangler pages deployment tail --project-name finance` command. T This app is currently deployed using Cloudflare Pages. The logs can be viewed with the `npx wrangler pages deployment tail --project-name finance` command.
## Development Environment Setup
For detailed setup instructions, including container building, environment configuration, and troubleshooting, see [ENVIRONMENT_SETUP.md](ENVIRONMENT_SETUP.md).
### GitHub MCP Server
The project uses GitHub's MCP server for development tasks. The server runs in a Docker container and is automatically configured when you open the project in a devcontainer.
#### Configuration
- The MCP server uses GitHub authentication via Personal Access Token
- Token is stored securely in `.devcontainer/.env` (not committed to repository)
- GitHub CLI is installed in the devcontainer for easier authentication management
- Container health monitoring is configured
#### Usage
The MCP server will automatically start when you open the project in a devcontainer. If you need to manually authenticate:
1. Run `gh auth login` in the terminal
2. Follow the prompts to authenticate with your GitHub account
### Database Setup
## Development Container Setup
### GitHub Personal Access Token
Before using the development container, you'll need a GitHub Personal Access Token (PAT) with the following permissions:
- `repo` (Full control of private repositories)
- `read:packages` (Download container images)
- `write:packages` (Upload container images)
- `delete:packages` (Optional: manage container versions)
- `workflow` (GitHub Actions integration)
### Using the Development Container
#### Command Line Interface
You can build and start the development container using the devcontainer CLI:
```bash
# Build the container
devcontainer build .
# Start the container (with post-create command)
devcontainer up --workspace-folder .
# Start the container (skip post-create)
devcontainer up --workspace-folder . --skip-post-create
```
#### VS Code
1. Open the project in VS Code
2. Press F1 and run "Dev Containers: Rebuild and Reopen in Container"
- To skip post-create steps: Press F1 and run "Dev Containers: Rebuild Container Without Cache"
### Troubleshooting
- If GitHub authentication fails, ensure your PAT is correctly set in `.devcontainer/.env`
- For network issues, try rebuilding the container with `--no-cache` option
- Check the VS Code "Dev Containers" output panel for detailed logs

9
bank_account.http Normal file
View File

@@ -0,0 +1,9 @@
POST /api/bank-account/update/2 HTTP/1.1
Content-Type: application/json
Host: europa:3050
Content-Length: 85
{"name": "BofA Joint Checking","bankName": "Bank of America","accountNumber": "4581"}
###

44
docker-compose.yml Normal file
View File

@@ -0,0 +1,44 @@
name: finance-api
services:
postgres:
container_name: postgres_container
image: postgres:15
environment:
POSTGRES_USER: ${POSTGRES_USER:-financeuser}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
POSTGRES_DB: ${POSTGRES_DB:-finance}
PGDATA: /data/postgres
volumes:
- postgres_data:/data/postgres
ports:
- "${POSTGRES_PORT}:5432"
networks:
- postgres
restart: unless-stopped
labels:
- diun.enable=true
pgadmin:
container_name: pgadmin_container
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-peter@peterwood.dev}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin}
PGADMIN_CONFIG_SERVER_MODE: 'False'
volumes:
- pgadmin_data:/var/lib/pgadmin
ports:
- "${PGADMIN_PORT:-5050}:80"
networks:
- postgres
restart: unless-stopped
labels:
- diun.enable=true
networks:
postgres:
driver: bridge
volumes:
postgres_data:
pgadmin_data:

413
overview.md Normal file
View File

@@ -0,0 +1,413 @@
Okay, let's set up a skeleton Fastify API project with Prisma to interact with a PostgreSQL database. This structure will provide the requested endpoints.
**Assumptions:**
1. You have Node.js and npm (or yarn) installed.
2. You have PostgreSQL installed and running locally.
3. You have a PostgreSQL database created (e.g., `finance_db`).
4. You have a PostgreSQL user with privileges on that database.
**Project Setup Steps:**
1. **Create Project Directory & Initialize:**
```bash
mkdir finance-api
cd finance-api
npm init -y
```
2. **Install Dependencies:**
```bash
# Runtime dependencies
npm install fastify @prisma/client dotenv
# Development dependencies
npm install --save-dev prisma typescript @types/node ts-node nodemon
# Or if you prefer plain Javascript, skip typescript/ts-node and adjust scripts
```
* `fastify`: The web framework.
* `@prisma/client`: The Prisma database client.
* `dotenv`: To load environment variables from a `.env` file.
* `prisma`: The Prisma CLI (for migrations, generation).
* `typescript`, `@types/node`, `ts-node`: For TypeScript support (recommended).
* `nodemon`: To automatically restart the server during development.
3. **Initialize Prisma:**
```bash
npx prisma init --datasource-provider postgresql
```
* This creates a `prisma` directory with a `schema.prisma` file and a `.env` file.
4. **Configure `.env`:**
Edit the newly created `.env` file and set your PostgreSQL connection URL:
```dotenv
# .env
# Replace with your actual database user, password, host, port, and database name
DATABASE_URL="postgresql://YOUR_USER:YOUR_PASSWORD@localhost:5432/finance_db?schema=public"
# API Server Configuration (Optional, but good practice)
API_HOST=0.0.0.0
API_PORT=3050
API_BASE_URL=https://finance.ptrwd.com # Used for documentation/reference, not binding
```
5. **Define Prisma Schema (`prisma/schema.prisma`):**
Define the model for your bank accounts.
```prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model BankAccount {
id Int @id @default(autoincrement())
name String // e.g., "Checking Account", "Savings XYZ"
bankName String // e.g., "Chase", "Wells Fargo"
accountNumber String @unique // Consider encryption in a real app
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("bank_accounts") // Optional: specify table name in snake_case
}
```
6. **Create Initial Database Migration:**
```bash
npx prisma migrate dev --name init_bank_account
```
* This command does two things:
* Creates an SQL migration file in `prisma/migrations`.
* Applies the migration to your database, creating the `bank_accounts` table.
7. **Generate Prisma Client:**
Ensure the client is generated based on your schema:
```bash
npx prisma generate
```
* This command reads your `schema.prisma` and generates the typed `@prisma/client`. You typically run this after any schema change.
8. **Create the Fastify Server (`src/server.ts` or `server.js`):**
```typescript
// src/server.ts (if using TypeScript)
// If using Javascript, remove type annotations and use require instead of import
import Fastify, { FastifyInstance, RouteShorthandOptions } from 'fastify';
import { Server, IncomingMessage, ServerResponse } from 'http';
import { PrismaClient, BankAccount } from '@prisma/client';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
const prisma = new PrismaClient();
const server: FastifyInstance = Fastify({
logger: true // Enable logging
});
// --- Type Definitions for Payloads/Params (Good Practice) ---
interface BankAccountCreateParams {
name: string;
bankName: string;
accountNumber: string;
}
interface BankAccountUpdateParams {
id: string;
}
interface BankAccountUpdateBody {
name?: string;
bankName?: string;
accountNumber?: string;
}
interface BankAccountDeleteParams {
id: string;
}
// --- API Routes ---
const API_PREFIX = '/api/bank-account';
// 1. Create Bank Account
server.post<{ Body: BankAccountCreateParams }>(
`${API_PREFIX}/create`,
async (request, reply): Promise<BankAccount> => {
try {
const { name, bankName, accountNumber } = request.body;
const newAccount = await prisma.bankAccount.create({
data: {
name,
bankName,
accountNumber,
},
});
reply.code(201); // Resource Created
return newAccount;
} catch (error: any) {
server.log.error(error);
// Basic duplicate check example (Prisma throws P2002)
if (error.code === 'P2002' && error.meta?.target?.includes('accountNumber')) {
reply.code(409); // Conflict
throw new Error(`Bank account with number ${request.body.accountNumber} already exists.`);
}
reply.code(500);
throw new Error('Failed to create bank account.');
}
}
);
// 2. Update Bank Account
server.post<{ Params: BankAccountUpdateParams; Body: BankAccountUpdateBody }>(
`${API_PREFIX}/update/:id`,
async (request, reply): Promise<BankAccount> => {
try {
const { id } = request.params;
const updateData = request.body;
// Ensure ID is a valid number before querying
const accountId = parseInt(id, 10);
if (isNaN(accountId)) {
reply.code(400);
throw new Error('Invalid account ID format.');
}
const updatedAccount = await prisma.bankAccount.update({
where: { id: accountId },
data: updateData,
});
return updatedAccount;
} catch (error: any) {
server.log.error(error);
// Handle case where account doesn't exist (Prisma throws P2025)
if (error.code === 'P2025') {
reply.code(404); // Not Found
throw new Error(`Bank account with ID ${request.params.id} not found.`);
}
// Handle potential duplicate account number on update
if (error.code === 'P2002' && error.meta?.target?.includes('accountNumber')) {
reply.code(409); // Conflict
throw new Error(`Bank account with number ${request.body.accountNumber} already exists.`);
}
reply.code(500);
throw new Error('Failed to update bank account.');
}
}
);
// 3. Delete Bank Account
server.delete<{ Params: BankAccountDeleteParams }>(
`${API_PREFIX}/delete/:id`,
async (request, reply): Promise<{ message: string; deletedAccount: BankAccount }> => {
try {
const { id } = request.params;
// Ensure ID is a valid number
const accountId = parseInt(id, 10);
if (isNaN(accountId)) {
reply.code(400);
throw new Error('Invalid account ID format.');
}
const deletedAccount = await prisma.bankAccount.delete({
where: { id: accountId },
});
return { message: `Bank account with ID ${id} deleted successfully.`, deletedAccount };
} catch (error: any) {
server.log.error(error);
// Handle case where account doesn't exist (Prisma throws P2025)
if (error.code === 'P2025') {
reply.code(404); // Not Found
throw new Error(`Bank account with ID ${request.params.id} not found.`);
}
reply.code(500);
throw new Error('Failed to delete bank account.');
}
}
);
// 4. Get All Bank Accounts
server.get(
`${API_PREFIX}/`, // Using trailing slash for consistency, Fastify often handles both
async (request, reply): Promise<BankAccount[]> => {
try {
const accounts = await prisma.bankAccount.findMany({
orderBy: { // Optional: Order results
createdAt: 'desc'
}
});
return accounts;
} catch (error: any) {
server.log.error(error);
reply.code(500);
throw new Error('Failed to retrieve bank accounts.');
}
}
);
// Optional: Get Single Bank Account by ID (Often useful)
server.get<{ Params: { id: string } }>(
`${API_PREFIX}/:id`,
async (request, reply): Promise<BankAccount> => {
try {
const { id } = request.params;
const accountId = parseInt(id, 10);
if (isNaN(accountId)) {
reply.code(400);
throw new Error('Invalid account ID format.');
}
const account = await prisma.bankAccount.findUnique({
where: { id: accountId },
});
if (!account) {
reply.code(404);
throw new Error(`Bank account with ID ${id} not found.`);
}
return account;
} catch (error: any) {
// Catch errors that might have been thrown before prisma query
if (!reply.sent) { // Check if a reply hasn't already been sent (e.g., by 400 error)
server.log.error(error);
reply.code(500);
throw new Error('Failed to retrieve bank account.');
}
// If reply already sent, just rethrow the original error
throw error;
}
}
);
// --- Graceful Shutdown ---
const gracefulShutdown = async (signal: string) => {
console.log(`*^! Received signal ${signal}. Shutting down...`);
try {
await server.close();
console.log('Fastify server closed.');
await prisma.$disconnect();
console.log('Prisma client disconnected.');
process.exit(0);
} catch (err) {
console.error('Error during shutdown:', err);
process.exit(1);
}
};
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
// --- Start Server ---
const start = async () => {
try {
const host = process.env.API_HOST || '0.0.0.0';
const port = parseInt(process.env.API_PORT || '3050', 10);
await server.listen({ port, host });
// Log after listen resolves (Fastify v3+)
// server.log.info(`Server listening at http://${host}:${port}`);
// Note: Fastify logger automatically logs listening address
} catch (err) {
server.log.error(err);
await prisma.$disconnect(); // Ensure prisma disconnects on startup error too
process.exit(1);
}
};
start();
// Export prisma instance if needed elsewhere (e.g., tests, scripts)
// export { prisma };
```
9. **Configure `tsconfig.json` (if using TypeScript):**
Create a `tsconfig.json` file in the root directory:
```json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2017", // Or newer
"module": "CommonJS",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"sourceMap": true, // Helpful for debugging
"resolveJsonModule": true // Allows importing JSON files
},
"include": ["src/**/*"], // Only compile files in src
"exclude": ["node_modules", "**/*.spec.ts"] // Exclude tests if you have them
}
```
10. **Add Scripts to `package.json`:**
```json
// package.json (add or modify the scripts section)
"scripts": {
"build": "tsc", // Compile TypeScript to JavaScript (if using TS)
"start": "node dist/server.js", // Run compiled JS (if using TS)
"dev": "nodemon --watch src --ext ts --exec ts-node src/server.ts", // Dev mode with TS
// --- OR if using plain JavaScript ---
// "start": "node server.js",
// "dev": "nodemon server.js",
// --- Prisma commands for convenience ---
"prisma:migrate": "prisma migrate dev",
"prisma:generate": "prisma generate",
"prisma:studio": "prisma studio" // GUI to view/edit data
},
```
11. **Run the Development Server:**
```bash
npm run dev
```
**How to Test (using `curl`):**
*(Replace `YOUR_ACCOUNT_ID` with an actual ID after creating an account)*
* **Create:**
```bash
curl -X POST -H "Content-Type: application/json" \
-d '{"name": "My Checking", "bankName": "Local Bank", "accountNumber": "123456789"}' \
http://localhost:3050/api/bank-account/create
```
* **Get All:**
```bash
curl http://localhost:3050/api/bank-account/
```
* **Update (using the ID returned from create):**
```bash
curl -X POST -H "Content-Type: application/json" \
-d '{"name": "My Primary Checking"}' \
http://localhost:3050/api/bank-account/update/YOUR_ACCOUNT_ID
```
* **Delete (using the ID):**
```bash
curl -X DELETE http://localhost:3050/api/bank-account/delete/YOUR_ACCOUNT_ID
```
* **Get Specific (using the ID):**
```bash
curl http://localhost:3050/api/bank-account/YOUR_ACCOUNT_ID
```
This skeleton provides the core structure. You can build upon this by adding more robust error handling, input validation (using Fastify's built-in schema validation), authentication/authorization, more complex queries, and organizing routes into separate files/plugins as the application grows.

679
package-lock.json generated

File diff suppressed because it is too large Load Diff

21
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,21 @@
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model BankAccount {
id Int @id @default(autoincrement())
name String // e.g., "Checking Account", "Savings XYZ"
bankName String // e.g., "Chase", "Wells Fargo"
accountNumber String @unique // Consider encryption in a real app
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("bank_accounts") // Optional: specify table name in snake_case
}

21
reset-environment.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
set -euo pipefail
echo "Resetting finance development environment..."
cd /home/acedanger/dev/finance || exit 1
npx prisma migrate reset --force || exit 1
docker compose down
rm -f package-lock.json
[ -d dist ] && rm -rf dist || true
[ -d node_modules ] && rm -rf node_modules || true
npm install || exit 1
npm run build || exit 1
docker compose -f docker-compose.yml up -d
echo "$(date '+%Y-%m-%d %H:%M:%S') - Environment reset complete."

View File

@@ -5,6 +5,8 @@ import { currentAccountId as currentAccountIdStore, refreshKey } from '../stores
import type { Account } from '../types'; import type { Account } from '../types';
import { formatCurrency } from '../utils'; import { formatCurrency } from '../utils';
type AccountSummaryProps = {};
export default function AccountSummary() { export default function AccountSummary() {
const currentAccountId = useStore(currentAccountIdStore); const currentAccountId = useStore(currentAccountIdStore);
const refreshCounter = useStore(refreshKey); const refreshCounter = useStore(refreshKey);

View File

@@ -1,4 +1,5 @@
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { useStore } from '@nanostores/react';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
currentAccountId as currentAccountIdStore, currentAccountId as currentAccountIdStore,
@@ -9,7 +10,9 @@ import {
import type { Transaction } from '../types'; import type { Transaction } from '../types';
import { formatCurrency, formatDate } from '../utils'; import { formatCurrency, formatDate } from '../utils';
export default function TransactionTable() { type TransactionTableProps = {};
export default function TransactionTable({}: TransactionTableProps) {
const currentAccountId = useStore(currentAccountIdStore); const currentAccountId = useStore(currentAccountIdStore);
const refreshCounter = useStore(refreshKey); const refreshCounter = useStore(refreshKey);

249
src/server.ts Normal file
View File

@@ -0,0 +1,249 @@
// src/server.ts
import { IncomingMessage, Server, ServerResponse } from 'http';
import { type BankAccount, PrismaClient } from '@prisma/client';
import dotenv from 'dotenv';
import Fastify, { type FastifyInstance } from 'fastify';
import type { ZodTypeProvider } from 'fastify-type-provider-zod';
import { z } from 'zod';
dotenv.config();
const prisma = new PrismaClient();
// Base schema for common fields, useful for reuse
const bankAccountBaseSchema = z.object({
name: z.string().min(1, { message: 'Name cannot be empty' }),
bankName: z.string().min(1, { message: 'Bank name cannot be empty' }),
accountNumber: z.string().min(1, { message: 'Account number cannot be empty' }),
});
// Schema for creating a bank account (all fields required)
const createBankAccountSchema = bankAccountBaseSchema;
// Schema for request parameters containing an ID
const paramsSchema = z.object({
// Use coerce to automatically convert string param to number
id: z.coerce.number().int().positive({ message: 'ID must be a positive integer' }),
});
// Schema for updating a bank account (all fields optional)
const updateBankAccountSchema = bankAccountBaseSchema.partial(); // Makes all fields optional
// --- Fastify Server Instance with Zod Type Provider ---
const server: FastifyInstance = Fastify({
logger: true,
}).withTypeProvider<ZodTypeProvider>(); // Enable Zod validation and typing
// --- API Routes ---
const API_PREFIX = '/api/bank-account';
// 1. Create Bank Account
server.post(
`${API_PREFIX}/create`,
{
schema: {
// Define Zod schema for the request body
body: createBankAccountSchema,
},
},
async (request, reply): Promise<BankAccount> => {
try {
// request.body is now typed and validated by Zod!
const newAccount = await prisma.bankAccount.create({
data: request.body, // Pass validated body directly
});
reply.code(201);
return newAccount;
} catch (error: any) {
server.log.error(error);
if (error.code === 'P2002' && error.meta?.target?.includes('accountNumber')) {
reply.code(409);
const body = createBankAccountSchema.parse(request.body);
throw new Error(`Bank account with number ${body.accountNumber} already exists.`);
}
reply.code(500);
throw new Error('Failed to create bank account.');
}
},
);
// 2. Update Bank Account
server.post(
`${API_PREFIX}/update/:id`,
{
schema: {
// Define Zod schemas for params and body
params: paramsSchema,
body: updateBankAccountSchema,
},
},
async (request, reply): Promise<BankAccount> => {
try {
// request.params.id is now a validated number
// request.body is now a validated partial object
const { id } = request.params;
const updateData = request.body;
// Prevent updating with an empty object
if (Object.keys(updateData).length === 0) {
reply.code(400);
throw new Error('Request body cannot be empty for update.');
}
const updatedAccount = await prisma.bankAccount.update({
where: { id: id }, // Use the validated numeric ID
data: updateData,
});
return updatedAccount;
} catch (error: any) {
server.log.error(error);
if (error.code === 'P2025') {
// Record to update not found
reply.code(404);
throw new Error(`Bank account with ID ${request.params.id} not found.`);
}
if (error.code === 'P2002' && error.meta?.target?.includes('accountNumber')) {
reply.code(409);
// Access accountNumber safely as it's optional in update
const attemptedNumber = request.body.accountNumber || '(unchanged)';
throw new Error(`Bank account with number ${attemptedNumber} already exists.`);
}
// Handle Zod validation errors specifically if needed (though Fastify usually does)
if (error instanceof z.ZodError) {
reply.code(400);
throw new Error(`Validation Error: ${error.errors.map((e) => e.message).join(', ')}`);
}
reply.code(500);
throw new Error('Failed to update bank account.');
}
},
);
// 3. Delete Bank Account
server.delete(
`${API_PREFIX}/delete/:id`,
{
schema: {
// Define Zod schema for params
params: paramsSchema,
},
},
async (request, reply): Promise<{ message: string; deletedAccount: BankAccount }> => {
try {
// request.params.id is now a validated number
const { id } = request.params;
const deletedAccount = await prisma.bankAccount.delete({
where: { id: id }, // Use the validated numeric ID
});
return { message: `Bank account with ID ${id} deleted successfully.`, deletedAccount };
} catch (error: any) {
server.log.error(error);
if (error.code === 'P2025') {
// Record to delete not found
reply.code(404);
throw new Error(`Bank account with ID ${request.params.id} not found.`);
}
// Handle Zod validation errors
if (error instanceof z.ZodError) {
reply.code(400);
throw new Error(`Validation Error: ${error.errors.map((e) => e.message).join(', ')}`);
}
reply.code(500);
throw new Error('Failed to delete bank account.');
}
},
);
// 4. Get All Bank Accounts
server.get(`${API_PREFIX}/`, async (request, reply): Promise<BankAccount[]> => {
// No input validation needed for getting all items usually
try {
const accounts = await prisma.bankAccount.findMany({
orderBy: { createdAt: 'desc' },
});
return accounts;
} catch (error: any) {
server.log.error(error);
reply.code(500);
throw new Error('Failed to retrieve bank accounts.');
}
});
// Optional: Get Single Bank Account by ID
server.get(
`${API_PREFIX}/:id`,
{
schema: {
// Define Zod schema for params
params: paramsSchema,
},
},
async (request, reply): Promise<BankAccount> => {
try {
// request.params.id is now a validated number
const { id } = request.params;
const account = await prisma.bankAccount.findUnique({
where: { id: id }, // Use the validated numeric ID
});
if (!account) {
reply.code(404);
throw new Error(`Bank account with ID ${id} not found.`);
}
return account;
} catch (error: any) {
// Handle Zod validation errors (though should be caught by Fastify earlier)
if (error instanceof z.ZodError) {
reply.code(400);
throw new Error(`Validation Error: ${error.errors.map((e) => e.message).join(', ')}`);
}
// If Prisma throws or other errors occur after validation
if (!reply.sent) {
// Specific check for Prisma's RecordNotFound (though findUnique returns null, not throws P2025 by default)
// The !account check above handles the "not found" case for findUnique
server.log.error(error); // Log other unexpected errors
reply.code(500);
throw new Error('Failed to retrieve bank account.');
}
// If reply already sent (e.g., 404), just rethrow the original error
throw error;
}
},
);
// --- Graceful Shutdown ---
const gracefulShutdown = async (signal: string) => {
console.log(`*^! Received signal ${signal}. Shutting down...`);
try {
await server.close();
console.log('Fastify server closed.');
await prisma.$disconnect();
console.log('Prisma client disconnected.');
process.exit(0);
} catch (err) {
console.error('Error during shutdown:', err);
process.exit(1);
}
};
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
// --- Start Server (unchanged) ---
const start = async () => {
try {
const host = process.env.API_HOST || '0.0.0.0';
const port = Number.parseInt(process.env.API_PORT || '3000', 10);
await server.listen({ port, host });
} catch (err) {
server.log.error(err);
await prisma.$disconnect();
process.exit(1);
}
};
start();

View File

@@ -1,8 +1,7 @@
import type { APIContext } from 'astro';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { listAccounts } from '../pages/api/accounts'; import { GET as getAccount } from '../pages/api/accounts/[id]/index';
import { getAccount } from '../pages/api/accounts/[id]'; import { GET as listTransactions } from '../pages/api/accounts/[id]/transactions/index';
import { listTransactions } from '../pages/api/accounts/[id]/transactions'; import { GET as listAccounts } from '../pages/api/accounts/index';
import { createMockAPIContext } from './setup'; import { createMockAPIContext } from './setup';
describe('Accounts API', () => { describe('Accounts API', () => {

View File

@@ -7,12 +7,13 @@
// - Add load testing for API endpoints // - Add load testing for API endpoints
// - Implement test data factories // - Implement test data factories
import type { APIContext } from 'astro';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { accounts, transactions } from '../data/store'; import { accounts, transactions } from '../data/store';
import { createTransaction } from '../pages/api/transactions'; import {
import { updateTransaction } from '../pages/api/transactions/[id]'; DELETE as deleteTransaction,
import { DELETE as deleteTransaction } from '../pages/api/transactions/[id]/index'; PUT as updateTransaction,
} from '../pages/api/transactions/[id]/index';
import { POST as createTransaction } from '../pages/api/transactions/index';
import type { Transaction } from '../types'; import type { Transaction } from '../types';
import { createMockAPIContext } from './setup'; import { createMockAPIContext } from './setup';
@@ -272,7 +273,7 @@ describe('Transactions API', () => {
const initialCount = transactions.length; const initialCount = transactions.length;
const response = await deleteTransaction( const response = await deleteTransaction(
createMockAPIContext({ params: { id: '1' } }) as APIContext, createMockAPIContext({ params: { id: '1' } }) as any,
); );
expect(response.status).toBe(204); expect(response.status).toBe(204);
@@ -291,7 +292,7 @@ describe('Transactions API', () => {
it('should return 404 for non-existent transaction', async () => { it('should return 404 for non-existent transaction', async () => {
const response = await deleteTransaction( const response = await deleteTransaction(
createMockAPIContext({ params: { id: '999' } }) as APIContext, createMockAPIContext({ params: { id: '999' } }) as any,
); );
const error = await response.json(); const error = await response.json();
@@ -312,7 +313,7 @@ describe('Transactions API', () => {
transactions.push(testTransaction); transactions.push(testTransaction);
const response = await deleteTransaction( const response = await deleteTransaction(
createMockAPIContext({ params: { id: 'test-delete' } }) as APIContext, createMockAPIContext({ params: { id: 'test-delete' } }) as any,
); );
const error = await response.json(); const error = await response.json();