Conversation
| foodLevel2: bool | ||
| foodLevel3: bool | ||
|
|
||
| class ContactTypePreferencesModel(BaseModel): |
There was a problem hiding this comment.
I think notifications could be laid out something like this in the future on web and mobile
on web:
if user clicks on phone -> redirects them to download mobile app
if user clicks on email -> updates user preference
note: The only notification that I can think of that would be email specific: Weekly/Monthly Digest which emails the user monthly stats about the fridge (not included yet)
| Authorizers: | ||
| FirebaseAuthorizer: | ||
| JwtConfiguration: | ||
| issuer: !Sub "https://securetoken.google.com/${FirebaseProjectId}" |
| ## DynamoDB Tables ## | ||
| ########################## | ||
|
|
||
| UserFridgeNotificationsTable: |
There was a problem hiding this comment.
Hash/PrimaryKey: userId
Range: fridgeId
| Unsubscribe: {base_url}/unsubscribe?token=eyJhbGciOiJ | ||
| """ | ||
|
|
||
| ses.send_email( |
| token=fcm_token, | ||
| ) | ||
|
|
||
| response = messaging.send(message) |
There was a problem hiding this comment.
API Docs: https://fridgefinder.github.io/CFM_Notification/
TODO: Review and Approve by others
| email: Optional[FridgePreferencesModel] = None | ||
| device: Optional[FridgePreferencesModel] = None | ||
|
|
||
| class UserFridgeNotificationModel(BaseModel): |
There was a problem hiding this comment.
Reviewer: Confirm fields and structure
mansoorsiddiqui
left a comment
There was a problem hiding this comment.
Good progress on the restructure. Left a few comments inline — the bigger picture stuff is around cross-service coupling and making sure we have a path to decouple the direct DB reads.
Cross-service concerns:
- The stream processor reads directly from the user table — schema changes in the user service could silently break notifications. I know there's a note about making this an API call — just want to make sure we're tracking it.
- Old tests were deleted but no new tests added. The new code has non-trivial logic (preference merging, condition mapping, stream processing) — are we planning to add tests in a follow-up? Would feel a lot more comfortable merging if at least the model and service layer had coverage.
- Error codes use
NOT_FOUND,ALREADY_EXISTShere vsUSER_NOT_FOUND,USER_ALREADY_EXISTSin the user service. Should we align?
| @@ -0,0 +1,125 @@ | |||
| from xml.parsers.expat import model | |||
There was a problem hiding this comment.
from xml.parsers.expat import modelWhat is this? Looks like a stray auto-import — model isn't used anywhere in the file. Should probably just remove this line.
| import os | ||
| import logging | ||
| import json | ||
| import requests |
There was a problem hiding this comment.
import requestsThis import isn't used anywhere in the handler, and requests is also listed in requirements.txt. That's extra cold start time and package size for nothing — can we remove both?
| logger.info(f"Fetching user details for userId: {user_id}") | ||
|
|
||
| try: | ||
| response = dynamodb.get_item( |
There was a problem hiding this comment.
So the notification service is reading directly from the User Service's DynamoDB table here. I get why — it's simpler than making an API call — but this means we need cross-stack IAM permissions and we're coupling to the user table's schema. If the user service changes how settings or fcmToken is stored, this breaks with no compile-time or deploy-time warning. You already have the note about making this an API call. Are we OK shipping this as-is for v1 and tracking it as tech debt?
| logger.setLevel(logging.INFO) | ||
|
|
||
| # Initialize Firebase when module is loaded | ||
| initialize_firebase() |
There was a problem hiding this comment.
initialize_firebase() runs at module load time. If this fails (bad credentials, network blip during cold start), the entire Lambda container is toast — every invocation will fail until AWS recycles it. Can we wrap this in a try/except, or do lazy init on first invocation?
| Raises: | ||
| ClientError: If DynamoDB operation fails | ||
| """ | ||
| response = self.db_client.query( |
There was a problem hiding this comment.
This does a single query() call without handling LastEvaluatedKey. DynamoDB caps responses at 1MB, so if a user subscribes to a ton of fridges, we'd get a truncated list with no indication anything was cut off. Are we expecting users to subscribe to enough fridges that this matters? If not, maybe just add a comment noting the limit. If yes, we need pagination.
| CONDITION_MAP = { | ||
| 'good': 'good', | ||
| 'dirty': 'dirty', | ||
| 'out of order': 'outOfOrder', |
There was a problem hiding this comment.
The keys here are space-separated strings coming from the status report database. This feels fragile — is there a shared contract or enum between the fridge report service and this service? If someone on the report service changes "out of order" to "outOfOrder", notifications for that condition just stop working.
|
|
||
| if not deleted: | ||
| return error_response(404, "User Fridge Notification not found", ErrorCode.NOT_FOUND, request_id=request_id) | ||
| return success_response(204, None, request_id=request_id) |
There was a problem hiding this comment.
json.dumps(None) produces the string "null", but a 204 should have no body. Might want to set body to "" or handle 204 specially in success_response.
| logger = logging.getLogger() | ||
| logger.setLevel(logging.INFO) | ||
|
|
||
| def get_ddb_connection() -> object: |
There was a problem hiding this comment.
This get_ddb_connection() function is copy-pasted in both this file and getAllUserNotifications/app.py. Can we move it into the shared layer alongside the repository and service classes?
| user_id = event.get('requestContext', {}).get('authorizer', {}).get('jwt', {}).get('claims', {}).get('sub') | ||
| return user_id | ||
|
|
||
| def validate_request_parameters( |
There was a problem hiding this comment.
This validate_request_parameters and the validate_input in getAllUserNotifications/app.py do essentially the same thing (check auth, check user matches JWT) with slightly different signatures. Could we extract a shared validation helper into the common layer?
| push: | ||
| branches: | ||
| - main | ||
| - N3_JP |
There was a problem hiding this comment.
Same as the user service PR — N3_JP branch is still in the trigger list. Remove before merging.




Description
API Docs: https://fridgefinder.github.io/CFM_Notification/
API
Base URLs:
https://notifications-api-dev.communityfridgefinder.comhttps://notifications-api-staging.communityfridgefinder.comhttps://notifications-api-prod.communityfridgefinder.comAuthentication: All endpoints (except health check) require Firebase JWT token in
Authorization: Bearer <token>header.Endpoints
Get Notification for Specific Fridge
GET /v1/users/{user_id}/fridge-notifications/{fridge_id}Retrieve notification preferences for a specific fridge.
Path Parameters:
user_id(string, required) - Firebase User ID (must match authenticated user)fridge_id(string, required) - Unique fridge identifierSuccess Response (200):
{ "userId": "abc123xyz456def789", "fridgeId": "fridge_123abc", "contactTypePreferences": { "email": { "good": true, ... } }, "createdAt": "2025-12-13T10:30:45.123Z", "updatedAt": "2025-12-13T15:20:30.456Z" }Create Notification Preferences
POST /v1/users/{user_id}/fridge-notifications/{fridge_id}Create new notification preferences for a fridge.
Path Parameters:
user_id(string, required) - Firebase User ID (must match authenticated user)fridge_id(string, required) - Unique fridge identifierRequest Body:
{ "contactTypePreferences": { "email": { "good": true, ... }, "device": { "good": false, ... } } }Success Response (201):
{ "userId": "abc123xyz456def789", "fridgeId": "fridge_123abc", "contactTypePreferences": { "email": { "good": true, ... }, "device": { "good": false, ... } }, "createdAt": "2025-12-13T10:30:45.123Z", "updatedAt": "2025-12-13T10:30:45.123Z" }Update Notification Preferences
PATCH /v1/users/{user_id}/fridge-notifications/{fridge_id}Update existing notification preferences for a fridge.
Path Parameters:
user_id(string, required) - Firebase User ID (must match authenticated user)fridge_id(string, required) - Unique fridge identifierRequest Body:
{ "contactTypePreferences": { "email": { "good": false, ... }, "device": { "good": true, ... } } }Success Response (200):
{ "userId": "abc123xyz456def789", "fridgeId": "fridge_123abc", "contactTypePreferences": { "email": { "good": false, ... }, "device": { "good": true, ... } }, "createdAt": "2025-12-13T10:30:45.123Z", "updatedAt": "2025-12-13T16:45:22.789Z" }Get All User Notifications
GET /v1/users/{user_id}/fridge-notificationsRetrieve all notification preferences for a user.
Path Parameters:
user_id(string, required) - Firebase User ID (must match authenticated user)Success Response (200):
{ "data": [ { "userId": "abc123xyz456def789", "fridgeId": "fridge_123abc", "contactTypePreferences": { "email": { "good": true, "dirty": true, "outOfOrder": true, "notAtLocation": false, "ghost": false, "foodLevel0": true, "foodLevel1": false, "foodLevel2": false, "foodLevel3": false }, "device": { "good": false, ... } }, "createdAt": "2025-12-13T10:30:45.123Z", "updatedAt": "2025-12-13T15:20:30.456Z" } ], "count": 1 }Delete Notification Preferences
DELETE /v1/users/{user_id}/fridge-notifications/{fridge_id}Remove notification preferences for a specific fridge.
Path Parameters:
user_id(string, required) - Firebase User ID (must match authenticated user)fridge_id(string, required) - Unique fridge identifierSuccess Response (204):
Error Responses
All errors follow a standardized format with an
errorobject containing acodeandmessage.Error Response Structure
{ "error": { "code": "ERROR_CODE", "message": "Human-readable error message" } }Error Examples
400 Bad Request - Validation Error
{ "error": { "code": "VALIDATION_ERROR", "message": "Input validation failed" } }404 Not Found
{ "error": { "code": "NOT_FOUND", "message": "Notification preference not found" } }409 Conflict
{ "error": { "code": "ALREADY_EXISTS", "message": "Notification preference already exists for this user and fridge" } }500 Internal Server Error
{ "error": { "code": "INTERNAL_SERVER_ERROR", "message": "An unexpected error occurred" } }401 Unauthorized
{ "message": "Unauthorized" }401 errors are returned from API Gateway if JWT token is not valid