MukeshKapoor25 commited on
Commit
6879d73
·
1 Parent(s): a462839

feat(notification-ms): add template mapping resolution for multi-channel dispatch

Browse files

- Strip "Bearer " prefix from WhatsApp access tokens to handle pre-formatted tokens
- Add token normalization in both WhatsApp send and validate methods
- Refactor _get_merchant_notification_config to use new _get_merchant_settings helper
- Fetch both notification_channels and notification_templates in single query
- Implement template name resolution per channel using merchant notification_templates mapping
- Add WhatsApp template name mapping with fallback to doc template_name
- Add SMS template ID resolution from merchant settings with fallback
- Add email template ID resolution from merchant settings with fallback
- Add logging for WhatsApp and SMS template resolution for debugging
- Enables merchants to map generic template names to channel-specific template identifiers

app/channels/whatsapp.py CHANGED
@@ -45,8 +45,12 @@ class WhatsAppChannel:
45
  return False, "WhatsApp channel not configured for this merchant", None
46
 
47
  whatsapp_number = WhatsAppChannel._normalize_number(recipient)
 
 
 
 
48
  headers = {
49
- "Authorization": f"Bearer {access_token.strip()}",
50
  "Content-Type": "application/json",
51
  }
52
 
@@ -112,8 +116,11 @@ class WhatsAppChannel:
112
  return False, False, None
113
 
114
  number = WhatsAppChannel._normalize_number(mobile)
 
 
 
115
  headers = {
116
- "Authorization": f"Bearer {access_token.strip()}",
117
  "Content-Type": "application/json",
118
  }
119
  try:
 
45
  return False, "WhatsApp channel not configured for this merchant", None
46
 
47
  whatsapp_number = WhatsAppChannel._normalize_number(recipient)
48
+ # Strip existing "Bearer " prefix if already included in stored token
49
+ token = access_token.strip()
50
+ if token.lower().startswith("bearer "):
51
+ token = token[7:]
52
  headers = {
53
+ "Authorization": f"Bearer {token}",
54
  "Content-Type": "application/json",
55
  }
56
 
 
116
  return False, False, None
117
 
118
  number = WhatsAppChannel._normalize_number(mobile)
119
+ token = access_token.strip()
120
+ if token.lower().startswith("bearer "):
121
+ token = token[7:]
122
  headers = {
123
+ "Authorization": f"Bearer {token}",
124
  "Content-Type": "application/json",
125
  }
126
  try:
app/services/dispatcher.py CHANGED
@@ -31,18 +31,31 @@ class NotificationDispatcher:
31
  """Dispatches a notification doc through its requested channels."""
32
 
33
  @staticmethod
34
- async def _get_merchant_notification_config(
35
  merchant_id: str,
36
  ) -> Optional[Dict[str, Any]]:
37
  """
38
- Fetch notification_channels from scm_merchant_settings for a merchant.
39
- Returns the notification_channels dict or None.
40
  """
41
  db = get_database()
42
  doc = await db[settings.MERCHANT_SETTINGS_COLLECTION].find_one(
43
  {"merchant_id": merchant_id},
44
- {"_id": 0, "notification_channels": 1},
45
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  if not doc:
47
  return None
48
  return doc.get("notification_channels")
@@ -108,9 +121,9 @@ class NotificationDispatcher:
108
  doc["status"] = NotificationStatus.FAILED.value
109
  return doc
110
 
111
- # Fetch merchant notification config
112
- notification_config = await NotificationDispatcher._get_merchant_notification_config(merchant_id)
113
- if not notification_config:
114
  await collection.update_one(
115
  {"notification_id": nid},
116
  {
@@ -127,6 +140,10 @@ class NotificationDispatcher:
127
  doc["status"] = NotificationStatus.FAILED.value
128
  return doc
129
 
 
 
 
 
130
  # Mark as sending
131
  await collection.update_one(
132
  {"notification_id": nid},
@@ -174,7 +191,9 @@ class NotificationDispatcher:
174
  }
175
  continue
176
 
177
- # Dispatch to channel resolve templates for SMS and email
 
 
178
 
179
  if channel_enum == NotificationChannel.PUSH:
180
  success, message, provider_id = await PushChannel.send(
@@ -185,29 +204,36 @@ class NotificationDispatcher:
185
  merchant_id=merchant_id,
186
  )
187
  elif channel_enum == NotificationChannel.WHATSAPP:
 
 
 
188
  success, message, provider_id = await WhatsAppChannel.send(
189
  recipient=doc["recipient"],
190
- template_name=doc["template_name"],
191
  template_data=raw_data,
192
  channel_config=ch_config,
193
  )
194
  elif channel_enum == NotificationChannel.EMAIL:
 
195
  resolved_data = await NotificationDispatcher._resolve_template(
196
- doc["template_name"], "email", raw_data
197
  )
198
  success, message, provider_id = await EmailChannel.send(
199
  recipient=doc["recipient"],
200
- template_name=doc["template_name"],
201
  template_data=resolved_data,
202
  channel_config=ch_config,
203
  )
204
  elif channel_enum == NotificationChannel.SMS:
 
 
 
205
  resolved_data = await NotificationDispatcher._resolve_template(
206
- doc["template_name"], "sms", raw_data
207
  )
208
  success, message, provider_id = await SMSChannel.send(
209
  recipient=doc["recipient"],
210
- template_name=doc["template_name"],
211
  template_data=resolved_data,
212
  channel_config=ch_config,
213
  )
 
31
  """Dispatches a notification doc through its requested channels."""
32
 
33
  @staticmethod
34
+ async def _get_merchant_settings(
35
  merchant_id: str,
36
  ) -> Optional[Dict[str, Any]]:
37
  """
38
+ Fetch notification_channels and notification_templates from scm_merchant_settings.
39
+ Returns dict with both keys, or None if not found.
40
  """
41
  db = get_database()
42
  doc = await db[settings.MERCHANT_SETTINGS_COLLECTION].find_one(
43
  {"merchant_id": merchant_id},
44
+ {"_id": 0, "notification_channels": 1, "notification_templates": 1},
45
  )
46
+ if not doc:
47
+ return None
48
+ return doc
49
+
50
+ @staticmethod
51
+ async def _get_merchant_notification_config(
52
+ merchant_id: str,
53
+ ) -> Optional[Dict[str, Any]]:
54
+ """
55
+ Fetch notification_channels from scm_merchant_settings for a merchant.
56
+ Returns the notification_channels dict or None.
57
+ """
58
+ doc = await NotificationDispatcher._get_merchant_settings(merchant_id)
59
  if not doc:
60
  return None
61
  return doc.get("notification_channels")
 
121
  doc["status"] = NotificationStatus.FAILED.value
122
  return doc
123
 
124
+ # Fetch merchant notification config + template mappings
125
+ merchant_settings = await NotificationDispatcher._get_merchant_settings(merchant_id)
126
+ if not merchant_settings:
127
  await collection.update_one(
128
  {"notification_id": nid},
129
  {
 
140
  doc["status"] = NotificationStatus.FAILED.value
141
  return doc
142
 
143
+ notification_config = merchant_settings.get("notification_channels", {})
144
+ # notification_templates maps template_name → {whatsapp_template_name, sms_template_id, email_template_id}
145
+ notification_templates = merchant_settings.get("notification_templates", {})
146
+
147
  # Mark as sending
148
  await collection.update_one(
149
  {"notification_id": nid},
 
191
  }
192
  continue
193
 
194
+ # Resolve channel-specific template name from merchant notification_templates mapping
195
+ # e.g. notification_templates["password_reset"] = {whatsapp_template_name: "...", sms_template_id: "..."}
196
+ tpl_mapping = notification_templates.get(doc["template_name"], {})
197
 
198
  if channel_enum == NotificationChannel.PUSH:
199
  success, message, provider_id = await PushChannel.send(
 
204
  merchant_id=merchant_id,
205
  )
206
  elif channel_enum == NotificationChannel.WHATSAPP:
207
+ # Use merchant-mapped WATI template name if available, else fall back to doc template_name
208
+ wa_template_name = tpl_mapping.get("whatsapp_template_name") or doc["template_name"]
209
+ logger.info(f"WhatsApp template resolved: {doc['template_name']} → {wa_template_name}")
210
  success, message, provider_id = await WhatsAppChannel.send(
211
  recipient=doc["recipient"],
212
+ template_name=wa_template_name,
213
  template_data=raw_data,
214
  channel_config=ch_config,
215
  )
216
  elif channel_enum == NotificationChannel.EMAIL:
217
+ email_tpl_id = tpl_mapping.get("email_template_id") or doc["template_name"]
218
  resolved_data = await NotificationDispatcher._resolve_template(
219
+ email_tpl_id, "email", raw_data
220
  )
221
  success, message, provider_id = await EmailChannel.send(
222
  recipient=doc["recipient"],
223
+ template_name=email_tpl_id,
224
  template_data=resolved_data,
225
  channel_config=ch_config,
226
  )
227
  elif channel_enum == NotificationChannel.SMS:
228
+ # sms_template_id is the name of the template in notification_templates collection
229
+ sms_tpl_name = tpl_mapping.get("sms_template_id") or doc["template_name"]
230
+ logger.info(f"SMS template resolved: {doc['template_name']} → {sms_tpl_name}")
231
  resolved_data = await NotificationDispatcher._resolve_template(
232
+ sms_tpl_name, "sms", raw_data
233
  )
234
  success, message, provider_id = await SMSChannel.send(
235
  recipient=doc["recipient"],
236
+ template_name=sms_tpl_name,
237
  template_data=resolved_data,
238
  channel_config=ch_config,
239
  )