Update code via agent code generation

This commit is contained in:
Automated Action 2025-06-02 11:37:11 +00:00
parent 33ce98c158
commit dc3702b55c
51 changed files with 3158 additions and 2 deletions

142
README.md
View File

@ -1,3 +1,141 @@
# FastAPI Application
# Logistics Management System API
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A comprehensive API for managing logistics operations including warehouses, inventory, orders, shipments, and more.
## Features
- **User Management**: Authentication, authorization, and user management
- **Product Management**: Create, update, and track products
- **Warehouse Management**: Manage multiple warehouses and their details
- **Inventory Management**: Track inventory levels across warehouses
- **Order Management**: Process customer orders from creation to delivery
- **Shipment Management**: Track shipments between warehouses or to customers
- **Health Check**: Monitor system health and database connectivity
## Tech Stack
- **Framework**: FastAPI
- **Database**: SQLite with SQLAlchemy ORM
- **Authentication**: JWT token-based authentication
- **Migration**: Alembic for database migrations
- **Validation**: Pydantic models for request/response validation
## Project Structure
```
├── app # Application source code
│ ├── api # API endpoint definitions
│ │ └── endpoints # API endpoints for each domain
│ ├── core # Core application code (config, security, etc.)
│ ├── db # Database-related code (session, migrations)
│ ├── models # SQLAlchemy models
│ ├── schemas # Pydantic schemas
│ └── services # Business logic services
├── migrations # Alembic migrations
├── alembic.ini # Alembic configuration
├── main.py # Application entry point
└── requirements.txt # Python dependencies
```
## Getting Started
### Prerequisites
- Python 3.8 or higher
- pip (Python package manager)
### Installation
1. Clone the repository:
```
git clone <repository-url>
cd logisticsmanagementsystem
```
2. Create a virtual environment (optional but recommended):
```
python -m venv venv
source venv/bin/activate # On Windows, use: venv\Scripts\activate
```
3. Install dependencies:
```
pip install -r requirements.txt
```
4. Run database migrations:
```
alembic upgrade head
```
5. Start the development server:
```
uvicorn main:app --reload
```
The API will be available at `http://localhost:8000`.
## API Documentation
Once the server is running, you can access the interactive API documentation:
- Swagger UI: `http://localhost:8000/docs`
- ReDoc: `http://localhost:8000/redoc`
## Key Endpoints
- **Authentication**:
- `POST /api/v1/auth/login`: Login and obtain access token
- `POST /api/v1/auth/register`: Register a new user
- **Users**:
- `GET /api/v1/users/me`: Get current user information
- `PUT /api/v1/users/me`: Update current user information
- **Products**:
- `GET /api/v1/products`: List all products
- `POST /api/v1/products`: Create a new product
- `GET /api/v1/products/{id}`: Get product details
- **Warehouses**:
- `GET /api/v1/warehouses`: List all warehouses
- `POST /api/v1/warehouses`: Create a new warehouse
- `GET /api/v1/warehouses/{id}`: Get warehouse details
- **Inventory**:
- `GET /api/v1/inventory`: List all inventory items
- `GET /api/v1/inventory/low-stock`: Get items with low stock levels
- `POST /api/v1/inventory`: Create a new inventory item
- **Orders**:
- `GET /api/v1/orders`: List all orders
- `POST /api/v1/orders`: Create a new order
- `GET /api/v1/orders/{id}`: Get order details
- **Shipments**:
- `GET /api/v1/shipments`: List all shipments
- `POST /api/v1/shipments`: Create a new shipment
- `GET /api/v1/shipments/{id}`: Get shipment details
- `GET /api/v1/shipments/tracking/{tracking_number}`: Track a shipment
- **Health**:
- `GET /health`: Check API and database health
- `GET /api/v1/health`: Detailed API health check
## Running in Production
For production deployment:
1. Set up a proper database (SQLite is for development only)
2. Change the `SECRET_KEY` to a secure random string
3. Configure CORS settings for your frontend domain
4. Use a proper ASGI server like Uvicorn or Gunicorn
Example production start command:
```
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
```
## License
This project is licensed under the MIT License.

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 example - 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

1
app/__init__.py Normal file
View File

@ -0,0 +1 @@
# Logistics Management System API

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

@ -0,0 +1 @@
# API module

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

@ -0,0 +1,22 @@
from fastapi import APIRouter
from app.api.endpoints import (
auth,
health,
inventory,
orders,
products,
shipments,
users,
warehouses,
)
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
api_router.include_router(users.router, prefix="/users", tags=["Users"])
api_router.include_router(products.router, prefix="/products", tags=["Products"])
api_router.include_router(warehouses.router, prefix="/warehouses", tags=["Warehouses"])
api_router.include_router(inventory.router, prefix="/inventory", tags=["Inventory"])
api_router.include_router(shipments.router, prefix="/shipments", tags=["Shipments"])
api_router.include_router(orders.router, prefix="/orders", tags=["Orders"])
api_router.include_router(health.router, prefix="/health", tags=["Health"])

View File

@ -0,0 +1 @@
# API endpoints

79
app/api/endpoints/auth.py Normal file
View File

@ -0,0 +1,79 @@
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.core.auth import create_access_token
from app.core.security import ACCESS_TOKEN_EXPIRE_MINUTES
from app.db.session import get_db
from app.schemas.token import Token
from app.schemas.user import User, UserCreate
from app.services import user as user_service
router = APIRouter()
@router.post("/login", response_model=Token)
def login_access_token(
db: Session = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
) -> Any:
"""
OAuth2 compatible token login, get an access token for future requests
"""
user = user_service.authenticate(
db, username_or_email=form_data.username, password=form_data.password
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username/email or password",
headers={"WWW-Authenticate": "Bearer"},
)
elif not user_service.is_active(user):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Inactive user",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": create_access_token(
sub=user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}
@router.post("/register", response_model=User)
def register_user(
*,
db: Session = Depends(get_db),
user_in: UserCreate,
) -> Any:
"""
Register a new user
"""
# Check if user with this email already exists
user = user_service.get_by_email(db, email=user_in.email)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The user with this email already exists",
)
# Check if user with this username already exists
user = user_service.get_by_username(db, username=user_in.username)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The user with this username already exists",
)
# Regular users cannot create a superuser
user_in.is_superuser = False
user = user_service.create(db, obj_in=user_in)
return user

View File

@ -0,0 +1,33 @@
from typing import Any
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from sqlalchemy import text
from app.db.session import get_db
router = APIRouter()
@router.get("/", status_code=status.HTTP_200_OK)
def health_check(db: Session = Depends(get_db)) -> dict[str, Any]:
"""
Health check endpoint to verify the application is running correctly
and database connection is working.
"""
health_data = {
"status": "ok",
"api": "healthy",
"database": "healthy"
}
# Check database connectivity
try:
# Execute a simple query
db.execute(text("SELECT 1"))
except Exception as e:
health_data["database"] = "unhealthy"
health_data["status"] = "error"
health_data["database_error"] = str(e)
return health_data

View File

@ -0,0 +1,172 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from app.core.deps import get_current_active_user, get_current_superuser
from app.db.session import get_db
from app.models.user import User
from app.schemas.inventory import (
Inventory,
InventoryCreate,
InventoryUpdate,
InventoryWithDetails,
)
from app.services import inventory as inventory_service
router = APIRouter()
@router.get("/", response_model=List[Inventory])
def read_inventory(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
warehouse_id: int = Query(None, description="Filter by warehouse ID"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve inventory items.
"""
inventory = inventory_service.get_multi(
db, skip=skip, limit=limit, warehouse_id=warehouse_id
)
return inventory
@router.get("/with-details", response_model=List[InventoryWithDetails])
def read_inventory_with_details(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
warehouse_id: int = Query(None, description="Filter by warehouse ID"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve inventory items with product and warehouse details.
"""
results = inventory_service.get_multi_with_details(
db, skip=skip, limit=limit, warehouse_id=warehouse_id
)
inventory_with_details = []
for inventory_item, product_name, warehouse_name in results:
inventory_dict = {**inventory_item.__dict__}
inventory_dict["product_name"] = product_name
inventory_dict["warehouse_name"] = warehouse_name
inventory_with_details.append(inventory_dict)
return inventory_with_details
@router.get("/low-stock", response_model=List[Inventory])
def read_low_stock_inventory(
db: Session = Depends(get_db),
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve inventory items that are below minimum stock level.
"""
inventory = inventory_service.get_low_stock_items(db, limit=limit)
return inventory
@router.post("/", response_model=Inventory)
def create_inventory(
*,
db: Session = Depends(get_db),
inventory_in: InventoryCreate,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Create new inventory item or update quantity if it already exists.
Requires superuser access.
"""
inventory = inventory_service.create(db, obj_in=inventory_in)
return inventory
@router.get("/{inventory_id}", response_model=Inventory)
def read_inventory_item(
*,
db: Session = Depends(get_db),
inventory_id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get inventory item by ID.
"""
inventory = inventory_service.get(db, inventory_id=inventory_id)
if not inventory:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Inventory item not found",
)
return inventory
@router.get("/{inventory_id}/details", response_model=InventoryWithDetails)
def read_inventory_item_with_details(
*,
db: Session = Depends(get_db),
inventory_id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get inventory item by ID with product and warehouse details.
"""
result = inventory_service.get_with_details(db, inventory_id=inventory_id)
if not result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Inventory item not found",
)
inventory, product_name, warehouse_name = result
inventory_dict = {**inventory.__dict__}
inventory_dict["product_name"] = product_name
inventory_dict["warehouse_name"] = warehouse_name
return inventory_dict
@router.put("/{inventory_id}", response_model=Inventory)
def update_inventory(
*,
db: Session = Depends(get_db),
inventory_id: int,
inventory_in: InventoryUpdate,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Update an inventory item. Requires superuser access.
"""
inventory = inventory_service.get(db, inventory_id=inventory_id)
if not inventory:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Inventory item not found",
)
inventory = inventory_service.update(db, db_obj=inventory, obj_in=inventory_in)
return inventory
@router.delete("/{inventory_id}", response_model=Inventory)
def delete_inventory(
*,
db: Session = Depends(get_db),
inventory_id: int,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Delete an inventory item. Requires superuser access.
"""
inventory = inventory_service.get(db, inventory_id=inventory_id)
if not inventory:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Inventory item not found",
)
inventory = inventory_service.remove(db, inventory_id=inventory_id)
return inventory

215
app/api/endpoints/orders.py Normal file
View File

@ -0,0 +1,215 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Path, status
from sqlalchemy.orm import Session
from app.core.deps import get_current_active_user
from app.db.session import get_db
from app.models.order import OrderStatus
from app.models.user import User
from app.schemas.order import Order, OrderCreate, OrderItem, OrderUpdate
from app.services import order as order_service
router = APIRouter()
@router.get("/", response_model=List[Order])
def read_orders(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve orders.
"""
if not current_user.is_superuser:
# Regular users can only see their own orders
orders = order_service.get_multi(
db, skip=skip, limit=limit, user_id=current_user.id
)
else:
# Superusers can see all orders
orders = order_service.get_multi(db, skip=skip, limit=limit)
return orders
@router.post("/", response_model=Order)
def create_order(
*,
db: Session = Depends(get_db),
order_in: OrderCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Create new order.
"""
# Regular users can only create orders for themselves
if not current_user.is_superuser and order_in.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You can only create orders for yourself",
)
try:
order = order_service.create(db, obj_in=order_in)
return order
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
@router.get("/{order_id}", response_model=Order)
def read_order(
*,
db: Session = Depends(get_db),
order_id: int = Path(..., description="The ID of the order to get"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get order by ID.
"""
order = order_service.get(db, order_id=order_id)
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found",
)
# Regular users can only see their own orders
if not current_user.is_superuser and order.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
return order
@router.put("/{order_id}", response_model=Order)
def update_order(
*,
db: Session = Depends(get_db),
order_id: int = Path(..., description="The ID of the order to update"),
order_in: OrderUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update an order.
"""
order = order_service.get(db, order_id=order_id)
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found",
)
# Regular users can only update their own orders
if not current_user.is_superuser and order.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
# Regular users cannot update orders that are already shipped or delivered
if not current_user.is_superuser and order.status in [OrderStatus.SHIPPED, OrderStatus.DELIVERED]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot update an order that has already been shipped or delivered",
)
order = order_service.update(db, db_obj=order, obj_in=order_in)
return order
@router.put("/{order_id}/status", response_model=Order)
def update_order_status(
*,
db: Session = Depends(get_db),
order_id: int = Path(..., description="The ID of the order to update"),
status: OrderStatus,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update order status.
"""
order = order_service.get(db, order_id=order_id)
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found",
)
# Only superusers can change order status
if not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
order = order_service.update_status(db, order_id=order_id, status=status)
return order
@router.delete("/{order_id}/cancel", response_model=Order)
def cancel_order(
*,
db: Session = Depends(get_db),
order_id: int = Path(..., description="The ID of the order to cancel"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Cancel an order if it hasn't been shipped yet.
"""
order = order_service.get(db, order_id=order_id)
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found",
)
# Regular users can only cancel their own orders
if not current_user.is_superuser and order.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
try:
order = order_service.cancel_order(db, order_id=order_id)
return order
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
@router.get("/{order_id}/items", response_model=List[OrderItem])
def read_order_items(
*,
db: Session = Depends(get_db),
order_id: int = Path(..., description="The ID of the order to get items for"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get all items for a specific order.
"""
order = order_service.get(db, order_id=order_id)
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found",
)
# Regular users can only see their own orders' items
if not current_user.is_superuser and order.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
items = order_service.get_order_items(db, order_id=order_id)
return items

View File

@ -0,0 +1,109 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from app.core.deps import get_current_active_user, get_current_superuser
from app.db.session import get_db
from app.models.user import User
from app.schemas.product import Product, ProductCreate, ProductUpdate
from app.services import product as product_service
router = APIRouter()
@router.get("/", response_model=List[Product])
def read_products(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
active_only: bool = Query(False, description="Filter only active products"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve products.
"""
products = product_service.get_multi(
db, skip=skip, limit=limit, active_only=active_only
)
return products
@router.post("/", response_model=Product)
def create_product(
*,
db: Session = Depends(get_db),
product_in: ProductCreate,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Create new product. Requires superuser access.
"""
product = product_service.get_by_sku(db, sku=product_in.sku)
if product:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A product with this SKU already exists",
)
product = product_service.create(db, obj_in=product_in)
return product
@router.get("/{product_id}", response_model=Product)
def read_product(
*,
db: Session = Depends(get_db),
product_id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get product by ID.
"""
product = product_service.get(db, product_id=product_id)
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found",
)
return product
@router.put("/{product_id}", response_model=Product)
def update_product(
*,
db: Session = Depends(get_db),
product_id: int,
product_in: ProductUpdate,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Update a product. Requires superuser access.
"""
product = product_service.get(db, product_id=product_id)
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found",
)
product = product_service.update(db, db_obj=product, obj_in=product_in)
return product
@router.delete("/{product_id}", response_model=Product)
def delete_product(
*,
db: Session = Depends(get_db),
product_id: int,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Delete a product. Requires superuser access.
"""
product = product_service.get(db, product_id=product_id)
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found",
)
product = product_service.remove(db, product_id=product_id)
return product

View File

@ -0,0 +1,190 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
from sqlalchemy.orm import Session
from app.core.deps import get_current_active_user
from app.db.session import get_db
from app.models.shipment import ShipmentStatus
from app.models.user import User
from app.schemas.shipment import (
Shipment,
ShipmentCreate,
ShipmentItem,
ShipmentTracking,
ShipmentUpdate,
)
from app.services import shipment as shipment_service
router = APIRouter()
@router.get("/", response_model=List[Shipment])
def read_shipments(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
order_id: int = Query(None, description="Filter by order ID"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve shipments.
"""
shipments = shipment_service.get_multi(
db, skip=skip, limit=limit, order_id=order_id
)
return shipments
@router.post("/", response_model=Shipment)
def create_shipment(
*,
db: Session = Depends(get_db),
shipment_in: ShipmentCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Create new shipment.
"""
# Set current user as creator if not provided
if not shipment_in.created_by_id:
shipment_in.created_by_id = current_user.id
# Only superusers can create shipments for other users
if not current_user.is_superuser and shipment_in.created_by_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You can only create shipments for yourself",
)
try:
shipment = shipment_service.create(db, obj_in=shipment_in)
return shipment
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
@router.get("/{shipment_id}", response_model=Shipment)
def read_shipment(
*,
db: Session = Depends(get_db),
shipment_id: int = Path(..., description="The ID of the shipment to get"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get shipment by ID.
"""
shipment = shipment_service.get(db, shipment_id=shipment_id)
if not shipment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Shipment not found",
)
return shipment
@router.get("/tracking/{tracking_number}", response_model=ShipmentTracking)
def read_shipment_by_tracking(
*,
db: Session = Depends(get_db),
tracking_number: str = Path(..., description="The tracking number of the shipment"),
) -> Any:
"""
Get shipment tracking information by tracking number. This endpoint is public.
"""
shipment = shipment_service.get_by_tracking_number(db, tracking_number=tracking_number)
if not shipment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Shipment not found",
)
return {
"tracking_number": shipment.tracking_number,
"status": shipment.status,
"carrier": shipment.carrier,
"estimated_delivery": shipment.estimated_delivery,
"actual_delivery": shipment.actual_delivery,
}
@router.put("/{shipment_id}", response_model=Shipment)
def update_shipment(
*,
db: Session = Depends(get_db),
shipment_id: int = Path(..., description="The ID of the shipment to update"),
shipment_in: ShipmentUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update a shipment.
"""
shipment = shipment_service.get(db, shipment_id=shipment_id)
if not shipment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Shipment not found",
)
# Only superusers or the creator can update shipments
if not current_user.is_superuser and shipment.created_by_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
shipment = shipment_service.update(db, db_obj=shipment, obj_in=shipment_in)
return shipment
@router.put("/{shipment_id}/status", response_model=Shipment)
def update_shipment_status(
*,
db: Session = Depends(get_db),
shipment_id: int = Path(..., description="The ID of the shipment to update"),
status: ShipmentStatus,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update shipment status.
"""
shipment = shipment_service.get(db, shipment_id=shipment_id)
if not shipment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Shipment not found",
)
# Only superusers or the creator can update shipment status
if not current_user.is_superuser and shipment.created_by_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
shipment = shipment_service.update_status(db, shipment_id=shipment_id, status=status)
return shipment
@router.get("/{shipment_id}/items", response_model=List[ShipmentItem])
def read_shipment_items(
*,
db: Session = Depends(get_db),
shipment_id: int = Path(..., description="The ID of the shipment to get items for"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get all items for a specific shipment.
"""
shipment = shipment_service.get(db, shipment_id=shipment_id)
if not shipment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Shipment not found",
)
items = shipment_service.get_shipment_items(db, shipment_id=shipment_id)
return items

156
app/api/endpoints/users.py Normal file
View File

@ -0,0 +1,156 @@
from typing import Any, List
from fastapi import APIRouter, Body, Depends, HTTPException, status
from fastapi.encoders import jsonable_encoder
from pydantic import EmailStr
from sqlalchemy.orm import Session
from app.core.deps import get_current_active_user, get_current_superuser
from app.db.session import get_db
from app.models.user import User
from app.schemas.user import User as UserSchema
from app.schemas.user import UserCreate, UserUpdate
from app.services import user as user_service
router = APIRouter()
@router.get("/", response_model=List[UserSchema])
def read_users(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Retrieve users. Requires superuser access.
"""
users = user_service.get_multi(db, skip=skip, limit=limit)
return users
@router.post("/", response_model=UserSchema)
def create_user(
*,
db: Session = Depends(get_db),
user_in: UserCreate,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Create new user. Requires superuser access.
"""
user = user_service.get_by_email(db, email=user_in.email)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The user with this email already exists",
)
user = user_service.get_by_username(db, username=user_in.username)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The user with this username already exists",
)
user = user_service.create(db, obj_in=user_in)
return user
@router.get("/me", response_model=UserSchema)
def read_user_me(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get current user.
"""
return current_user
@router.put("/me", response_model=UserSchema)
def update_user_me(
*,
db: Session = Depends(get_db),
password: str = Body(None),
full_name: str = Body(None),
email: EmailStr = Body(None),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update own user.
"""
current_user_data = jsonable_encoder(current_user)
user_in = UserUpdate(**current_user_data)
if password is not None:
user_in.password = password
if full_name is not None:
user_in.full_name = full_name
if email is not None:
user_in.email = email
user = user_service.update(db, db_obj=current_user, obj_in=user_in)
return user
@router.get("/{user_id}", response_model=UserSchema)
def read_user_by_id(
user_id: int,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db),
) -> Any:
"""
Get a specific user by id.
"""
user = user_service.get(db, user_id=user_id)
if user == current_user:
return user
if not user_service.is_superuser(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
return user
@router.put("/{user_id}", response_model=UserSchema)
def update_user(
*,
db: Session = Depends(get_db),
user_id: int,
user_in: UserUpdate,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Update a user. Requires superuser access.
"""
user = user_service.get(db, user_id=user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
user = user_service.update(db, db_obj=user, obj_in=user_in)
return user
@router.delete("/{user_id}", response_model=UserSchema)
def delete_user(
*,
db: Session = Depends(get_db),
user_id: int,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Delete a user. Requires superuser access.
"""
user = user_service.get(db, user_id=user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
if user.id == current_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete self",
)
user = user_service.remove(db, user_id=user_id)
return user

View File

@ -0,0 +1,109 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from app.core.deps import get_current_active_user, get_current_superuser
from app.db.session import get_db
from app.models.user import User
from app.schemas.warehouse import Warehouse, WarehouseCreate, WarehouseUpdate
from app.services import warehouse as warehouse_service
router = APIRouter()
@router.get("/", response_model=List[Warehouse])
def read_warehouses(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
active_only: bool = Query(False, description="Filter only active warehouses"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve warehouses.
"""
warehouses = warehouse_service.get_multi(
db, skip=skip, limit=limit, active_only=active_only
)
return warehouses
@router.post("/", response_model=Warehouse)
def create_warehouse(
*,
db: Session = Depends(get_db),
warehouse_in: WarehouseCreate,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Create new warehouse. Requires superuser access.
"""
warehouse = warehouse_service.get_by_code(db, code=warehouse_in.code)
if warehouse:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A warehouse with this code already exists",
)
warehouse = warehouse_service.create(db, obj_in=warehouse_in)
return warehouse
@router.get("/{warehouse_id}", response_model=Warehouse)
def read_warehouse(
*,
db: Session = Depends(get_db),
warehouse_id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get warehouse by ID.
"""
warehouse = warehouse_service.get(db, warehouse_id=warehouse_id)
if not warehouse:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Warehouse not found",
)
return warehouse
@router.put("/{warehouse_id}", response_model=Warehouse)
def update_warehouse(
*,
db: Session = Depends(get_db),
warehouse_id: int,
warehouse_in: WarehouseUpdate,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Update a warehouse. Requires superuser access.
"""
warehouse = warehouse_service.get(db, warehouse_id=warehouse_id)
if not warehouse:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Warehouse not found",
)
warehouse = warehouse_service.update(db, db_obj=warehouse, obj_in=warehouse_in)
return warehouse
@router.delete("/{warehouse_id}", response_model=Warehouse)
def delete_warehouse(
*,
db: Session = Depends(get_db),
warehouse_id: int,
current_user: User = Depends(get_current_superuser),
) -> Any:
"""
Delete a warehouse. Requires superuser access.
"""
warehouse = warehouse_service.get(db, warehouse_id=warehouse_id)
if not warehouse:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Warehouse not found",
)
warehouse = warehouse_service.remove(db, warehouse_id=warehouse_id)
return warehouse

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

@ -0,0 +1 @@
# Core functionality

70
app/core/auth.py Normal file
View File

@ -0,0 +1,70 @@
from datetime import datetime, timedelta
from typing import Any, Optional, Union
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.core.security import ACCESS_TOKEN_EXPIRE_MINUTES, ALGORITHM, SECRET_KEY
from app.db.session import get_db
from app.models.user import User
from app.schemas.token import TokenPayload
from app.services import user as user_service
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
def create_access_token(*, sub: Union[str, Any], expires_delta: Optional[timedelta] = None) -> str:
"""
Create a JWT access token for authentication
"""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = {"exp": expire, "sub": str(sub)}
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)) -> User:
"""
Validate access token and return current user
"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
token_data = TokenPayload(**payload)
except (JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
user = user_service.get(db, user_id=token_data.sub)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
return user
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
"""
Validate that the current user is active
"""
if not user_service.is_active(current_user):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user")
return current_user
def get_current_superuser(current_user: User = Depends(get_current_active_user)) -> User:
"""
Validate that the current user is a superuser
"""
if not user_service.is_superuser(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions"
)
return current_user

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

@ -0,0 +1,37 @@
from pathlib import Path
from typing import List, Union
from pydantic import AnyHttpUrl, Field, validator
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
API_V1_STR: str = "/api/v1"
PROJECT_NAME: str = "Logistics Management System"
# CORS configuration
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
@validator("BACKEND_CORS_ORIGINS", pre=True)
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",")]
elif isinstance(v, (list, str)):
return v
raise ValueError(v)
# Database settings
DB_DIR: Path = Path("/app") / "storage" / "db"
SQLALCHEMY_DATABASE_URL: str = Field(
default_factory=lambda: f"sqlite:///{Settings.DB_DIR}/db.sqlite"
)
class Config:
case_sensitive = True
env_file = ".env"
settings = Settings()
# Create the database directory if it doesn't exist
settings.DB_DIR.mkdir(parents=True, exist_ok=True)

50
app/core/deps.py Normal file
View File

@ -0,0 +1,50 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.core.security import ALGORITHM, SECRET_KEY
from app.db.session import get_db
from app.models.user import User
from app.schemas.token import TokenPayload
from app.services import user as user_service
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
) -> User:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
token_data = TokenPayload(**payload)
except (JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
user = user_service.get(db, user_id=token_data.sub)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
return user
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
if not user_service.is_active(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user"
)
return current_user
def get_current_superuser(current_user: User = Depends(get_current_active_user)) -> User:
if not user_service.is_superuser(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions"
)
return current_user

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

@ -0,0 +1,32 @@
from datetime import datetime, timedelta
from typing import Any, Union
from jose import jwt
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# These values would typically be set in the environment or config
SECRET_KEY = "CHANGEME_IN_PRODUCTION_THIS_IS_A_PLACEHOLDER_SECRET"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(
subject: Union[str, Any], expires_delta: timedelta = None
) -> str:
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)

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

@ -0,0 +1 @@
# Database module

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

@ -0,0 +1,9 @@
# Import all the models, so that Base has them before being
# imported by Alembic
from app.db.session import Base # noqa
from app.models.user import User # noqa
from app.models.product import Product # noqa
from app.models.warehouse import Warehouse # noqa
from app.models.inventory import Inventory # noqa
from app.models.shipment import Shipment # noqa
from app.models.order import Order # noqa

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

@ -0,0 +1,21 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
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)
Base = declarative_base()
# Dependency to get DB session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

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

@ -0,0 +1 @@
# Database models

25
app/models/inventory.py Normal file
View File

@ -0,0 +1,25 @@
from sqlalchemy import Column, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy.orm import relationship
from app.db.session import Base
class Inventory(Base):
__tablename__ = "inventory"
id = Column(Integer, primary_key=True, index=True)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False)
quantity = Column(Integer, nullable=False, default=0)
location = Column(String) # Specific location within the warehouse (e.g., "Aisle 5, Shelf B")
min_stock_level = Column(Integer, default=0)
max_stock_level = Column(Integer, default=0)
# Ensure a product can only have one inventory record per warehouse
__table_args__ = (
UniqueConstraint('product_id', 'warehouse_id', name='uix_product_warehouse'),
)
# Relationships
product = relationship("Product", back_populates="inventory_items")
warehouse = relationship("Warehouse", back_populates="inventory")

48
app/models/order.py Normal file
View File

@ -0,0 +1,48 @@
import enum
from sqlalchemy import Column, DateTime, Enum, Float, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.session import Base
class OrderStatus(str, enum.Enum):
PENDING = "pending"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class Order(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
order_number = Column(String, unique=True, index=True, nullable=False)
status = Column(Enum(OrderStatus), default=OrderStatus.PENDING)
total_amount = Column(Float, nullable=False, default=0.0)
shipping_address = Column(String, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
user = relationship("User", back_populates="orders")
order_items = relationship("OrderItem", back_populates="order")
shipments = relationship("Shipment", back_populates="order")
class OrderItem(Base):
__tablename__ = "order_items"
id = Column(Integer, primary_key=True, index=True)
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
quantity = Column(Integer, nullable=False, default=1)
unit_price = Column(Float, nullable=False)
total_price = Column(Float, nullable=False)
# Relationships
order = relationship("Order", back_populates="order_items")
product = relationship("Product", back_populates="order_items")

22
app/models/product.py Normal file
View File

@ -0,0 +1,22 @@
from sqlalchemy import Boolean, Column, Float, Integer, String, Text
from sqlalchemy.orm import relationship
from app.db.session import Base
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True, nullable=False)
sku = Column(String, unique=True, index=True, nullable=False)
description = Column(Text)
weight = Column(Float, nullable=False, default=0.0) # in kg
dimensions = Column(String) # format: "length x width x height" in cm
price = Column(Float, nullable=False, default=0.0)
is_active = Column(Boolean, default=True)
# Relationships
inventory_items = relationship("Inventory", back_populates="product")
order_items = relationship("OrderItem", back_populates="product")
shipment_items = relationship("ShipmentItem", back_populates="product")

54
app/models/shipment.py Normal file
View File

@ -0,0 +1,54 @@
import enum
from sqlalchemy import Column, DateTime, Enum, Float, ForeignKey, Integer, String, Text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.session import Base
class ShipmentStatus(str, enum.Enum):
PENDING = "pending"
PROCESSING = "processing"
IN_TRANSIT = "in_transit"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class Shipment(Base):
__tablename__ = "shipments"
id = Column(Integer, primary_key=True, index=True)
tracking_number = Column(String, unique=True, index=True, nullable=False)
order_id = Column(Integer, ForeignKey("orders.id"), nullable=True) # Optional - can be internal movement
origin_warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False)
destination_warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=True) # Null for direct customer delivery
customer_address = Column(Text, nullable=True) # Required if destination_warehouse_id is null
status = Column(Enum(ShipmentStatus), default=ShipmentStatus.PENDING)
carrier = Column(String, nullable=True)
estimated_delivery = Column(DateTime(timezone=True), nullable=True)
actual_delivery = Column(DateTime(timezone=True), nullable=True)
shipping_cost = Column(Float, default=0.0)
created_by_id = Column(Integer, ForeignKey("users.id"), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
order = relationship("Order", back_populates="shipments")
origin_warehouse = relationship("Warehouse", foreign_keys=[origin_warehouse_id], back_populates="shipments_from")
destination_warehouse = relationship("Warehouse", foreign_keys=[destination_warehouse_id], back_populates="shipments_to")
created_by = relationship("User", back_populates="shipments")
items = relationship("ShipmentItem", back_populates="shipment")
class ShipmentItem(Base):
__tablename__ = "shipment_items"
id = Column(Integer, primary_key=True, index=True)
shipment_id = Column(Integer, ForeignKey("shipments.id"), nullable=False)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
quantity = Column(Integer, nullable=False, default=1)
# Relationships
shipment = relationship("Shipment", back_populates="items")
product = relationship("Product", back_populates="shipment_items")

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

@ -0,0 +1,20 @@
from sqlalchemy import Boolean, Column, Integer, String
from sqlalchemy.orm import relationship
from app.db.session import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
username = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
full_name = Column(String)
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
# Relationships
orders = relationship("Order", back_populates="user")
shipments = relationship("Shipment", back_populates="created_by")

23
app/models/warehouse.py Normal file
View File

@ -0,0 +1,23 @@
from sqlalchemy import Boolean, Column, Integer, String, Text
from sqlalchemy.orm import relationship
from app.db.session import Base
class Warehouse(Base):
__tablename__ = "warehouses"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True, nullable=False)
code = Column(String, unique=True, index=True, nullable=False)
address = Column(Text, nullable=False)
city = Column(String, nullable=False)
state = Column(String, nullable=False)
country = Column(String, nullable=False)
postal_code = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
# Relationships
inventory = relationship("Inventory", back_populates="warehouse")
shipments_from = relationship("Shipment", foreign_keys="Shipment.origin_warehouse_id", back_populates="origin_warehouse")
shipments_to = relationship("Shipment", foreign_keys="Shipment.destination_warehouse_id", back_populates="destination_warehouse")

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

@ -0,0 +1,34 @@
# Import all schemas here for convenience
from app.schemas.inventory import (
Inventory,
InventoryCreate,
InventoryInDB,
InventoryUpdate,
InventoryWithDetails,
)
from app.schemas.order import (
Order,
OrderCreate,
OrderInDB,
OrderItem,
OrderItemCreate,
OrderUpdate,
)
from app.schemas.product import Product, ProductCreate, ProductInDB, ProductUpdate
from app.schemas.shipment import (
Shipment,
ShipmentCreate,
ShipmentInDB,
ShipmentItem,
ShipmentItemCreate,
ShipmentTracking,
ShipmentUpdate,
)
from app.schemas.token import Token, TokenPayload
from app.schemas.user import User, UserCreate, UserInDB, UserUpdate
from app.schemas.warehouse import (
Warehouse,
WarehouseCreate,
WarehouseInDB,
WarehouseUpdate,
)

48
app/schemas/inventory.py Normal file
View File

@ -0,0 +1,48 @@
from typing import Optional
from pydantic import BaseModel, Field
# Shared properties
class InventoryBase(BaseModel):
product_id: Optional[int] = None
warehouse_id: Optional[int] = None
quantity: Optional[int] = Field(0, ge=0)
location: Optional[str] = None
min_stock_level: Optional[int] = Field(0, ge=0)
max_stock_level: Optional[int] = Field(0, ge=0)
# Properties to receive via API on creation
class InventoryCreate(InventoryBase):
product_id: int
warehouse_id: int
quantity: int
# Properties to receive via API on update
class InventoryUpdate(InventoryBase):
pass
class InventoryInDBBase(InventoryBase):
id: int
class Config:
from_attributes = True
# Additional properties to return via API
class Inventory(InventoryInDBBase):
pass
# Additional properties stored in DB
class InventoryInDB(InventoryInDBBase):
pass
# Inventory with related product and warehouse information
class InventoryWithDetails(Inventory):
product_name: str
warehouse_name: str

74
app/schemas/order.py Normal file
View File

@ -0,0 +1,74 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field
from app.models.order import OrderStatus
# Order Item Schema
class OrderItemBase(BaseModel):
product_id: int
quantity: int = Field(1, gt=0)
unit_price: Optional[float] = None
class OrderItemCreate(OrderItemBase):
pass
class OrderItemUpdate(OrderItemBase):
pass
class OrderItemInDBBase(OrderItemBase):
id: int
order_id: int
total_price: float
class Config:
from_attributes = True
class OrderItem(OrderItemInDBBase):
pass
class OrderItemInDB(OrderItemInDBBase):
pass
# Order Schema
class OrderBase(BaseModel):
user_id: Optional[int] = None
shipping_address: Optional[str] = None
status: Optional[OrderStatus] = OrderStatus.PENDING
class OrderCreate(OrderBase):
user_id: int
shipping_address: str
items: List[OrderItemCreate]
class OrderUpdate(OrderBase):
pass
class OrderInDBBase(OrderBase):
id: int
order_number: str
total_amount: float
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class Order(OrderInDBBase):
items: List[OrderItem] = []
class OrderInDB(OrderInDBBase):
pass

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

@ -0,0 +1,43 @@
from typing import Optional
from pydantic import BaseModel, Field
# Shared properties
class ProductBase(BaseModel):
name: Optional[str] = None
sku: Optional[str] = None
description: Optional[str] = None
weight: Optional[float] = Field(None, description="Weight in kg")
dimensions: Optional[str] = Field(None, description="Format: 'length x width x height' in cm")
price: Optional[float] = None
is_active: Optional[bool] = True
# Properties to receive via API on creation
class ProductCreate(ProductBase):
name: str
sku: str
price: float
# Properties to receive via API on update
class ProductUpdate(ProductBase):
pass
class ProductInDBBase(ProductBase):
id: int
class Config:
from_attributes = True
# Additional properties to return via API
class Product(ProductInDBBase):
pass
# Additional properties stored in DB
class ProductInDB(ProductInDBBase):
pass

89
app/schemas/shipment.py Normal file
View File

@ -0,0 +1,89 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field
from app.models.shipment import ShipmentStatus
# Shipment Item Schema
class ShipmentItemBase(BaseModel):
product_id: int
quantity: int = Field(1, gt=0)
class ShipmentItemCreate(ShipmentItemBase):
pass
class ShipmentItemUpdate(ShipmentItemBase):
pass
class ShipmentItemInDBBase(ShipmentItemBase):
id: int
shipment_id: int
class Config:
from_attributes = True
class ShipmentItem(ShipmentItemInDBBase):
pass
class ShipmentItemInDB(ShipmentItemInDBBase):
pass
# Shipment Schema
class ShipmentBase(BaseModel):
order_id: Optional[int] = None
origin_warehouse_id: Optional[int] = None
destination_warehouse_id: Optional[int] = None
customer_address: Optional[str] = None
status: Optional[ShipmentStatus] = ShipmentStatus.PENDING
carrier: Optional[str] = None
estimated_delivery: Optional[datetime] = None
shipping_cost: Optional[float] = 0.0
created_by_id: Optional[int] = None
class ShipmentCreate(ShipmentBase):
origin_warehouse_id: int
created_by_id: int
items: List[ShipmentItemCreate]
# Either destination_warehouse_id or customer_address must be provided
# This will be validated in the service layer
class ShipmentUpdate(ShipmentBase):
pass
class ShipmentInDBBase(ShipmentBase):
id: int
tracking_number: str
actual_delivery: Optional[datetime] = None
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class Shipment(ShipmentInDBBase):
items: List[ShipmentItem] = []
class ShipmentInDB(ShipmentInDBBase):
pass
# Schema for tracking info that can be shared publicly
class ShipmentTracking(BaseModel):
tracking_number: str
status: ShipmentStatus
carrier: Optional[str] = None
estimated_delivery: Optional[datetime] = None
actual_delivery: Optional[datetime] = None

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

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

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

@ -0,0 +1,41 @@
from typing import Optional
from pydantic import BaseModel, EmailStr
# Shared properties
class UserBase(BaseModel):
email: Optional[EmailStr] = None
username: Optional[str] = None
is_active: Optional[bool] = True
is_superuser: bool = False
full_name: Optional[str] = None
# Properties to receive via API on creation
class UserCreate(UserBase):
email: EmailStr
username: str
password: str
# Properties to receive via API on update
class UserUpdate(UserBase):
password: Optional[str] = None
class UserInDBBase(UserBase):
id: Optional[int] = None
class Config:
from_attributes = True
# Additional properties to return via API
class User(UserInDBBase):
pass
# Additional properties stored in DB but not returned by API
class UserInDB(UserInDBBase):
hashed_password: str

48
app/schemas/warehouse.py Normal file
View File

@ -0,0 +1,48 @@
from typing import Optional
from pydantic import BaseModel
# Shared properties
class WarehouseBase(BaseModel):
name: Optional[str] = None
code: Optional[str] = None
address: Optional[str] = None
city: Optional[str] = None
state: Optional[str] = None
country: Optional[str] = None
postal_code: Optional[str] = None
is_active: Optional[bool] = True
# Properties to receive via API on creation
class WarehouseCreate(WarehouseBase):
name: str
code: str
address: str
city: str
state: str
country: str
postal_code: str
# Properties to receive via API on update
class WarehouseUpdate(WarehouseBase):
pass
class WarehouseInDBBase(WarehouseBase):
id: int
class Config:
from_attributes = True
# Additional properties to return via API
class Warehouse(WarehouseInDBBase):
pass
# Additional properties stored in DB
class WarehouseInDB(WarehouseInDBBase):
pass

1
app/services/__init__.py Normal file
View File

@ -0,0 +1 @@
# Business logic services

138
app/services/inventory.py Normal file
View File

@ -0,0 +1,138 @@
from typing import Any, Dict, List, Optional, Tuple, Union
from sqlalchemy.orm import Session
from app.models.inventory import Inventory
from app.models.product import Product
from app.models.warehouse import Warehouse
from app.schemas.inventory import InventoryCreate, InventoryUpdate
def get(db: Session, inventory_id: int) -> Optional[Inventory]:
return db.query(Inventory).filter(Inventory.id == inventory_id).first()
def get_by_product_and_warehouse(
db: Session, *, product_id: int, warehouse_id: int
) -> Optional[Inventory]:
return (
db.query(Inventory)
.filter(Inventory.product_id == product_id, Inventory.warehouse_id == warehouse_id)
.first()
)
def get_multi(
db: Session, *, skip: int = 0, limit: int = 100, warehouse_id: Optional[int] = None
) -> List[Inventory]:
query = db.query(Inventory)
if warehouse_id:
query = query.filter(Inventory.warehouse_id == warehouse_id)
return query.offset(skip).limit(limit).all()
def create(db: Session, *, obj_in: InventoryCreate) -> Inventory:
# Check if inventory already exists for this product/warehouse combination
existing = get_by_product_and_warehouse(
db, product_id=obj_in.product_id, warehouse_id=obj_in.warehouse_id
)
if existing:
# Update quantity instead of creating new
existing.quantity += obj_in.quantity
db.add(existing)
db.commit()
db.refresh(existing)
return existing
# Create new inventory record
db_obj = Inventory(
product_id=obj_in.product_id,
warehouse_id=obj_in.warehouse_id,
quantity=obj_in.quantity,
location=obj_in.location,
min_stock_level=obj_in.min_stock_level,
max_stock_level=obj_in.max_stock_level,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
db: Session, *, db_obj: Inventory, obj_in: Union[InventoryUpdate, Dict[str, Any]]
) -> Inventory:
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.dict(exclude_unset=True)
for field in update_data:
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def remove(db: Session, *, inventory_id: int) -> Inventory:
obj = db.query(Inventory).get(inventory_id)
db.delete(obj)
db.commit()
return obj
def get_with_details(db: Session, inventory_id: int) -> Optional[Tuple[Inventory, str, str]]:
"""Get inventory with product and warehouse names"""
result = (
db.query(Inventory, Product.name, Warehouse.name)
.join(Product)
.join(Warehouse)
.filter(Inventory.id == inventory_id)
.first()
)
if result:
inventory, product_name, warehouse_name = result
return inventory, product_name, warehouse_name
return None
def get_multi_with_details(
db: Session, *, skip: int = 0, limit: int = 100, warehouse_id: Optional[int] = None
) -> List[Tuple[Inventory, str, str]]:
"""Get multiple inventory items with product and warehouse names"""
query = (
db.query(Inventory, Product.name, Warehouse.name)
.join(Product)
.join(Warehouse)
)
if warehouse_id:
query = query.filter(Inventory.warehouse_id == warehouse_id)
return query.offset(skip).limit(limit).all()
def update_quantity(
db: Session, *, inventory_id: int, quantity_change: int
) -> Optional[Inventory]:
"""Update inventory quantity by adding the quantity_change (can be negative)"""
inventory = get(db, inventory_id)
if not inventory:
return None
inventory.quantity += quantity_change
if inventory.quantity < 0:
inventory.quantity = 0
db.add(inventory)
db.commit()
db.refresh(inventory)
return inventory
def get_low_stock_items(db: Session, *, limit: int = 100) -> List[Inventory]:
"""Get inventory items where quantity is below min_stock_level"""
return (
db.query(Inventory)
.filter(Inventory.quantity < Inventory.min_stock_level)
.limit(limit)
.all()
)

142
app/services/order.py Normal file
View File

@ -0,0 +1,142 @@
import uuid
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from sqlalchemy.orm import Session
from app.models.order import Order, OrderItem, OrderStatus
from app.models.product import Product
from app.schemas.order import OrderCreate, OrderUpdate
def generate_order_number() -> str:
"""Generate a unique order number"""
# Format: ORD-{random_uuid}
return f"ORD-{uuid.uuid4().hex[:8].upper()}"
def get(db: Session, order_id: int) -> Optional[Order]:
return db.query(Order).filter(Order.id == order_id).first()
def get_by_order_number(db: Session, order_number: str) -> Optional[Order]:
return db.query(Order).filter(Order.order_number == order_number).first()
def get_multi(
db: Session, *, skip: int = 0, limit: int = 100, user_id: Optional[int] = None
) -> List[Order]:
query = db.query(Order)
if user_id:
query = query.filter(Order.user_id == user_id)
return query.order_by(Order.created_at.desc()).offset(skip).limit(limit).all()
def create(db: Session, *, obj_in: OrderCreate) -> Order:
# Calculate total order amount based on current product prices
total_amount = 0.0
order_items = []
for item in obj_in.items:
product = db.query(Product).get(item.product_id)
if not product:
raise ValueError(f"Product with ID {item.product_id} not found")
# Use provided unit price or get from product
unit_price = item.unit_price if item.unit_price is not None else product.price
item_total = unit_price * item.quantity
total_amount += item_total
order_items.append({
"product_id": item.product_id,
"quantity": item.quantity,
"unit_price": unit_price,
"total_price": item_total
})
# Create order
db_obj = Order(
user_id=obj_in.user_id,
order_number=generate_order_number(),
status=obj_in.status or OrderStatus.PENDING,
total_amount=total_amount,
shipping_address=obj_in.shipping_address,
created_at=datetime.now(),
)
db.add(db_obj)
db.flush() # Get the order ID without committing transaction
# Create order items
for item_data in order_items:
order_item = OrderItem(
order_id=db_obj.id,
product_id=item_data["product_id"],
quantity=item_data["quantity"],
unit_price=item_data["unit_price"],
total_price=item_data["total_price"]
)
db.add(order_item)
db.commit()
db.refresh(db_obj)
return db_obj
def update(db: Session, *, db_obj: Order, obj_in: Union[OrderUpdate, Dict[str, Any]]) -> Order:
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.dict(exclude_unset=True)
# Don't allow changing order_number or created_at
update_data.pop("order_number", None)
update_data.pop("created_at", None)
# Set updated_at
update_data["updated_at"] = datetime.now()
for field in update_data:
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update_status(db: Session, *, order_id: int, status: OrderStatus) -> Optional[Order]:
"""Update order status"""
order = get(db, order_id)
if not order:
return None
order.status = status
order.updated_at = datetime.now()
db.add(order)
db.commit()
db.refresh(order)
return order
def cancel_order(db: Session, *, order_id: int) -> Optional[Order]:
"""Cancel an order if it hasn't been shipped yet"""
order = get(db, order_id)
if not order:
return None
if order.status in [OrderStatus.SHIPPED, OrderStatus.DELIVERED]:
raise ValueError("Cannot cancel an order that has already been shipped or delivered")
order.status = OrderStatus.CANCELLED
order.updated_at = datetime.now()
db.add(order)
db.commit()
db.refresh(order)
return order
def get_order_items(db: Session, *, order_id: int) -> List[OrderItem]:
"""Get all items for a specific order"""
return db.query(OrderItem).filter(OrderItem.order_id == order_id).all()

61
app/services/product.py Normal file
View File

@ -0,0 +1,61 @@
from typing import Any, Dict, List, Optional, Union
from sqlalchemy.orm import Session
from app.models.product import Product
from app.schemas.product import ProductCreate, ProductUpdate
def get(db: Session, product_id: int) -> Optional[Product]:
return db.query(Product).filter(Product.id == product_id).first()
def get_by_sku(db: Session, sku: str) -> Optional[Product]:
return db.query(Product).filter(Product.sku == sku).first()
def get_multi(
db: Session, *, skip: int = 0, limit: int = 100, active_only: bool = False
) -> List[Product]:
query = db.query(Product)
if active_only:
query = query.filter(Product.is_active == True)
return query.offset(skip).limit(limit).all()
def create(db: Session, *, obj_in: ProductCreate) -> Product:
db_obj = Product(
name=obj_in.name,
sku=obj_in.sku,
description=obj_in.description,
weight=obj_in.weight,
dimensions=obj_in.dimensions,
price=obj_in.price,
is_active=obj_in.is_active,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
db: Session, *, db_obj: Product, obj_in: Union[ProductUpdate, Dict[str, Any]]
) -> Product:
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.dict(exclude_unset=True)
for field in update_data:
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def remove(db: Session, *, product_id: int) -> Product:
obj = db.query(Product).get(product_id)
db.delete(obj)
db.commit()
return obj

174
app/services/shipment.py Normal file
View File

@ -0,0 +1,174 @@
import uuid
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from sqlalchemy.orm import Session
from app.models.inventory import Inventory
from app.models.order import Order, OrderStatus
from app.models.shipment import Shipment, ShipmentItem, ShipmentStatus
from app.schemas.shipment import ShipmentCreate, ShipmentUpdate
from app.services import inventory as inventory_service
def generate_tracking_number() -> str:
"""Generate a unique tracking number"""
# Format: TRK-{random_uuid}
return f"TRK-{uuid.uuid4().hex[:12].upper()}"
def get(db: Session, shipment_id: int) -> Optional[Shipment]:
return db.query(Shipment).filter(Shipment.id == shipment_id).first()
def get_by_tracking_number(db: Session, tracking_number: str) -> Optional[Shipment]:
return db.query(Shipment).filter(Shipment.tracking_number == tracking_number).first()
def get_multi(
db: Session, *, skip: int = 0, limit: int = 100, order_id: Optional[int] = None
) -> List[Shipment]:
query = db.query(Shipment)
if order_id:
query = query.filter(Shipment.order_id == order_id)
return query.order_by(Shipment.created_at.desc()).offset(skip).limit(limit).all()
def create(db: Session, *, obj_in: ShipmentCreate) -> Shipment:
# Validate shipment data
if not obj_in.destination_warehouse_id and not obj_in.customer_address:
raise ValueError("Either destination_warehouse_id or customer_address must be provided")
# Create shipment
db_obj = Shipment(
tracking_number=generate_tracking_number(),
order_id=obj_in.order_id,
origin_warehouse_id=obj_in.origin_warehouse_id,
destination_warehouse_id=obj_in.destination_warehouse_id,
customer_address=obj_in.customer_address,
status=obj_in.status or ShipmentStatus.PENDING,
carrier=obj_in.carrier,
estimated_delivery=obj_in.estimated_delivery,
shipping_cost=obj_in.shipping_cost or 0.0,
created_by_id=obj_in.created_by_id,
created_at=datetime.now(),
)
db.add(db_obj)
db.flush() # Get the shipment ID without committing transaction
# Create shipment items and update inventory
for item in obj_in.items:
shipment_item = ShipmentItem(
shipment_id=db_obj.id,
product_id=item.product_id,
quantity=item.quantity
)
db.add(shipment_item)
# Reduce inventory at origin warehouse
inventory_item = inventory_service.get_by_product_and_warehouse(
db, product_id=item.product_id, warehouse_id=obj_in.origin_warehouse_id
)
if not inventory_item:
raise ValueError(f"Product {item.product_id} not available in warehouse {obj_in.origin_warehouse_id}")
if inventory_item.quantity < item.quantity:
raise ValueError(f"Insufficient quantity for product {item.product_id} in warehouse {obj_in.origin_warehouse_id}")
inventory_item.quantity -= item.quantity
db.add(inventory_item)
# If this is warehouse-to-warehouse transfer, increase inventory at destination
if obj_in.destination_warehouse_id:
dest_inventory = inventory_service.get_by_product_and_warehouse(
db, product_id=item.product_id, warehouse_id=obj_in.destination_warehouse_id
)
if dest_inventory:
dest_inventory.quantity += item.quantity
db.add(dest_inventory)
else:
# Create new inventory record at destination
new_dest_inventory = Inventory(
product_id=item.product_id,
warehouse_id=obj_in.destination_warehouse_id,
quantity=item.quantity,
)
db.add(new_dest_inventory)
# If this shipment is related to an order, update order status
if obj_in.order_id:
order = db.query(Order).get(obj_in.order_id)
if order and order.status == OrderStatus.PROCESSING:
order.status = OrderStatus.SHIPPED
order.updated_at = datetime.now()
db.add(order)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
db: Session, *, db_obj: Shipment, obj_in: Union[ShipmentUpdate, Dict[str, Any]]
) -> Shipment:
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.dict(exclude_unset=True)
# Don't allow changing tracking_number or created_at
update_data.pop("tracking_number", None)
update_data.pop("created_at", None)
# Set updated_at
update_data["updated_at"] = datetime.now()
# Check for status changes
current_status = db_obj.status
new_status = update_data.get("status")
if new_status and new_status != current_status:
# Handle status change logic
if new_status == ShipmentStatus.DELIVERED:
# Set actual delivery time if not already set
if not update_data.get("actual_delivery"):
update_data["actual_delivery"] = datetime.now()
# If this shipment is related to an order, update order status
if db_obj.order_id:
order = db.query(Order).get(db_obj.order_id)
if order and order.status != OrderStatus.DELIVERED:
order.status = OrderStatus.DELIVERED
order.updated_at = datetime.now()
db.add(order)
# Update shipment fields
for field in update_data:
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update_status(
db: Session, *, shipment_id: int, status: ShipmentStatus, actual_delivery: Optional[datetime] = None
) -> Optional[Shipment]:
"""Update shipment status"""
shipment = get(db, shipment_id)
if not shipment:
return None
update_data = {"status": status, "updated_at": datetime.now()}
if status == ShipmentStatus.DELIVERED and not actual_delivery:
update_data["actual_delivery"] = datetime.now()
elif actual_delivery:
update_data["actual_delivery"] = actual_delivery
return update(db, db_obj=shipment, obj_in=update_data)
def get_shipment_items(db: Session, *, shipment_id: int) -> List[ShipmentItem]:
"""Get all items for a specific shipment"""
return db.query(ShipmentItem).filter(ShipmentItem.shipment_id == shipment_id).all()

84
app/services/user.py Normal file
View File

@ -0,0 +1,84 @@
from typing import Any, Dict, List, Optional, Union
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(db: Session, user_id: int) -> Optional[User]:
return db.query(User).filter(User.id == user_id).first()
def get_by_email(db: Session, email: str) -> Optional[User]:
return db.query(User).filter(User.email == email).first()
def get_by_username(db: Session, username: str) -> Optional[User]:
return db.query(User).filter(User.username == username).first()
def get_multi(db: Session, *, skip: int = 0, limit: int = 100) -> List[User]:
return db.query(User).offset(skip).limit(limit).all()
def create(db: Session, *, obj_in: UserCreate) -> User:
db_obj = User(
email=obj_in.email,
username=obj_in.username,
hashed_password=get_password_hash(obj_in.password),
full_name=obj_in.full_name,
is_superuser=obj_in.is_superuser,
is_active=obj_in.is_active,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
) -> User:
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_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 in update_data:
if field not in ["hashed_password"]: # Only set attributes explicitly
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def remove(db: Session, *, user_id: int) -> User:
obj = db.query(User).get(user_id)
db.delete(obj)
db.commit()
return obj
def authenticate(db: Session, *, username_or_email: str, password: str) -> Optional[User]:
user = get_by_email(db, email=username_or_email)
if not user:
user = get_by_username(db, username=username_or_email)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
def is_active(user: User) -> bool:
return user.is_active
def is_superuser(user: User) -> bool:
return user.is_superuser

62
app/services/warehouse.py Normal file
View File

@ -0,0 +1,62 @@
from typing import Any, Dict, List, Optional, Union
from sqlalchemy.orm import Session
from app.models.warehouse import Warehouse
from app.schemas.warehouse import WarehouseCreate, WarehouseUpdate
def get(db: Session, warehouse_id: int) -> Optional[Warehouse]:
return db.query(Warehouse).filter(Warehouse.id == warehouse_id).first()
def get_by_code(db: Session, code: str) -> Optional[Warehouse]:
return db.query(Warehouse).filter(Warehouse.code == code).first()
def get_multi(
db: Session, *, skip: int = 0, limit: int = 100, active_only: bool = False
) -> List[Warehouse]:
query = db.query(Warehouse)
if active_only:
query = query.filter(Warehouse.is_active == True)
return query.offset(skip).limit(limit).all()
def create(db: Session, *, obj_in: WarehouseCreate) -> Warehouse:
db_obj = Warehouse(
name=obj_in.name,
code=obj_in.code,
address=obj_in.address,
city=obj_in.city,
state=obj_in.state,
country=obj_in.country,
postal_code=obj_in.postal_code,
is_active=obj_in.is_active,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
db: Session, *, db_obj: Warehouse, obj_in: Union[WarehouseUpdate, Dict[str, Any]]
) -> Warehouse:
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.dict(exclude_unset=True)
for field in update_data:
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def remove(db: Session, *, warehouse_id: int) -> Warehouse:
obj = db.query(Warehouse).get(warehouse_id)
db.delete(obj)
db.commit()
return obj

56
main.py Normal file
View File

@ -0,0 +1,56 @@
import uvicorn
from fastapi import Depends, FastAPI
from sqlalchemy import text
from sqlalchemy.orm import Session
from starlette.middleware.cors import CORSMiddleware
from app.api.api import api_router
from app.core.config import settings
from app.db.session import get_db
app = FastAPI(
title=settings.PROJECT_NAME,
openapi_url=f"{settings.API_V1_STR}/openapi.json",
description="Logistics Management System API",
version="0.1.0",
)
# Set all CORS enabled origins
if settings.BACKEND_CORS_ORIGINS:
app.add_middleware(
CORSMiddleware,
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(api_router, prefix=settings.API_V1_STR)
@app.get("/health", tags=["Health"])
async def health_check(db: Session = Depends(get_db)):
"""
Root health check endpoint to verify the application is running correctly
and database connection is working.
"""
health_data = {
"status": "ok",
"api": "healthy",
"database": "healthy"
}
# Check database connectivity
try:
# Execute a simple query
db.execute(text("SELECT 1"))
except Exception as e:
health_data["database"] = "unhealthy"
health_data["status"] = "error"
health_data["database_error"] = str(e)
return health_data
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

26
migrations/README Normal file
View File

@ -0,0 +1,26 @@
# Alembic Migrations
This directory contains database migration scripts managed by Alembic.
## Structure
- `alembic.ini`: Configuration file for Alembic
- `env.py`: Environment configuration for Alembic
- `script.py.mako`: Template for generating migration scripts
- `versions/`: Directory containing migration scripts
## Usage
To apply migrations:
```bash
alembic upgrade head
```
To create a new migration (after changing models):
```bash
alembic revision -m "description of changes"
```
For more information, see the [Alembic documentation](https://alembic.sqlalchemy.org/en/latest/).

1
migrations/__init__.py Normal file
View File

@ -0,0 +1 @@
# Alembic migrations package

84
migrations/env.py Normal file
View File

@ -0,0 +1,84 @@
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
# 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.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
from app.db.base import Base
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() -> None:
"""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"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""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, # Key configuration for SQLite
# The following would ensure that foreign key constraints are enforced in SQLite
process_revision_directives=None,
# This configures how to reflect existing schema into models
compare_type=True,
compare_server_default=True,
)
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() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,177 @@
"""Initial schema
Revision ID: 001
Revises:
Create Date: 2023-09-10
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.sql import func
# revision identifiers, used by Alembic.
revision = '001'
down_revision = None
branch_labels = None
depends_on = None
# Enums
order_status_values = ['pending', 'processing', 'shipped', 'delivered', 'cancelled']
shipment_status_values = ['pending', 'processing', 'in_transit', 'delivered', 'cancelled']
def upgrade() -> None:
# Create users table
op.create_table(
'users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('username', sa.String(), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('full_name', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True, default=True),
sa.Column('is_superuser', sa.Boolean(), nullable=True, default=False),
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)
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
# Create products table
op.create_table(
'products',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('sku', sa.String(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('weight', sa.Float(), nullable=False, default=0.0),
sa.Column('dimensions', sa.String(), nullable=True),
sa.Column('price', sa.Float(), nullable=False, default=0.0),
sa.Column('is_active', sa.Boolean(), nullable=True, default=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_products_id'), 'products', ['id'], unique=False)
op.create_index(op.f('ix_products_name'), 'products', ['name'], unique=False)
op.create_index(op.f('ix_products_sku'), 'products', ['sku'], unique=True)
# Create warehouses table
op.create_table(
'warehouses',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('code', sa.String(), nullable=False),
sa.Column('address', sa.Text(), nullable=False),
sa.Column('city', sa.String(), nullable=False),
sa.Column('state', sa.String(), nullable=False),
sa.Column('country', sa.String(), nullable=False),
sa.Column('postal_code', sa.String(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=True, default=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_warehouses_id'), 'warehouses', ['id'], unique=False)
op.create_index(op.f('ix_warehouses_name'), 'warehouses', ['name'], unique=False)
op.create_index(op.f('ix_warehouses_code'), 'warehouses', ['code'], unique=True)
# Create inventory table
op.create_table(
'inventory',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('warehouse_id', sa.Integer(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False, default=0),
sa.Column('location', sa.String(), nullable=True),
sa.Column('min_stock_level', sa.Integer(), nullable=True, default=0),
sa.Column('max_stock_level', sa.Integer(), nullable=True, default=0),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.ForeignKeyConstraint(['warehouse_id'], ['warehouses.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('product_id', 'warehouse_id', name='uix_product_warehouse')
)
op.create_index(op.f('ix_inventory_id'), 'inventory', ['id'], unique=False)
# Create orders table
op.create_table(
'orders',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('order_number', sa.String(), nullable=False),
sa.Column('status', sa.Enum(*order_status_values, name='orderstatus'), nullable=True, default='pending'),
sa.Column('total_amount', sa.Float(), nullable=False, default=0.0),
sa.Column('shipping_address', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=func.now(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_orders_id'), 'orders', ['id'], unique=False)
op.create_index(op.f('ix_orders_order_number'), 'orders', ['order_number'], unique=True)
# Create order_items table
op.create_table(
'order_items',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('order_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False, default=1),
sa.Column('unit_price', sa.Float(), nullable=False),
sa.Column('total_price', sa.Float(), nullable=False),
sa.ForeignKeyConstraint(['order_id'], ['orders.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_order_items_id'), 'order_items', ['id'], unique=False)
# Create shipments table
op.create_table(
'shipments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('tracking_number', sa.String(), nullable=False),
sa.Column('order_id', sa.Integer(), nullable=True),
sa.Column('origin_warehouse_id', sa.Integer(), nullable=False),
sa.Column('destination_warehouse_id', sa.Integer(), nullable=True),
sa.Column('customer_address', sa.Text(), nullable=True),
sa.Column('status', sa.Enum(*shipment_status_values, name='shipmentstatus'), nullable=True, default='pending'),
sa.Column('carrier', sa.String(), nullable=True),
sa.Column('estimated_delivery', sa.DateTime(timezone=True), nullable=True),
sa.Column('actual_delivery', sa.DateTime(timezone=True), nullable=True),
sa.Column('shipping_cost', sa.Float(), nullable=True, default=0.0),
sa.Column('created_by_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=func.now(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['destination_warehouse_id'], ['warehouses.id'], ),
sa.ForeignKeyConstraint(['order_id'], ['orders.id'], ),
sa.ForeignKeyConstraint(['origin_warehouse_id'], ['warehouses.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_shipments_id'), 'shipments', ['id'], unique=False)
op.create_index(op.f('ix_shipments_tracking_number'), 'shipments', ['tracking_number'], unique=True)
# Create shipment_items table
op.create_table(
'shipment_items',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('shipment_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False, default=1),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.ForeignKeyConstraint(['shipment_id'], ['shipments.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_shipment_items_id'), 'shipment_items', ['id'], unique=False)
def downgrade() -> None:
# Drop all tables in reverse order of creation
op.drop_table('shipment_items')
op.drop_table('shipments')
op.drop_table('order_items')
op.drop_table('orders')
op.drop_table('inventory')
op.drop_table('warehouses')
op.drop_table('products')
op.drop_table('users')
# Drop enums
op.execute('DROP TYPE IF EXISTS orderstatus')
op.execute('DROP TYPE IF EXISTS shipmentstatus')

11
requirements.txt Normal file
View File

@ -0,0 +1,11 @@
fastapi>=0.103.1,<0.104.0
uvicorn>=0.23.2,<0.24.0
sqlalchemy>=2.0.20,<2.1.0
alembic>=1.12.0,<1.13.0
pydantic>=2.3.0,<2.4.0
pydantic-settings>=2.0.3,<2.1.0
python-multipart>=0.0.6,<0.1.0
python-jose[cryptography]>=3.3.0,<3.4.0
passlib[bcrypt]>=1.7.4,<1.8.0
email-validator>=2.0.0,<2.1.0
ruff>=0.0.290,<0.1.0