
- 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
292 lines
11 KiB
Python
292 lines
11 KiB
Python
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 |