feat: implement complete CMT backend with API endpoints and test suite

- Add 7 core API endpoints: users, transactions, partners, products, inventory, payments, credit
- Implement role-based authentication (admin/write/read-only access)
- Add comprehensive database models with proper relationships
- Include full test coverage for all endpoints and business logic
- Set up Alembic migrations and Docker configuration
- Configure FastAPI app with CORS and database integration
This commit is contained in:
2025-09-14 21:04:07 +02:00
parent 49c813778b
commit c086f64363
48 changed files with 6992 additions and 126 deletions
View File
+198
View File
@@ -0,0 +1,198 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.db import get_session
from app.core.auth import require_any_access, require_write_access, require_admin
from app.schemas.models import Credit, Partner, Transaction
from app.schemas.schemas import (
CreditCreate,
CreditUpdate,
CreditResponse,
UserResponse
)
from typing import List
router = APIRouter(prefix="/credit", tags=["credit"])
# Create Credit
@router.post("/", response_model=CreditResponse, status_code=status.HTTP_201_CREATED)
def create_credit(
credit: CreditCreate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
"""Create new credit account (requires write access)."""
# Validate partner exists
partner = session.get(Partner, credit.partner_id)
if not partner:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Partner not found"
)
# Validate transaction exists
transaction = session.get(Transaction, credit.transaction_id)
if not transaction:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Transaction not found"
)
# Check if credit account already exists for this partner
existing_credit = session.exec(
select(Credit).where(Credit.partner_id == credit.partner_id)
).first()
if existing_credit:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Credit account already exists for this partner"
)
# Create credit with audit fields
credit_data = credit.model_dump()
credit_data["created_by"] = current_user.id
credit_data["updated_by"] = current_user.id
db_credit = Credit(**credit_data)
session.add(db_credit)
session.commit()
session.refresh(db_credit)
return db_credit
# Read all Credit accounts
@router.get("/", response_model=List[CreditResponse])
def read_credits(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get all credit accounts (requires authentication)."""
credits = session.exec(
select(Credit).offset(skip).limit(limit)
).all()
return credits
# Read Credit by partner
@router.get("/partner/{partner_id}", response_model=CreditResponse)
def read_credit_by_partner(
partner_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get credit account for a specific partner (requires authentication)."""
# Validate partner exists
partner = session.get(Partner, partner_id)
if not partner:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Partner not found"
)
credit = session.exec(
select(Credit).where(Credit.partner_id == partner_id)
).first()
if not credit:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Credit account not found for this partner"
)
return credit
# Read single Credit by ID
@router.get("/{credit_id}", response_model=CreditResponse)
def read_credit_by_id(
credit_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get specific credit account by ID (requires authentication)."""
credit = session.get(Credit, credit_id)
if not credit:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Credit account not found"
)
return credit
# Update Credit
@router.put("/{credit_id}", response_model=CreditResponse)
def update_credit(
credit_id: int,
credit: CreditUpdate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
"""Update specific credit account (requires write access)."""
db_credit = session.get(Credit, credit_id)
if not db_credit:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Credit account not found"
)
update_data = credit.model_dump(exclude_unset=True)
# Validate partner if being updated
if "partner_id" in update_data:
partner = session.get(Partner, update_data["partner_id"])
if not partner:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Partner not found"
)
# Check for duplicate partner (excluding current record)
existing_credit = session.exec(
select(Credit).where(
Credit.partner_id == update_data["partner_id"],
Credit.id != credit_id
)
).first()
if existing_credit:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Credit account already exists for this partner"
)
# Validate transaction if being updated
if "transaction_id" in update_data:
transaction = session.get(Transaction, update_data["transaction_id"])
if not transaction:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Transaction not found"
)
# Track who updated
update_data["updated_by"] = current_user.id
# Update credit
for key, value in update_data.items():
setattr(db_credit, key, value)
session.add(db_credit)
session.commit()
session.refresh(db_credit)
return db_credit
# Delete Credit
@router.delete("/{credit_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_credit(
credit_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_admin)
):
"""Delete specific credit account (admin only)."""
credit = session.get(Credit, credit_id)
if not credit:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Credit account not found"
)
session.delete(credit)
session.commit()
return None
+175
View File
@@ -0,0 +1,175 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.db import get_session
from app.core.auth import require_any_access, require_write_access, require_admin
from app.schemas.models import Inventory, Product
from app.schemas.schemas import (
InventoryCreate,
InventoryUpdate,
InventoryResponse,
UserResponse
)
from typing import List
router = APIRouter(prefix="/inventory", tags=["inventory"])
# Create Inventory
@router.post("/", response_model=InventoryResponse, status_code=status.HTTP_201_CREATED)
def create_inventory(
inventory: InventoryCreate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
"""Create new inventory entry (requires write access)."""
# Validate product exists
product = session.get(Product, inventory.product_id)
if not product:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Product not found"
)
# Check if inventory already exists for this product
existing_inventory = session.exec(
select(Inventory).where(Inventory.product_id == inventory.product_id)
).first()
if existing_inventory:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Inventory entry already exists for this product"
)
# Create inventory
inventory_data = inventory.model_dump()
db_inventory = Inventory(**inventory_data)
session.add(db_inventory)
session.commit()
session.refresh(db_inventory)
return db_inventory
# Read all Inventory
@router.get("/", response_model=List[InventoryResponse])
def read_inventory(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get all inventory entries (requires authentication)."""
inventory = session.exec(
select(Inventory).offset(skip).limit(limit)
).all()
return inventory
# Read Inventory by product
@router.get("/product/{product_id}", response_model=InventoryResponse)
def read_inventory_by_product(
product_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get inventory for a specific product (requires authentication)."""
# Validate product exists
product = session.get(Product, product_id)
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found"
)
inventory = session.exec(
select(Inventory).where(Inventory.product_id == product_id)
).first()
if not inventory:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Inventory not found for this product"
)
return inventory
# Read single Inventory by ID
@router.get("/{inventory_id}", response_model=InventoryResponse)
def read_inventory_by_id(
inventory_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get specific inventory entry by ID (requires authentication)."""
inventory = session.get(Inventory, inventory_id)
if not inventory:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Inventory entry not found"
)
return inventory
# Update Inventory
@router.put("/{inventory_id}", response_model=InventoryResponse)
def update_inventory(
inventory_id: int,
inventory: InventoryUpdate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
"""Update specific inventory entry (requires write access)."""
db_inventory = session.get(Inventory, inventory_id)
if not db_inventory:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Inventory entry not found"
)
update_data = inventory.model_dump(exclude_unset=True)
# Validate product if being updated
if "product_id" in update_data:
product = session.get(Product, update_data["product_id"])
if not product:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Product not found"
)
# Check for duplicate product (excluding current record)
existing_inventory = session.exec(
select(Inventory).where(
Inventory.product_id == update_data["product_id"],
Inventory.id != inventory_id
)
).first()
if existing_inventory:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Inventory entry already exists for this product"
)
# Update inventory
for key, value in update_data.items():
setattr(db_inventory, key, value)
session.add(db_inventory)
session.commit()
session.refresh(db_inventory)
return db_inventory
# Delete Inventory
@router.delete("/{inventory_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_inventory(
inventory_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_admin)
):
"""Delete specific inventory entry (admin only)."""
inventory = session.get(Inventory, inventory_id)
if not inventory:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Inventory entry not found"
)
session.delete(inventory)
session.commit()
return None
+126
View File
@@ -0,0 +1,126 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.db import get_session
from app.core.auth import require_any_access, require_write_access, require_admin
from app.schemas.models import Partner
from app.schemas.schemas import (
PartnerCreate,
PartnerUpdate,
PartnerResponse,
UserResponse
)
from typing import List
router = APIRouter(prefix="/partners", tags=["partners"])
# Create Partner
@router.post("/", response_model=PartnerResponse, status_code=status.HTTP_201_CREATED)
def create_partner(
partner: PartnerCreate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
"""Create a new partner (requires write access)."""
# Check if TIN number already exists
statement = select(Partner).where(Partner.tin_number == partner.tin_number)
existing_partner = session.exec(statement).first()
if existing_partner:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Partner with this TIN number already exists"
)
# Create new partner
partner_data = partner.model_dump()
db_partner = Partner(**partner_data)
session.add(db_partner)
session.commit()
session.refresh(db_partner)
return db_partner
# Read all Partners
@router.get("/", response_model=List[PartnerResponse])
def read_partners(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get all partners (requires authentication)."""
partners = session.exec(select(Partner).offset(skip).limit(limit)).all()
return partners
# Read single Partner by ID
@router.get("/{partner_id}", response_model=PartnerResponse)
def read_partner(
partner_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get specific partner by ID (requires authentication)."""
partner = session.get(Partner, partner_id)
if not partner:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Partner not found"
)
return partner
# Update Partner
@router.put("/{partner_id}", response_model=PartnerResponse)
def update_partner(
partner_id: int,
partner: PartnerUpdate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
"""Update specific partner (requires write access)."""
db_partner = session.get(Partner, partner_id)
if not db_partner:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Partner not found"
)
# Check for TIN number conflicts if updating TIN
update_data = partner.model_dump(exclude_unset=True)
if "tin_number" in update_data:
statement = select(Partner).where(
Partner.tin_number == update_data["tin_number"],
Partner.id != partner_id
)
existing_partner = session.exec(statement).first()
if existing_partner:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Partner with this TIN number already exists"
)
# Update partner
for key, value in update_data.items():
setattr(db_partner, key, value)
session.add(db_partner)
session.commit()
session.refresh(db_partner)
return db_partner
# Delete Partner
@router.delete("/{partner_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_partner(
partner_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_admin)
):
"""Delete specific partner (admin only)."""
partner = session.get(Partner, partner_id)
if not partner:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Partner not found"
)
session.delete(partner)
session.commit()
return None
+155
View File
@@ -0,0 +1,155 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.db import get_session
from app.core.auth import require_any_access, require_write_access, require_admin
from app.schemas.models import Payment, Transaction
from app.schemas.schemas import (
PaymentCreate,
PaymentUpdate,
PaymentResponse,
UserResponse
)
from typing import List
router = APIRouter(prefix="/payments", tags=["payments"])
# Create Payment
@router.post("/", response_model=PaymentResponse, status_code=status.HTTP_201_CREATED)
def create_payment(
payment: PaymentCreate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
"""Create new payment (requires write access)."""
# Validate transaction exists
transaction = session.get(Transaction, payment.transaction_id)
if not transaction:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Transaction not found"
)
# Create payment with audit fields
payment_data = payment.model_dump()
payment_data["created_by"] = current_user.id
payment_data["updated_by"] = current_user.id
db_payment = Payment(**payment_data)
session.add(db_payment)
session.commit()
session.refresh(db_payment)
return db_payment
# Read all Payments
@router.get("/", response_model=List[PaymentResponse])
def read_payments(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get all payments (requires authentication)."""
payments = session.exec(
select(Payment).offset(skip).limit(limit)
).all()
return payments
# Read Payments by transaction
@router.get("/transaction/{transaction_id}", response_model=List[PaymentResponse])
def read_payments_by_transaction(
transaction_id: int,
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get payments for a specific transaction (requires authentication)."""
# Validate transaction exists
transaction = session.get(Transaction, transaction_id)
if not transaction:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Transaction not found"
)
statement = select(Payment).where(
Payment.transaction_id == transaction_id
).offset(skip).limit(limit)
payments = session.exec(statement).all()
return payments
# Read single Payment by ID
@router.get("/{payment_id}", response_model=PaymentResponse)
def read_payment_by_id(
payment_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get specific payment by ID (requires authentication)."""
payment = session.get(Payment, payment_id)
if not payment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Payment not found"
)
return payment
# Update Payment
@router.put("/{payment_id}", response_model=PaymentResponse)
def update_payment(
payment_id: int,
payment: PaymentUpdate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
"""Update specific payment (requires write access)."""
db_payment = session.get(Payment, payment_id)
if not db_payment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Payment not found"
)
update_data = payment.model_dump(exclude_unset=True)
# Validate transaction if being updated
if "transaction_id" in update_data:
transaction = session.get(Transaction, update_data["transaction_id"])
if not transaction:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Transaction not found"
)
# Track who updated
update_data["updated_by"] = current_user.id
# Update payment
for key, value in update_data.items():
setattr(db_payment, key, value)
session.add(db_payment)
session.commit()
session.refresh(db_payment)
return db_payment
# Delete Payment
@router.delete("/{payment_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_payment(
payment_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_admin)
):
"""Delete specific payment (admin only)."""
payment = session.get(Payment, payment_id)
if not payment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Payment not found"
)
session.delete(payment)
session.commit()
return None
+166
View File
@@ -0,0 +1,166 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.db import get_session
from app.core.auth import require_any_access, require_write_access, require_admin
from app.schemas.models import Product
from app.schemas.schemas import (
ProductCreate,
ProductUpdate,
ProductResponse,
UserResponse
)
from typing import List
router = APIRouter(prefix="/products", tags=["products"])
# Create Product
@router.post("/", response_model=ProductResponse, status_code=status.HTTP_201_CREATED)
def create_product(
product: ProductCreate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
"""Create a new product (requires write access)."""
# Check if product code already exists
statement = select(Product).where(Product.product_code == product.product_code)
existing_product = session.exec(statement).first()
if existing_product:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Product with this code already exists"
)
# Check if product name already exists
statement = select(Product).where(Product.product_name == product.product_name)
existing_product = session.exec(statement).first()
if existing_product:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Product with this name already exists"
)
# Create new product
product_data = product.model_dump()
db_product = Product(**product_data)
session.add(db_product)
session.commit()
session.refresh(db_product)
return db_product
# Read all Products
@router.get("/", response_model=List[ProductResponse])
def read_products(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get all products (requires authentication)."""
products = session.exec(select(Product).offset(skip).limit(limit)).all()
return products
# Read single Product by ID
@router.get("/{product_id}", response_model=ProductResponse)
def read_product(
product_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get specific product by ID (requires authentication)."""
product = session.get(Product, product_id)
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found"
)
return product
# Read Product by code
@router.get("/code/{product_code}", response_model=ProductResponse)
def read_product_by_code(
product_code: str,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get specific product by code (requires authentication)."""
statement = select(Product).where(Product.product_code == product_code)
product = session.exec(statement).first()
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found"
)
return product
# Update Product
@router.put("/{product_id}", response_model=ProductResponse)
def update_product(
product_id: int,
product: ProductUpdate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
"""Update specific product (requires write access)."""
db_product = session.get(Product, product_id)
if not db_product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found"
)
update_data = product.model_dump(exclude_unset=True)
# Check for product code conflicts if updating code
if "product_code" in update_data:
statement = select(Product).where(
Product.product_code == update_data["product_code"],
Product.id != product_id
)
existing_product = session.exec(statement).first()
if existing_product:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Product with this code already exists"
)
# Check for product name conflicts if updating name
if "product_name" in update_data:
statement = select(Product).where(
Product.product_name == update_data["product_name"],
Product.id != product_id
)
existing_product = session.exec(statement).first()
if existing_product:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Product with this name already exists"
)
# Update product
for key, value in update_data.items():
setattr(db_product, key, value)
session.add(db_product)
session.commit()
session.refresh(db_product)
return db_product
# Delete Product
@router.delete("/{product_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_product(
product_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_admin)
):
"""Delete specific product (admin only)."""
product = session.get(Product, product_id)
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found"
)
session.delete(product)
session.commit()
return None
+197
View File
@@ -0,0 +1,197 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.db import get_session
from app.core.auth import require_any_access, require_write_access, require_admin
from app.schemas.models import Transaction_details, Partner, Product
from app.schemas.schemas import (
TransactionDetailsCreate,
TransactionDetailsUpdate,
TransactionDetailsResponse,
UserResponse
)
from typing import List
router = APIRouter(prefix="/transaction-details", tags=["transaction-details"])
# Create Transaction Details
@router.post("/", response_model=TransactionDetailsResponse, status_code=status.HTTP_201_CREATED)
def create_transaction_details(
transaction_details: TransactionDetailsCreate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
"""Create new transaction details (requires write access)."""
# Validate partner exists
partner = session.get(Partner, transaction_details.partner_id)
if not partner:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Partner not found"
)
# Validate product exists
product = session.get(Product, transaction_details.product_id)
if not product:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Product not found"
)
# Create transaction details with audit fields
transaction_details_data = transaction_details.model_dump()
transaction_details_data["created_by"] = current_user.id
transaction_details_data["updated_by"] = current_user.id
db_transaction_details = Transaction_details(**transaction_details_data)
session.add(db_transaction_details)
session.commit()
session.refresh(db_transaction_details)
return db_transaction_details
# Read all Transaction Details
@router.get("/", response_model=List[TransactionDetailsResponse])
def read_transaction_details(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get all transaction details (requires authentication)."""
transaction_details = session.exec(
select(Transaction_details).offset(skip).limit(limit)
).all()
return transaction_details
# Read Transaction Details by partner
@router.get("/partner/{partner_id}", response_model=List[TransactionDetailsResponse])
def read_transaction_details_by_partner(
partner_id: int,
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get transaction details for a specific partner (requires authentication)."""
# Validate partner exists
partner = session.get(Partner, partner_id)
if not partner:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Partner not found"
)
statement = select(Transaction_details).where(
Transaction_details.partner_id == partner_id
).offset(skip).limit(limit)
transaction_details = session.exec(statement).all()
return transaction_details
# Read Transaction Details by product
@router.get("/product/{product_id}", response_model=List[TransactionDetailsResponse])
def read_transaction_details_by_product(
product_id: int,
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get transaction details for a specific product (requires authentication)."""
# Validate product exists
product = session.get(Product, product_id)
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found"
)
statement = select(Transaction_details).where(
Transaction_details.product_id == product_id
).offset(skip).limit(limit)
transaction_details = session.exec(statement).all()
return transaction_details
# Read single Transaction Details by ID
@router.get("/{transaction_details_id}", response_model=TransactionDetailsResponse)
def read_transaction_details_by_id(
transaction_details_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
"""Get specific transaction details by ID (requires authentication)."""
transaction_details = session.get(Transaction_details, transaction_details_id)
if not transaction_details:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Transaction details not found"
)
return transaction_details
# Update Transaction Details
@router.put("/{transaction_details_id}", response_model=TransactionDetailsResponse)
def update_transaction_details(
transaction_details_id: int,
transaction_details: TransactionDetailsUpdate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
"""Update specific transaction details (requires write access)."""
db_transaction_details = session.get(Transaction_details, transaction_details_id)
if not db_transaction_details:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Transaction details not found"
)
update_data = transaction_details.model_dump(exclude_unset=True)
# Validate partner if being updated
if "partner_id" in update_data:
partner = session.get(Partner, update_data["partner_id"])
if not partner:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Partner not found"
)
# Validate product if being updated
if "product_id" in update_data:
product = session.get(Product, update_data["product_id"])
if not product:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Product not found"
)
# Track who updated
update_data["updated_by"] = current_user.id
# Update transaction details
for key, value in update_data.items():
setattr(db_transaction_details, key, value)
session.add(db_transaction_details)
session.commit()
session.refresh(db_transaction_details)
return db_transaction_details
# Delete Transaction Details
@router.delete("/{transaction_details_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_transaction_details(
transaction_details_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_admin)
):
"""Delete specific transaction details (admin only)."""
transaction_details = session.get(Transaction_details, transaction_details_id)
if not transaction_details:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Transaction details not found"
)
session.delete(transaction_details)
session.commit()
return None
+88
View File
@@ -0,0 +1,88 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.db import get_session
from app.core.auth import require_any_access, require_write_access, get_current_active_user
from app.schemas.models import Transaction
from app.schemas.schemas import TransactionCreate, TransactionUpdate, TransactionResponse, UserResponse
from typing import List, Optional
router = APIRouter(prefix="/transactions", tags=["transactions"])
# Create Transaction
@router.post("/", response_model=TransactionResponse, status_code=status.HTTP_201_CREATED)
def create_transaction(
transaction: TransactionCreate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
# Set created_by and updated_by to current user
transaction_data = transaction.model_dump(exclude_unset=True)
transaction_data["created_by"] = current_user.id
transaction_data["updated_by"] = current_user.id
db_transaction = Transaction(**transaction_data)
session.add(db_transaction)
session.commit()
session.refresh(db_transaction)
return db_transaction
# Read all Transactions
@router.get("/", response_model=List[TransactionResponse])
def read_transactions(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
transactions = session.exec(select(Transaction).offset(skip).limit(limit)).all()
return transactions
# Read single Transaction by ID
@router.get("/{transaction_id}", response_model=TransactionResponse)
def read_transaction(
transaction_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_any_access)
):
transaction = session.get(Transaction, transaction_id)
if not transaction:
raise HTTPException(status_code=404, detail="Transaction not found")
return transaction
# Update Transaction
@router.put("/{transaction_id}", response_model=TransactionResponse)
def update_transaction(
transaction_id: int,
transaction: TransactionUpdate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
db_transaction = session.get(Transaction, transaction_id)
if not db_transaction:
raise HTTPException(status_code=404, detail="Transaction not found")
update_data = transaction.model_dump(exclude_unset=True)
update_data["updated_by"] = current_user.id # Track who updated
for key, value in update_data.items():
setattr(db_transaction, key, value)
session.add(db_transaction)
session.commit()
session.refresh(db_transaction)
return db_transaction
# Delete Transaction
@router.delete("/{transaction_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_transaction(
transaction_id: int,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_write_access)
):
transaction = session.get(Transaction, transaction_id)
if not transaction:
raise HTTPException(status_code=404, detail="Transaction not found")
session.delete(transaction)
session.commit()
return None
+203
View File
@@ -0,0 +1,203 @@
# backend/app/api/v1/users.py
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBearer
from sqlmodel import Session, select
from app.core.db import get_session
from app.core.auth import (
authenticate_user,
create_access_token,
get_password_hash,
get_current_active_user,
get_token_expiration_minutes,
require_admin,
require_write_access,
require_any_access
)
from app.schemas.models import User
from app.schemas.schemas import (
UserCreate,
UserUpdate,
UserLogin,
Token,
UserResponse
)
router = APIRouter(prefix="/users", tags=["users"])
@router.post("/login", response_model=Token)
def login(user_credentials: UserLogin, session: Session = Depends(get_session)):
"""Authenticate user and return JWT token with role-based expiration."""
user = authenticate_user(session, user_credentials.username, user_credentials.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Get role-based expiration time
expire_minutes = get_token_expiration_minutes(user.role)
access_token_expires = timedelta(minutes=expire_minutes)
# Create token with user data
access_token = create_access_token(
data={
"sub": user.username,
"user_id": user.id,
"role": user.role.value
},
expires_delta=access_token_expires
)
return Token(
access_token=access_token,
token_type="bearer",
expires_in=expire_minutes * 60, # Convert to seconds
user=UserResponse(
id=user.id,
username=user.username,
role=user.role
)
)
@router.get("/me", response_model=UserResponse)
def get_current_user_info(current_user: User = Depends(get_current_active_user)):
"""Get current user information from token."""
return UserResponse(
id=current_user.id,
username=current_user.username,
role=current_user.role
)
@router.get("/", response_model=list[UserResponse])
def get_all_users(
session: Session = Depends(get_session),
current_user: User = Depends(require_any_access),
skip: int = 0,
limit: int = 100
):
"""Get all users (requires any authenticated role)."""
statement = select(User).offset(skip).limit(limit)
users = session.exec(statement).all()
return [
UserResponse(id=user.id, username=user.username, role=user.role)
for user in users
]
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def create_user(
user: UserCreate,
session: Session = Depends(get_session),
current_user: User = Depends(require_admin)
):
"""Create a new user (admin only)."""
# Check if username already exists
statement = select(User).where(User.username == user.username)
existing_user = session.exec(statement).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already registered"
)
# Create new user with hashed password
hashed_password = get_password_hash(user.password)
db_user = User(
username=user.username,
password_hash=hashed_password,
role=user.role
)
session.add(db_user)
session.commit()
session.refresh(db_user)
return UserResponse(
id=db_user.id,
username=db_user.username,
role=db_user.role
)
@router.get("/{user_id}", response_model=UserResponse)
def get_user(
user_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_any_access)
):
"""Get specific user by ID (requires authentication)."""
user = session.get(User, user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return UserResponse(
id=user.id,
username=user.username,
role=user.role
)
# Update to handle user self-updating password
@router.put("/{user_id}", response_model=UserResponse)
def update_user(
user_id: int,
user_update: UserUpdate,
session: Session = Depends(get_session),
current_user: UserResponse = Depends(require_admin)
):
"""Update specific user (admin only)."""
user = session.get(User, user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Update only provided fields
update_data = user_update.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(user, key, value)
session.add(user)
session.commit()
session.refresh(user)
return UserResponse(
id=user.id,
username=user.username,
role=user.role
)
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_user(
user_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(require_admin)
):
"""Delete specific user (admin only)."""
user = session.get(User, user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Prevent self-deletion
if user.id == current_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete your own account"
)
session.delete(user)
session.commit()
return None