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
|
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
|
Filter invoices by creation date or due date:
|
||||||
2. Specify one of the allowed values:
|
|
||||||
- `asc`: Ascending order (oldest invoices first)
|
|
||||||
- `desc`: Descending order (newest invoices first, this is the default)
|
|
||||||
|
|
||||||
#### 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:
|
### Available Fields:
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime, date
|
||||||
from typing import Optional
|
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 fastapi.responses import JSONResponse
|
||||||
|
from sqlalchemy import func
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
@ -13,6 +14,7 @@ from app.schemas.invoice import (
|
|||||||
InvoiceDB,
|
InvoiceDB,
|
||||||
InvoiceStatusUpdate,
|
InvoiceStatusUpdate,
|
||||||
InvoiceUpdate,
|
InvoiceUpdate,
|
||||||
|
InvoiceAdvancedFilter,
|
||||||
invoice_db_filterable,
|
invoice_db_filterable,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -69,17 +71,35 @@ def create_invoice(invoice_data: InvoiceCreate, db: Session = Depends(get_db)):
|
|||||||
return db_invoice
|
return db_invoice
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/", response_model=List[InvoiceDB])
|
||||||
def get_invoices(
|
def get_invoices(
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
fields: Optional[str] = None,
|
fields: Optional[str] = None,
|
||||||
status: 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)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Retrieve a list of invoices.
|
Retrieve a list of invoices with advanced filtering and sorting options.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- skip: Number of records to skip
|
- skip: Number of records to skip
|
||||||
@ -88,9 +108,39 @@ def get_invoices(
|
|||||||
If not provided, all fields will be returned.
|
If not provided, all fields will be returned.
|
||||||
- status: Filter invoices by status (e.g., "PENDING", "PAID", "CANCELLED")
|
- status: Filter invoices by status (e.g., "PENDING", "PAID", "CANCELLED")
|
||||||
If not provided, all invoices regardless of status will be returned.
|
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
|
# Build the query with filters
|
||||||
query = db.query(Invoice)
|
query = db.query(Invoice)
|
||||||
|
|
||||||
@ -107,24 +157,54 @@ def get_invoices(
|
|||||||
# Apply the filter
|
# Apply the filter
|
||||||
query = query.filter(Invoice.status == status)
|
query = query.filter(Invoice.status == status)
|
||||||
|
|
||||||
# Apply sorting
|
# Apply date range filters
|
||||||
if sort_order:
|
if filter_params.created_after:
|
||||||
# Validate sort_order value
|
# Convert date to datetime with time at start of day (00:00:00)
|
||||||
allowed_sort_orders = ["asc", "desc"]
|
created_after_datetime = datetime.combine(filter_params.created_after, datetime.min.time())
|
||||||
if sort_order.lower() not in allowed_sort_orders:
|
query = query.filter(Invoice.date_created >= created_after_datetime)
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
if filter_params.created_before:
|
||||||
detail=f"Sort order must be one of {', '.join(allowed_sort_orders)}"
|
# 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)
|
||||||
# Apply sorting based on sort_order
|
|
||||||
if sort_order.lower() == "asc":
|
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())
|
query = query.order_by(Invoice.date_created.asc())
|
||||||
else: # Default to 'desc'
|
else: # Default to 'desc'
|
||||||
query = query.order_by(Invoice.date_created.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
|
# Apply pagination
|
||||||
invoices = query.offset(skip).limit(limit).all()
|
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 typing import Any, Dict, List, Optional, Type, TypeVar, Generic, Union
|
||||||
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
@ -152,4 +152,63 @@ class InvoiceStatusUpdate(BaseModel):
|
|||||||
allowed_statuses = ["PENDING", "PAID", "CANCELLED"]
|
allowed_statuses = ["PENDING", "PAID", "CANCELLED"]
|
||||||
if v not in allowed_statuses:
|
if v not in allowed_statuses:
|
||||||
raise ValueError(f"Status must be one of {', '.join(allowed_statuses)}")
|
raise ValueError(f"Status must be one of {', '.join(allowed_statuses)}")
|
||||||
return v
|
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