diff --git a/README.md b/README.md index 9d56f18..1ff1777 100644 --- a/README.md +++ b/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: diff --git a/app/api/routes/invoices.py b/app/api/routes/invoices.py index efac0b6..31556dc 100644 --- a/app/api/routes/invoices.py +++ b/app/api/routes/invoices.py @@ -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 sorting based on sort_order - if sort_order.lower() == "asc": + # 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) + + 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() diff --git a/app/schemas/invoice.py b/app/schemas/invoice.py index 92e4ba7..587befa 100644 --- a/app/schemas/invoice.py +++ b/app/schemas/invoice.py @@ -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 @@ -152,4 +152,63 @@ class InvoiceStatusUpdate(BaseModel): allowed_statuses = ["PENDING", "PAID", "CANCELLED"] if v not in allowed_statuses: raise ValueError(f"Status must be one of {', '.join(allowed_statuses)}") - return v \ No newline at end of file + 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 \ No newline at end of file