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:
Automated Action 2025-05-30 08:42:46 +00:00
parent d1e61b66a6
commit 040210f43f
3 changed files with 248 additions and 37 deletions

View File

@ -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:

View File

@ -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()

View File

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