Implement fintech payment service backend with FastAPI and SQLite

- Set up project structure with FastAPI
- Implement user and account management
- Add send and receive money functionality
- Set up transaction processing system
- Add JWT authentication
- Configure SQLAlchemy with SQLite
- Set up Alembic for database migrations
- Create comprehensive API documentation
This commit is contained in:
Automated Action 2025-06-17 11:53:41 +00:00
parent 87562d096c
commit 2c6298ca4b
35 changed files with 1991 additions and 2 deletions

187
README.md
View File

@ -1,3 +1,186 @@
# FastAPI Application
# Fintech Payment Service
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A FastAPI backend for fintech payment services, providing APIs for users, accounts, and money transfer operations.
## Features
- **User Management**: Create and manage user accounts
- **Account Management**: Create and manage financial accounts
- **Transaction Processing**:
- Send money to external accounts
- Receive money from external sources
- Make internal transfers between accounts
- View transaction history
- **Security**: JWT authentication for secure API access
## Tech Stack
- **Framework**: FastAPI
- **Database**: SQLite
- **ORM**: SQLAlchemy
- **Migrations**: Alembic
- **Authentication**: JWT (JSON Web Tokens)
- **Password Hashing**: Bcrypt
## API Endpoints
### Authentication
- `POST /api/v1/login/access-token` - Get access token
### Users
- `POST /api/v1/users/` - Create a new user
- `GET /api/v1/users/me` - Get current user info
- `PUT /api/v1/users/me` - Update current user info
- `GET /api/v1/users/{user_id}` - Get user by ID
### Accounts
- `GET /api/v1/accounts/` - List user accounts
- `POST /api/v1/accounts/` - Create a new account
- `GET /api/v1/accounts/{account_id}` - Get account details
- `PUT /api/v1/accounts/{account_id}` - Update account details
- `DELETE /api/v1/accounts/{account_id}` - Delete an account
### Transactions
- `GET /api/v1/transactions/` - List user transactions
- `GET /api/v1/transactions/account/{account_id}` - List account transactions
- `POST /api/v1/transactions/deposit` - Create a deposit
- `POST /api/v1/transactions/withdrawal` - Create a withdrawal
- `POST /api/v1/transactions/transfer` - Transfer between accounts
- `POST /api/v1/transactions/receive` - Receive money from external source
- `POST /api/v1/transactions/send` - Send money to external destination
- `GET /api/v1/transactions/{transaction_id}` - Get transaction details
### Health Check
- `GET /api/v1/health` - Check API health
## Setup Instructions
### Prerequisites
- Python 3.8+
- pip
### Installation
1. Clone the repository:
```bash
git clone <repository-url>
cd fintechpaymentservice
```
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Set up environment variables:
```bash
# Create a .env file with the following variables
SECRET_KEY=your_secret_key_here
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
```
4. Run database migrations:
```bash
alembic upgrade head
```
5. Start the server:
```bash
uvicorn main:app --reload
```
6. Access the API documentation:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
## Usage Examples
### Create a User
```bash
curl -X POST "http://localhost:8000/api/v1/users/" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
"password": "securepassword"
}'
```
### Get Access Token
```bash
curl -X POST "http://localhost:8000/api/v1/login/access-token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=user@example.com&password=securepassword"
```
### Create an Account
```bash
curl -X POST "http://localhost:8000/api/v1/accounts/" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"account_type": "savings",
"currency": "USD"
}'
```
### Receive Money
```bash
curl -X POST "http://localhost:8000/api/v1/transactions/receive" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"receiver_account_id": 1,
"external_sender_info": "Bank XYZ - Account 12345",
"amount": 500.00,
"currency": "USD",
"description": "Salary payment"
}'
```
### Send Money
```bash
curl -X POST "http://localhost:8000/api/v1/transactions/send" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sender_account_id": 1,
"external_receiver_info": "Bank ABC - Account 67890",
"amount": 100.00,
"currency": "USD",
"description": "Utility bill payment"
}'
```
## Development
### Running Tests
```bash
pytest
```
### Code Linting
```bash
ruff check .
```
### Code Formatting
```bash
ruff format .
```

85
alembic.ini Normal file
View File

@ -0,0 +1,85 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; this defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat migrations/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# SQLite URL using absolute path
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks=black
# black.type=console_scripts
# black.entrypoint=black
# black.options=-l 79
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

0
app/__init__.py Normal file
View File

0
app/api/__init__.py Normal file
View File

46
app/api/deps.py Normal file
View File

@ -0,0 +1,46 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app import models, schemas
from app.core.config import settings
from app.db.session import get_db
from app.crud.user import get_user_by_id
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/login/access-token")
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
) -> models.User:
"""
Get the current user from the token
"""
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
token_data = schemas.TokenPayload(**payload)
except (JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
user = get_user_by_id(db, id=token_data.sub)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
def get_current_active_user(
current_user: models.User = Depends(get_current_user),
) -> models.User:
"""
Get the current active user
"""
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user

11
app/api/v1/__init__.py Normal file
View File

@ -0,0 +1,11 @@
from fastapi import APIRouter
from app.api.v1 import health, users, login, accounts, transactions
api_router = APIRouter()
api_router.include_router(health.router, prefix="/health", tags=["health"])
api_router.include_router(login.router, prefix="/login", tags=["login"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(accounts.router, prefix="/accounts", tags=["accounts"])
api_router.include_router(transactions.router, prefix="/transactions", tags=["transactions"])

92
app/api/v1/accounts.py Normal file
View File

@ -0,0 +1,92 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app import crud, models, schemas
from app.api import deps
router = APIRouter()
@router.get("/", response_model=List[schemas.Account])
def read_accounts(
skip: int = 0,
limit: int = 100,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Retrieve accounts for the current user.
"""
accounts = crud.get_user_accounts(db, owner_id=current_user.id, skip=skip, limit=limit)
return accounts
@router.post("/", response_model=schemas.Account)
def create_account(
account_in: schemas.AccountCreate,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Create new account for the current user.
"""
account = crud.create_account(db, account_in=account_in, owner_id=current_user.id)
return account
@router.get("/{account_id}", response_model=schemas.Account)
def read_account(
account_id: int,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Get account by ID.
"""
account = crud.get_account_by_id(db, id=account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
if account.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return account
@router.put("/{account_id}", response_model=schemas.Account)
def update_account(
account_id: int,
account_in: schemas.AccountUpdate,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Update an account.
"""
account = crud.get_account_by_id(db, id=account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
if account.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
account = crud.update_account(db, account=account, account_in=account_in)
return account
@router.delete("/{account_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
def delete_account(
account_id: int,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> None:
"""
Delete an account.
"""
account = crud.get_account_by_id(db, id=account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
if account.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
if account.balance > 0:
raise HTTPException(status_code=400, detail="Cannot delete account with positive balance")
crud.delete_account(db, account=account)
return None

22
app/api/v1/health.py Normal file
View File

@ -0,0 +1,22 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db.session import get_db
router = APIRouter()
@router.get("")
def health_check(db: Session = Depends(get_db)):
"""
Check the health of the application.
This endpoint can be used to verify that the application is running
and able to connect to the database.
"""
try:
# Try to execute a simple query
db.execute("SELECT 1")
return {"status": "healthy", "database": "connected"}
except Exception as e:
return {"status": "unhealthy", "database": str(e)}

40
app/api/v1/login.py Normal file
View File

@ -0,0 +1,40 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app import crud, schemas
from app.api import deps
from app.core import security
from app.core.config import settings
router = APIRouter()
@router.post("/access-token", response_model=schemas.Token)
def login_access_token(
db: Session = Depends(deps.get_db),
form_data: OAuth2PasswordRequestForm = Depends(),
) -> Any:
"""
OAuth2 compatible token login, get an access token for future requests.
"""
user = crud.authenticate(
db, email=form_data.username, password=form_data.password
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
if not user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": security.create_access_token(
user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}

196
app/api/v1/transactions.py Normal file
View File

@ -0,0 +1,196 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app import crud, models, schemas
from app.api import deps
router = APIRouter()
@router.get("/", response_model=List[schemas.Transaction])
def read_transactions(
skip: int = 0,
limit: int = 100,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Retrieve transactions for the current user.
"""
transactions = crud.get_user_transactions(db, user_id=current_user.id, skip=skip, limit=limit)
return transactions
@router.get("/account/{account_id}", response_model=List[schemas.Transaction])
def read_account_transactions(
account_id: int,
skip: int = 0,
limit: int = 100,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Retrieve transactions for a specific account.
"""
account = crud.get_account_by_id(db, id=account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
if account.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
transactions = crud.get_account_transactions(db, account_id=account_id, skip=skip, limit=limit)
return transactions
@router.post("/deposit", response_model=schemas.Transaction)
def create_deposit(
deposit_in: schemas.DepositCreate,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Create a deposit transaction.
"""
# Verify account ownership
account = crud.get_account_by_id(db, id=deposit_in.account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
if account.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Create and process transaction
transaction = crud.create_deposit(db, deposit_in=deposit_in, user_id=current_user.id)
transaction = crud.process_transaction(db, transaction=transaction)
return transaction
@router.post("/withdrawal", response_model=schemas.Transaction)
def create_withdrawal(
withdrawal_in: schemas.WithdrawalCreate,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Create a withdrawal transaction.
"""
# Verify account ownership
account = crud.get_account_by_id(db, id=withdrawal_in.account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
if account.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Check sufficient balance
if account.balance < withdrawal_in.amount:
raise HTTPException(status_code=400, detail="Insufficient funds")
# Create and process transaction
transaction = crud.create_withdrawal(db, withdrawal_in=withdrawal_in, user_id=current_user.id)
transaction = crud.process_transaction(db, transaction=transaction)
return transaction
@router.post("/transfer", response_model=schemas.Transaction)
def create_transfer(
transfer_in: schemas.TransferCreate,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Create a transfer transaction between accounts.
"""
# Verify sender account ownership
sender_account = crud.get_account_by_id(db, id=transfer_in.sender_account_id)
if not sender_account:
raise HTTPException(status_code=404, detail="Sender account not found")
if sender_account.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Verify receiver account exists
receiver_account = crud.get_account_by_id(db, id=transfer_in.receiver_account_id)
if not receiver_account:
raise HTTPException(status_code=404, detail="Receiver account not found")
# Check sufficient balance
if sender_account.balance < transfer_in.amount:
raise HTTPException(status_code=400, detail="Insufficient funds")
# Create and process transaction
transaction = crud.create_transfer(db, transfer_in=transfer_in, user_id=current_user.id)
transaction = crud.process_transaction(db, transaction=transaction)
return transaction
@router.post("/receive", response_model=schemas.Transaction)
def receive_money(
receive_in: schemas.ReceiveMoneyCreate,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Receive money from an external source.
"""
# Verify account ownership
account = crud.get_account_by_id(db, id=receive_in.receiver_account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
if account.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Create and process transaction
transaction = crud.create_receive_money(db, receive_in=receive_in, user_id=current_user.id)
transaction = crud.process_transaction(db, transaction=transaction)
return transaction
@router.post("/send", response_model=schemas.Transaction)
def send_money(
send_in: schemas.SendMoneyCreate,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Send money to an external destination.
"""
# Verify account ownership
account = crud.get_account_by_id(db, id=send_in.sender_account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
if account.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Check sufficient balance
if account.balance < send_in.amount:
raise HTTPException(status_code=400, detail="Insufficient funds")
# Create and process transaction
transaction = crud.create_send_money(db, send_in=send_in, user_id=current_user.id)
transaction = crud.process_transaction(db, transaction=transaction)
return transaction
@router.get("/{transaction_id}", response_model=schemas.Transaction)
def read_transaction(
transaction_id: int,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Get transaction by ID.
"""
transaction = crud.get_transaction_by_id(db, id=transaction_id)
if not transaction:
raise HTTPException(status_code=404, detail="Transaction not found")
# Verify user is involved in the transaction
if transaction.sender_id != current_user.id and transaction.receiver_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return transaction

70
app/api/v1/users.py Normal file
View File

@ -0,0 +1,70 @@
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app import crud, models, schemas
from app.api import deps
router = APIRouter()
@router.post("/", response_model=schemas.User)
def create_user(
user_in: schemas.UserCreate,
db: Session = Depends(deps.get_db),
) -> Any:
"""
Create a new user.
"""
user = crud.get_user_by_email(db, email=user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="A user with this email already exists",
)
user = crud.create_user(db, user_in=user_in)
return user
@router.get("/me", response_model=schemas.User)
def read_user_me(
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Get current user.
"""
return current_user
@router.put("/me", response_model=schemas.User)
def update_user_me(
user_in: schemas.UserUpdate,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Update current user.
"""
user = crud.update_user(db, user=current_user, user_in=user_in)
return user
@router.get("/{user_id}", response_model=schemas.User)
def read_user_by_id(
user_id: int,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Get a specific user by id.
"""
user = crud.get_user_by_id(db, id=user_id)
if user == current_user:
return user
if not user:
raise HTTPException(
status_code=404,
detail="User not found",
)
return user

0
app/core/__init__.py Normal file
View File

26
app/core/config.py Normal file
View File

@ -0,0 +1,26 @@
from pydantic_settings import BaseSettings
from pathlib import Path
class Settings(BaseSettings):
PROJECT_NAME: str = "Fintech Payment Service"
PROJECT_DESCRIPTION: str = "A FastAPI backend for fintech payment services"
VERSION: str = "0.1.0"
# Security settings
SECRET_KEY: str = "supersecretkey" # Change in production
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# Database settings
DB_DIR = Path("/app") / "storage" / "db"
DB_DIR.mkdir(parents=True, exist_ok=True)
SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite"
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = True
settings = Settings()

42
app/core/security.py Normal file
View File

@ -0,0 +1,42 @@
from datetime import datetime, timedelta
from typing import Any, Union
from jose import jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def create_access_token(
subject: Union[str, Any], expires_delta: timedelta = None
) -> str:
"""
Create a JWT access token
"""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(
to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM
)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify a password against a hash
"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""
Hash a password
"""
return pwd_context.hash(password)

37
app/crud/__init__.py Normal file
View File

@ -0,0 +1,37 @@
from app.crud.user import get_user_by_id, get_user_by_email, get_users, create_user, update_user, delete_user, authenticate
from app.crud.account import get_account_by_id, get_account_by_number, get_user_accounts, create_account, update_account, delete_account, update_account_balance
from app.crud.transaction import get_transaction_by_id, get_transaction_by_reference, get_user_transactions, get_account_transactions, create_deposit, create_withdrawal, create_transfer, create_receive_money, create_send_money, update_transaction, process_transaction
# Define __all__ to make explicit what's exported from this module
__all__ = [
# User CRUD
"get_user_by_id",
"get_user_by_email",
"get_users",
"create_user",
"update_user",
"delete_user",
"authenticate",
# Account CRUD
"get_account_by_id",
"get_account_by_number",
"get_user_accounts",
"create_account",
"update_account",
"delete_account",
"update_account_balance",
# Transaction CRUD
"get_transaction_by_id",
"get_transaction_by_reference",
"get_user_transactions",
"get_account_transactions",
"create_deposit",
"create_withdrawal",
"create_transfer",
"create_receive_money",
"create_send_money",
"update_transaction",
"process_transaction",
]

94
app/crud/account.py Normal file
View File

@ -0,0 +1,94 @@
from typing import Optional, List
import random
import string
from sqlalchemy.orm import Session
from app.models.account import Account
from app.schemas.account import AccountCreate, AccountUpdate
def generate_account_number() -> str:
"""
Generate a random account number
"""
# Generate a 10-digit account number
return ''.join(random.choices(string.digits, k=10))
def get_account_by_id(db: Session, id: int) -> Optional[Account]:
"""
Get an account by ID
"""
return db.query(Account).filter(Account.id == id).first()
def get_account_by_number(db: Session, account_number: str) -> Optional[Account]:
"""
Get an account by account number
"""
return db.query(Account).filter(Account.account_number == account_number).first()
def get_user_accounts(db: Session, owner_id: int, skip: int = 0, limit: int = 100) -> List[Account]:
"""
Get all accounts owned by a user
"""
return db.query(Account).filter(Account.owner_id == owner_id).offset(skip).limit(limit).all()
def create_account(db: Session, account_in: AccountCreate, owner_id: int) -> Account:
"""
Create a new account for a user
"""
# Generate a unique account number
account_number = generate_account_number()
while get_account_by_number(db, account_number):
account_number = generate_account_number()
db_account = Account(
account_number=account_number,
owner_id=owner_id,
account_type=account_in.account_type,
balance=0.0,
currency=account_in.currency,
)
db.add(db_account)
db.commit()
db.refresh(db_account)
return db_account
def update_account(db: Session, account: Account, account_in: AccountUpdate) -> Account:
"""
Update an account
"""
update_data = account_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(account, field, value)
db.add(account)
db.commit()
db.refresh(account)
return account
def delete_account(db: Session, account: Account) -> Account:
"""
Delete an account
"""
db.delete(account)
db.commit()
return account
def update_account_balance(db: Session, account: Account, amount: float) -> Account:
"""
Update an account balance
"""
account.balance += amount
db.add(account)
db.commit()
db.refresh(account)
return account

292
app/crud/transaction.py Normal file
View File

@ -0,0 +1,292 @@
from typing import Optional, List
import random
import string
from datetime import datetime
from sqlalchemy.orm import Session
from sqlalchemy import or_
from app.models.transaction import Transaction, TransactionType, TransactionStatus
from app.models.account import Account
from app.crud.account import update_account_balance
from app.schemas.transaction import (
TransferCreate,
DepositCreate,
WithdrawalCreate,
ReceiveMoneyCreate,
SendMoneyCreate,
TransactionUpdate
)
def generate_transaction_reference() -> str:
"""
Generate a random transaction reference
"""
# Generate a 12-character transaction reference
chars = string.ascii_uppercase + string.digits
return 'TX-' + ''.join(random.choices(chars, k=12))
def get_transaction_by_id(db: Session, id: int) -> Optional[Transaction]:
"""
Get a transaction by ID
"""
return db.query(Transaction).filter(Transaction.id == id).first()
def get_transaction_by_reference(db: Session, reference: str) -> Optional[Transaction]:
"""
Get a transaction by reference
"""
return db.query(Transaction).filter(Transaction.transaction_reference == reference).first()
def get_user_transactions(db: Session, user_id: int, skip: int = 0, limit: int = 100) -> List[Transaction]:
"""
Get all transactions for a user (sent or received)
"""
return db.query(Transaction).filter(
or_(
Transaction.sender_id == user_id,
Transaction.receiver_id == user_id
)
).offset(skip).limit(limit).all()
def get_account_transactions(db: Session, account_id: int, skip: int = 0, limit: int = 100) -> List[Transaction]:
"""
Get all transactions for an account (sent or received)
"""
return db.query(Transaction).filter(
or_(
Transaction.sender_account_id == account_id,
Transaction.receiver_account_id == account_id
)
).offset(skip).limit(limit).all()
def create_deposit(db: Session, deposit_in: DepositCreate, user_id: int) -> Transaction:
"""
Create a deposit transaction
"""
# Generate a unique transaction reference
transaction_reference = generate_transaction_reference()
while get_transaction_by_reference(db, transaction_reference):
transaction_reference = generate_transaction_reference()
# Create the transaction
db_transaction = Transaction(
transaction_reference=transaction_reference,
receiver_id=user_id,
receiver_account_id=deposit_in.account_id,
amount=deposit_in.amount,
currency=deposit_in.currency,
transaction_type=TransactionType.DEPOSIT,
status=TransactionStatus.PENDING,
description=deposit_in.description,
)
db.add(db_transaction)
db.commit()
db.refresh(db_transaction)
return db_transaction
def create_withdrawal(db: Session, withdrawal_in: WithdrawalCreate, user_id: int) -> Transaction:
"""
Create a withdrawal transaction
"""
# Generate a unique transaction reference
transaction_reference = generate_transaction_reference()
while get_transaction_by_reference(db, transaction_reference):
transaction_reference = generate_transaction_reference()
# Create the transaction
db_transaction = Transaction(
transaction_reference=transaction_reference,
sender_id=user_id,
sender_account_id=withdrawal_in.account_id,
amount=withdrawal_in.amount,
currency=withdrawal_in.currency,
transaction_type=TransactionType.WITHDRAWAL,
status=TransactionStatus.PENDING,
description=withdrawal_in.description,
)
db.add(db_transaction)
db.commit()
db.refresh(db_transaction)
return db_transaction
def create_transfer(db: Session, transfer_in: TransferCreate, user_id: int) -> Transaction:
"""
Create a transfer transaction
"""
# Generate a unique transaction reference
transaction_reference = generate_transaction_reference()
while get_transaction_by_reference(db, transaction_reference):
transaction_reference = generate_transaction_reference()
# Create the transaction
db_transaction = Transaction(
transaction_reference=transaction_reference,
sender_id=user_id,
sender_account_id=transfer_in.sender_account_id,
receiver_account_id=transfer_in.receiver_account_id,
amount=transfer_in.amount,
currency=transfer_in.currency,
transaction_type=TransactionType.TRANSFER,
status=TransactionStatus.PENDING,
description=transfer_in.description,
)
# Get the receiver account to set the receiver_id
receiver_account = db.query(Account).filter(Account.id == transfer_in.receiver_account_id).first()
if receiver_account:
db_transaction.receiver_id = receiver_account.owner_id
db.add(db_transaction)
db.commit()
db.refresh(db_transaction)
return db_transaction
def create_receive_money(db: Session, receive_in: ReceiveMoneyCreate, user_id: int) -> Transaction:
"""
Create a transaction for receiving money from an external source
"""
# Generate a unique transaction reference
transaction_reference = generate_transaction_reference()
while get_transaction_by_reference(db, transaction_reference):
transaction_reference = generate_transaction_reference()
# Create the transaction
db_transaction = Transaction(
transaction_reference=transaction_reference,
receiver_id=user_id,
receiver_account_id=receive_in.receiver_account_id,
amount=receive_in.amount,
currency=receive_in.currency,
transaction_type=TransactionType.DEPOSIT,
status=TransactionStatus.PENDING,
description=f"External deposit from: {receive_in.external_sender_info}. {receive_in.description or ''}",
)
db.add(db_transaction)
db.commit()
db.refresh(db_transaction)
return db_transaction
def create_send_money(db: Session, send_in: SendMoneyCreate, user_id: int) -> Transaction:
"""
Create a transaction for sending money to an external destination
"""
# Generate a unique transaction reference
transaction_reference = generate_transaction_reference()
while get_transaction_by_reference(db, transaction_reference):
transaction_reference = generate_transaction_reference()
# Create the transaction
db_transaction = Transaction(
transaction_reference=transaction_reference,
sender_id=user_id,
sender_account_id=send_in.sender_account_id,
amount=send_in.amount,
currency=send_in.currency,
transaction_type=TransactionType.WITHDRAWAL,
status=TransactionStatus.PENDING,
description=f"External withdrawal to: {send_in.external_receiver_info}. {send_in.description or ''}",
)
db.add(db_transaction)
db.commit()
db.refresh(db_transaction)
return db_transaction
def update_transaction(db: Session, transaction: Transaction, transaction_in: TransactionUpdate) -> Transaction:
"""
Update a transaction
"""
update_data = transaction_in.dict(exclude_unset=True)
# If status is changing to completed, set the completed_at timestamp
if "status" in update_data and update_data["status"] == TransactionStatus.COMPLETED and transaction.status != TransactionStatus.COMPLETED:
update_data["completed_at"] = datetime.utcnow()
for field, value in update_data.items():
setattr(transaction, field, value)
db.add(transaction)
db.commit()
db.refresh(transaction)
return transaction
def process_transaction(db: Session, transaction: Transaction) -> Transaction:
"""
Process a pending transaction
"""
# Only process pending transactions
if transaction.status != TransactionStatus.PENDING:
return transaction
try:
# Process based on transaction type
if transaction.transaction_type == TransactionType.DEPOSIT:
# Handle deposit
if transaction.receiver_account_id:
receiver_account = db.query(Account).filter(Account.id == transaction.receiver_account_id).first()
if receiver_account:
update_account_balance(db, receiver_account, transaction.amount)
transaction.status = TransactionStatus.COMPLETED
transaction.completed_at = datetime.utcnow()
elif transaction.transaction_type == TransactionType.WITHDRAWAL:
# Handle withdrawal
if transaction.sender_account_id:
sender_account = db.query(Account).filter(Account.id == transaction.sender_account_id).first()
if sender_account:
# Check if there's enough balance
if sender_account.balance >= transaction.amount:
update_account_balance(db, sender_account, -transaction.amount)
transaction.status = TransactionStatus.COMPLETED
transaction.completed_at = datetime.utcnow()
else:
transaction.status = TransactionStatus.FAILED
transaction.description = transaction.description + " (Insufficient funds)"
elif transaction.transaction_type == TransactionType.TRANSFER:
# Handle transfer between accounts
if transaction.sender_account_id and transaction.receiver_account_id:
sender_account = db.query(Account).filter(Account.id == transaction.sender_account_id).first()
receiver_account = db.query(Account).filter(Account.id == transaction.receiver_account_id).first()
if sender_account and receiver_account:
# Check if there's enough balance
if sender_account.balance >= transaction.amount:
update_account_balance(db, sender_account, -transaction.amount)
update_account_balance(db, receiver_account, transaction.amount)
transaction.status = TransactionStatus.COMPLETED
transaction.completed_at = datetime.utcnow()
else:
transaction.status = TransactionStatus.FAILED
transaction.description = transaction.description + " (Insufficient funds)"
db.add(transaction)
db.commit()
db.refresh(transaction)
return transaction
except Exception as e:
# If there's an error, mark the transaction as failed
transaction.status = TransactionStatus.FAILED
transaction.description = transaction.description + f" (Error: {str(e)})"
db.add(transaction)
db.commit()
db.refresh(transaction)
return transaction

85
app/crud/user.py Normal file
View File

@ -0,0 +1,85 @@
from typing import Optional, List
from sqlalchemy.orm import Session
from app.core.security import get_password_hash, verify_password
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate
def get_user_by_id(db: Session, id: int) -> Optional[User]:
"""
Get a user by ID
"""
return db.query(User).filter(User.id == id).first()
def get_user_by_email(db: Session, email: str) -> Optional[User]:
"""
Get a user by email
"""
return db.query(User).filter(User.email == email).first()
def get_users(db: Session, skip: int = 0, limit: int = 100) -> List[User]:
"""
Get a list of users
"""
return db.query(User).offset(skip).limit(limit).all()
def create_user(db: Session, user_in: UserCreate) -> User:
"""
Create a new user
"""
db_user = User(
email=user_in.email,
first_name=user_in.first_name,
last_name=user_in.last_name,
hashed_password=get_password_hash(user_in.password),
is_active=user_in.is_active,
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
def update_user(db: Session, user: User, user_in: UserUpdate) -> User:
"""
Update a user
"""
update_data = user_in.dict(exclude_unset=True)
if update_data.get("password"):
hashed_password = get_password_hash(update_data["password"])
del update_data["password"]
update_data["hashed_password"] = hashed_password
for field, value in update_data.items():
setattr(user, field, value)
db.add(user)
db.commit()
db.refresh(user)
return user
def delete_user(db: Session, user: User) -> User:
"""
Delete a user
"""
db.delete(user)
db.commit()
return user
def authenticate(db: Session, email: str, password: str) -> Optional[User]:
"""
Authenticate a user
"""
user = get_user_by_email(db, email=email)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user

0
app/db/__init__.py Normal file
View File

3
app/db/base.py Normal file
View File

@ -0,0 +1,3 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

18
app/db/session.py Normal file
View File

@ -0,0 +1,18 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
engine = create_engine(
settings.SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False},
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Dependency to get DB session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

13
app/models/__init__.py Normal file
View File

@ -0,0 +1,13 @@
from app.models.user import User
from app.models.account import Account, AccountType
from app.models.transaction import Transaction, TransactionType, TransactionStatus
# Define __all__ to make explicit what's exported from this module
__all__ = [
"User",
"Account",
"AccountType",
"Transaction",
"TransactionType",
"TransactionStatus",
]

38
app/models/account.py Normal file
View File

@ -0,0 +1,38 @@
from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
from app.db.base import Base
class AccountType(str, enum.Enum):
SAVINGS = "savings"
CHECKING = "checking"
INVESTMENT = "investment"
class Account(Base):
__tablename__ = "accounts"
id = Column(Integer, primary_key=True, index=True)
account_number = Column(String, unique=True, index=True, nullable=False)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
account_type = Column(Enum(AccountType), nullable=False)
balance = Column(Float, default=0.0, nullable=False)
currency = Column(String, default="USD", nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
owner = relationship("User", back_populates="accounts")
sent_transactions = relationship(
"Transaction",
foreign_keys="[Transaction.sender_account_id]",
back_populates="sender_account"
)
received_transactions = relationship(
"Transaction",
foreign_keys="[Transaction.receiver_account_id]",
back_populates="receiver_account"
)

50
app/models/transaction.py Normal file
View File

@ -0,0 +1,50 @@
from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
from app.db.base import Base
class TransactionType(str, enum.Enum):
DEPOSIT = "deposit"
WITHDRAWAL = "withdrawal"
TRANSFER = "transfer"
class TransactionStatus(str, enum.Enum):
PENDING = "pending"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class Transaction(Base):
__tablename__ = "transactions"
id = Column(Integer, primary_key=True, index=True)
transaction_reference = Column(String, unique=True, index=True, nullable=False)
sender_id = Column(Integer, ForeignKey("users.id"), nullable=True)
sender_account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True)
receiver_id = Column(Integer, ForeignKey("users.id"), nullable=True)
receiver_account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True)
amount = Column(Float, nullable=False)
currency = Column(String, default="USD", nullable=False)
transaction_type = Column(Enum(TransactionType), nullable=False)
status = Column(Enum(TransactionStatus), default=TransactionStatus.PENDING, nullable=False)
description = Column(String, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
completed_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
sender = relationship("User", foreign_keys=[sender_id], back_populates="sent_transactions")
receiver = relationship("User", foreign_keys=[receiver_id], back_populates="received_transactions")
sender_account = relationship("Account", foreign_keys=[sender_account_id], back_populates="sent_transactions")
receiver_account = relationship("Account", foreign_keys=[receiver_account_id], back_populates="received_transactions")

31
app/models/user.py Normal file
View File

@ -0,0 +1,31 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
first_name = Column(String, nullable=False)
last_name = Column(String, nullable=False)
hashed_password = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
accounts = relationship("Account", back_populates="owner", cascade="all, delete-orphan")
sent_transactions = relationship(
"Transaction",
foreign_keys="[Transaction.sender_id]",
back_populates="sender"
)
received_transactions = relationship(
"Transaction",
foreign_keys="[Transaction.receiver_id]",
back_populates="receiver"
)

44
app/schemas/__init__.py Normal file
View File

@ -0,0 +1,44 @@
from app.schemas.user import User, UserCreate, UserUpdate, UserInDB
from app.schemas.account import Account, AccountCreate, AccountUpdate, AccountType
from app.schemas.transaction import (
Transaction,
TransactionType,
TransactionStatus,
DepositCreate,
WithdrawalCreate,
TransferCreate,
ReceiveMoneyCreate,
SendMoneyCreate,
TransactionUpdate
)
from app.schemas.token import Token, TokenPayload
# Define __all__ to make explicit what's exported from this module
__all__ = [
# User schemas
"User",
"UserCreate",
"UserUpdate",
"UserInDB",
# Account schemas
"Account",
"AccountCreate",
"AccountUpdate",
"AccountType",
# Transaction schemas
"Transaction",
"TransactionType",
"TransactionStatus",
"DepositCreate",
"WithdrawalCreate",
"TransferCreate",
"ReceiveMoneyCreate",
"SendMoneyCreate",
"TransactionUpdate",
# Token schemas
"Token",
"TokenPayload",
]

43
app/schemas/account.py Normal file
View File

@ -0,0 +1,43 @@
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
from enum import Enum
class AccountType(str, Enum):
SAVINGS = "savings"
CHECKING = "checking"
INVESTMENT = "investment"
class AccountBase(BaseModel):
account_type: AccountType
currency: str = "USD"
class AccountCreate(AccountBase):
"""Request model for creating a new account"""
pass
class AccountUpdate(BaseModel):
"""Request model for updating an existing account"""
account_type: Optional[AccountType] = None
currency: Optional[str] = None
class AccountInDBBase(AccountBase):
id: int
account_number: str
owner_id: int
balance: float
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class Account(AccountInDBBase):
"""Response model for account data"""
pass

11
app/schemas/token.py Normal file
View File

@ -0,0 +1,11 @@
from pydantic import BaseModel
from typing import Optional
class Token(BaseModel):
access_token: str
token_type: str
class TokenPayload(BaseModel):
sub: Optional[int] = None

View File

@ -0,0 +1,79 @@
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from enum import Enum
class TransactionType(str, Enum):
DEPOSIT = "deposit"
WITHDRAWAL = "withdrawal"
TRANSFER = "transfer"
class TransactionStatus(str, Enum):
PENDING = "pending"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class TransactionBase(BaseModel):
amount: float = Field(..., gt=0)
currency: str = "USD"
description: Optional[str] = None
class DepositCreate(TransactionBase):
"""Request model for creating a deposit"""
account_id: int
class WithdrawalCreate(TransactionBase):
"""Request model for creating a withdrawal"""
account_id: int
class TransferCreate(TransactionBase):
"""Request model for creating a transfer between accounts"""
sender_account_id: int
receiver_account_id: int
class ReceiveMoneyCreate(TransactionBase):
"""Request model for receiving money from external source"""
receiver_account_id: int
external_sender_info: str
class SendMoneyCreate(TransactionBase):
"""Request model for sending money to external destination"""
sender_account_id: int
external_receiver_info: str
class TransactionUpdate(BaseModel):
"""Request model for updating a transaction"""
status: Optional[TransactionStatus] = None
description: Optional[str] = None
class TransactionInDBBase(TransactionBase):
id: int
transaction_reference: str
sender_id: Optional[int] = None
sender_account_id: Optional[int] = None
receiver_id: Optional[int] = None
receiver_account_id: Optional[int] = None
transaction_type: TransactionType
status: TransactionStatus
created_at: datetime
updated_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
class Config:
from_attributes = True
class Transaction(TransactionInDBBase):
"""Response model for transaction data"""
pass

41
app/schemas/user.py Normal file
View File

@ -0,0 +1,41 @@
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime
class UserBase(BaseModel):
email: EmailStr
first_name: str = Field(..., min_length=1, max_length=100)
last_name: str = Field(..., min_length=1, max_length=100)
is_active: Optional[bool] = True
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
first_name: Optional[str] = Field(None, min_length=1, max_length=100)
last_name: Optional[str] = Field(None, min_length=1, max_length=100)
is_active: Optional[bool] = None
password: Optional[str] = Field(None, min_length=8)
class UserInDBBase(UserBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class User(UserInDBBase):
"""Response model for user without sensitive data"""
pass
class UserInDB(UserInDBBase):
"""Model with password hash for internal use"""
hashed_password: str

61
main.py Normal file
View File

@ -0,0 +1,61 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi
from app.api.v1 import api_router
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
description=settings.PROJECT_DESCRIPTION,
version=settings.VERSION,
openapi_url="/openapi.json",
docs_url="/docs",
redoc_url="/redoc",
)
# Set up CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API router
app.include_router(api_router, prefix="/api/v1")
@app.get("/")
def root():
"""Root endpoint that returns basic information about the API."""
return {
"name": settings.PROJECT_NAME,
"description": settings.PROJECT_DESCRIPTION,
"version": settings.VERSION,
"documentation": "/docs",
"redoc": "/redoc",
"health_check": "/api/v1/health",
}
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title=settings.PROJECT_NAME,
version=settings.VERSION,
description=settings.PROJECT_DESCRIPTION,
routes=app.routes,
)
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

83
migrations/env.py Normal file
View File

@ -0,0 +1,83 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# Import the models for Alembic migrations
from app.db.base import Base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
render_as_batch=True, # Important for SQLite
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
is_sqlite = connection.dialect.name == "sqlite"
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=is_sqlite, # Important for SQLite
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,118 @@
"""Initial migration
Revision ID: f86c03d2e03d
Revises:
Create Date: 2023-06-01 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
import enum
# revision identifiers, used by Alembic.
revision = 'f86c03d2e03d'
down_revision = None
branch_labels = None
depends_on = None
# Enums for account and transaction tables
class AccountType(str, enum.Enum):
SAVINGS = "savings"
CHECKING = "checking"
INVESTMENT = "investment"
class TransactionType(str, enum.Enum):
DEPOSIT = "deposit"
WITHDRAWAL = "withdrawal"
TRANSFER = "transfer"
class TransactionStatus(str, enum.Enum):
PENDING = "pending"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
def upgrade():
# Create users table
op.create_table(
'users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('first_name', sa.String(), nullable=False),
sa.Column('last_name', sa.String(), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=True, default=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)')),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
# Create accounts table
op.create_table(
'accounts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('account_number', sa.String(), nullable=False),
sa.Column('owner_id', sa.Integer(), nullable=False),
sa.Column('account_type', sa.Enum('savings', 'checking', 'investment', name='accounttype'), nullable=False),
sa.Column('balance', sa.Float(), nullable=False, default=0.0),
sa.Column('currency', sa.String(), nullable=False, default='USD'),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)')),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_accounts_account_number'), 'accounts', ['account_number'], unique=True)
op.create_index(op.f('ix_accounts_id'), 'accounts', ['id'], unique=False)
# Create transactions table
op.create_table(
'transactions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('transaction_reference', sa.String(), nullable=False),
sa.Column('sender_id', sa.Integer(), nullable=True),
sa.Column('sender_account_id', sa.Integer(), nullable=True),
sa.Column('receiver_id', sa.Integer(), nullable=True),
sa.Column('receiver_account_id', sa.Integer(), nullable=True),
sa.Column('amount', sa.Float(), nullable=False),
sa.Column('currency', sa.String(), nullable=False, default='USD'),
sa.Column('transaction_type', sa.Enum('deposit', 'withdrawal', 'transfer', name='transactiontype'), nullable=False),
sa.Column('status', sa.Enum('pending', 'completed', 'failed', 'cancelled', name='transactionstatus'), nullable=False, default='pending'),
sa.Column('description', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)')),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['receiver_account_id'], ['accounts.id'], ),
sa.ForeignKeyConstraint(['receiver_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['sender_account_id'], ['accounts.id'], ),
sa.ForeignKeyConstraint(['sender_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_transactions_id'), 'transactions', ['id'], unique=False)
op.create_index(op.f('ix_transactions_transaction_reference'), 'transactions', ['transaction_reference'], unique=True)
def downgrade():
# Drop tables in reverse order
op.drop_index(op.f('ix_transactions_transaction_reference'), table_name='transactions')
op.drop_index(op.f('ix_transactions_id'), table_name='transactions')
op.drop_table('transactions')
op.drop_index(op.f('ix_accounts_id'), table_name='accounts')
op.drop_index(op.f('ix_accounts_account_number'), table_name='accounts')
op.drop_table('accounts')
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')
# Drop enum types
sa.Enum(name='transactionstatus').drop(op.get_bind(), checkfirst=False)
sa.Enum(name='transactiontype').drop(op.get_bind(), checkfirst=False)
sa.Enum(name='accounttype').drop(op.get_bind(), checkfirst=False)

11
requirements.txt Normal file
View File

@ -0,0 +1,11 @@
fastapi>=0.100.0
uvicorn>=0.22.0
pydantic>=2.0.0
pydantic-settings>=2.0.0
sqlalchemy>=2.0.0
alembic>=1.11.1
python-jose[cryptography]>=3.3.0
passlib[bcrypt]>=1.7.4
python-multipart>=0.0.6
ruff>=0.0.272
python-dotenv>=1.0.0