From b6a8e172a48fb0bc20a8454b41514dd28ecc790a Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Mon, 22 Nov 2021 16:41:37 -0500 Subject: [PATCH] Work on order related endpoints. --- app/main.py | 7 +- app/methods/order_line_methods.py | 135 ++++++++++++++++++++++++------ app/methods/order_methods.py | 10 ++- app/models/order_line_models.py | 91 +++++++++++++++++++- app/models/order_models.py | 4 +- app/routers/order.py | 7 +- app/routers/order_line.py | 94 +++++++++++++++++++++ 7 files changed, 312 insertions(+), 36 deletions(-) create mode 100644 app/routers/order_line.py diff --git a/app/main.py b/app/main.py index ecc1a92..deeedcf 100644 --- a/app/main.py +++ b/app/main.py @@ -18,7 +18,7 @@ from app.lib_general import log, logging from app.log import log # Import the routers here first: -from app.routers import api_crud, api, importing, account, address, archive, archive_content, contact, cont_edu_cert, cont_edu_cert_person, event, event_badge, event_badge_template, event_exhibit, event_file, event_importing, event_location, event_person, event_person_detail, event_person_tracking, event_presentation, event_presenter, event_registration, event_session, flask_cfg, hosted_file, journal, journal_entry, log_client_viewing, lookup, membership_cfg, membership_group, membership_group_person, membership_person, membership_person_profile, membership_type, membership_type_person, order, order_cart, organization, page, person, post, post_comment, product, site, site_domain, user, user_person, websockets#, e_impexium +from app.routers import api_crud, api, importing, account, address, archive, archive_content, contact, cont_edu_cert, cont_edu_cert_person, event, event_badge, event_badge_template, event_exhibit, event_file, event_importing, event_location, event_person, event_person_detail, event_person_tracking, event_presentation, event_presenter, event_registration, event_session, flask_cfg, hosted_file, journal, journal_entry, log_client_viewing, lookup, membership_cfg, membership_group, membership_group_person, membership_person, membership_person_profile, membership_type, membership_type_person, order, order_line, order_cart, organization, page, person, post, post_comment, product, site, site_domain, user, user_person, websockets#, e_impexium from app.db_sql import db @@ -235,6 +235,11 @@ app.include_router( # prefix='/order', tags=['Order'], ) +app.include_router( + order_line.router, + # prefix='/order', + tags=['Order Line'], +) app.include_router( order_cart.router, prefix='/order/cart', diff --git a/app/methods/order_line_methods.py b/app/methods/order_line_methods.py index dfa1cc0..86dcb06 100644 --- a/app/methods/order_line_methods.py +++ b/app/methods/order_line_methods.py @@ -8,7 +8,7 @@ from app.db_sql import redis_lookup_id_random, sql_insert, sql_select, sql_updat from app.lib_general import log, logging -from app.models.order_line_models import Order_Line_Base +from app.models.order_line_models import Order_Line_Base, Order_Line_Full_Detail_Base # ### BEGIN ### API Order Line Methods ### create_order_line_obj() ### @@ -42,7 +42,7 @@ def load_order_line_obj( exclude_unset: bool = True, # NOTE: Normally this is True model_as_dict: bool = False, # NOTE: Normally this is False ) -> Order_Line_Base|dict|bool: - log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL + log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) if order_line_id := redis_lookup_id_random(record_id_random=order_line_id, table_name='order_line'): pass @@ -68,6 +68,31 @@ def load_order_line_obj( # ### END ### API Order Line Methods ### load_order_line_obj() ### +# ### BEGIN ### API Order Line Methods ### load_order_line_obj_full_detail() ### +# Updated 2021-11-22 +def load_order_line_obj_full_detail( + order_line_rec: dict, + by_alias: bool = True, + exclude_unset: bool = True, # NOTE: Normally this is True + model_as_dict: bool = False, # NOTE: Normally this is False + ) -> Order_Line_Full_Detail_Base|dict|bool: + log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL + log.debug(locals()) + + try: + order_line_obj_full_detail = Order_Line_Full_Detail_Base(**order_line_rec) + log.debug(order_line_obj_full_detail) + except ValidationError as e: + log.error(e.json()) + return False + + if model_as_dict: + return order_line_obj_full_detail.dict(by_alias=by_alias, exclude_unset=exclude_unset) # pylint: disable=no-member + else: + return order_line_obj_full_detail +# ### END ### API Order Line Methods ### load_order_line_obj_full_detail() ### + + # ### BEGIN ### API Order Line Methods ### update_order_line_obj() ### def update_order_line_obj( order_line_id: int|str, # Ideally the int ID should be passed. This allows for updating of the id_random value. @@ -135,28 +160,73 @@ def update_order_line_obj( def get_order_line_rec_list( for_obj_type: str, for_obj_id: str, + from_datetime: datetime.datetime = None, # For the order, not order_line + to_datetime: datetime.datetime = None, # For the order, not order_line + prod_type = 'all', # all, cont_edu_cert, event, fundraising, membership, etc + status: str = 'closed', # started, in progress, complete, all + full_detail: bool = False, + # enabled: str = 'enabled', # enabled, disabled, all limit: int = 1000, ) -> list|bool: - log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL + log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) if for_obj_id := redis_lookup_id_random(record_id_random=for_obj_id, table_name=for_obj_type): pass else: return False + data = {} data[f'{for_obj_type}_id'] = for_obj_id # data['for_obj_type'] = for_obj_type - sql_obj_type_id = f'`tbl`.{for_obj_type}_id = :{for_obj_type}_id' + sql_obj_type_id = f'`order_line`.{for_obj_type}_id = :{for_obj_type}_id' - # if enabled in ['enabled', 'disabled', 'all']: - # if enabled == 'enabled': - # data['enable'] = True - # sql_enabled = f'AND `tbl`.enable = :enable' - # elif enabled == 'disabled': - # data['enable'] = False - # sql_enabled = f'AND `tbl`.enable = :enable' - # elif enabled == 'all': - # sql_enabled = '' - sql_enabled = '' + allowed_prod_type_li = ['cont_edu_cert', 'event', 'fundraising', 'membership', 'other'] # TEMPORARY list... + sql_prod_type = '' + if prod_type in allowed_prod_type_li: + if prod_type == 'closed' or prod_type == 'complete': + data['prod_type'] = ['closed', 'complete'] + sql_prod_type = f'AND `order_line`.product_type IN :prod_type' + elif prod_type == 'locked' or prod_type == 'in progress': + data['prod_type'] = ['locked', 'in progress'] + sql_prod_type = f'AND `order_line`.product_type IN :prod_type' + else: + data['prod_type'] = prod_type + sql_prod_type = f'AND `order_line`.product_type = :prod_type' + elif prod_type == 'all': + sql_prod_type = f'AND `order_line`.product_type IS NOT NULL' + else: + log.warning('The prod_type value passed is not allowed. Returning None') + return False + + allowed_status_li = ['open', 'locked', 'in progress', 'reopened', 'closed', 'complete', 'canceled', 'other'] # TEMPORARY list... + sql_status = '' + if status in allowed_status_li: + if status == 'closed' or status == 'complete': + data['status'] = ['closed', 'complete'] + sql_status = f'AND `order_line`.order_status IN :status' + elif status == 'locked' or status == 'in progress': + data['status'] = ['locked', 'in progress'] + sql_status = f'AND `order_line`.order_status IN :status' + else: + data['status'] = status + sql_status = f'AND `order_line`.order_status = :status' + elif status == 'all': + sql_status = f'AND `order_line`.order_status IS NOT NULL' + else: + log.warning('The status value passed is not allowed. Returning None') + return False + + if from_datetime and to_datetime: + data['from_datetime'] = from_datetime + data['to_datetime'] = to_datetime + sql_from_to_datetime = f'AND `order_line`.order_created_on >= :from_datetime AND `order_line`.order_created_on <= :to_datetime' + elif from_datetime: + data['from_datetime'] = from_datetime + sql_from_to_datetime = f'AND `order_line`.order_created_on >= :from_datetime' + elif to_datetime: + data['to_datetime'] = to_datetime + sql_from_to_datetime = f'AND `order_line`.order_created_on <= :to_datetime' + else: + sql_from_to_datetime = '' if limit: data['limit'] = limit @@ -164,21 +234,38 @@ def get_order_line_rec_list( else: sql_limit = '' - sql = f""" - SELECT `tbl`.id AS 'order_line_id', `tbl`.id_random AS 'order_line_id_random' - FROM `order_line` AS `tbl` - WHERE - {sql_obj_type_id} - {sql_enabled} - ORDER BY `tbl`.created_on DESC, `tbl`.updated_on DESC - {sql_limit}; - """ + log.debug(data) + + if not full_detail: + sql = f""" + SELECT `order_line`.id AS 'order_line_id', `order_line`.id_random AS 'order_line_id_random' + FROM `v_order_line` AS `order_line` + WHERE + {sql_obj_type_id} + {sql_prod_type} + {sql_status} + {sql_from_to_datetime} + ORDER BY order_line.name, `order_line`.created_on DESC, `order_line`.updated_on DESC + {sql_limit}; + """ + else: + sql = f""" + SELECT * + FROM `v_order_line_full_detail` AS `order_line` + WHERE + {sql_obj_type_id} + {sql_prod_type} + {sql_status} + {sql_from_to_datetime} + ORDER BY order_line.name, `order_line`.created_on DESC, `order_line`.updated_on DESC + {sql_limit}; + """ + log.debug(sql) if order_line_rec_li_result := sql_select(data=data, sql=sql, as_list=True): order_line_rec_li = order_line_rec_li_result else: order_line_rec_li = [] - log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(order_line_rec_li_result) return order_line_rec_li diff --git a/app/methods/order_methods.py b/app/methods/order_methods.py index 03ffa76..efb1483 100644 --- a/app/methods/order_methods.py +++ b/app/methods/order_methods.py @@ -277,7 +277,7 @@ def load_order_obj( # ### BEGIN ### API Order Methods ### get_order_rec_list() ### -# Updated 2021-11-19 +# Updated 2021-11-22 def get_order_rec_list( for_obj_type: str, for_obj_id: str, @@ -288,11 +288,12 @@ def get_order_rec_list( enabled: str = 'enabled', # enabled, disabled, all limit: int = 1000, ) -> list|bool: - log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL + log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) if for_obj_id := redis_lookup_id_random(record_id_random=for_obj_id, table_name=for_obj_type): pass else: return False + data = {} data[f'{for_obj_type}_id'] = for_obj_id # data['for_obj_type'] = for_obj_type @@ -313,7 +314,7 @@ def get_order_rec_list( data['status'] = status sql_status = f'AND `order`.status = :status' elif status == 'all': - sql_status = '' + sql_status = f'AND `order`.status IS NOT NULL' else: log.warning('The status value passed is not allowed. Returning None') return False @@ -348,6 +349,8 @@ def get_order_rec_list( else: sql_limit = '' + log.debug(data) + sql = f""" SELECT `order`.id AS 'order_id', `order`.id_random AS 'order_id_random' FROM `order` AS `order` @@ -359,6 +362,7 @@ def get_order_rec_list( ORDER BY `order`.created_on DESC, `order`.updated_on DESC {sql_limit}; """ + log.debug(sql) if order_rec_li_result := sql_select(data=data, sql=sql, as_list=True): order_rec_li = order_rec_li_result diff --git a/app/models/order_line_models.py b/app/models/order_line_models.py index 5ebb4ff..584ebc0 100644 --- a/app/models/order_line_models.py +++ b/app/models/order_line_models.py @@ -11,7 +11,7 @@ from .common_field_schema import base_fields, default_num_bytes class Order_Line_Base(BaseModel): - log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL + log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) id_random: Optional[str] = Field( @@ -29,6 +29,11 @@ class Order_Line_Base(BaseModel): product_id_random: str product_id: Optional[int] + account_id_random: Optional[str] + account_id: Optional[int] + + account_name: Optional[str] + product_for_type: Optional[str] # Copied from product record product_for_id_random: Optional[str] # Copied from product record NOPE product_for_id: Optional[int] # Copied from product record @@ -65,11 +70,14 @@ class Order_Line_Base(BaseModel): for_person_id: Optional[int] for_person_id_random: Optional[str] for_person_given_name: Optional[str] # Dynamic from v_order_line + for_person_family_name: Optional[str] # Dynamic from v_order_line + for_person_display_name: Optional[str] # Dynamic from v_order_line for_person_full_name: Optional[str] # Dynamic from v_order_line name: Optional[str] # Should be the same as product_name above quantity: int = Field(0, ge=0, lt=150) amount: int = Field(0, ge=0, lt=1500000) + dollar_amount: Optional[str] recurring: Optional[bool] = False message: Optional[str] @@ -78,6 +86,13 @@ class Order_Line_Base(BaseModel): created_on: Optional[datetime.datetime] = None updated_on: Optional[datetime.datetime] = None + # Including convenience data + # This is only for convenience. Probably going to keep unless it causes a problem. + order_status: Optional[str] + order_notes: Optional[str] + order_created_on: Optional[datetime.datetime] = None + order_updated_on: Optional[datetime.datetime] = None + # Including other related objects # product: Optional[Union[Product_Base, None]] # Future use? # for_person: Optional[Union[Person_Base, None]] # Future use? @@ -157,6 +172,15 @@ class Order_Line_Base(BaseModel): # return redis_lookup_id_random(record_id_random=values['curr_product_for_id_random'], table_name=values['curr_product_for_type']) # return None + @validator('account_id', always=True) + def account_id_lookup(cls, v, values, **kwargs): + log.setLevel(logging.WARNING) + log.debug(locals()) + + if values.get('account_id_random', None): + return redis_lookup_id_random(record_id_random=values['account_id_random'], table_name='account') + return None + class Config: underscore_attrs_are_private = True allow_population_by_field_name = True @@ -164,7 +188,7 @@ class Order_Line_Base(BaseModel): class Order_Line_DB_Base(BaseModel): - log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL + log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) id_random: Optional[str] = Field( @@ -209,4 +233,67 @@ class Order_Line_DB_Base(BaseModel): underscore_attrs_are_private = True allow_population_by_field_name = True + +class Order_Line_Full_Detail_Base(Order_Line_Base): + log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL + log.debug(locals()) + + person_id: Optional[int] + person_id_random: Optional[str] + person_given_name: Optional[str] + person_family_name: Optional[str] + person_display_name: Optional[str] + person_full_name: Optional[str] + + person_contact_id: Optional[int] + person_contact_id_random: Optional[str] + + person_contact_for_type: Optional[str] + person_contact_for_id: Optional[int] + + person_contact_name: Optional[str] + person_contact_email: Optional[str] + person_contact_cc_email: Optional[str] + + person_contact_phone_mobile: Optional[str] + person_contact_phone_home: Optional[str] + person_contact_phone_office: Optional[str] + person_contact_phone_land: Optional[str] + person_contact_phone_fax: Optional[str] + person_contact_phone_other: Optional[str] + + person_contact_address_id: Optional[int] + person_contact_address_id_random: Optional[str] + + person_contact_address_for_type: Optional[str] + person_contact_address_for_id: Optional[int] + + person_contact_address_name: Optional[str] + person_contact_address_organization_name: Optional[str] + person_contact_address_line_1: Optional[str] + person_contact_address_line_2: Optional[str] + person_contact_address_line_3: Optional[str] + person_contact_address_city: Optional[str] + person_contact_address_country_subdivision_code: Optional[str] + person_contact_address_country_subdivision_name: Optional[str] # From country subdivision lookup table + person_contact_address_state_province: Optional[str] # Avoid using + person_contact_address_postal_code: Optional[str] + person_contact_address_country_alpha_2_code: Optional[str] + person_contact_address_country_name: Optional[str] # From country lookup table + person_contact_address_country: Optional[str] # Avoid using + + person_contact_address_lu_time_zone_id: Optional[str] + + # Including convenience data + # This is only for convenience. Probably going to keep unless it causes a problem. + person_email: Optional[str] + person_cc_email: Optional[str] + # Maybe add timezone in the future? + + class Config: + underscore_attrs_are_private = True + allow_population_by_field_name = True + + + Order_Line_Base.update_forward_refs() diff --git a/app/models/order_models.py b/app/models/order_models.py index dcdd08d..e717749 100644 --- a/app/models/order_models.py +++ b/app/models/order_models.py @@ -15,7 +15,7 @@ from app.models.user_models import User_Base class Order_Base(BaseModel): - log.setLevel(logging.WARNING) + log.setLevel(logging.INFO) log.debug(locals()) id_random: Optional[str] = Field( @@ -118,7 +118,7 @@ class Order_Base(BaseModel): class Order_DB_Base(BaseModel): - log.setLevel(logging.WARNING) + log.setLevel(logging.INFO) log.debug(locals()) account_id_random: Optional[str] diff --git a/app/routers/order.py b/app/routers/order.py index 5a2b982..5144018 100644 --- a/app/routers/order.py +++ b/app/routers/order.py @@ -210,7 +210,7 @@ async def get_order_obj( # ### END ### API Order Routes ### get_order_obj() ### -# ### BEGIN ### API Order ### get_account_obj_order_list() ### +# ### BEGIN ### API Order ### get_obj_id_order_list() ### # Updated 2021-11-19 @router.get('/{obj_type}/{obj_id}/order/list', response_model=Resp_Body_Base) async def get_obj_id_order_list( @@ -220,7 +220,7 @@ async def get_obj_id_order_list( to_datetime: datetime.datetime = None, inc_order_cfg: bool = False, inc_order_line_list: bool = False, - # inc_order_w_prod_type: str = 'all', # all, membership, fundraising, event, etc + # w_prod_type: str = 'all', # all, cont_edu_cert, event, fundraising, membership, etc inc_person: bool = False, status: str = 'closed', # open, locked, reopened?, closed (complete), canceled, other enabled: str = 'enabled', @@ -244,7 +244,6 @@ async def get_obj_id_order_list( log.debug(from_datetime) log.debug(to_datetime) - # Updated 2021-11-19 if order_rec_list_result := get_order_rec_list( for_obj_type = obj_type, for_obj_id = obj_id, @@ -279,7 +278,7 @@ async def get_obj_id_order_list( return mk_resp(data=False, status_code=400, response=response) # Bad Request return mk_resp(data=response_data) -# ### END ### API Order ### get_account_obj_order_list() ### +# ### END ### API Order ### get_obj_id_order_list() ### # ### BEGIN ### API Order ### get_account_obj_order_list() ### diff --git a/app/routers/order_line.py b/app/routers/order_line.py new file mode 100644 index 0000000..07b6e51 --- /dev/null +++ b/app/routers/order_line.py @@ -0,0 +1,94 @@ +import datetime +from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query, Response, status +from pydantic import BaseModel, EmailStr, Field +from typing import Dict, List, Optional, Set, Union + +from app.lib_general import * +from app.config import settings +from app.db_sql import * + +from app.routers.api_crud import delete_obj_template, get_obj_template, get_obj_li_template, patch_obj_template, post_obj_template + +from app.methods.order_line_methods import get_order_line_rec_list, load_order_line_obj, load_order_line_obj_full_detail + +from app.models.response_models import Resp_Body_Base, mk_resp + + +router = APIRouter() + + +# ### BEGIN ### API Order Line ### get_obj_id_order_line_list() ### +# Updated 2021-11-22 +@router.get('/{obj_type}/{obj_id}/order/line/list', response_model=Resp_Body_Base) +async def get_obj_id_order_line_list( + obj_type: str = Query(..., min_length=4, max_length=25), # Expects account or order or person + obj_id: str = Query(..., min_length=11, max_length=22), + from_datetime: datetime.datetime = None, + to_datetime: datetime.datetime = None, + prod_type: str = 'all', # all, cont_edu_cert, event, fundraising, membership, etc + status: str = 'closed', # open, locked, reopened?, closed (complete), canceled, other + full_detail: bool = False, # Uses a different view: v_order_line_full_detail + # enabled: str = 'enabled', + limit: int = 50, + by_alias: Optional[bool] = True, + exclude_unset: Optional[bool] = True, + x_account_id: str = Header(..., min_length=11, max_length=22), + response: Response = Response, + ): + log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL + log.debug(locals()) + + if obj_type in ['account', 'order', 'person']: + if obj_id := redis_lookup_id_random(record_id_random=obj_id, table_name=obj_type): pass + else: return mk_resp(data=None, status_code=404, response=response) + else: + log.warning('Likely bad request') + return mk_resp(data=False, status_code=400, response=response) # Bad Request + + log.debug(from_datetime) + log.debug(to_datetime) + + if order_line_rec_list_result := get_order_line_rec_list( + for_obj_type = obj_type, + for_obj_id = obj_id, + from_datetime = from_datetime, + to_datetime = to_datetime, + prod_type = prod_type, + status = status, + full_detail = full_detail, + # enabled = enabled, + limit = limit, + ): + order_line_result_list = [] + for order_line_rec in order_line_rec_list_result: + if not full_detail: + if load_order_line_result := load_order_line_obj( + order_line_id = order_line_rec.get('order_line_id', None), + limit = limit, + by_alias = by_alias, + exclude_unset = exclude_unset, + # model_as_dict = model_as_dict, + ): + order_line_result_list.append(load_order_line_result) + else: + order_line_result_list.append(None) + else: # Uses a different view: v_order_line_full_detail + if load_order_line_result := load_order_line_obj_full_detail( + order_line_rec = order_line_rec, + by_alias = by_alias, + exclude_unset = exclude_unset, + # model_as_dict = model_as_dict, + ): + order_line_result_list.append(load_order_line_result) + else: + order_line_result_list.append(None) + response_data = order_line_result_list + elif order_line_rec_list_result is None: + log.info('No results') + return mk_resp(data=None, status_code=404, response=response) # Not Found + else: + log.warning('Likely bad request') + return mk_resp(data=False, status_code=400, response=response) # Bad Request + + return mk_resp(data=response_data) +# ### END ### API Order Line ### get_obj_id_order_line_list() ###