Implement retail management and payment API with FastAPI

This API provides endpoints for:
- Product and inventory management
- Customer management
- Order processing
- Payment processing with Stripe integration
- User authentication

generated with BackendIM... (backend.im)
This commit is contained in:
Automated Action 2025-05-12 12:00:19 +00:00
parent 16f8e726bf
commit 609e7fb237
45 changed files with 2180 additions and 2 deletions

118
README.md
View File

@ -1,3 +1,117 @@
# FastAPI Application
# Retail Management and Payment API
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A FastAPI-based REST API for retail management including inventory, orders, sales, and payment processing with Stripe integration.
## Features
- **Product Management**: Create, read, update, and delete products
- **Inventory Management**: Track and update product inventory
- **Customer Management**: Manage customer information
- **Order Management**: Process and track customer orders
- **Payment Processing**: Process payments via Stripe integration
- **Authentication**: JWT-based user authentication and authorization
## Tech Stack
- **Framework**: FastAPI
- **Database**: SQLite (with SQLAlchemy ORM)
- **Migrations**: Alembic
- **Authentication**: JWT (JSON Web Tokens)
- **Payment Processing**: Stripe SDK
## API Endpoints
### Authentication
- `POST /api/v1/auth/login` - Get access token
- `POST /api/v1/auth/register` - Register a new user
- `GET /api/v1/auth/me` - Get current user info
### Products
- `GET /api/v1/products` - List all products
- `POST /api/v1/products` - Create a new product
- `GET /api/v1/products/{product_id}` - Get a single product
- `PUT /api/v1/products/{product_id}` - Update a product
- `DELETE /api/v1/products/{product_id}` - Delete a product
### Inventory
- `GET /api/v1/inventory` - List all inventory items
- `POST /api/v1/inventory` - Create a new inventory record
- `GET /api/v1/inventory/{product_id}` - Get inventory for a product
- `PUT /api/v1/inventory/{inventory_id}` - Update an inventory record
- `PUT /api/v1/inventory/{inventory_id}/adjust` - Adjust inventory quantity
### Customers
- `GET /api/v1/customers` - List all customers
- `POST /api/v1/customers` - Create a new customer
- `GET /api/v1/customers/{customer_id}` - Get a customer
- `PUT /api/v1/customers/{customer_id}` - Update a customer
- `DELETE /api/v1/customers/{customer_id}` - Delete a customer
### Orders
- `GET /api/v1/orders` - List all orders
- `POST /api/v1/orders` - Create a new order
- `GET /api/v1/orders/{order_id}` - Get an order
- `PUT /api/v1/orders/{order_id}` - Update an order
- `DELETE /api/v1/orders/{order_id}` - Cancel an order
### Payments
- `POST /api/v1/payments/create-payment-intent/{order_id}` - Create a Stripe payment intent
- `POST /api/v1/payments/webhook` - Handle Stripe webhook events
### Health Check
- `GET /health` - API health status check
## Setup and Installation
### Prerequisites
- Python 3.8+
- pip (Python package manager)
### Installation Steps
1. Clone the repository:
```bash
git clone [repository_url]
cd retailmanagementandpaymentapi
```
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Set up environment variables (create a `.env` file in the project root):
```
JWT_SECRET_KEY=your-secret-key-change-in-production
STRIPE_API_KEY=your-stripe-api-key
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret
```
4. Run database migrations:
```bash
alembic upgrade head
```
5. Start the development server:
```bash
uvicorn main:app --reload
```
## API Documentation
Once the server is running, you can access the auto-generated API documentation:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
## Testing the API
For testing the API endpoints, you can use the Swagger UI documentation or tools like Postman or curl.
### Stripe Testing
For testing Stripe integration:
- Use Stripe's test API keys
- Use Stripe's test card numbers for payment simulation:
- Test successful payment: 4242 4242 4242 4242
- Test failed payment: 4000 0000 0000 0002

85
alembic.ini Normal file
View File

@ -0,0 +1,85 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
# 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 alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
[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
alembic/__init__.py Normal file
View File

@ -0,0 +1 @@
# Alembic initialization

83
alembic/env.py Normal file
View File

@ -0,0 +1,83 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
from app.db.base import Base # noqa
target_metadata = Base.metadata
# Import all models here
# from app.models import user, product, inventory, customer, order # noqa
# 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.
from app.core.config import settings # noqa
config.set_main_option("sqlalchemy.url", settings.SQLALCHEMY_DATABASE_URL)
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

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

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

View File

@ -0,0 +1,152 @@
"""Initial migration
Revision ID: 00001
Revises:
Create Date: 2025-05-12
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '00001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# Create user table
op.create_table(
'user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=True),
sa.Column('username', sa.String(), nullable=True),
sa.Column('hashed_password', sa.String(), nullable=True),
sa.Column('full_name', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('is_superuser', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True)
# Create product table
op.create_table(
'product',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('sku', sa.String(), nullable=True),
sa.Column('price', sa.Float(), nullable=False),
sa.Column('image_url', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_product_id'), 'product', ['id'], unique=False)
op.create_index(op.f('ix_product_name'), 'product', ['name'], unique=False)
op.create_index(op.f('ix_product_sku'), 'product', ['sku'], unique=True)
# Create inventory table
op.create_table(
'inventory',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=True),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('location', sa.String(), nullable=True),
sa.Column('last_restock_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('product_id')
)
op.create_index(op.f('ix_inventory_id'), 'inventory', ['id'], unique=False)
# Create customer table
op.create_table(
'customer',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('first_name', sa.String(), nullable=False),
sa.Column('last_name', sa.String(), nullable=False),
sa.Column('email', sa.String(), nullable=True),
sa.Column('phone', sa.String(), nullable=True),
sa.Column('address', sa.String(), nullable=True),
sa.Column('city', sa.String(), nullable=True),
sa.Column('state', sa.String(), nullable=True),
sa.Column('zip_code', sa.String(), nullable=True),
sa.Column('country', sa.String(), nullable=True),
sa.Column('stripe_customer_id', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_customer_email'), 'customer', ['email'], unique=True)
op.create_index(op.f('ix_customer_id'), 'customer', ['id'], unique=False)
# Create order table
op.create_table(
'order',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('customer_id', sa.Integer(), nullable=True),
sa.Column('order_number', sa.String(), nullable=True),
sa.Column('status', sa.String(), nullable=True),
sa.Column('total_amount', sa.Float(), nullable=True),
sa.Column('payment_intent_id', sa.String(), nullable=True),
sa.Column('payment_status', sa.String(), nullable=True),
sa.Column('shipping_address', sa.String(), nullable=True),
sa.Column('shipping_city', sa.String(), nullable=True),
sa.Column('shipping_state', sa.String(), nullable=True),
sa.Column('shipping_zip', sa.String(), nullable=True),
sa.Column('shipping_country', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['customer_id'], ['customer.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_order_id'), 'order', ['id'], unique=False)
op.create_index(op.f('ix_order_order_number'), 'order', ['order_number'], unique=True)
# Create order_item table
op.create_table(
'orderitem',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('order_id', sa.Integer(), nullable=True),
sa.Column('product_id', sa.Integer(), nullable=True),
sa.Column('quantity', sa.Integer(), nullable=True),
sa.Column('unit_price', sa.Float(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.ForeignKeyConstraint(['order_id'], ['order.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_orderitem_id'), 'orderitem', ['id'], unique=False)
def downgrade():
op.drop_index(op.f('ix_orderitem_id'), table_name='orderitem')
op.drop_table('orderitem')
op.drop_index(op.f('ix_order_order_number'), table_name='order')
op.drop_index(op.f('ix_order_id'), table_name='order')
op.drop_table('order')
op.drop_index(op.f('ix_customer_id'), table_name='customer')
op.drop_index(op.f('ix_customer_email'), table_name='customer')
op.drop_table('customer')
op.drop_index(op.f('ix_inventory_id'), table_name='inventory')
op.drop_table('inventory')
op.drop_index(op.f('ix_product_sku'), table_name='product')
op.drop_index(op.f('ix_product_name'), table_name='product')
op.drop_index(op.f('ix_product_id'), table_name='product')
op.drop_table('product')
op.drop_index(op.f('ix_user_username'), table_name='user')
op.drop_index(op.f('ix_user_id'), table_name='user')
op.drop_index(op.f('ix_user_email'), table_name='user')
op.drop_table('user')

1
app/__init__.py Normal file
View File

@ -0,0 +1 @@
# App initialization

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

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

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

@ -0,0 +1,59 @@
from typing import Generator, Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app import crud, models, schemas
from app.core import security
from app.core.config import settings
from app.db.session import SessionLocal
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
def get_db() -> Generator:
try:
db = SessionLocal()
yield db
finally:
db.close()
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(reusable_oauth2)
) -> models.User:
try:
payload = jwt.decode(
token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]
)
token_data = schemas.TokenPayload(**payload)
except (jwt.JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
user = crud.user.get(db, id=token_data.sub)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
def get_current_active_user(
current_user: models.User = Depends(get_current_user),
) -> models.User:
if not crud.user.is_active(current_user):
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
def get_current_active_superuser(
current_user: models.User = Depends(get_current_user),
) -> models.User:
if not crud.user.is_superuser(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions"
)
return current_user

View File

@ -0,0 +1,13 @@
from fastapi import APIRouter
from app.api.routes import auth, products, inventory, customers, orders, payments, health
router = APIRouter()
router.include_router(auth.router, prefix="/api/v1/auth", tags=["authentication"])
router.include_router(products.router, prefix="/api/v1/products", tags=["products"])
router.include_router(inventory.router, prefix="/api/v1/inventory", tags=["inventory"])
router.include_router(customers.router, prefix="/api/v1/customers", tags=["customers"])
router.include_router(orders.router, prefix="/api/v1/orders", tags=["orders"])
router.include_router(payments.router, prefix="/api/v1/payments", tags=["payments"])
router.include_router(health.router, prefix="", tags=["health"])

71
app/api/routes/auth.py Normal file
View File

@ -0,0 +1,71 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app import crud, models, schemas
from app.api import deps
from app.core import security
from app.core.config import settings
router = APIRouter()
@router.post("/login", response_model=schemas.Token)
def login_access_token(
db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends()
) -> Any:
"""
OAuth2 compatible token login, get an access token for future requests
"""
user = crud.user.authenticate(
db, username=form_data.username, password=form_data.password
)
if not user:
raise HTTPException(status_code=400, detail="Incorrect username or password")
elif not crud.user.is_active(user):
raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": security.create_access_token(
user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}
@router.post("/register", response_model=schemas.User)
def register_user(
*,
db: Session = Depends(deps.get_db),
user_in: schemas.UserCreate,
) -> Any:
"""
Register a new user
"""
user = crud.user.get_by_email(db, email=user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="A user with this email already exists",
)
user = crud.user.get_by_username(db, username=user_in.username)
if user:
raise HTTPException(
status_code=400,
detail="A user with this username already exists",
)
user = crud.user.create(db, obj_in=user_in)
return user
@router.get("/me", response_model=schemas.User)
def read_users_me(
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Get current user
"""
return current_user

128
app/api/routes/customers.py Normal file
View File

@ -0,0 +1,128 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from sqlalchemy.orm import Session
from app import crud, models, schemas
from app.api import deps
router = APIRouter()
@router.get("/", response_model=List[schemas.Customer])
def list_customers(
db: Session = Depends(deps.get_db),
skip: int = Query(0, description="Skip first N items"),
limit: int = Query(100, description="Limit the number of items returned"),
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Retrieve customers
"""
customers = crud.customer.get_multi(db, skip=skip, limit=limit)
return customers
@router.post("/", response_model=schemas.Customer)
def create_customer(
*,
db: Session = Depends(deps.get_db),
customer_in: schemas.CustomerCreate,
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Create new customer
"""
# Check if email already exists
customer = crud.customer.get_by_email(db, email=customer_in.email)
if customer:
raise HTTPException(
status_code=400,
detail="A customer with this email already exists",
)
# For now, we'll create the customer without a Stripe ID
# In a real app, we'd create the Stripe customer first
customer = crud.customer.create(db, obj_in=customer_in)
return customer
@router.get("/{customer_id}", response_model=schemas.Customer)
def get_customer(
*,
db: Session = Depends(deps.get_db),
customer_id: int = Path(..., description="The ID of the customer to get"),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Get a specific customer by id
"""
customer = crud.customer.get(db, id=customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# Check if user is superuser or if they're accessing their own customer record
if not crud.user.is_superuser(current_user):
if not customer.user_id or customer.user_id != current_user.id:
raise HTTPException(
status_code=403,
detail="Not enough permissions to access this customer"
)
return customer
@router.put("/{customer_id}", response_model=schemas.Customer)
def update_customer(
*,
db: Session = Depends(deps.get_db),
customer_id: int = Path(..., description="The ID of the customer to update"),
customer_in: schemas.CustomerUpdate,
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Update a customer
"""
customer = crud.customer.get(db, id=customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# Check if user is superuser or if they're updating their own customer record
if not crud.user.is_superuser(current_user):
if not customer.user_id or customer.user_id != current_user.id:
raise HTTPException(
status_code=403,
detail="Not enough permissions to update this customer"
)
# If email is being updated, check if it's already in use
if customer_in.email and customer_in.email != customer.email:
existing = crud.customer.get_by_email(db, email=customer_in.email)
if existing and existing.id != customer_id:
raise HTTPException(
status_code=400,
detail="A customer with this email already exists",
)
customer = crud.customer.update(db, db_obj=customer, obj_in=customer_in)
return customer
@router.delete("/{customer_id}", response_model=schemas.Customer)
def delete_customer(
*,
db: Session = Depends(deps.get_db),
customer_id: int = Path(..., description="The ID of the customer to delete"),
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Delete a customer (admin only)
"""
customer = crud.customer.get(db, id=customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# In a real app, we'd need to handle Stripe customer deletion and check for orders
customer = crud.customer.remove(db, id=customer_id)
return customer

31
app/api/routes/health.py Normal file
View File

@ -0,0 +1,31 @@
from typing import Any, Dict
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.api import deps
from app.db.session import SessionLocal
router = APIRouter()
@router.get("/health", response_model=Dict[str, Any])
def health_check(db: Session = Depends(deps.get_db)) -> Any:
"""
Check API health status
"""
# Check database connection
db_status = "healthy"
try:
# Execute a simple query
db.execute("SELECT 1")
except Exception as e:
db_status = f"unhealthy: {str(e)}"
return {
"status": "ok",
"components": {
"database": db_status
},
"version": "0.1.0"
}

126
app/api/routes/inventory.py Normal file
View File

@ -0,0 +1,126 @@
from typing import Any, List
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
from sqlalchemy.orm import Session
from app import crud, models, schemas
from app.api import deps
router = APIRouter()
@router.get("/", response_model=List[schemas.Inventory])
def list_inventory(
db: Session = Depends(deps.get_db),
skip: int = Query(0, description="Skip first N items"),
limit: int = Query(100, description="Limit the number of items returned"),
low_stock_only: bool = Query(False, description="Only return low stock items"),
low_stock_threshold: int = Query(10, description="Threshold for low stock"),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Retrieve inventory items with optional filtering
"""
if low_stock_only:
items = crud.inventory.get_low_stock(
db, threshold=low_stock_threshold, skip=skip, limit=limit
)
else:
items = crud.inventory.get_multi(db, skip=skip, limit=limit)
return items
@router.get("/{product_id}", response_model=schemas.Inventory)
def get_product_inventory(
*,
db: Session = Depends(deps.get_db),
product_id: int = Path(..., description="The ID of the product"),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Get inventory for a specific product
"""
# Verify product exists
product = crud.product.get(db, id=product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
inventory = crud.inventory.get_by_product_id(db, product_id=product_id)
if not inventory:
raise HTTPException(status_code=404, detail="Inventory not found for this product")
return inventory
@router.post("/", response_model=schemas.Inventory)
def create_inventory(
*,
db: Session = Depends(deps.get_db),
inventory_in: schemas.InventoryCreate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Create inventory record for a product
"""
# Verify product exists
product = crud.product.get(db, id=inventory_in.product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
# Check if inventory already exists for this product
existing = crud.inventory.get_by_product_id(db, product_id=inventory_in.product_id)
if existing:
raise HTTPException(
status_code=400,
detail="Inventory already exists for this product",
)
inventory = crud.inventory.create(db, obj_in=inventory_in)
return inventory
@router.put("/{inventory_id}", response_model=schemas.Inventory)
def update_inventory(
*,
db: Session = Depends(deps.get_db),
inventory_id: int = Path(..., description="The ID of the inventory record to update"),
inventory_in: schemas.InventoryUpdate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Update an inventory record
"""
inventory = crud.inventory.get(db, id=inventory_id)
if not inventory:
raise HTTPException(status_code=404, detail="Inventory not found")
inventory = crud.inventory.update(db, db_obj=inventory, obj_in=inventory_in)
return inventory
@router.put("/{inventory_id}/adjust", response_model=schemas.Inventory)
def adjust_inventory(
*,
db: Session = Depends(deps.get_db),
inventory_id: int = Path(..., description="The ID of the inventory record to adjust"),
quantity_change: int = Body(..., description="Amount to adjust (positive to add, negative to subtract)"),
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Adjust inventory quantity
"""
inventory = crud.inventory.get(db, id=inventory_id)
if not inventory:
raise HTTPException(status_code=404, detail="Inventory not found")
# Check if adjustment would result in negative quantity
if inventory.quantity + quantity_change < 0:
raise HTTPException(
status_code=400,
detail="Adjustment would result in negative inventory",
)
inventory = crud.inventory.update_quantity(
db, inventory_id=inventory_id, quantity_change=quantity_change
)
return inventory

191
app/api/routes/orders.py Normal file
View File

@ -0,0 +1,191 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from sqlalchemy.orm import Session
from app import crud, models, schemas
from app.api import deps
router = APIRouter()
@router.get("/", response_model=List[schemas.Order])
def list_orders(
db: Session = Depends(deps.get_db),
skip: int = Query(0, description="Skip first N items"),
limit: int = Query(100, description="Limit the number of items returned"),
customer_id: int = Query(None, description="Filter by customer ID"),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Retrieve orders with optional customer filtering
"""
if customer_id:
# Check permissions if not superuser
if not crud.user.is_superuser(current_user):
customer = crud.customer.get(db, id=customer_id)
if not customer or not customer.user_id or customer.user_id != current_user.id:
raise HTTPException(
status_code=403,
detail="Not enough permissions to access these orders"
)
orders = crud.order.get_by_customer(db, customer_id=customer_id, skip=skip, limit=limit)
else:
# Non-superusers can only see their own orders
if not crud.user.is_superuser(current_user):
# Find customer associated with current user
customer = db.query(models.Customer).filter(models.Customer.user_id == current_user.id).first()
if not customer:
return []
orders = crud.order.get_by_customer(db, customer_id=customer.id, skip=skip, limit=limit)
else:
orders = crud.order.get_multi(db, skip=skip, limit=limit)
return orders
@router.post("/", response_model=schemas.Order)
def create_order(
*,
db: Session = Depends(deps.get_db),
order_in: schemas.OrderCreate,
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Create a new order
"""
# Verify customer exists
customer = crud.customer.get(db, id=order_in.customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# Check permissions if not superuser
if not crud.user.is_superuser(current_user):
if not customer.user_id or customer.user_id != current_user.id:
raise HTTPException(
status_code=403,
detail="Not enough permissions to create order for this customer"
)
# Verify products exist and have enough inventory
for item in order_in.items:
product = crud.product.get(db, id=item.product_id)
if not product:
raise HTTPException(
status_code=404,
detail=f"Product with ID {item.product_id} not found"
)
# Check inventory
inventory = crud.inventory.get_by_product_id(db, product_id=item.product_id)
if not inventory or inventory.quantity < item.quantity:
raise HTTPException(
status_code=400,
detail=f"Not enough inventory for product {product.name}"
)
# Create the order
order = crud.order.create_with_items(db, obj_in=order_in)
# Update inventory
for item in order.items:
inventory = crud.inventory.get_by_product_id(db, product_id=item.product_id)
if inventory:
crud.inventory.update_quantity(
db, inventory_id=inventory.id, quantity_change=-item.quantity
)
return order
@router.get("/{order_id}", response_model=schemas.Order)
def get_order(
*,
db: Session = Depends(deps.get_db),
order_id: int = Path(..., description="The ID of the order to get"),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Get a specific order by id
"""
order = crud.order.get_with_items(db, order_id=order_id)
if not order:
raise HTTPException(status_code=404, detail="Order not found")
# Check permissions if not superuser
if not crud.user.is_superuser(current_user):
customer = crud.customer.get(db, id=order.customer_id)
if not customer or not customer.user_id or customer.user_id != current_user.id:
raise HTTPException(
status_code=403,
detail="Not enough permissions to access this order"
)
return order
@router.put("/{order_id}", response_model=schemas.Order)
def update_order(
*,
db: Session = Depends(deps.get_db),
order_id: int = Path(..., description="The ID of the order to update"),
order_in: schemas.OrderUpdate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Update an order's details (admin only)
"""
order = crud.order.get(db, id=order_id)
if not order:
raise HTTPException(status_code=404, detail="Order not found")
order = crud.order.update(db, db_obj=order, obj_in=order_in)
return order
@router.delete("/{order_id}", response_model=schemas.Order)
def cancel_order(
*,
db: Session = Depends(deps.get_db),
order_id: int = Path(..., description="The ID of the order to cancel"),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Cancel an order and restore inventory
"""
order = crud.order.get_with_items(db, order_id=order_id)
if not order:
raise HTTPException(status_code=404, detail="Order not found")
# Check permissions if not superuser
if not crud.user.is_superuser(current_user):
customer = crud.customer.get(db, id=order.customer_id)
if not customer or not customer.user_id or customer.user_id != current_user.id:
raise HTTPException(
status_code=403,
detail="Not enough permissions to cancel this order"
)
# Check if order can be canceled
if order.status not in ["pending", "processing"]:
raise HTTPException(
status_code=400,
detail=f"Cannot cancel order with status {order.status}"
)
# Update order status
order = crud.order.update(
db, db_obj=order, obj_in={"status": "cancelled", "payment_status": "refunded"}
)
# Restore inventory
for item in order.items:
inventory = crud.inventory.get_by_product_id(db, product_id=item.product_id)
if inventory:
crud.inventory.update_quantity(
db, inventory_id=inventory.id, quantity_change=item.quantity
)
# TODO: Handle Stripe refund if payment was made
return order

152
app/api/routes/payments.py Normal file
View File

@ -0,0 +1,152 @@
import json
from typing import Any
import stripe
from fastapi import APIRouter, Depends, HTTPException, Path, Request, Response
from sqlalchemy.orm import Session
from app import crud, models, schemas
from app.api import deps
from app.core.config import settings
router = APIRouter()
# Initialize Stripe
stripe.api_key = settings.STRIPE_API_KEY
@router.post("/create-payment-intent/{order_id}", response_model=schemas.PaymentIntentResponse)
async def create_payment_intent(
*,
db: Session = Depends(deps.get_db),
order_id: int = Path(..., description="The ID of the order to create payment for"),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Create a Stripe payment intent for an order
"""
# Get the order
order = crud.order.get(db, id=order_id)
if not order:
raise HTTPException(status_code=404, detail="Order not found")
# Check permissions if not superuser
if not crud.user.is_superuser(current_user):
customer = crud.customer.get(db, id=order.customer_id)
if not customer or not customer.user_id or customer.user_id != current_user.id:
raise HTTPException(
status_code=403,
detail="Not enough permissions to create payment for this order"
)
# Check if payment is already made
if order.payment_status == "paid":
raise HTTPException(
status_code=400,
detail="Payment has already been processed for this order"
)
# Get or create Stripe customer
customer = crud.customer.get(db, id=order.customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
stripe_customer_id = customer.stripe_customer_id
if not stripe_customer_id:
# Create a new Stripe customer
stripe_customer = stripe.Customer.create(
email=customer.email,
name=f"{customer.first_name} {customer.last_name}",
metadata={"customer_id": customer.id}
)
stripe_customer_id = stripe_customer["id"]
# Update the customer record with Stripe ID
crud.customer.update(db, db_obj=customer, obj_in={"stripe_customer_id": stripe_customer_id})
# Create a payment intent
try:
intent = stripe.PaymentIntent.create(
amount=int(order.total_amount * 100), # Convert to cents
currency="usd",
customer=stripe_customer_id,
metadata={
"order_id": order.id,
"order_number": order.order_number
}
)
# Update the order with the payment intent ID
crud.order.update_payment_status(
db, order_id=order.id, payment_intent_id=intent["id"], payment_status="unpaid"
)
# Return the client secret
return {
"client_secret": intent["client_secret"],
"payment_intent_id": intent["id"]
}
except stripe.error.StripeError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/webhook", status_code=200)
async def stripe_webhook(request: Request, response: Response) -> Any:
"""
Handle Stripe webhook events
"""
# Get the webhook body
payload = await request.body()
sig_header = request.headers.get("stripe-signature")
try:
event = stripe.Webhook.construct_event(
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
)
except ValueError:
# Invalid payload
response.status_code = 400
return {"error": "Invalid payload"}
except stripe.error.SignatureVerificationError:
# Invalid signature
response.status_code = 400
return {"error": "Invalid signature"}
# Handle the event
db = next(deps.get_db())
if event["type"] == "payment_intent.succeeded":
payment_intent = event["data"]["object"]
# Extract order ID from metadata
order_id = payment_intent["metadata"].get("order_id")
if order_id:
# Update order payment status
order = crud.order.get(db, id=int(order_id))
if order:
crud.order.update_payment_status(
db,
order_id=order.id,
payment_intent_id=payment_intent["id"],
payment_status="paid"
)
elif event["type"] == "payment_intent.payment_failed":
payment_intent = event["data"]["object"]
# Extract order ID from metadata
order_id = payment_intent["metadata"].get("order_id")
if order_id:
# Update order payment status
order = crud.order.get(db, id=int(order_id))
if order:
crud.order.update(
db,
db_obj=order,
obj_in={"status": "payment_failed"}
)
# You can handle more events here as needed
return {"status": "success"}

125
app/api/routes/products.py Normal file
View File

@ -0,0 +1,125 @@
from typing import Any, List
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
from sqlalchemy.orm import Session
from app import crud, models, schemas
from app.api import deps
router = APIRouter()
@router.get("/", response_model=List[schemas.Product])
def list_products(
db: Session = Depends(deps.get_db),
skip: int = Query(0, description="Skip first N items"),
limit: int = Query(100, description="Limit the number of items returned"),
active_only: bool = Query(False, description="Only return active products"),
) -> Any:
"""
Retrieve products with optional filtering
"""
if active_only:
products = crud.product.get_active(db, skip=skip, limit=limit)
else:
products = crud.product.get_multi(db, skip=skip, limit=limit)
return products
@router.post("/", response_model=schemas.Product)
def create_product(
*,
db: Session = Depends(deps.get_db),
product_in: schemas.ProductCreate,
inventory_quantity: int = Body(0, description="Initial inventory quantity"),
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Create new product with optional initial inventory
"""
product = crud.product.get_by_sku(db, sku=product_in.sku)
if product:
raise HTTPException(
status_code=400,
detail="A product with this SKU already exists",
)
product = crud.product.create_with_inventory(
db, obj_in=product_in, inventory_quantity=inventory_quantity
)
return product
@router.get("/{product_id}", response_model=schemas.ProductWithInventory)
def get_product(
*,
db: Session = Depends(deps.get_db),
product_id: int = Path(..., description="The ID of the product to get"),
) -> Any:
"""
Get a specific product by id with inventory information
"""
product = crud.product.get(db, id=product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
# Get inventory information
inventory = crud.inventory.get_by_product_id(db, product_id=product_id)
inventory_quantity = inventory.quantity if inventory else 0
# Create combined response
product_data = schemas.Product.from_orm(product)
product_with_inventory = schemas.ProductWithInventory(
**product_data.dict(),
inventory_quantity=inventory_quantity
)
return product_with_inventory
@router.put("/{product_id}", response_model=schemas.Product)
def update_product(
*,
db: Session = Depends(deps.get_db),
product_id: int = Path(..., description="The ID of the product to update"),
product_in: schemas.ProductUpdate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Update a product
"""
product = crud.product.get(db, id=product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
# If SKU is being updated, check for uniqueness
if product_in.sku and product_in.sku != product.sku:
existing = crud.product.get_by_sku(db, sku=product_in.sku)
if existing:
raise HTTPException(
status_code=400,
detail="A product with this SKU already exists",
)
product = crud.product.update(db, db_obj=product, obj_in=product_in)
return product
@router.delete("/{product_id}", response_model=schemas.Product)
def delete_product(
*,
db: Session = Depends(deps.get_db),
product_id: int = Path(..., description="The ID of the product to delete"),
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Delete a product
"""
product = crud.product.get(db, id=product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
# Check for any pending orders with this product
# TODO: Implement order item check
product = crud.product.remove(db, id=product_id)
return product

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

@ -0,0 +1 @@
# Core module initialization

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

@ -0,0 +1,35 @@
from pathlib import Path
from typing import List
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
# Base settings
PROJECT_NAME: str = "Retail Management and Payment API"
API_V1_STR: str = "/api/v1"
# CORS
CORS_ORIGINS: List[str] = ["*"]
# JWT
JWT_SECRET_KEY: str = "your-secret-key-change-in-production"
JWT_ALGORITHM: str = "HS256"
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# Database
DB_DIR = Path("/app") / "storage" / "db"
DB_DIR.mkdir(parents=True, exist_ok=True)
SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite"
# Stripe
STRIPE_API_KEY: str = "your-stripe-api-key"
STRIPE_WEBHOOK_SECRET: str = "your-stripe-webhook-secret"
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = True
settings = Settings()

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

@ -0,0 +1,31 @@
from datetime import datetime, timedelta
from typing import Any, Optional, Union
from jose import jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def create_access_token(subject: Union[str, Any], expires_delta: Optional[timedelta] = None) -> str:
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(
to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_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)

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

@ -0,0 +1,5 @@
from app.crud.user import user
from app.crud.product import product
from app.crud.inventory import inventory
from app.crud.customer import customer
from app.crud.order import order

66
app/crud/base.py Normal file
View File

@ -0,0 +1,66 @@
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.db.base_class import Base
ModelType = TypeVar("ModelType", bound=Base)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: Type[ModelType]):
"""
CRUD object with default methods to Create, Read, Update, Delete (CRUD).
**Parameters**
* `model`: A SQLAlchemy model class
* `schema`: A Pydantic model (schema) class
"""
self.model = model
def get(self, db: Session, id: Any) -> Optional[ModelType]:
return db.query(self.model).filter(self.model.id == id).first()
def get_multi(
self, db: Session, *, skip: int = 0, limit: int = 100
) -> List[ModelType]:
return db.query(self.model).offset(skip).limit(limit).all()
def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
obj_in_data = jsonable_encoder(obj_in)
db_obj = self.model(**obj_in_data) # type: ignore
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
self,
db: Session,
*,
db_obj: ModelType,
obj_in: Union[UpdateSchemaType, Dict[str, Any]]
) -> ModelType:
obj_data = jsonable_encoder(db_obj)
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.dict(exclude_unset=True)
for field in obj_data:
if 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(self, db: Session, *, id: int) -> ModelType:
obj = db.query(self.model).get(id)
db.delete(obj)
db.commit()
return obj

36
app/crud/customer.py Normal file
View File

@ -0,0 +1,36 @@
from typing import Optional
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.models.customer import Customer
from app.schemas.customer import CustomerCreate, CustomerUpdate
class CRUDCustomer(CRUDBase[Customer, CustomerCreate, CustomerUpdate]):
def get_by_email(self, db: Session, *, email: str) -> Optional[Customer]:
return db.query(Customer).filter(Customer.email == email).first()
def get_by_stripe_id(self, db: Session, *, stripe_customer_id: str) -> Optional[Customer]:
return db.query(Customer).filter(Customer.stripe_customer_id == stripe_customer_id).first()
def create_with_stripe_id(self, db: Session, *, obj_in: CustomerCreate, stripe_customer_id: str) -> Customer:
db_obj = Customer(
first_name=obj_in.first_name,
last_name=obj_in.last_name,
email=obj_in.email,
phone=obj_in.phone,
address=obj_in.address,
city=obj_in.city,
state=obj_in.state,
zip_code=obj_in.zip_code,
country=obj_in.country,
stripe_customer_id=stripe_customer_id
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
customer = CRUDCustomer(Customer)

27
app/crud/inventory.py Normal file
View File

@ -0,0 +1,27 @@
from typing import List
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.models.inventory import Inventory
from app.schemas.inventory import InventoryCreate, InventoryUpdate
class CRUDInventory(CRUDBase[Inventory, InventoryCreate, InventoryUpdate]):
def get_by_product_id(self, db: Session, *, product_id: int) -> Inventory:
return db.query(Inventory).filter(Inventory.product_id == product_id).first()
def update_quantity(self, db: Session, *, inventory_id: int, quantity_change: int) -> Inventory:
inventory = self.get(db, id=inventory_id)
if inventory:
inventory.quantity += quantity_change
db.add(inventory)
db.commit()
db.refresh(inventory)
return inventory
def get_low_stock(self, db: Session, *, threshold: int = 10, skip: int = 0, limit: int = 100) -> List[Inventory]:
return db.query(Inventory).filter(Inventory.quantity <= threshold).offset(skip).limit(limit).all()
inventory = CRUDInventory(Inventory)

77
app/crud/order.py Normal file
View File

@ -0,0 +1,77 @@
import uuid
from typing import List, Optional
from sqlalchemy import desc
from sqlalchemy.orm import Session, joinedload
from app.crud.base import CRUDBase
from app.models.order import Order, OrderItem
from app.schemas.order import OrderCreate, OrderUpdate, OrderItemCreate
class CRUDOrder(CRUDBase[Order, OrderCreate, OrderUpdate]):
def get_by_order_number(self, db: Session, *, order_number: str) -> Optional[Order]:
return db.query(Order).filter(Order.order_number == order_number).first()
def get_by_customer(self, db: Session, *, customer_id: int, skip: int = 0, limit: int = 100) -> List[Order]:
return db.query(Order).filter(Order.customer_id == customer_id).order_by(desc(Order.created_at)).offset(skip).limit(limit).all()
def get_by_payment_intent(self, db: Session, *, payment_intent_id: str) -> Optional[Order]:
return db.query(Order).filter(Order.payment_intent_id == payment_intent_id).first()
def get_with_items(self, db: Session, *, order_id: int) -> Optional[Order]:
return db.query(Order).options(joinedload(Order.items)).filter(Order.id == order_id).first()
def create_with_items(self, db: Session, *, obj_in: OrderCreate) -> Order:
# Generate unique order number
order_number = f"ORD-{uuid.uuid4().hex[:8].upper()}"
# Create the order
db_obj = Order(
customer_id=obj_in.customer_id,
order_number=order_number,
status=obj_in.status,
total_amount=obj_in.total_amount,
payment_status="unpaid",
shipping_address=obj_in.shipping_address,
shipping_city=obj_in.shipping_city,
shipping_state=obj_in.shipping_state,
shipping_zip=obj_in.shipping_zip,
shipping_country=obj_in.shipping_country
)
db.add(db_obj)
db.flush() # Flush to get the order id
# Create the order items
for item_in in obj_in.items:
order_item = OrderItem(
order_id=db_obj.id,
product_id=item_in.product_id,
quantity=item_in.quantity,
unit_price=item_in.unit_price
)
db.add(order_item)
db.commit()
db.refresh(db_obj)
return db_obj
def update_payment_status(self, db: Session, *, order_id: int, payment_intent_id: str, payment_status: str) -> Order:
order = self.get(db, id=order_id)
if order:
order.payment_intent_id = payment_intent_id
order.payment_status = payment_status
# Update order status based on payment status
if payment_status == "paid":
order.status = "processing"
elif payment_status == "refunded":
order.status = "cancelled"
db.add(order)
db.commit()
db.refresh(order)
return order
order = CRUDOrder(Order)

44
app/crud/product.py Normal file
View File

@ -0,0 +1,44 @@
from typing import List, Optional
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.models.product import Product
from app.schemas.product import ProductCreate, ProductUpdate
class CRUDProduct(CRUDBase[Product, ProductCreate, ProductUpdate]):
def get_by_sku(self, db: Session, *, sku: str) -> Optional[Product]:
return db.query(Product).filter(Product.sku == sku).first()
def get_active(self, db: Session, *, skip: int = 0, limit: int = 100) -> List[Product]:
return db.query(Product).filter(Product.is_active == True).offset(skip).limit(limit).all()
def create_with_inventory(self, db: Session, *, obj_in: ProductCreate, inventory_quantity: int = 0) -> Product:
from app.models.inventory import Inventory
# Create product
db_obj = Product(
name=obj_in.name,
description=obj_in.description,
sku=obj_in.sku,
price=obj_in.price,
image_url=obj_in.image_url,
is_active=obj_in.is_active
)
db.add(db_obj)
db.flush()
# Create inventory record
inventory = Inventory(
product_id=db_obj.id,
quantity=inventory_quantity
)
db.add(inventory)
db.commit()
db.refresh(db_obj)
return db_obj
product = CRUDProduct(Product)

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

@ -0,0 +1,60 @@
from typing import Any, Dict, Optional, Union
from sqlalchemy.orm import Session
from app.core.security import get_password_hash, verify_password
from app.crud.base import CRUDBase
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
return db.query(User).filter(User.email == email).first()
def get_by_username(self, db: Session, *, username: str) -> Optional[User]:
return db.query(User).filter(User.username == username).first()
def create(self, 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_active=obj_in.is_active,
is_superuser=obj_in.is_superuser,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
self, 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
return super().update(db, db_obj=db_obj, obj_in=update_data)
def authenticate(self, db: Session, *, username: str, password: str) -> Optional[User]:
user = self.get_by_username(db, username=username)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
def is_active(self, user: User) -> bool:
return user.is_active
def is_superuser(self, user: User) -> bool:
return user.is_superuser
user = CRUDUser(User)

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

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

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

@ -0,0 +1,8 @@
# Import all the models, so that Base has them before being
# imported by Alembic
from app.db.base_class import Base # noqa
from app.models.product import Product # noqa
from app.models.inventory import Inventory # noqa
from app.models.customer import Customer # noqa
from app.models.order import Order, OrderItem # noqa
from app.models.user import User # noqa

14
app/db/base_class.py Normal file
View File

@ -0,0 +1,14 @@
from typing import Any
from sqlalchemy.ext.declarative import as_declarative, declared_attr
@as_declarative()
class Base:
id: Any
__name__: str
# Generate __tablename__ automatically
@declared_attr
def __tablename__(cls) -> str:
return cls.__name__.lower()

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

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

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

@ -0,0 +1,6 @@
# Import all models here
from app.models.user import User
from app.models.product import Product
from app.models.inventory import Inventory
from app.models.customer import Customer
from app.models.order import Order, OrderItem

26
app/models/customer.py Normal file
View File

@ -0,0 +1,26 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base_class import Base
class Customer(Base):
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("user.id"), nullable=True)
first_name = Column(String, nullable=False)
last_name = Column(String, nullable=False)
email = Column(String, unique=True, index=True)
phone = Column(String, nullable=True)
address = Column(String, nullable=True)
city = Column(String, nullable=True)
state = Column(String, nullable=True)
zip_code = Column(String, nullable=True)
country = Column(String, nullable=True)
stripe_customer_id = Column(String, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
orders = relationship("Order", back_populates="customer")
user = relationship("User")

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

@ -0,0 +1,18 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base_class import Base
class Inventory(Base):
id = Column(Integer, primary_key=True, index=True)
product_id = Column(Integer, ForeignKey("product.id"), unique=True)
quantity = Column(Integer, nullable=False, default=0)
location = Column(String, nullable=True)
last_restock_date = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
product = relationship("Product", back_populates="inventory")

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

@ -0,0 +1,39 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Table
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base_class import Base
class Order(Base):
id = Column(Integer, primary_key=True, index=True)
customer_id = Column(Integer, ForeignKey("customer.id"))
order_number = Column(String, unique=True, index=True)
status = Column(String, default="pending") # pending, paid, shipped, delivered, cancelled
total_amount = Column(Float)
payment_intent_id = Column(String, nullable=True)
payment_status = Column(String, default="unpaid") # unpaid, paid, refunded
shipping_address = Column(String, nullable=True)
shipping_city = Column(String, nullable=True)
shipping_state = Column(String, nullable=True)
shipping_zip = Column(String, nullable=True)
shipping_country = Column(String, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
customer = relationship("Customer", back_populates="orders")
items = relationship("OrderItem", back_populates="order")
class OrderItem(Base):
id = Column(Integer, primary_key=True, index=True)
order_id = Column(Integer, ForeignKey("order.id"))
product_id = Column(Integer, ForeignKey("product.id"))
quantity = Column(Integer, default=1)
unit_price = Column(Float)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
order = relationship("Order", back_populates="items")
product = relationship("Product", back_populates="order_items")

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

@ -0,0 +1,21 @@
from sqlalchemy import Column, Integer, String, Float, Text, Boolean, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base_class import Base
class Product(Base):
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
description = Column(Text, nullable=True)
sku = Column(String, unique=True, index=True)
price = Column(Float, nullable=False)
image_url = Column(String, nullable=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
inventory = relationship("Inventory", back_populates="product", uselist=False)
order_items = relationship("OrderItem", back_populates="product")

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

@ -0,0 +1,16 @@
from sqlalchemy import Boolean, Column, Integer, String, DateTime
from sqlalchemy.sql import func
from app.db.base_class import Base
class User(Base):
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
full_name = Column(String, nullable=True)
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())

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

@ -0,0 +1,6 @@
# Import all schemas here
from app.schemas.user import User, UserCreate, UserUpdate, Token
from app.schemas.product import Product, ProductCreate, ProductUpdate, ProductWithInventory
from app.schemas.inventory import Inventory, InventoryCreate, InventoryUpdate
from app.schemas.customer import Customer, CustomerCreate, CustomerUpdate
from app.schemas.order import Order, OrderCreate, OrderUpdate, OrderItem, PaymentIntentResponse

42
app/schemas/customer.py Normal file
View File

@ -0,0 +1,42 @@
from typing import Optional
from pydantic import BaseModel, EmailStr
class CustomerBase(BaseModel):
first_name: str
last_name: str
email: EmailStr
phone: Optional[str] = None
address: Optional[str] = None
city: Optional[str] = None
state: Optional[str] = None
zip_code: Optional[str] = None
country: Optional[str] = None
class CustomerCreate(CustomerBase):
pass
class CustomerUpdate(BaseModel):
first_name: Optional[str] = None
last_name: Optional[str] = None
email: Optional[EmailStr] = None
phone: Optional[str] = None
address: Optional[str] = None
city: Optional[str] = None
state: Optional[str] = None
zip_code: Optional[str] = None
country: Optional[str] = None
class CustomerInDBBase(CustomerBase):
id: int
stripe_customer_id: Optional[str] = None
class Config:
orm_mode = True
class Customer(CustomerInDBBase):
pass

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

@ -0,0 +1,31 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel
class InventoryBase(BaseModel):
product_id: int
quantity: int
location: Optional[str] = None
last_restock_date: Optional[datetime] = None
class InventoryCreate(InventoryBase):
pass
class InventoryUpdate(BaseModel):
quantity: Optional[int] = None
location: Optional[str] = None
last_restock_date: Optional[datetime] = None
class InventoryInDBBase(InventoryBase):
id: int
class Config:
orm_mode = True
class Inventory(InventoryInDBBase):
pass

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

@ -0,0 +1,71 @@
from typing import List, Optional
from datetime import datetime
from pydantic import BaseModel
class OrderItemBase(BaseModel):
product_id: int
quantity: int = 1
unit_price: float
class OrderItemCreate(OrderItemBase):
pass
class OrderItemInDBBase(OrderItemBase):
id: int
order_id: int
class Config:
orm_mode = True
class OrderItem(OrderItemInDBBase):
pass
class OrderBase(BaseModel):
customer_id: int
status: str = "pending"
total_amount: float
shipping_address: Optional[str] = None
shipping_city: Optional[str] = None
shipping_state: Optional[str] = None
shipping_zip: Optional[str] = None
shipping_country: Optional[str] = None
class OrderCreate(OrderBase):
items: List[OrderItemCreate]
class OrderUpdate(BaseModel):
status: Optional[str] = None
payment_status: Optional[str] = None
shipping_address: Optional[str] = None
shipping_city: Optional[str] = None
shipping_state: Optional[str] = None
shipping_zip: Optional[str] = None
shipping_country: Optional[str] = None
class OrderInDBBase(OrderBase):
id: int
order_number: str
payment_intent_id: Optional[str] = None
payment_status: str
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
class Order(OrderInDBBase):
items: List[OrderItem] = []
class PaymentIntentResponse(BaseModel):
client_secret: str
payment_intent_id: str

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

@ -0,0 +1,39 @@
from typing import Optional
from pydantic import BaseModel
class ProductBase(BaseModel):
name: str
description: Optional[str] = None
sku: str
price: float
image_url: Optional[str] = None
is_active: bool = True
class ProductCreate(ProductBase):
pass
class ProductUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
sku: Optional[str] = None
price: Optional[float] = None
image_url: Optional[str] = None
is_active: Optional[bool] = None
class ProductInDBBase(ProductBase):
id: int
class Config:
orm_mode = True
class Product(ProductInDBBase):
pass
class ProductWithInventory(Product):
inventory_quantity: int = 0

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

@ -0,0 +1,44 @@
from typing import Optional
from pydantic import BaseModel, EmailStr
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
class UserCreate(UserBase):
email: EmailStr
username: str
password: str
class UserUpdate(UserBase):
password: Optional[str] = None
class UserInDBBase(UserBase):
id: Optional[int] = None
class Config:
orm_mode = True
class User(UserInDBBase):
pass
class UserInDB(UserInDBBase):
hashed_password: str
class Token(BaseModel):
access_token: str
token_type: str
class TokenPayload(BaseModel):
sub: Optional[int] = None

27
main.py Normal file
View File

@ -0,0 +1,27 @@
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.routes import router as api_router
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
description="Retail Management and Payment API",
version="0.1.0",
)
# Set up CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers
app.include_router(api_router)
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

12
requirements.txt Normal file
View File

@ -0,0 +1,12 @@
fastapi==0.99.1
uvicorn==0.23.2
sqlalchemy==2.0.19
pydantic==2.1.1
pydantic-settings==2.0.3
alembic==1.11.1
stripe==6.5.0
python-dotenv==1.0.0
python-multipart==0.0.6
passlib==1.7.4
python-jose==3.3.0
bcrypt==4.0.1