From b3bb2abf1a98457a6253dfec602f355221e9bd3e Mon Sep 17 00:00:00 2001 From: Tyler Zale Date: Fri, 24 May 2024 10:24:11 -0700 Subject: [PATCH] fix new vs reuse sbc account flow --- .../versions/20240524_894fe4847de6_.py | 30 ++++++++ strr-api/src/strr_api/models/rental.py | 1 + .../strr_api/requests/RegistrationRequest.py | 29 +++++++- strr-api/src/strr_api/resources/account.py | 28 ++++--- .../src/strr_api/resources/registrations.py | 5 +- .../responses/RegistrationResponse.py | 11 +-- .../schemas/schemas/registration.json | 17 ++++- .../strr_api/services/registration_service.py | 12 +-- ...json => registration_new_sbc_account.json} | 0 .../json/registration_use_sbc_account.json | 73 +++++++++++++++++++ strr-api/tests/unit/resources/test_account.py | 5 +- .../unit/resources/test_registrations.py | 3 + strr-api/tests/unit/schemas/test_utils.py | 11 +-- 13 files changed, 189 insertions(+), 36 deletions(-) create mode 100644 strr-api/migrations/versions/20240524_894fe4847de6_.py rename strr-api/tests/mocks/json/{registration.json => registration_new_sbc_account.json} (100%) create mode 100644 strr-api/tests/mocks/json/registration_use_sbc_account.json diff --git a/strr-api/migrations/versions/20240524_894fe4847de6_.py b/strr-api/migrations/versions/20240524_894fe4847de6_.py new file mode 100644 index 00000000..2055144d --- /dev/null +++ b/strr-api/migrations/versions/20240524_894fe4847de6_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 894fe4847de6 +Revises: 3d7b4953b1ee +Create Date: 2024-05-24 09:34:10.209903 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '894fe4847de6' +down_revision = '3d7b4953b1ee' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('registrations', schema=None) as batch_op: + batch_op.add_column(sa.Column('sbc_account_id', sa.INTEGER(), autoincrement=False, nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('registrations', schema=None) as batch_op: + batch_op.drop_column('sbc_account_id') + # ### end Alembic commands ### diff --git a/strr-api/src/strr_api/models/rental.py b/strr-api/src/strr_api/models/rental.py index 8e463075..d74e3ac2 100644 --- a/strr-api/src/strr_api/models/rental.py +++ b/strr-api/src/strr_api/models/rental.py @@ -99,6 +99,7 @@ class Registration(db.Model): __tablename__ = "registrations" id = db.Column(db.Integer, primary_key=True, autoincrement=True) + sbc_account_id = db.Column(db.Integer, nullable=True) rental_property_id = db.Column(db.Integer, db.ForeignKey("rental_properties.id"), nullable=False) submission_date = db.Column(db.DateTime, default=datetime.now, nullable=False) updated_date = db.Column(db.DateTime, default=datetime.now, nullable=False) diff --git a/strr-api/src/strr_api/requests/RegistrationRequest.py b/strr-api/src/strr_api/requests/RegistrationRequest.py index 41b20dd6..20909263 100644 --- a/strr-api/src/strr_api/requests/RegistrationRequest.py +++ b/strr-api/src/strr_api/requests/RegistrationRequest.py @@ -24,13 +24,36 @@ def __init__(self, street, city, region, postalCode, country, streetAdditional=N self.postalCode = postalCode self.country = country + def to_dict(self): + """Convert object to dictionary for json serialization.""" + return { + key: value + for key, value in { + "street": self.street, + "streetAdditional": self.streetAdditional, + "city": self.city, + "region": self.region, + "postalCode": self.postalCode, + "country": self.country, + }.items() + if value is not None + } + class SelectedAccount: """SelectedAccount payload object.""" - def __init__(self, name, mailingAddress): - self.name = name - self.mailingAddress = SBCMailingAddress(**mailingAddress) + def __init__(self, sbc_account_id=None, name=None, mailingAddress=None): + self.sbc_account_id = None + self.name = None + self.mailingAddress = None + + if sbc_account_id: + self.sbc_account_id = sbc_account_id + if name: + self.name = name + if mailingAddress: + self.mailingAddress = SBCMailingAddress(**mailingAddress) class Registration: diff --git a/strr-api/src/strr_api/resources/account.py b/strr-api/src/strr_api/resources/account.py index 7e8e24ee..78905002 100644 --- a/strr-api/src/strr_api/resources/account.py +++ b/strr-api/src/strr_api/resources/account.py @@ -41,7 +41,7 @@ from http import HTTPStatus from flasgger import swag_from -from flask import Blueprint, jsonify, request +from flask import Blueprint, g, jsonify, request from flask_cors import cross_origin from strr_api.common.auth import jwt @@ -125,15 +125,23 @@ def create_account(): registration_request = RegistrationRequest(**json_input) selected_account = registration_request.selectedAccount - # TODO: link SBC account to User account - AuthService.create_user_account(token, selected_account.name, selected_account.mailingAddress) + # SBC Account lookup or creation + sbc_account_id = None + if selected_account.sbc_account_id: + sbc_account_id = selected_account.sbc_account_id + else: + new_account = AuthService.create_user_account( + token, selected_account.name, selected_account.mailingAddress.to_dict() + ) + sbc_account_id = new_account.get("id") # DO POSTAL CODE VALIDATION IF COUNTRY IS CANADA - selected_account.mailingAddress.postalCode = validate_and_format_canadian_postal_code( - selected_account.mailingAddress.country, - selected_account.mailingAddress.region, - selected_account.mailingAddress.postalCode, - ) + if selected_account.mailingAddress: + selected_account.mailingAddress.postalCode = validate_and_format_canadian_postal_code( + selected_account.mailingAddress.country, + selected_account.mailingAddress.region, + selected_account.mailingAddress.postalCode, + ) registration_request.registration.unitAddress.postalCode = validate_and_format_canadian_postal_code( registration_request.registration.unitAddress.country, @@ -164,7 +172,9 @@ def create_account(): ) ) - registration = RegistrationService.save_registration(token, registration_request.registration) + registration = RegistrationService.save_registration( + g.jwt_oidc_token_info, sbc_account_id, registration_request.registration + ) return jsonify(Registration.from_db(registration).model_dump(mode="json")), HTTPStatus.CREATED except ValidationException as auth_exception: return exception_response(auth_exception) diff --git a/strr-api/src/strr_api/resources/registrations.py b/strr-api/src/strr_api/resources/registrations.py index a3170a17..7bf16524 100644 --- a/strr-api/src/strr_api/resources/registrations.py +++ b/strr-api/src/strr_api/resources/registrations.py @@ -40,7 +40,7 @@ from http import HTTPStatus from flasgger import swag_from -from flask import Blueprint, jsonify +from flask import Blueprint, g, jsonify from flask_cors import cross_origin from strr_api.common.auth import jwt @@ -70,8 +70,7 @@ def get_registrations(): """ try: - token = jwt.get_token_auth_header() - registrations = RegistrationService.list_registrations(token) + registrations = RegistrationService.list_registrations(g.jwt_oidc_token_info) return ( jsonify([Registration.from_db(registration).model_dump(mode="json") for registration in registrations]), HTTPStatus.OK, diff --git a/strr-api/src/strr_api/responses/RegistrationResponse.py b/strr-api/src/strr_api/responses/RegistrationResponse.py index 58845f86..1b8831d1 100644 --- a/strr-api/src/strr_api/responses/RegistrationResponse.py +++ b/strr-api/src/strr_api/responses/RegistrationResponse.py @@ -20,13 +20,6 @@ class SBCMailingAddress(BaseModel): country: str -class SelectedAccount(BaseModel): - """SelectedAccount response object.""" - - name: str - mailingAddress: SBCMailingAddress - - class ListingDetails(BaseModel): """ListingDetails response object.""" @@ -78,7 +71,7 @@ class ContactDetails(BaseModel): phoneNumber: str extension: Optional[str] = None faxNumber: Optional[str] = None - emailAddress: str + emailAddress: Optional[str] = None class Contact(BaseModel): @@ -94,6 +87,7 @@ class Registration(BaseModel): """Registration response object.""" id: int + sbc_account_id: Optional[int] = None submissionDate: datetime updatedDate: datetime status: str @@ -108,6 +102,7 @@ def from_db(cls, source: models.Registration): """Return a Registration object from a database model.""" return cls( id=source.id, + sbc_account_id=source.sbc_account_id, submissionDate=source.submission_date, updatedDate=source.updated_date, status=source.status.name, diff --git a/strr-api/src/strr_api/schemas/schemas/registration.json b/strr-api/src/strr_api/schemas/schemas/registration.json index 6e994c45..e54042b7 100644 --- a/strr-api/src/strr_api/schemas/schemas/registration.json +++ b/strr-api/src/strr_api/schemas/schemas/registration.json @@ -114,6 +114,9 @@ "properties": { "selectedAccount": { "type": "object", + "sbc_account_id": { + "type": "integer" + }, "name": { "description": "Account Name", "type": "string" @@ -157,8 +160,18 @@ } } }, - "required": [ - "name" + "oneOf": [ + { + "required": [ + "sbc_account_id" + ] + }, + { + "required": [ + "name", + "mailingAddress" + ] + } ] }, "registration": { diff --git a/strr-api/src/strr_api/services/registration_service.py b/strr-api/src/strr_api/services/registration_service.py index 7b4f543a..b1c873ba 100644 --- a/strr-api/src/strr_api/services/registration_service.py +++ b/strr-api/src/strr_api/services/registration_service.py @@ -41,12 +41,13 @@ class RegistrationService: """Service to save and load regristration details from the database.""" @classmethod - def save_registration(cls, token, registration_request: requests.Registration): + def save_registration(cls, jwt_oidc_token_info, sbc_account_id, registration_request: requests.Registration): """Save STRR property registration to database.""" # TODO: FUTURE SPRINT - handle the other cases where jwt doesn't have the info - user = models.User.get_or_create_user_by_jwt(token) - user.preferredname = (registration_request.primaryContact.details.preferredName,) + user = models.User.get_or_create_user_by_jwt(jwt_oidc_token_info) + user.email = registration_request.primaryContact.details.emailAddress + user.preferredname = registration_request.primaryContact.details.preferredName user.phone_extension = registration_request.primaryContact.details.extension user.fax_number = registration_request.primaryContact.details.faxNumber user.phone_number = registration_request.primaryContact.details.phoneNumber @@ -100,6 +101,7 @@ def save_registration(cls, token, registration_request: requests.Registration): db.session.refresh(property_manager) registration = models.Registration( + sbc_account_id=sbc_account_id, status=RegistrationStatus.PENDING, rental_property=models.RentalProperty( property_manager_id=property_manager.id, @@ -128,9 +130,9 @@ def save_registration(cls, token, registration_request: requests.Registration): return registration @classmethod - def list_registrations(cls, token): + def list_registrations(cls, jwt_oidc_token_info): """List all registrations for current user.""" - user = models.User.find_by_jwt_token(token) + user = models.User.find_by_jwt_token(jwt_oidc_token_info) return ( models.Registration.query.join( models.PropertyManager, models.PropertyManager.id == models.Registration.rental_property_id diff --git a/strr-api/tests/mocks/json/registration.json b/strr-api/tests/mocks/json/registration_new_sbc_account.json similarity index 100% rename from strr-api/tests/mocks/json/registration.json rename to strr-api/tests/mocks/json/registration_new_sbc_account.json diff --git a/strr-api/tests/mocks/json/registration_use_sbc_account.json b/strr-api/tests/mocks/json/registration_use_sbc_account.json new file mode 100644 index 00000000..3acf5c4c --- /dev/null +++ b/strr-api/tests/mocks/json/registration_use_sbc_account.json @@ -0,0 +1,73 @@ +{ + "selectedAccount": { + "sbc_account_id": 3299 + }, + "registration": { + "primaryContact": { + "name": { + "firstName": "The", + "middleName": "First", + "lastName": "Guy" + }, + "dateOfBirth": "1986-10-23", + "details": { + "preferredName": "Mickey", + "phoneNumber": "604-999-9999", + "extension": "x64", + "faxNumber": "604-777-7777", + "emailAddress": "test@test.test" + }, + "mailingAddress": { + "country": "CA", + "address": "12766 227st", + "addressLineTwo": "", + "city": "MAPLE RIDGE", + "province": "BC", + "postalCode": "V2X 6K6" + } + }, + "secondaryContact": { + "name": { + "firstName": "The", + "middleName": "Other", + "lastName": "Guy" + }, + "dateOfBirth": "1986-10-23", + "details": { + "preferredName": "Mouse", + "phoneNumber": "604-888-8888", + "extension": "", + "faxNumber": "", + "emailAddress": "test2@test.test" + }, + "mailingAddress": { + "country": "CA", + "address": "12766 227st", + "addressLineTwo": "", + "city": "MAPLE RIDGE", + "province": "BC", + "postalCode": "V2X 6K6" + } + }, + "unitDetails": { + "parcelIdentifier": "000-460-991", + "businessLicense": "", + "propertyType": "PRIMARY", + "ownershipType": "OWN" + }, + "unitAddress": { + "nickname": "My Rental Property", + "country": "CA", + "address": "12166 GREENWELL ST MAPLE RIDGE", + "addressLineTwo": "", + "city": "MAPLE RIDGE", + "province": "BC", + "postalCode": "V2X 7N1" + }, + "listingDetails": [ + { + "url": "https://www.airbnb.ca/rooms/26359027" + } + ] + } +} \ No newline at end of file diff --git a/strr-api/tests/unit/resources/test_account.py b/strr-api/tests/unit/resources/test_account.py index 1b06db08..45d83ab2 100644 --- a/strr-api/tests/unit/resources/test_account.py +++ b/strr-api/tests/unit/resources/test_account.py @@ -3,6 +3,8 @@ from http import HTTPStatus from unittest.mock import patch +from flask import g + from tests.unit.utils.mocks import ( empty_json, fake_get_token_auth_header, @@ -11,7 +13,7 @@ no_op, ) -REGISTRATION = "registration" +REGISTRATION = "registration_new_sbc_account" MOCK_ACCOUNT_REQUEST = os.path.join( os.path.dirname(os.path.realpath(__file__)), f"../../mocks/json/{REGISTRATION}.json" ) @@ -45,6 +47,7 @@ def test_me_401(client): @patch("flask_jwt_oidc.JwtManager._validate_token", new=no_op) def test_create_account_201(client): with open(MOCK_ACCOUNT_REQUEST) as f: + g.jwt_oidc_token_info = None data = json.load(f) rv = client.post("/account", json=data) assert rv.status_code == HTTPStatus.CREATED diff --git a/strr-api/tests/unit/resources/test_registrations.py b/strr-api/tests/unit/resources/test_registrations.py index 2d7e4e6f..e151dc6b 100644 --- a/strr-api/tests/unit/resources/test_registrations.py +++ b/strr-api/tests/unit/resources/test_registrations.py @@ -1,6 +1,8 @@ from http import HTTPStatus from unittest.mock import patch +from flask import g + from tests.unit.utils.mocks import fake_get_token_auth_header, fake_user_from_token, no_op @@ -8,6 +10,7 @@ @patch("flask_jwt_oidc.JwtManager.get_token_auth_header", new=fake_get_token_auth_header) @patch("flask_jwt_oidc.JwtManager._validate_token", new=no_op) def test_registrations_200(client): + g.jwt_oidc_token_info = None rv = client.get("/registrations") assert rv.status_code == HTTPStatus.OK diff --git a/strr-api/tests/unit/schemas/test_utils.py b/strr-api/tests/unit/schemas/test_utils.py index 3d8203cd..84f2fed5 100644 --- a/strr-api/tests/unit/schemas/test_utils.py +++ b/strr-api/tests/unit/schemas/test_utils.py @@ -3,27 +3,28 @@ from strr_api.schemas import utils -REGISTRATION = "registration" +REGISTRATION = "registration_new_sbc_account" +REGISTRATION_SCHEMA = "registration" MOCK_ACCOUNT_REQUEST = os.path.join( os.path.dirname(os.path.realpath(__file__)), f"../../mocks/json/{REGISTRATION}.json" ) def test_get_schema(): - schema_store = utils.get_schema(f"{REGISTRATION}.json") + schema_store = utils.get_schema(f"{REGISTRATION_SCHEMA}.json") assert schema_store is not None def test_validate_schema(): with open(MOCK_ACCOUNT_REQUEST) as f: data = json.load(f) - valid, error = utils.validate_schema(data, f"{REGISTRATION}") + valid, error = utils.validate_schema(data, f"{REGISTRATION_SCHEMA}") assert valid assert not error def test_validate_schema_error(): - valid, error = utils.validate_schema({"a": "b"}, f"{REGISTRATION}") + valid, error = utils.validate_schema({"a": "b"}, f"{REGISTRATION_SCHEMA}") assert not valid assert error @@ -31,6 +32,6 @@ def test_validate_schema_error(): def test_validate(): with open(MOCK_ACCOUNT_REQUEST) as f: data = json.load(f) - valid, error = utils.validate(data, f"{REGISTRATION}") + valid, error = utils.validate(data, f"{REGISTRATION_SCHEMA}") assert valid assert not error