diff --git a/app/models.py b/app/models.py index 4d8e97c3b..d141d8c29 100644 --- a/app/models.py +++ b/app/models.py @@ -1,6 +1,7 @@ import datetime import itertools import uuid +from enum import Enum from flask import current_app, url_for from notifications_utils.clients.encryption.encryption_client import EncryptionError @@ -433,6 +434,32 @@ class Organization(db.Model): def domain_list(self): return [domain.domain for domain in self.domains] + @property + def agreement(self): + try: + active_agreements = [ + agreement + for agreement in self.agreements + if agreement.status == AgreementStatus.ACTIVE + ] + return active_agreements[0] + except IndexError: + return None + + @property + def agreement_active(self): + try: + return self.agreement.status == AgreementStatus.ACTIVE + except AttributeError: + return False + + @property + def has_mou(self): + try: + return self.agreement.type == AgreementType.MOU + except AttributeError: + return False + def serialize(self): return { "id": str(self.id), @@ -2353,14 +2380,34 @@ class WebauthnCredential(db.Model): } +class AgreementType(Enum): + MOU = "MOU" + IAA = "IAA" + + +class AgreementStatus(Enum): + ACTIVE = "active" + EXPIRED = "expired" + + class Agreement(db.Model): __tablename__ = "agreements" id = db.Column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=False ) - type = db.Column(db.String(3), nullable=False, unique=True, index=True) + type = db.Column( + db.Enum(AgreementType, name="agreement_types"), + index=False, + unique=False, + nullable=False, + ) partner_name = db.Column(db.String(255), nullable=False, unique=True, index=True) - status = db.Column(db.String(255), nullable=False, unique=True, index=True) + status = db.Column( + db.Enum(AgreementStatus, name="agreement_statuses"), + index=False, + unique=False, + nullable=False, + ) start_time = db.Column(db.DateTime, nullable=True) end_time = db.Column(db.DateTime, nullable=True) url = db.Column(db.String(255), nullable=False, unique=True, index=True) @@ -2370,6 +2417,7 @@ class Agreement(db.Model): db.ForeignKey("organization.id"), nullable=True, ) + organization = db.relationship("Organization", backref="agreements") def serialize(self): return { diff --git a/migrations/versions/0406_adjust_agreement_model.py b/migrations/versions/0406_adjust_agreement_model.py new file mode 100644 index 000000000..4b91b8c9a --- /dev/null +++ b/migrations/versions/0406_adjust_agreement_model.py @@ -0,0 +1,53 @@ +""" + +Revision ID: eb7747053d5d +Revises: 0404_expire_invites +Create Date: 2023-11-17 15:39:45.470089 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +revision = "0406_adjust_agreement_model" +down_revision = "0405_add_preferred_timezone" + +agreement_type_name = "agreement_types" +agreement_type_options = ("MOU", "IAA") +agreement_types = sa.Enum(*agreement_type_options, name=agreement_type_name) + +agreement_status_name = "agreement_statuses" +agreement_status_options = ("active", "expired") +agreement_statuses = sa.Enum(*agreement_status_options, name=agreement_status_name) + + +def upgrade(): + agreement_types.create(op.get_bind()) + op.execute( + f"ALTER TABLE agreements ALTER COLUMN type TYPE {agreement_type_name} using type::text::{agreement_type_name}" + ) + + agreement_statuses.create(op.get_bind()) + op.execute( + f"ALTER TABLE agreements ALTER COLUMN status TYPE {agreement_status_name} using status::text::{agreement_status_name}" + ) + + +def downgrade(): + op.alter_column( + "agreements", + "status", + existing_type=agreement_statuses, + type_=sa.VARCHAR(length=255), + existing_nullable=False, + ) + op.execute(f"DROP TYPE {agreement_status_name}") + + op.alter_column( + "agreements", + "type", + existing_type=agreement_types, + type_=sa.VARCHAR(length=3), + existing_nullable=False, + ) + op.execute(f"DROP TYPE {agreement_type_name}") diff --git a/tests/app/dao/test_invited_user_dao.py b/tests/app/dao/test_invited_user_dao.py index 4b49c1948..b0a7d75de 100644 --- a/tests/app/dao/test_invited_user_dao.py +++ b/tests/app/dao/test_invited_user_dao.py @@ -133,16 +133,18 @@ def test_should_delete_all_invitations_more_than_one_day_old( def test_should_not_delete_invitations_less_than_two_days_old( sample_user, sample_service ): + two_days = timedelta(days=2) + one_second = timedelta(seconds=1) make_invitation( sample_user, sample_service, - age=timedelta(hours=47, minutes=59, seconds=59), + age=two_days - one_second, # Not quite two days email_address="valid@2.com", ) make_invitation( sample_user, sample_service, - age=timedelta(hours=48), + age=two_days, email_address="expired@1.com", ) diff --git a/tests/app/test_model.py b/tests/app/test_model.py index e2a2d1221..faab07182 100644 --- a/tests/app/test_model.py +++ b/tests/app/test_model.py @@ -15,6 +15,8 @@ from app.models import ( NOTIFICATION_TECHNICAL_FAILURE, SMS_TYPE, Agreement, + AgreementStatus, + AgreementType, AnnualBilling, Notification, NotificationHistory, @@ -28,6 +30,7 @@ from app.models import ( from tests.app.db import ( create_inbound_number, create_notification, + create_organization, create_rate, create_reply_to_email, create_service, @@ -412,6 +415,46 @@ def test_rate_str(): assert rate.__str__() == "1.5 sms 2023-01-01 00:00:00" +@pytest.mark.parametrize( + ["agreement_type", "expected"], + ( + (AgreementType.IAA, False), + (AgreementType.MOU, True), + ), +) +def test_organization_agreement_mou(notify_db_session, agreement_type, expected): + now = datetime.utcnow() + agree = Agreement() + agree.id = "whatever" + agree.start_time = now + agree.end_time = now + agree.status = AgreementStatus.ACTIVE + agree.type = agreement_type + organization = create_organization(name="Something") + organization.agreements.append(agree) + assert organization.has_mou == expected + + +@pytest.mark.parametrize( + ["agreement_status", "expected"], + ( + (AgreementStatus.EXPIRED, False), + (AgreementStatus.ACTIVE, True), + ), +) +def test_organization_agreement_active(notify_db_session, agreement_status, expected): + now = datetime.utcnow() + agree = Agreement() + agree.id = "whatever" + agree.start_time = now + agree.end_time = now + agree.status = agreement_status + agree.type = AgreementType.IAA + organization = create_organization(name="Something") + organization.agreements.append(agree) + assert organization.agreement_active == expected + + def test_agreement_serialize(): agree = Agreement() agree.id = "abc"