Add advanced search and filtering functionality
- Implement date range filtering for creation and due dates - Add customer name and email search with case-insensitive partial matching - Support amount range filtering with min and max values - Enhance sorting with multiple field options - Update README with comprehensive documentation for all filter options
This commit is contained in:
parent
d1e61b66a6
commit
040210f43f
96
README.md
96
README.md
@ -195,27 +195,99 @@ To combine with other parameters:
|
||||
GET /api/v1/invoices?status=PENDING&fields=id,invoice_number,total_amount&limit=5
|
||||
```
|
||||
|
||||
### Sorting
|
||||
### Advanced Filtering
|
||||
|
||||
The API supports sorting invoices by their creation date.
|
||||
The API supports advanced filtering options for more precise control over your invoice queries.
|
||||
|
||||
#### How to Use Sorting:
|
||||
#### Date Range Filtering
|
||||
|
||||
1. Add a `sort_order` query parameter to the GET request
|
||||
2. Specify one of the allowed values:
|
||||
- `asc`: Ascending order (oldest invoices first)
|
||||
- `desc`: Descending order (newest invoices first, this is the default)
|
||||
Filter invoices by creation date or due date:
|
||||
|
||||
#### Example:
|
||||
- `created_after`: Filter invoices created on or after this date (YYYY-MM-DD)
|
||||
- `created_before`: Filter invoices created on or before this date (YYYY-MM-DD)
|
||||
- `due_after`: Filter invoices due on or after this date (YYYY-MM-DD)
|
||||
- `due_before`: Filter invoices due on or before this date (YYYY-MM-DD)
|
||||
|
||||
Examples:
|
||||
|
||||
To get invoices in chronological order (oldest first):
|
||||
```
|
||||
GET /api/v1/invoices?sort_order=asc
|
||||
# Get invoices created in January 2023
|
||||
GET /api/v1/invoices?created_after=2023-01-01&created_before=2023-01-31
|
||||
|
||||
# Get overdue invoices (due before today)
|
||||
GET /api/v1/invoices?due_before=2023-07-01&status=PENDING
|
||||
```
|
||||
|
||||
To combine sorting with other parameters:
|
||||
#### Customer Filtering
|
||||
|
||||
Search for invoices by customer information:
|
||||
|
||||
- `customer_name`: Filter invoices by customer name (case-insensitive, partial match)
|
||||
- `customer_email`: Filter invoices by customer email (case-insensitive, partial match)
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
GET /api/v1/invoices?sort_order=desc&status=PENDING&limit=10
|
||||
# Find all invoices for customers with "smith" in their name
|
||||
GET /api/v1/invoices?customer_name=smith
|
||||
|
||||
# Find invoices for customers with a specific email domain
|
||||
GET /api/v1/invoices?customer_email=example.com
|
||||
```
|
||||
|
||||
#### Amount Range Filtering
|
||||
|
||||
Filter invoices by total amount:
|
||||
|
||||
- `min_amount`: Filter invoices with total amount greater than or equal to this value
|
||||
- `max_amount`: Filter invoices with total amount less than or equal to this value
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
# Get invoices with amounts between $100 and $500
|
||||
GET /api/v1/invoices?min_amount=100&max_amount=500
|
||||
|
||||
# Get high-value invoices
|
||||
GET /api/v1/invoices?min_amount=1000
|
||||
```
|
||||
|
||||
### Advanced Sorting
|
||||
|
||||
The API supports sorting invoices by various fields:
|
||||
|
||||
- `sort_by`: Field to sort by. Available options:
|
||||
- `date_created`: Sort by creation date (default)
|
||||
- `due_date`: Sort by due date
|
||||
- `total_amount`: Sort by invoice amount
|
||||
- `customer_name`: Sort alphabetically by customer name
|
||||
- `sort_order`: Sort direction:
|
||||
- `asc`: Ascending order (oldest/smallest/A-Z first)
|
||||
- `desc`: Descending order (newest/largest/Z-A first, this is the default)
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
# Get highest-value invoices first
|
||||
GET /api/v1/invoices?sort_by=total_amount&sort_order=desc
|
||||
|
||||
# Get invoices sorted alphabetically by customer name
|
||||
GET /api/v1/invoices?sort_by=customer_name&sort_order=asc
|
||||
|
||||
# Get invoices with the earliest due dates first
|
||||
GET /api/v1/invoices?sort_by=due_date&sort_order=asc
|
||||
```
|
||||
|
||||
### Combining Filters
|
||||
|
||||
All filter parameters can be combined for precise results:
|
||||
|
||||
```
|
||||
# Get pending invoices over $500 created in 2023, sorted by amount (highest first)
|
||||
GET /api/v1/invoices?status=PENDING&min_amount=500&created_after=2023-01-01&sort_by=total_amount&sort_order=desc
|
||||
|
||||
# Get paid invoices for a specific customer
|
||||
GET /api/v1/invoices?status=PAID&customer_name=acme&sort_by=date_created&sort_order=desc
|
||||
```
|
||||
|
||||
### Available Fields:
|
||||
|
@ -1,8 +1,9 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from datetime import datetime, date
|
||||
from typing import Optional, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
@ -13,6 +14,7 @@ from app.schemas.invoice import (
|
||||
InvoiceDB,
|
||||
InvoiceStatusUpdate,
|
||||
InvoiceUpdate,
|
||||
InvoiceAdvancedFilter,
|
||||
invoice_db_filterable,
|
||||
)
|
||||
|
||||
@ -69,17 +71,35 @@ def create_invoice(invoice_data: InvoiceCreate, db: Session = Depends(get_db)):
|
||||
return db_invoice
|
||||
|
||||
|
||||
@router.get("/")
|
||||
@router.get("/", response_model=List[InvoiceDB])
|
||||
def get_invoices(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
fields: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
sort_order: Optional[str] = "desc",
|
||||
|
||||
# Date range parameters
|
||||
created_after: Optional[date] = Query(None, description="Filter invoices created on or after this date (YYYY-MM-DD)"),
|
||||
created_before: Optional[date] = Query(None, description="Filter invoices created on or before this date (YYYY-MM-DD)"),
|
||||
due_after: Optional[date] = Query(None, description="Filter invoices due on or after this date (YYYY-MM-DD)"),
|
||||
due_before: Optional[date] = Query(None, description="Filter invoices due on or before this date (YYYY-MM-DD)"),
|
||||
|
||||
# Customer search parameters
|
||||
customer_name: Optional[str] = Query(None, description="Filter invoices by customer name (case-insensitive, partial match)"),
|
||||
customer_email: Optional[str] = Query(None, description="Filter invoices by customer email (case-insensitive, partial match)"),
|
||||
|
||||
# Amount range parameters
|
||||
min_amount: Optional[float] = Query(None, description="Filter invoices with total amount greater than or equal to this value"),
|
||||
max_amount: Optional[float] = Query(None, description="Filter invoices with total amount less than or equal to this value"),
|
||||
|
||||
# Sorting parameters
|
||||
sort_by: Optional[str] = Query(None, description="Field to sort by (date_created, due_date, total_amount, customer_name)"),
|
||||
sort_order: Optional[str] = Query("desc", description="Sort order: 'asc' for ascending, 'desc' for descending"),
|
||||
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Retrieve a list of invoices.
|
||||
Retrieve a list of invoices with advanced filtering and sorting options.
|
||||
|
||||
Parameters:
|
||||
- skip: Number of records to skip
|
||||
@ -88,9 +108,39 @@ def get_invoices(
|
||||
If not provided, all fields will be returned.
|
||||
- status: Filter invoices by status (e.g., "PENDING", "PAID", "CANCELLED")
|
||||
If not provided, all invoices regardless of status will be returned.
|
||||
- sort_order: Sort invoices by date_created field, either "asc" for ascending or "desc" for descending order.
|
||||
Default is "desc" (newest first).
|
||||
|
||||
Date Range Filtering:
|
||||
- created_after: Filter invoices created on or after this date (YYYY-MM-DD)
|
||||
- created_before: Filter invoices created on or before this date (YYYY-MM-DD)
|
||||
- due_after: Filter invoices due on or after this date (YYYY-MM-DD)
|
||||
- due_before: Filter invoices due on or before this date (YYYY-MM-DD)
|
||||
|
||||
Customer Filtering:
|
||||
- customer_name: Filter invoices by customer name (case-insensitive, partial match)
|
||||
- customer_email: Filter invoices by customer email (case-insensitive, partial match)
|
||||
|
||||
Amount Range Filtering:
|
||||
- min_amount: Filter invoices with total amount greater than or equal to this value
|
||||
- max_amount: Filter invoices with total amount less than or equal to this value
|
||||
|
||||
Sorting:
|
||||
- sort_by: Field to sort by (date_created, due_date, total_amount, customer_name)
|
||||
- sort_order: Sort order: 'asc' for ascending, 'desc' for descending (default: desc)
|
||||
"""
|
||||
# Validate filter parameters using the schema
|
||||
filter_params = InvoiceAdvancedFilter(
|
||||
created_after=created_after,
|
||||
created_before=created_before,
|
||||
due_after=due_after,
|
||||
due_before=due_before,
|
||||
customer_name=customer_name,
|
||||
customer_email=customer_email,
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
)
|
||||
|
||||
# Build the query with filters
|
||||
query = db.query(Invoice)
|
||||
|
||||
@ -107,24 +157,54 @@ def get_invoices(
|
||||
# Apply the filter
|
||||
query = query.filter(Invoice.status == status)
|
||||
|
||||
# Apply sorting
|
||||
if sort_order:
|
||||
# Validate sort_order value
|
||||
allowed_sort_orders = ["asc", "desc"]
|
||||
if sort_order.lower() not in allowed_sort_orders:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Sort order must be one of {', '.join(allowed_sort_orders)}"
|
||||
)
|
||||
# Apply date range filters
|
||||
if filter_params.created_after:
|
||||
# Convert date to datetime with time at start of day (00:00:00)
|
||||
created_after_datetime = datetime.combine(filter_params.created_after, datetime.min.time())
|
||||
query = query.filter(Invoice.date_created >= created_after_datetime)
|
||||
|
||||
# Apply sorting based on sort_order
|
||||
if sort_order.lower() == "asc":
|
||||
if filter_params.created_before:
|
||||
# Convert date to datetime with time at end of day (23:59:59)
|
||||
created_before_datetime = datetime.combine(filter_params.created_before, datetime.max.time())
|
||||
query = query.filter(Invoice.date_created <= created_before_datetime)
|
||||
|
||||
if filter_params.due_after:
|
||||
# Convert date to datetime with time at start of day (00:00:00)
|
||||
due_after_datetime = datetime.combine(filter_params.due_after, datetime.min.time())
|
||||
query = query.filter(Invoice.due_date >= due_after_datetime)
|
||||
|
||||
if filter_params.due_before:
|
||||
# Convert date to datetime with time at end of day (23:59:59)
|
||||
due_before_datetime = datetime.combine(filter_params.due_before, datetime.max.time())
|
||||
query = query.filter(Invoice.due_date <= due_before_datetime)
|
||||
|
||||
# Apply customer name and email filters (case-insensitive partial matches)
|
||||
if filter_params.customer_name:
|
||||
query = query.filter(func.lower(Invoice.customer_name).contains(filter_params.customer_name.lower()))
|
||||
|
||||
if filter_params.customer_email:
|
||||
query = query.filter(func.lower(Invoice.customer_email).contains(filter_params.customer_email.lower()))
|
||||
|
||||
# Apply amount range filters
|
||||
if filter_params.min_amount is not None:
|
||||
query = query.filter(Invoice.total_amount >= filter_params.min_amount)
|
||||
|
||||
if filter_params.max_amount is not None:
|
||||
query = query.filter(Invoice.total_amount <= filter_params.max_amount)
|
||||
|
||||
# Apply sorting based on sort_by and sort_order
|
||||
if filter_params.sort_by:
|
||||
sort_field = getattr(Invoice, filter_params.sort_by)
|
||||
if filter_params.sort_order == "asc":
|
||||
query = query.order_by(sort_field.asc())
|
||||
else: # Default to 'desc'
|
||||
query = query.order_by(sort_field.desc())
|
||||
else:
|
||||
# If sort_by is not provided, sort by date_created
|
||||
if filter_params.sort_order == "asc":
|
||||
query = query.order_by(Invoice.date_created.asc())
|
||||
else: # Default to 'desc'
|
||||
query = query.order_by(Invoice.date_created.desc())
|
||||
else:
|
||||
# Default sorting (descending - newest first)
|
||||
query = query.order_by(Invoice.date_created.desc())
|
||||
|
||||
# Apply pagination
|
||||
invoices = query.offset(skip).limit(limit).all()
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, date
|
||||
from typing import Any, Dict, List, Optional, Type, TypeVar, Generic, Union
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
@ -153,3 +153,62 @@ class InvoiceStatusUpdate(BaseModel):
|
||||
if v not in allowed_statuses:
|
||||
raise ValueError(f"Status must be one of {', '.join(allowed_statuses)}")
|
||||
return v
|
||||
|
||||
|
||||
class InvoiceAdvancedFilter(BaseModel):
|
||||
"""
|
||||
Schema for advanced filtering of invoices.
|
||||
"""
|
||||
# Date range filtering
|
||||
created_after: Optional[date] = Field(
|
||||
None, description="Filter invoices created on or after this date (YYYY-MM-DD)"
|
||||
)
|
||||
created_before: Optional[date] = Field(
|
||||
None, description="Filter invoices created on or before this date (YYYY-MM-DD)"
|
||||
)
|
||||
due_after: Optional[date] = Field(
|
||||
None, description="Filter invoices due on or after this date (YYYY-MM-DD)"
|
||||
)
|
||||
due_before: Optional[date] = Field(
|
||||
None, description="Filter invoices due on or before this date (YYYY-MM-DD)"
|
||||
)
|
||||
|
||||
# Customer filtering
|
||||
customer_name: Optional[str] = Field(
|
||||
None, description="Filter invoices by customer name (case-insensitive, partial match)"
|
||||
)
|
||||
customer_email: Optional[str] = Field(
|
||||
None, description="Filter invoices by customer email (case-insensitive, partial match)"
|
||||
)
|
||||
|
||||
# Amount range filtering
|
||||
min_amount: Optional[float] = Field(
|
||||
None, description="Filter invoices with total amount greater than or equal to this value"
|
||||
)
|
||||
max_amount: Optional[float] = Field(
|
||||
None, description="Filter invoices with total amount less than or equal to this value"
|
||||
)
|
||||
|
||||
# Advanced sorting
|
||||
sort_by: Optional[str] = Field(
|
||||
None, description="Field to sort by (date_created, due_date, total_amount, customer_name)"
|
||||
)
|
||||
sort_order: Optional[str] = Field(
|
||||
"desc", description="Sort order: 'asc' for ascending, 'desc' for descending"
|
||||
)
|
||||
|
||||
@validator("sort_by")
|
||||
def validate_sort_by(cls, v):
|
||||
if v is not None:
|
||||
allowed_sort_fields = ["date_created", "due_date", "total_amount", "customer_name"]
|
||||
if v not in allowed_sort_fields:
|
||||
raise ValueError(f"Sort field must be one of {', '.join(allowed_sort_fields)}")
|
||||
return v
|
||||
|
||||
@validator("sort_order")
|
||||
def validate_sort_order(cls, v):
|
||||
if v is not None:
|
||||
allowed_sort_orders = ["asc", "desc"]
|
||||
if v.lower() not in allowed_sort_orders:
|
||||
raise ValueError(f"Sort order must be one of {', '.join(allowed_sort_orders)}")
|
||||
return v.lower() if v else v
|
Loading…
x
Reference in New Issue
Block a user