Skip to content

Commit

Permalink
(PC-30737)[API] feat: add postal code and activity edition for native
Browse files Browse the repository at this point in the history
The POST /profile route is duplicated into a PATCH /profile route, more
fitting of how the route behaves. The POST route will be decommissioned
when no clients longer use it.
  • Loading branch information
dnguyen1-pass authored and cepehang committed Jul 24, 2024
1 parent 01197e2 commit c68ec81
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 27 deletions.
8 changes: 6 additions & 2 deletions api/src/pcapi/core/users/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,7 @@ def update_user_info(
marketing_email_subscription: bool | T_UNCHANGED = UNCHANGED,
new_nav_pro_date: datetime.datetime | None | T_UNCHANGED = UNCHANGED,
new_nav_pro_eligibility_date: datetime.datetime | None | T_UNCHANGED = UNCHANGED,
activity: users_models.ActivityEnum | T_UNCHANGED = UNCHANGED,
commit: bool = True,
) -> history_api.ObjectUpdateSnapshot:
old_email = None
Expand Down Expand Up @@ -725,6 +726,10 @@ def update_user_info(
)
pro_new_nav_state.eligibilityDate = new_nav_pro_eligibility_date
db.session.add(pro_new_nav_state)
if activity is not UNCHANGED:
if user.activity != activity.value:
snapshot.set("activity", old=user.activity, new=activity.value)
user.activity = activity.value

# keep using repository as long as user is validated in pcapi.validation.models.user
if commit:
Expand Down Expand Up @@ -1003,6 +1008,7 @@ def update_notification_subscription(
"marketing_email": subscriptions.marketing_email,
"subscribed_themes": subscriptions.subscribed_themes,
}
db.session.flush()

logger.info(
"Notification subscription update",
Expand All @@ -1024,8 +1030,6 @@ def update_notification_subscription(
technical_message_id="subscription_update",
)

repository.save(user)


def reset_recredit_amount_to_show(user: models.User) -> None:
user.recreditAmountToShow = None
Expand Down
23 changes: 18 additions & 5 deletions api/src/pcapi/routes/native/v1/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from pcapi.domain import password
from pcapi.models import api_errors
from pcapi.models.feature import FeatureToggle
from pcapi.repository import atomic
from pcapi.repository import transaction
from pcapi.routes.native.security import authenticated_and_active_user_required
from pcapi.routes.native.security import authenticated_maybe_inactive_user_required
Expand All @@ -51,18 +52,30 @@ def get_user_profile(user: users_models.User) -> serializers.UserProfileResponse
return serializers.UserProfileResponse.from_orm(user)


@blueprint.native_route("/profile", methods=["POST"])
@blueprint.native_route("/profile", methods=["POST", "PATCH"])
@spectree_serialize(
response_model=serializers.UserProfileResponse,
on_success_status=200,
api=blueprint.api,
)
@authenticated_and_active_user_required
def update_user_profile(
user: users_models.User, body: serializers.UserProfileUpdateRequest
@atomic()
def patch_user_profile(
user: users_models.User, body: serializers.UserProfilePatchRequest
) -> serializers.UserProfileResponse:
api.update_notification_subscription(user, body.subscriptions, body.origin)
external_attributes_api.update_external_user(user)
profile_update_dict = body.dict(exclude_unset=True)

if "subscriptions" in profile_update_dict:
api.update_notification_subscription(user, body.subscriptions, body.origin)
profile_update_dict.pop("subscriptions", None)
profile_update_dict.pop("origin", None)

if "activity_id" in profile_update_dict:
activity_id = profile_update_dict.pop("activity_id", None)
profile_update_dict["activity"] = users_models.ActivityEnum[activity_id.value] if activity_id else None

api.update_user_info(user, author=user, **profile_update_dict)

return serializers.UserProfileResponse.from_orm(user)


Expand Down
6 changes: 5 additions & 1 deletion api/src/pcapi/routes/native/v1/serialization/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import pcapi.core.finance.models as finance_models
from pcapi.core.offers import models as offers_models
from pcapi.core.subscription import api as subscription_api
from pcapi.core.subscription import profile_options
from pcapi.core.users import api as users_api
from pcapi.core.users import constants as users_constants
from pcapi.core.users import young_status
Expand Down Expand Up @@ -214,7 +215,10 @@ def _is_cultural_survey_active() -> bool:
return FeatureToggle.ENABLE_NATIVE_CULTURAL_SURVEY.is_active() or FeatureToggle.ENABLE_CULTURAL_SURVEY.is_active()


class UserProfileUpdateRequest(ConfiguredBaseModel):
class UserProfilePatchRequest(ConfiguredBaseModel):
activity_id: profile_options.ActivityIdEnum | None
city: str | None
postal_code: str | None
subscriptions: NotificationSubscriptions | None
origin: str | None

Expand Down
61 changes: 46 additions & 15 deletions api/tests/routes/native/openapi_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2208,6 +2208,21 @@ def test_public_api(client):
"title": "UserProfileEmailUpdate",
"type": "object",
},
"UserProfilePatchRequest": {
"properties": {
"activityId": {"anyOf": [{"$ref": "#/components/schemas/ActivityIdEnum"}], "nullable": True},
"city": {"nullable": True, "title": "City", "type": "string"},
"origin": {"nullable": True, "title": "Origin", "type": "string"},
"postalCode": {"nullable": True, "title": "Postalcode", "type": "string"},
"subscriptions": {
"anyOf": [{"$ref": "#/components/schemas/NotificationSubscriptions"}],
"nullable": True,
"title": "NotificationSubscriptions",
},
},
"title": "UserProfilePatchRequest",
"type": "object",
},
"UserProfileResponse": {
"properties": {
"birthDate": {"format": "date", "nullable": True, "title": "Birthdate", "type": "string"},
Expand Down Expand Up @@ -2294,18 +2309,6 @@ def test_public_api(client):
"title": "UserProfileResponse",
"type": "object",
},
"UserProfileUpdateRequest": {
"properties": {
"origin": {"nullable": True, "title": "Origin", "type": "string"},
"subscriptions": {
"anyOf": [{"$ref": "#/components/schemas/NotificationSubscriptions"}],
"nullable": True,
"title": "NotificationSubscriptions",
},
},
"title": "UserProfileUpdateRequest",
"type": "object",
},
"UserReportedOffersResponse": {
"properties": {
"reportedOffers": {
Expand Down Expand Up @@ -3413,13 +3416,41 @@ def test_public_api(client):
}
},
"/native/v1/profile": {
"patch": {
"description": "",
"operationId": "patch__native_v1_profile",
"parameters": [],
"requestBody": {
"content": {
"application/json": {"schema": {"$ref": "#/components/schemas/UserProfilePatchRequest"}}
}
},
"responses": {
"200": {
"content": {
"application/json": {"schema": {"$ref": "#/components/schemas/UserProfileResponse"}}
},
"description": "OK",
},
"403": {"description": "Forbidden"},
"422": {
"content": {
"application/json": {"schema": {"$ref": "#/components/schemas/ValidationError"}}
},
"description": "Unprocessable Entity",
},
},
"security": [{"JWTAuth": []}],
"summary": "patch_user_profile <PATCH>",
"tags": [],
},
"post": {
"description": "",
"operationId": "post__native_v1_profile",
"parameters": [],
"requestBody": {
"content": {
"application/json": {"schema": {"$ref": "#/components/schemas/UserProfileUpdateRequest"}}
"application/json": {"schema": {"$ref": "#/components/schemas/UserProfilePatchRequest"}}
}
},
"responses": {
Expand All @@ -3438,9 +3469,9 @@ def test_public_api(client):
},
},
"security": [{"JWTAuth": []}],
"summary": "update_user_profile <POST>",
"summary": "patch_user_profile <POST>",
"tags": [],
}
},
},
"/native/v1/profile/email_update/cancel": {
"post": {
Expand Down
55 changes: 51 additions & 4 deletions api/tests/routes/native/v1/account_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,7 @@ def test_update_user_profile(self, app, client):
user = users_factories.UserFactory(email=self.identifier, password=password)

client.with_token(user.email)
response = client.post(
response = client.patch(
"/native/v1/profile",
json={
"subscriptions": {"marketingPush": True, "marketingEmail": False, "subscribedThemes": ["cinema"]},
Expand All @@ -892,7 +892,7 @@ def test_unsubscribe_push_notifications(self, client, app):
user = users_factories.UserFactory(email=self.identifier)

client.with_token(email=self.identifier)
response = client.post(
response = client.patch(
"/native/v1/profile",
json={"subscriptions": {"marketingPush": False, "marketingEmail": False, "subscribedThemes": []}},
)
Expand Down Expand Up @@ -928,7 +928,7 @@ def test_unsubscribe_push_notifications(self, client, app):
}
}

def test_log_data(self, client, caplog):
def test_subscription_logging_to_data(self, client, caplog):
users_factories.UserFactory(
email=self.identifier,
notificationSubscriptions={
Expand All @@ -940,7 +940,7 @@ def test_log_data(self, client, caplog):

with caplog.at_level(logging.INFO):
client.with_token(email=self.identifier)
response = client.post(
response = client.patch(
"/native/v1/profile",
json={
"subscriptions": {
Expand All @@ -966,6 +966,53 @@ def test_log_data(self, client, caplog):
}
assert caplog.records[0].technical_message_id == "subscription_update"

def test_postal_code_update(self, client):
user = users_factories.UserFactory(email=self.identifier)

client.with_token(email=self.identifier)
response = client.patch("/native/v1/profile", json={"postalCode": "38000", "city": "Grenoble"})

assert response.status_code == 200
assert user.postalCode == "38000"
assert user.city == "Grenoble"

def test_activity_update(self, client):
user = users_factories.UserFactory(email=self.identifier)

client.with_token(email=self.identifier)
response = client.patch("/native/v1/profile", json={"activity_id": users_models.ActivityEnum.UNEMPLOYED.name})

assert response.status_code == 200
assert user.activity == users_models.ActivityEnum.UNEMPLOYED.value

def test_empty_patch(self, client):
user = users_factories.UserFactory(
email=self.identifier,
activity=users_models.ActivityEnum.UNEMPLOYED.value,
city="Grenoble",
postalCode="38000",
notificationSubscriptions={
"marketing_push": True,
"marketing_email": True,
"subscribed_themes": ["musique", "visites"],
},
)

client.with_token(email=self.identifier)
response = client.patch("/native/v1/profile", json={})

assert response.status_code == 200
assert user.activity == users_models.ActivityEnum.UNEMPLOYED.value
assert user.city == "Grenoble"
assert user.postalCode == "38000"
assert user.notificationSubscriptions == {
"marketing_push": True,
"marketing_email": True,
"subscribed_themes": ["musique", "visites"],
}


class ResetRecreditAmountToShow:
def test_update_user_profile_reset_recredit_amount_to_show(self, client, app):
user = users_factories.UnderageBeneficiaryFactory(email=self.identifier, recreditAmountToShow=30)

Expand Down

0 comments on commit c68ec81

Please sign in to comment.