From 2c386e7bfba87b6b7e217371d081a0aca70fa134 Mon Sep 17 00:00:00 2001 From: Felix Evers Date: Mon, 21 Apr 2025 13:04:49 +0200 Subject: [PATCH] feat: support multiple calculations at once --- app/main.py | 3 +- app/migrations/env.py | 1 + app/models/contract.py | 5 +- app/models/customer.py | 9 ++- app/models/customer_product.py | 15 +++- app/models/customer_product_contract.py | 1 + app/models/invoice.py | 3 +- app/models/message.py | 3 +- app/models/product.py | 5 +- app/models/product_contract.py | 1 + app/models/product_plan.py | 3 +- app/models/user.py | 3 +- app/models/voucher.py | 1 + app/routers/contract.py | 1 + app/routers/customer.py | 1 + app/routers/customer_product.py | 100 +++++++++++++++++------- app/routers/invoice.py | 1 + app/routers/product.py | 1 + app/routers/voucher.py | 1 + app/utils/database/connection.py | 1 + app/utils/database/session.py | 1 + app/utils/security/token.py | 3 +- 22 files changed, 117 insertions(+), 46 deletions(-) diff --git a/app/main.py b/app/main.py index 33515f1..7d873de 100644 --- a/app/main.py +++ b/app/main.py @@ -5,13 +5,14 @@ from fastapi import FastAPI, HTTPException, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse +from sqlalchemy.exc import OperationalError + from routers.contract import router as contract_router from routers.customer import router as customer_router from routers.customer_product import router as customer_product_router from routers.invoice import router as invoice_router from routers.product import router as product_router from routers.voucher import router as voucher_router -from sqlalchemy.exc import OperationalError from utils.config import keycloak_openid, settings from utils.database.connection import engine from utils.helpers.example import create_example_data diff --git a/app/migrations/env.py b/app/migrations/env.py index caa838d..b5c8cac 100644 --- a/app/migrations/env.py +++ b/app/migrations/env.py @@ -5,6 +5,7 @@ from alembic import context from sqlalchemy import engine_from_config, pool + from utils.database.connection import SQLALCHEMY_DATABASE_URL, Base # this is the Alembic Config object, which provides diff --git a/app/models/contract.py b/app/models/contract.py index 746918c..02fca58 100644 --- a/app/models/contract.py +++ b/app/models/contract.py @@ -2,12 +2,13 @@ from uuid import UUID as UUID4 from uuid import uuid4 -from models.product_contract import ProductContract -from models.static import ContractKeyEnum from pydantic import BaseModel from sqlalchemy import Column, DateTime, Enum, String from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship + +from models.product_contract import ProductContract +from models.static import ContractKeyEnum from utils.database.connection import Base diff --git a/app/models/customer.py b/app/models/customer.py index 834cf86..de2308a 100644 --- a/app/models/customer.py +++ b/app/models/customer.py @@ -2,15 +2,16 @@ from uuid import UUID as UUID4 from uuid import uuid4 +from pydantic import BaseModel +from sqlalchemy import Column, DateTime, String +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship + from models.customer_product import CustomerProduct from models.invoice import Invoice from models.message import Message from models.static import HouseNumber, Mail, PhoneNumber, PostalCode, WebURL from models.user import User -from pydantic import BaseModel -from sqlalchemy import Column, DateTime, String -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import relationship from utils.database.connection import Base diff --git a/app/models/customer_product.py b/app/models/customer_product.py index 62417e5..b0d3288 100644 --- a/app/models/customer_product.py +++ b/app/models/customer_product.py @@ -2,12 +2,13 @@ from uuid import UUID as UUID4 from uuid import uuid4 -from models.product import ProductBase -from models.product_plan import ProductPlanBase from pydantic import BaseModel from sqlalchemy import Column, DateTime, ForeignKey, Integer from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship + +from models.product import ProductBase +from models.product_plan import ProductPlanBase from utils.database.connection import Base @@ -88,3 +89,13 @@ class CustomerProductCalculation(BaseModel): final_price: float before_price: float saving: float + + +class ProductCalculationResult(BaseModel): + product_uuid: UUID4 + calculation: CustomerProductCalculation + + +class CustomerProductsCalculation(BaseModel): + overall: CustomerProductCalculation + products: list[ProductCalculationResult] diff --git a/app/models/customer_product_contract.py b/app/models/customer_product_contract.py index 6b82cfd..876dacb 100644 --- a/app/models/customer_product_contract.py +++ b/app/models/customer_product_contract.py @@ -3,6 +3,7 @@ from sqlalchemy import Column, DateTime, ForeignKey from sqlalchemy.dialects.postgresql import UUID + from utils.database.connection import Base diff --git a/app/models/invoice.py b/app/models/invoice.py index 747731a..b098999 100644 --- a/app/models/invoice.py +++ b/app/models/invoice.py @@ -3,11 +3,12 @@ from uuid import UUID as UUID4 from uuid import uuid4 -from models.static import InvoiceStatusEnum from pydantic import BaseModel from sqlalchemy import Column, DateTime, Enum, ForeignKey, Numeric, String from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship + +from models.static import InvoiceStatusEnum from utils.database.connection import Base diff --git a/app/models/message.py b/app/models/message.py index 02a9dc8..030fdd1 100644 --- a/app/models/message.py +++ b/app/models/message.py @@ -1,10 +1,11 @@ from datetime import datetime from uuid import uuid4 -from models.static import MessageStatusEnum from sqlalchemy import Column, DateTime, Enum, ForeignKey, String, Text from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship + +from models.static import MessageStatusEnum from utils.database.connection import Base diff --git a/app/models/product.py b/app/models/product.py index 368ac29..2e517df 100644 --- a/app/models/product.py +++ b/app/models/product.py @@ -2,12 +2,13 @@ from uuid import UUID as UUID4 from uuid import uuid4 -from models.product_contract import ProductContract -from models.product_plan import ProductPlan, ProductPlanBase from pydantic import BaseModel from sqlalchemy import Column, DateTime, String, Text from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship + +from models.product_contract import ProductContract +from models.product_plan import ProductPlan, ProductPlanBase from utils.database.connection import Base diff --git a/app/models/product_contract.py b/app/models/product_contract.py index 94cda28..f1bbb88 100644 --- a/app/models/product_contract.py +++ b/app/models/product_contract.py @@ -3,6 +3,7 @@ from sqlalchemy import Column, ForeignKey from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship + from utils.database.connection import Base diff --git a/app/models/product_plan.py b/app/models/product_plan.py index cd2268c..cd2ef88 100644 --- a/app/models/product_plan.py +++ b/app/models/product_plan.py @@ -2,11 +2,12 @@ from uuid import UUID as UUID4 from uuid import uuid4 -from models.static import PlanTypeEnum from pydantic import BaseModel from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, Numeric, String from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship + +from models.static import PlanTypeEnum from utils.database.connection import Base diff --git a/app/models/user.py b/app/models/user.py index 2e948d0..f26d493 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,10 +1,11 @@ from datetime import datetime from uuid import uuid4 -from models.static import RoleEnum from sqlalchemy import Column, DateTime, Enum, ForeignKey, String from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship + +from models.static import RoleEnum from utils.database.connection import Base diff --git a/app/models/voucher.py b/app/models/voucher.py index 1aba3f3..c512327 100644 --- a/app/models/voucher.py +++ b/app/models/voucher.py @@ -6,6 +6,7 @@ from sqlalchemy import Column, DateTime, ForeignKey, Integer, Numeric, String, Text from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship + from utils.database.connection import Base diff --git a/app/routers/contract.py b/app/routers/contract.py index d6f6616..6b65fef 100644 --- a/app/routers/contract.py +++ b/app/routers/contract.py @@ -1,6 +1,7 @@ from uuid import UUID from fastapi import APIRouter, Depends, HTTPException + from models.contract import Contract, ContractBase, ContractsByProductRead from models.product_contract import ProductContract from utils.database.session import get_database diff --git a/app/routers/customer.py b/app/routers/customer.py index 53b7ba4..da9119d 100644 --- a/app/routers/customer.py +++ b/app/routers/customer.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException + from models.customer import Customer, CustomerBase, CustomerUpdate, User from models.static import RoleEnum from utils.database.session import get_database diff --git a/app/routers/customer_product.py b/app/routers/customer_product.py index 6d28e29..a28033e 100644 --- a/app/routers/customer_product.py +++ b/app/routers/customer_product.py @@ -2,7 +2,8 @@ from uuid import UUID from dateutil.relativedelta import relativedelta -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Body, Depends, HTTPException + from models.customer import User from models.customer_product import ( CustomerProduct, @@ -10,7 +11,9 @@ CustomerProductCalculation, CustomerProductCalculationRequest, CustomerProductCreate, + CustomerProductsCalculation, ExtendedCustomerProductBase, + ProductCalculationResult, ) from models.customer_product_contract import CustomerProductContract from models.invoice import Invoice @@ -150,35 +153,72 @@ async def read_all_by_customer(user: User = Depends(get_user)): return user.customer.products -@router.post("/calculate/", response_model=CustomerProductCalculation) -async def calculate(data: CustomerProductCalculationRequest, - session=Depends(get_database)): - product = session.query(Product).filter_by(uuid=data.product_uuid).first() - - if not product: - raise HTTPException(status_code=404, detail="Product not found.") - - product_plan = session.query(ProductPlan).filter_by( - uuid=data.product_plan_uuid).first() - - if not product_plan or product_plan.product.uuid != product.uuid: - raise HTTPException(404, detail="Plan does not exist on that product.") - - voucher = None - - if data.voucher_uuid: - voucher = session.query(Voucher).filter_by( - uuid=data.voucher_uuid).first() - - if not voucher or not voucher.valid or voucher.product_plan != product_plan.uuid: - raise HTTPException(404, detail="Voucher not valid.") - - final_price, before_price = calculate_full_pricing_in_euro( - product_plan, voucher) - saving = before_price - final_price - - return {"final_price": final_price, - "before_price": before_price, "saving": saving} +@router.post("/calculate/", response_model=CustomerProductsCalculation) +async def calculate( + products: list[CustomerProductCalculationRequest] = Body( + ..., + description="List of product calculation requests. Each must have a unique product_uuid." + ), + session=Depends(get_database) +): + seen_uuids = set() + product_results: list[ProductCalculationResult] = [] + total_final_price = 0 + total_before_price = 0 + total_saving = 0 + + for request_data in products: + product_uuid = request_data.product_uuid + + if product_uuid in seen_uuids: + raise HTTPException( + status_code=400, + detail=f"Duplicate product_uuid: {product_uuid}") + seen_uuids.add(product_uuid) + + product = session.query(Product).filter_by(uuid=product_uuid).first() + if not product: + raise HTTPException(status_code=404, detail="Product not found.") + + plan = session.query(ProductPlan).filter_by( + uuid=request_data.product_plan_uuid).first() + if not plan or plan.product.uuid != product.uuid: + raise HTTPException(status_code=404, + detail="Plan does not exist on that product.") + + voucher = None + if request_data.voucher_uuid: + voucher = session.query(Voucher).filter_by( + uuid=request_data.voucher_uuid).first() + if not voucher or not voucher.valid or voucher.product_plan != plan.uuid: + raise HTTPException( + status_code=404, detail="Voucher not valid.") + + final_price, before_price = calculate_full_pricing_in_euro( + plan, voucher) + saving = before_price - final_price + + total_final_price += final_price + total_before_price += before_price + total_saving += saving + + product_results.append(ProductCalculationResult( + product_uuid=product_uuid, + calculation=CustomerProductCalculation( + final_price=final_price, + before_price=before_price, + saving=saving + ) + )) + + return CustomerProductsCalculation( + overall=CustomerProductCalculation( + final_price=total_final_price, + before_price=total_before_price, + saving=total_saving + ), + products=product_results + ) @router.delete("/{uuid}") diff --git a/app/routers/invoice.py b/app/routers/invoice.py index 58a8b09..dc8e467 100644 --- a/app/routers/invoice.py +++ b/app/routers/invoice.py @@ -2,6 +2,7 @@ import stripe from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request + from models.invoice import Invoice, InvoiceBase, InvoicePayRequest, InvoiceStatusEnum from models.user import User from utils.config import settings, stripe_return_url diff --git a/app/routers/product.py b/app/routers/product.py index 60d0dfc..d50b434 100644 --- a/app/routers/product.py +++ b/app/routers/product.py @@ -1,6 +1,7 @@ from uuid import UUID from fastapi import APIRouter, Depends, HTTPException + from models.customer_product import CustomerProduct from models.product import Product, ProductBase from models.user import User diff --git a/app/routers/voucher.py b/app/routers/voucher.py index d9ccf83..c2f2701 100644 --- a/app/routers/voucher.py +++ b/app/routers/voucher.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException + from models.voucher import Voucher, VoucherBase from utils.database.session import get_database diff --git a/app/utils/database/connection.py b/app/utils/database/connection.py index a5ff596..7ecddb2 100644 --- a/app/utils/database/connection.py +++ b/app/utils/database/connection.py @@ -1,6 +1,7 @@ from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker + from utils.config import settings SQLALCHEMY_DATABASE_URL = ( diff --git a/app/utils/database/session.py b/app/utils/database/session.py index 0a55951..519e81f 100644 --- a/app/utils/database/session.py +++ b/app/utils/database/session.py @@ -1,4 +1,5 @@ from sqlalchemy.orm import Session + from utils.database.connection import SessionLocal diff --git a/app/utils/security/token.py b/app/utils/security/token.py index 22c9600..0923614 100644 --- a/app/utils/security/token.py +++ b/app/utils/security/token.py @@ -3,8 +3,9 @@ from fastapi import Depends, HTTPException, Request, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from keycloak.exceptions import KeycloakAuthenticationError, KeycloakPostError -from models.user import User from pydantic import BaseModel + +from models.user import User from utils.config import keycloak_openid from utils.database.session import get_database