From b6fd7de0acbe986980907f9304dec335faf74d57 Mon Sep 17 00:00:00 2001 From: James McKinney <26463+jpmckinney@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:14:31 -0400 Subject: [PATCH] docs: Organize and document more model fields --- app/models.py | 108 +++++++++++++++++++++++++++++++++++++------ app/settings.py | 7 ++- docs/api/index.rst | 1 + docs/api/models.rst | 6 +++ docs/api/parsers.rst | 6 +++ tests/conftest.py | 21 +++++---- 6 files changed, 124 insertions(+), 25 deletions(-) create mode 100644 docs/api/parsers.rst diff --git a/app/models.py b/app/models.py index 7fdcf80c..7f1e623f 100644 --- a/app/models.py +++ b/app/models.py @@ -334,8 +334,14 @@ class LenderBase(SQLModel): name: str = Field(default="", unique=True) email_group: str = Field(default="") type: str = Field(default="") # LENDER_TYPES - sla_days: int | None logo_filename: str = Field(default="") + + #: The number of days within which the lender agrees to respond to application changes. + #: + #: .. seealso:: :attr:`~app.settings.Settings.progress_to_remind_started_applications` + sla_days: int | None + #: Additional HTML content to include in a :attr:`app.models.MessageType.APPROVED_APPLICATION` message, if the + #: "additional_comments" key in the application's :attr:`app.models.APplication.lender_approved_data` isn't set. default_pre_approval_message: str = Field(default="") @@ -492,40 +498,107 @@ def last_updated(cls, session: Session) -> datetime | None: class ApplicationBase(SQLModel): + #: The secure identifier for the application, for passwordless login. uuid: str = Field(unique=True) + #: The email address at which the borrower is contacted. primary_email: str = Field(default="") - status: ApplicationStatus = Field(default=ApplicationStatus.PENDING) + #: The hashed award and borrower identifiers, for privacy-preserving long-term identification. award_borrower_identifier: str = Field(default="") - contract_amount_submitted: Decimal | None = Field(max_digits=16, decimal_places=2) - disbursed_final_amount: Decimal | None = Field(max_digits=16, decimal_places=2) + + # Request + amount_requested: Decimal | None = Field(max_digits=16, decimal_places=2) currency: str = Field(default="COP", description="ISO 4217 currency code") repayment_years: int | None repayment_months: int | None payment_start_date: datetime | None calculator_data: dict[str, Any] = Field(default_factory=dict, sa_type=JSON) - borrower_credit_product_selected_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + + # Status + + #: The status of the application. + status: ApplicationStatus = Field(default=ApplicationStatus.PENDING) + #: Whether the borrower has confirmed the credit product but not yet submitted the application, or + #: the lender has requested information and the borrower has not yet uploaded documents. pending_documents: bool = Field(default=False) + #: Whether the borrower has changed the primary email for the application, but hasn't confirmed it. pending_email_confirmation: bool = Field(default=False) - borrower_submitted_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) - borrower_accepted_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + + # Timeline + + #: The time at which the application expires. + #: + #: .. seealso:: :attr:`~app.settings.Settings.application_expiration_days` + expired_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + + #: The time at which the application transitioned to :attr:`~app.models.ApplicationStatus.DECLINED`. borrower_declined_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) - overdued_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + #: The reason(s) for which the borrower declined the invitation. + #: + #: .. seealso:: :class:`app.parsers.ApplicationDeclineFeedbackPayload` borrower_declined_preferences_data: dict[str, Any] = Field(default_factory=dict, sa_type=JSON) + #: Whether the borrower declined only this invitation or all invitations. + #: + #: .. seealso:: :class:`app.parsers.ApplicationDeclinePayload` borrower_declined_data: dict[str, Any] = Field(default_factory=dict, sa_type=JSON) + + #: The time at which the application transitioned to :attr:`~app.models.ApplicationStatus.ACCEPTED`. + borrower_accepted_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + #: The time at which the borrower most recently selected a credit product. + borrower_credit_product_selected_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + + #: The time at which the application transitioned from :attr:`~app.models.ApplicationStatus.SUBMITTED`. + borrower_submitted_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + + #: The time at which the application transitioned to :attr:`~app.models.ApplicationStatus.STARTED`, + #: from :attr:`~app.models.ApplicationStatus.SUBMITTED`. lender_started_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) - secop_data_verification: dict[str, Any] = Field(default_factory=dict, sa_type=JSON) + + #: The time at which the application transitioned to :attr:`~app.models.ApplicationStatus.INFORMATION_REQUESTED`. + information_requested_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + + #: The time at which the application transitioned to :attr:`~app.models.ApplicationStatus.REJECTED`. + lender_rejected_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + #: The reason(s) for which the application was rejected. + #: + #: .. seealso:: :class:`app.parsers.LenderRejectedApplication` + lender_rejected_data: dict[str, Any] = Field(default_factory=dict, sa_type=JSON) + + #: The time at which the application transitioned to :attr:`~app.models.ApplicationStatus.APPROVED`. lender_approved_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + #: The reason(s) for which the application was approved. + #: + #: .. seealso:: :class:`app.parsers.LenderApprovedData` lender_approved_data: dict[str, Any] = Field(default_factory=dict, sa_type=JSON) - lender_rejected_data: dict[str, Any] = Field(default_factory=dict, sa_type=JSON) - lender_rejected_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + #: Whether the borrower fields (keys) have been verified (``bool`` values) by the lender. + secop_data_verification: dict[str, Any] = Field(default_factory=dict, sa_type=JSON) + + #: The time at which the application transitioned to :attr:`~app.models.ApplicationStatus.CONTRACT_UPLOADED`. borrower_uploaded_contract_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + #: The amount of the contract submitted by the borrower. + contract_amount_submitted: Decimal | None = Field(max_digits=16, decimal_places=2) + + #: The time at which the application transitioned to :attr:`~app.models.ApplicationStatus.COMPLETED`. lender_completed_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + #: The amount of the loan disbursed by the lender. + disbursed_final_amount: Decimal | None = Field(max_digits=16, decimal_places=2) + #: The total number of days waiting for the lender. + #: + #: .. seealso:: :meth:`app.models.Application.days_waiting_for_lender` completed_in_days: int | None - expired_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) - archived_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) - information_requested_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + + #: The time at which the application was most recently overdue (reset once completed). + #: + #: .. seealso:: :attr:`~app.settings.Settings.progress_to_remind_started_applications` + overdued_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + #: The time at which the application transitioned to :attr:`~app.models.ApplicationStatus.LAPSED`. + #: + #: .. seealso:: :meth:`app.models.Application.lapseable` application_lapsed_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) + #: The time at which the application was archived. + #: + #: .. seealso:: :meth:`app.models.Application.archivable` + archived_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True))) # Relationships award_id: int | None = Field(foreign_key="award.id", index=True) @@ -821,8 +894,11 @@ class BorrowerDocument(BorrowerDocumentBase, ActiveRecordMixin, table=True): class Message(SQLModel, ActiveRecordMixin, table=True): id: int | None = Field(default=None, primary_key=True) + #: The type of email message. type: MessageType + #: The SES ``MessageId``. external_message_id: str = Field(default="") + #: The body of the email message, if directly provided by a lender. body: str = Field(default="") # Relationships @@ -864,10 +940,14 @@ class EventLog(SQLModel, ActiveRecordMixin, table=True): class UserBase(SQLModel): id: int | None = Field(default=None, primary_key=True) + #: The authorization group of the user. type: UserType = Field(default=UserType.FI) language: str = Field(default="es", description="ISO 639-1 language code") + #: The email address with which the user logs in and is contacted. email: str = Field(unique=True) + #: The name by which the user is addressed in emails and identified in application action histories. name: str = Field(default="") + #: The Cognito ``Username``. external_id: str = Field(default="", index=True) # Relationships diff --git a/app/settings.py b/app/settings.py index 356b7976..b13fbd1b 100644 --- a/app/settings.py +++ b/app/settings.py @@ -59,7 +59,10 @@ class Settings(BaseSettings): #: The number of days after the application is created, after which a PENDING or DECLINED application becomes #: inaccessible to the borrower. #: - #: .. seealso:: :doc:`fetch-awards` + #: .. seealso:: + #: + #: - :attr:`~app.settings.Settings.reminder_days_before_expiration` + #: - :doc:`fetch-awards` application_expiration_days: int = 7 #: The number of days before a PENDING application's expiration date, past which the borrower is sent a reminder. #: @@ -76,7 +79,7 @@ class Settings(BaseSettings): #: - :attr:`app.models.Application.pending_submission_reminder` reminder_days_before_lapsed: int = 3 #: Lenders agree to respond to application changes (STARTED, CONTRACT_UPLOADED) within a number of days, known as - #: Service Level Agreement (SLA) days. + #: Service Level Agreement (SLA) days (:attr:`app.models.Lender.sla_days`). #: #: This is the ratio of SLA days for which to wait for the lender to respond, after which an application is #: overdue, and the lender is sent a reminder. diff --git a/docs/api/index.rst b/docs/api/index.rst index 87a8dc5b..6324fa27 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -8,6 +8,7 @@ API reference settings models enums + parsers util exceptions diff --git a/docs/api/models.rst b/docs/api/models.rst index 4d6acb20..63954521 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -5,4 +5,10 @@ Models .. autoclass:: app.models.Application +.. autoclass:: app.models.Lender + .. autoclass:: app.models.CreditProduct + +.. autoclass:: app.models.Message + +.. autoclass:: app.models.User diff --git a/docs/api/parsers.rst b/docs/api/parsers.rst new file mode 100644 index 00000000..cf390c3f --- /dev/null +++ b/docs/api/parsers.rst @@ -0,0 +1,6 @@ +Parsers +======= + +.. automodule:: app.parsers + :members: + :undoc-members: diff --git a/tests/conftest.py b/tests/conftest.py index a6a182f1..22bce951 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -303,18 +303,27 @@ def application_payload(application_uuid, award, borrower): "uuid": application_uuid, "primary_email": "test@example.com", "award_borrower_identifier": "test_hash_12345678", - "contract_amount_submitted": None, + # Request "amount_requested": 10000, "currency": "COP", + "repayment_months": None, "calculator_data": {}, + # Status "pending_documents": True, "pending_email_confirmation": True, - "borrower_submitted_at": None, - "borrower_accepted_at": None, + # Timeline "borrower_declined_at": None, "borrower_declined_preferences_data": {}, "borrower_declined_data": {}, + "borrower_accepted_at": None, + "borrower_submitted_at": None, "lender_started_at": None, + "lender_rejected_at": None, + "lender_rejected_data": {}, + "lender_approved_at": None, + "lender_approved_data": {}, + "borrower_uploaded_contract_at": None, + "contract_amount_submitted": None, "secop_data_verification": { "legal_name": False, "address": True, @@ -324,12 +333,6 @@ def application_payload(application_uuid, award, borrower): "sector": True, "email": True, }, - "lender_approved_at": None, - "lender_approved_data": {}, - "lender_rejected_data": {}, - "lender_rejected_at": None, - "repayment_months": None, - "borrower_uploaded_contract_at": None, "completed_in_days": None, "archived_at": None, # Relationships