firefighter-incident 0.0.13__py3-none-any.whl → 0.0.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. firefighter/_version.py +16 -3
  2. firefighter/api/serializers.py +17 -8
  3. firefighter/api/urls.py +8 -1
  4. firefighter/api/views/_base.py +1 -1
  5. firefighter/api/views/components.py +5 -5
  6. firefighter/api/views/incidents.py +9 -9
  7. firefighter/confluence/signals/incident_updated.py +2 -2
  8. firefighter/firefighter/settings/components/raid.py +3 -0
  9. firefighter/incidents/admin.py +24 -24
  10. firefighter/incidents/enums.py +22 -2
  11. firefighter/incidents/factories.py +14 -5
  12. firefighter/incidents/forms/close_incident.py +4 -4
  13. firefighter/incidents/forms/closure_reason.py +45 -0
  14. firefighter/incidents/forms/create_incident.py +4 -4
  15. firefighter/incidents/forms/unified_incident.py +406 -0
  16. firefighter/incidents/forms/update_status.py +91 -5
  17. firefighter/incidents/menus.py +2 -2
  18. firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +7 -5
  19. firefighter/incidents/migrations/0009_update_sla.py +7 -5
  20. firefighter/incidents/migrations/0020_create_incident_category_model.py +64 -0
  21. firefighter/incidents/migrations/0021_copy_component_data_to_incident_category.py +57 -0
  22. firefighter/incidents/migrations/0022_add_incident_category_fields.py +34 -0
  23. firefighter/incidents/migrations/0023_populate_incident_category_references.py +57 -0
  24. firefighter/incidents/migrations/0024_remove_component_fields_and_model.py +26 -0
  25. firefighter/incidents/migrations/0025_make_incident_category_required.py +24 -0
  26. firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py +39 -0
  27. firefighter/incidents/migrations/0027_add_closure_fields.py +40 -0
  28. firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
  29. firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
  30. firefighter/incidents/models/__init__.py +1 -1
  31. firefighter/incidents/models/group.py +1 -1
  32. firefighter/incidents/models/incident.py +47 -20
  33. firefighter/incidents/models/{component.py → incident_category.py} +30 -29
  34. firefighter/incidents/models/incident_update.py +3 -3
  35. firefighter/incidents/static/css/main.min.css +1 -1
  36. firefighter/incidents/tables.py +9 -9
  37. firefighter/incidents/templates/layouts/partials/incident_card.html +1 -1
  38. firefighter/incidents/templates/layouts/partials/incident_timeline.html +2 -2
  39. firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
  40. firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html} +13 -13
  41. firefighter/incidents/templates/pages/{component_list.html → incident_category_list.html} +2 -2
  42. firefighter/incidents/templates/pages/incident_detail.html +3 -3
  43. firefighter/incidents/urls.py +6 -6
  44. firefighter/incidents/views/components/details.py +9 -9
  45. firefighter/incidents/views/components/list.py +9 -9
  46. firefighter/incidents/views/reports.py +5 -5
  47. firefighter/incidents/views/users/details.py +2 -2
  48. firefighter/incidents/views/views.py +7 -7
  49. firefighter/jira_app/client.py +1 -1
  50. firefighter/logging/custom_json_formatter.py +2 -1
  51. firefighter/pagerduty/tasks/trigger_oncall.py +1 -1
  52. firefighter/raid/admin.py +0 -11
  53. firefighter/raid/apps.py +9 -26
  54. firefighter/raid/client.py +5 -5
  55. firefighter/raid/forms.py +84 -213
  56. firefighter/raid/migrations/0003_delete_raidarea.py +16 -0
  57. firefighter/raid/models.py +2 -21
  58. firefighter/raid/serializers.py +5 -4
  59. firefighter/raid/service.py +29 -27
  60. firefighter/raid/signals/incident_created.py +42 -15
  61. firefighter/raid/signals/incident_updated.py +3 -2
  62. firefighter/raid/utils.py +1 -1
  63. firefighter/raid/views/__init__.py +1 -1
  64. firefighter/slack/admin.py +8 -8
  65. firefighter/slack/management/commands/switch_test_users.py +272 -0
  66. firefighter/slack/messages/slack_messages.py +24 -9
  67. firefighter/slack/migrations/0005_add_incident_categories_fields.py +33 -0
  68. firefighter/slack/migrations/0006_copy_components_to_incident_categories.py +57 -0
  69. firefighter/slack/migrations/0007_remove_components_fields.py +22 -0
  70. firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py +33 -0
  71. firefighter/slack/models/conversation.py +3 -3
  72. firefighter/slack/models/incident_channel.py +1 -1
  73. firefighter/slack/models/user.py +1 -1
  74. firefighter/slack/models/user_group.py +3 -3
  75. firefighter/slack/rules.py +2 -2
  76. firefighter/slack/signals/create_incident_conversation.py +6 -0
  77. firefighter/slack/signals/get_users.py +2 -2
  78. firefighter/slack/signals/incident_updated.py +8 -2
  79. firefighter/slack/utils.py +2 -2
  80. firefighter/slack/views/events/home.py +2 -2
  81. firefighter/slack/views/modals/__init__.py +4 -0
  82. firefighter/slack/views/modals/base_modal/form_utils.py +78 -0
  83. firefighter/slack/views/modals/close.py +18 -5
  84. firefighter/slack/views/modals/closure_reason.py +193 -0
  85. firefighter/slack/views/modals/open.py +83 -12
  86. firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
  87. firefighter/slack/views/modals/opening/details/unified.py +203 -0
  88. firefighter/slack/views/modals/opening/select_impact.py +5 -2
  89. firefighter/slack/views/modals/opening/set_details.py +3 -2
  90. firefighter/slack/views/modals/postmortem.py +10 -2
  91. firefighter/slack/views/modals/update_status.py +32 -6
  92. firefighter/slack/views/modals/utils.py +51 -0
  93. firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
  94. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/METADATA +2 -2
  95. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/RECORD +133 -88
  96. firefighter_tests/conftest.py +4 -5
  97. firefighter_tests/test_api/test_api_landbot.py +1 -1
  98. firefighter_tests/test_firefighter/test_sso.py +146 -0
  99. firefighter_tests/test_incidents/test_enums.py +100 -0
  100. firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
  101. firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
  102. firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
  103. firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
  104. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
  105. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
  106. firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
  107. firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
  108. firefighter_tests/test_incidents/test_incident_urls.py +3 -3
  109. firefighter_tests/test_incidents/test_models/test_incident_category.py +165 -0
  110. firefighter_tests/test_incidents/test_models/test_incident_model.py +70 -2
  111. firefighter_tests/test_raid/conftest.py +154 -0
  112. firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
  113. firefighter_tests/test_raid/test_priority_mapping.py +267 -0
  114. firefighter_tests/test_raid/test_raid_client.py +580 -0
  115. firefighter_tests/test_raid/test_raid_forms.py +552 -0
  116. firefighter_tests/test_raid/test_raid_models.py +185 -0
  117. firefighter_tests/test_raid/test_raid_serializers.py +507 -0
  118. firefighter_tests/test_raid/test_raid_service.py +442 -0
  119. firefighter_tests/test_raid/test_raid_signals.py +187 -0
  120. firefighter_tests/test_raid/test_raid_views.py +196 -0
  121. firefighter_tests/test_slack/messages/__init__.py +0 -0
  122. firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
  123. firefighter_tests/test_slack/views/modals/conftest.py +140 -0
  124. firefighter_tests/test_slack/views/modals/test_close.py +71 -9
  125. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
  126. firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
  127. firefighter_tests/test_slack/views/modals/test_open.py +146 -2
  128. firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
  129. firefighter_tests/test_slack/views/modals/test_update_status.py +331 -7
  130. firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
  131. firefighter/raid/views/open_normal.py +0 -139
  132. firefighter/slack/views/modals/opening/details/critical.py +0 -88
  133. firefighter_fixtures/raid/area.json +0 -1
  134. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/WHEEL +0 -0
  135. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/entry_points.txt +0 -0
  136. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/licenses/LICENSE +0 -0
firefighter/raid/forms.py CHANGED
@@ -3,12 +3,10 @@ from __future__ import annotations
3
3
  import logging
4
4
  from typing import TYPE_CHECKING, Any, Never
5
5
 
6
- from django import forms
7
6
  from django.conf import settings
8
7
  from django.db import models
9
8
  from slack_sdk.errors import SlackApiError
10
9
 
11
- from firefighter.incidents.forms.create_incident import CreateIncidentFormBase
12
10
  from firefighter.incidents.forms.select_impact import SelectImpactForm
13
11
  from firefighter.incidents.models.priority import Priority
14
12
  from firefighter.jira_app.client import (
@@ -21,15 +19,7 @@ from firefighter.raid.messages import (
21
19
  SlackMessageRaidCreatedIssue,
22
20
  SlackMessageRaidModifiedIssue,
23
21
  )
24
- from firefighter.raid.models import FeatureTeam, JiraTicket, RaidArea
25
- from firefighter.raid.service import (
26
- create_issue_customer,
27
- create_issue_documentation_request,
28
- create_issue_feature_request,
29
- create_issue_internal,
30
- create_issue_seller,
31
- get_jira_user_from_user,
32
- )
22
+ from firefighter.raid.models import JiraTicket
33
23
  from firefighter.raid.utils import get_domain_from_email
34
24
  from firefighter.slack.models.conversation import Conversation
35
25
 
@@ -60,204 +50,11 @@ def initial_priority() -> Priority:
60
50
  return Priority.objects.get(default=True)
61
51
 
62
52
 
63
- class CreateNormalIncidentFormBase(CreateIncidentFormBase):
64
- platform = forms.ChoiceField(
65
- label="Platform",
66
- choices=PlatformChoices.choices,
67
- )
68
- title = forms.CharField(
69
- label="Title",
70
- max_length=128,
71
- min_length=10,
72
- widget=forms.TextInput(attrs={"placeholder": "What's going on?"}),
73
- )
74
- description = forms.CharField(
75
- label="Summary",
76
- min_length=10,
77
- max_length=1200,
78
- )
79
- suggested_team_routing = forms.ModelChoiceField(
80
- queryset=FeatureTeam.objects.only("name"),
81
- label="Feature Team or Train",
82
- required=True,
83
- )
84
- priority = forms.ModelChoiceField(
85
- label="Priority",
86
- queryset=Priority.objects.filter(enabled_create=True),
87
- initial=initial_priority,
88
- widget=forms.HiddenInput(),
89
- )
90
-
91
- field_order = [
92
- "area",
93
- "platform",
94
- "title",
95
- "description",
96
- "seller_contract_id",
97
- "is_key_account",
98
- "is_seller_in_golden_list",
99
- "zoho_desk_ticket_id",
100
- "zendesk_ticket_id",
101
- "suggested_team_routing",
102
- ]
103
-
104
- def trigger_incident_workflow(
105
- self,
106
- creator: User,
107
- impacts_data: dict[str, ImpactLevel],
108
- *args: Any,
109
- **kwargs: Any,
110
- ) -> Any:
111
- raise NotImplementedError
112
-
113
-
114
- class CreateNormalCustomerIncidentForm(CreateNormalIncidentFormBase):
115
- area = forms.ModelChoiceField(queryset=RaidArea.objects.filter(area="Customers"))
116
- zendesk_ticket_id = forms.CharField(
117
- label="Zendesk Ticket ID", max_length=128, min_length=2, required=False
118
- )
119
-
120
- # XXX business impact: infer from impact/add in impact modal?
121
- def trigger_incident_workflow(
122
- self,
123
- creator: User,
124
- impacts_data: dict[str, ImpactLevel],
125
- *args: Never,
126
- **kwargs: Never,
127
- ) -> None:
128
- jira_user: JiraUser = get_jira_user_from_user(creator)
129
- issue_data = create_issue_customer(
130
- title=self.cleaned_data["title"],
131
- description=self.cleaned_data["description"],
132
- priority=self.cleaned_data["priority"].value,
133
- reporter=jira_user.id,
134
- platform=self.cleaned_data["platform"],
135
- business_impact=str(get_business_impact(impacts_data)),
136
- team_to_be_routed=self.cleaned_data["suggested_team_routing"],
137
- area=self.cleaned_data["area"].name,
138
- zendesk_ticket_id=self.cleaned_data["zendesk_ticket_id"],
139
- labels=[""],
140
- )
141
- process_jira_issue(
142
- issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
143
- )
144
-
145
-
146
- class CreateRaidDocumentationRequestIncidentForm(CreateNormalIncidentFormBase):
147
- def trigger_incident_workflow(
148
- self,
149
- creator: User,
150
- impacts_data: dict[str, ImpactLevel],
151
- *args: Never,
152
- **kwargs: Never,
153
- ) -> None:
154
- jira_user: JiraUser = get_jira_user_from_user(creator)
155
- issue_data = create_issue_documentation_request(
156
- title=self.cleaned_data["title"],
157
- description=self.cleaned_data["description"],
158
- priority=self.cleaned_data["priority"].value,
159
- reporter=jira_user.id,
160
- platform=self.cleaned_data["platform"],
161
- labels=[""],
162
- )
163
-
164
- process_jira_issue(
165
- issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
166
- )
167
-
168
-
169
- class CreateRaidFeatureRequestIncidentForm(CreateNormalIncidentFormBase):
170
- def trigger_incident_workflow(
171
- self,
172
- creator: User,
173
- impacts_data: dict[str, ImpactLevel],
174
- *args: Never,
175
- **kwargs: Never,
176
- ) -> None:
177
- jira_user: JiraUser = get_jira_user_from_user(creator)
178
- issue_data = create_issue_feature_request(
179
- title=self.cleaned_data["title"],
180
- description=self.cleaned_data["description"],
181
- priority=self.cleaned_data["priority"].value,
182
- reporter=jira_user.id,
183
- platform=self.cleaned_data["platform"],
184
- labels=[""],
185
- )
186
-
187
- process_jira_issue(
188
- issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
189
- )
190
-
191
-
192
- class CreateRaidInternalIncidentForm(CreateNormalIncidentFormBase):
193
- area = forms.ModelChoiceField(
194
- queryset=RaidArea.objects.filter(area="Internal").order_by("name")
195
- )
196
-
197
- def trigger_incident_workflow(
198
- self,
199
- creator: User,
200
- impacts_data: dict[str, ImpactLevel],
201
- *args: Never,
202
- **kwargs: Never,
203
- ) -> None:
204
- jira_user: JiraUser = get_jira_user_from_user(creator)
205
- issue_data = create_issue_internal(
206
- title=self.cleaned_data["title"],
207
- description=self.cleaned_data["description"],
208
- priority=self.cleaned_data["priority"].value,
209
- reporter=jira_user.id,
210
- platform=self.cleaned_data["platform"],
211
- business_impact=str(get_business_impact(impacts_data)),
212
- team_to_be_routed=self.cleaned_data["suggested_team_routing"],
213
- area=self.cleaned_data["area"].name,
214
- labels=[""],
215
- )
216
-
217
- process_jira_issue(
218
- issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
219
- )
220
-
221
-
222
- class RaidCreateIncidentSellerForm(CreateNormalIncidentFormBase):
223
- area = forms.ModelChoiceField(queryset=RaidArea.objects.filter(area="Sellers"))
224
- seller_contract_id = forms.CharField(
225
- label="Seller Contract ID", max_length=128, min_length=0
226
- )
227
- is_key_account = forms.BooleanField(label="Is it a Key Account?", required=False)
228
- is_seller_in_golden_list = forms.BooleanField(
229
- label="Is the seller in the Golden List?", required=False
230
- )
231
- zoho_desk_ticket_id = forms.CharField(
232
- required=False, label="Zoho Desk Ticket ID", max_length=128, min_length=1
233
- )
234
-
235
- def trigger_incident_workflow(
236
- self,
237
- creator: User,
238
- impacts_data: dict[str, ImpactLevel],
239
- *args: Never,
240
- **kwargs: Never,
241
- ) -> None:
242
- jira_user: JiraUser = get_jira_user_from_user(creator)
243
- issue_data = create_issue_seller(
244
- title=self.cleaned_data["title"],
245
- description=self.cleaned_data["description"],
246
- priority=self.cleaned_data["priority"].value,
247
- reporter=jira_user.id,
248
- platform=self.cleaned_data["platform"],
249
- business_impact=str(get_business_impact(impacts_data)),
250
- team_to_be_routed=self.cleaned_data["suggested_team_routing"],
251
- area=self.cleaned_data["area"].name,
252
- zoho_desk_ticket_id=self.cleaned_data["zoho_desk_ticket_id"],
253
- is_key_account=self.cleaned_data["is_key_account"],
254
- is_seller_in_golden_list=self.cleaned_data["is_seller_in_golden_list"],
255
- seller_contract_id=self.cleaned_data["seller_contract_id"],
256
- labels=[""],
257
- )
258
- process_jira_issue(
259
- issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
260
- )
53
+ # NOTE: Incident creation forms have been unified and moved to:
54
+ # firefighter.incidents.forms.unified_incident.UnifiedIncidentForm
55
+ # This handles all incident types (P1-P5) with dynamic field visibility.
56
+ # The following utility functions remain here as they are used by the unified form
57
+ # and by JIRA webhook handlers.
261
58
 
262
59
 
263
60
  def process_jira_issue(
@@ -335,10 +132,15 @@ def alert_slack_new_jira_ticket(
335
132
  message,
336
133
  unfurl_links=False,
337
134
  )
338
- except SlackApiError:
339
- logger.exception(
340
- f"Couldn't send private message to reporter {reporter_user.slack_user}"
341
- )
135
+ except SlackApiError as e:
136
+ if e.response.get("error") == "messages_tab_disabled":
137
+ logger.warning(
138
+ f"User {reporter_user.slack_user} has disabled private messages from bots"
139
+ )
140
+ else:
141
+ logger.exception(
142
+ f"Couldn't send private message to reporter {reporter_user.slack_user}"
143
+ )
342
144
 
343
145
  # Get the right channels from tags
344
146
  channels = get_internal_alert_conversations(jira_ticket)
@@ -442,6 +244,75 @@ def get_business_impact(impacts_data: dict[str, ImpactLevel]) -> str | None:
442
244
  return impact_form.business_impact_new
443
245
 
444
246
 
247
+ def prepare_jira_fields(
248
+ *,
249
+ title: str,
250
+ description: str,
251
+ priority: int,
252
+ reporter: str,
253
+ incident_category: str,
254
+ environments: list[str],
255
+ platforms: list[str],
256
+ impacts_data: dict[str, ImpactLevel],
257
+ optional_fields: dict[str, Any] | None = None,
258
+ ) -> dict[str, Any]:
259
+ """Prepare all fields for jira_client.create_issue().
260
+
261
+ This function centralizes Jira field preparation for both P1-P3 and P4-P5 incidents,
262
+ ensuring all custom fields are properly passed.
263
+
264
+ Args:
265
+ title: Incident title
266
+ description: Incident description
267
+ priority: Priority value (1-5)
268
+ reporter: Jira user account ID
269
+ incident_category: Category name
270
+ environments: List of environment values (e.g. ["PRD", "STG"])
271
+ platforms: List of platform values (e.g. ["platform-FR", "platform-DE"])
272
+ impacts_data: Dictionary of impact data for business_impact computation
273
+ optional_fields: Optional dictionary containing:
274
+ - zendesk_ticket_id: Zendesk ticket ID (customer-specific)
275
+ - seller_contract_id: Seller contract ID (seller-specific)
276
+ - zoho_desk_ticket_id: Zoho Desk ticket ID (seller-specific)
277
+ - is_key_account: Key account flag (seller-specific)
278
+ - is_seller_in_golden_list: Golden list flag (seller-specific)
279
+ - suggested_team_routing: Suggested team routing (P4-P5 only)
280
+
281
+ Returns:
282
+ Dictionary of kwargs ready for jira_client.create_issue()
283
+ """
284
+ business_impact = get_business_impact(impacts_data)
285
+ platform = platforms[0] if platforms else PlatformChoices.ALL.value
286
+
287
+ # Extract optional fields with defaults
288
+ opt = optional_fields or {}
289
+ zendesk_ticket_id = opt.get("zendesk_ticket_id", "")
290
+ seller_contract_id = opt.get("seller_contract_id", "")
291
+ zoho_desk_ticket_id = opt.get("zoho_desk_ticket_id", "")
292
+ is_key_account = opt.get("is_key_account")
293
+ is_seller_in_golden_list = opt.get("is_seller_in_golden_list")
294
+ suggested_team_routing = opt.get("suggested_team_routing")
295
+
296
+ return {
297
+ "issuetype": "Incident",
298
+ "summary": title,
299
+ "description": description,
300
+ "priority": priority,
301
+ "reporter": reporter,
302
+ "assignee": None,
303
+ "incident_category": incident_category,
304
+ "environments": environments, # ✅ Always pass environments list
305
+ "platform": platform,
306
+ "business_impact": business_impact,
307
+ "zendesk_ticket_id": zendesk_ticket_id,
308
+ "seller_contract_id": seller_contract_id,
309
+ "zoho_desk_ticket_id": zoho_desk_ticket_id,
310
+ "is_key_account": is_key_account if is_key_account is not None else False,
311
+ "is_seller_in_golden_list": is_seller_in_golden_list if is_seller_in_golden_list is not None else False,
312
+ "suggested_team_routing": suggested_team_routing,
313
+ }
314
+
315
+
445
316
  def get_partner_alert_conversations(user_domain: str) -> QuerySet[Conversation]:
446
317
  # Get the right channel from tags
447
318
  return Conversation.objects.filter(tag__contains=f"raid_alert__{user_domain}")
@@ -0,0 +1,16 @@
1
+ # Generated by Django 4.2.23 on 2025-09-17 17:49
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("raid", "0002_featureteam_remove_qualifierrotation_jira_user_and_more"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.DeleteModel(
14
+ name="RaidArea",
15
+ ),
16
+ ]
@@ -58,25 +58,6 @@ class JiraTicketImpact(models.Model):
58
58
  return f"{self.jira_ticket.key}: {self.impact}"
59
59
 
60
60
 
61
- class RaidArea(models.Model):
62
- id = models.AutoField(primary_key=True)
63
- name = models.CharField(max_length=128)
64
- area = models.CharField(
65
- choices=(
66
- ("Sellers", "Sellers"),
67
- ("Internal", "Internal"),
68
- ("Customers", "Customers"),
69
- ),
70
- max_length=32,
71
- )
72
-
73
- class Meta(TypedModelMeta):
74
- unique_together = ("name", "area")
75
-
76
- def __str__(self) -> str:
77
- return self.name
78
-
79
-
80
61
  class FeatureTeam(models.Model):
81
62
  id = models.AutoField(primary_key=True)
82
63
  name = models.CharField(max_length=80)
@@ -95,8 +76,8 @@ class FeatureTeam(models.Model):
95
76
 
96
77
  @property
97
78
  def get_team(self) -> str:
98
- return "{self.name} {self.jira_project_key}"
79
+ return f"{self.name} {self.jira_project_key}"
99
80
 
100
81
  @property
101
82
  def get_key(self) -> str:
102
- return "{self.jira_project_key}"
83
+ return f"{self.jira_project_key}"
@@ -114,7 +114,7 @@ class LandbotIssueRequestSerializer(serializers.ModelSerializer[JiraTicket]):
114
114
  ),
115
115
  help_text="List of labels to be added to the ticket. Labels cannot contain spaces and must not exceed 255 characters.",
116
116
  )
117
- impacted_area = serializers.CharField(
117
+ incident_category = serializers.CharField(
118
118
  max_length=128,
119
119
  write_only=True,
120
120
  allow_null=True,
@@ -131,7 +131,8 @@ class LandbotIssueRequestSerializer(serializers.ModelSerializer[JiraTicket]):
131
131
  ],
132
132
  )
133
133
  priority = serializers.IntegerField(
134
- min_value=1, max_value=4, write_only=True, allow_null=True
134
+ min_value=1, max_value=5, write_only=True, allow_null=True,
135
+ help_text="Priority level 1-5 (1=Critical, 2=High, 3=Medium, 4=Low, 5=Lowest)"
135
136
  )
136
137
  business_impact = serializers.ChoiceField(
137
138
  write_only=True, choices=["High", "Medium", "Low"], allow_null=True
@@ -198,7 +199,7 @@ class LandbotIssueRequestSerializer(serializers.ModelSerializer[JiraTicket]):
198
199
  seller_contract_id=validated_data["seller_contract_id"],
199
200
  zoho_desk_ticket_id=validated_data["zoho"],
200
201
  platform=validated_data["platform"],
201
- area=validated_data["impacted_area"],
202
+ incident_category=validated_data["incident_category"],
202
203
  business_impact=validated_data["business_impact"],
203
204
  environments=validated_data["environments"],
204
205
  suggested_team_routing=validated_data["suggested_team_routing"],
@@ -239,7 +240,7 @@ class LandbotIssueRequestSerializer(serializers.ModelSerializer[JiraTicket]):
239
240
  "zoho",
240
241
  "platform",
241
242
  "reporter_email",
242
- "impacted_area",
243
+ "incident_category",
243
244
  "labels",
244
245
  "environments",
245
246
  "issue_type",
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ from dataclasses import dataclass
4
5
  from typing import TYPE_CHECKING
5
6
 
6
7
  from django.conf import settings
@@ -133,7 +134,7 @@ def create_issue_internal(
133
134
  platform: str,
134
135
  business_impact: str | None,
135
136
  team_to_be_routed: str | None,
136
- area: str | None,
137
+ incident_category: str | None,
137
138
  ) -> JiraObject:
138
139
  """Creates a Jira Incident Issue of type Internal.
139
140
 
@@ -146,7 +147,7 @@ def create_issue_internal(
146
147
  platform (str): Platform of the issue
147
148
  business_impact (str): Business impact of the issue
148
149
  team_to_be_routed (str): Team to be routed
149
- area (str): Area of the issue
150
+ incident_category (str): Incident category of the issue
150
151
  """
151
152
  issue = jira_client.create_issue(
152
153
  issuetype="Incident",
@@ -159,23 +160,30 @@ def create_issue_internal(
159
160
  platform=platform,
160
161
  business_impact=business_impact,
161
162
  suggested_team_routing=team_to_be_routed,
162
- area=area,
163
+ incident_category=incident_category,
163
164
  )
164
165
  check_issue_id(issue, title=title, reporter=reporter)
165
166
  return issue
166
167
 
167
168
 
169
+ @dataclass
170
+ class CustomerIssueData:
171
+ """Data container for customer issue creation parameters."""
172
+ priority: int | None
173
+ labels: list[str] | None
174
+ platform: str
175
+ business_impact: str | None
176
+ team_to_be_routed: str | None
177
+ area: str | None
178
+ zendesk_ticket_id: str | None
179
+ incident_category: str | None = None
180
+
181
+
168
182
  def create_issue_customer(
169
183
  title: str,
170
184
  description: str,
171
185
  reporter: str,
172
- priority: int | None,
173
- labels: list[str] | None,
174
- platform: str,
175
- business_impact: str | None,
176
- team_to_be_routed: str | None,
177
- area: str | None,
178
- zendesk_ticket_id: str | None,
186
+ issue_data: CustomerIssueData,
179
187
  ) -> JiraObject:
180
188
  """Creates a Jira Incident issue of type Customer.
181
189
 
@@ -183,13 +191,7 @@ def create_issue_customer(
183
191
  title (str): Summary of the issue
184
192
  description (str): Description of the issue
185
193
  reporter (str): Jira account id of the reporter
186
- priority (int): Priority of the issue
187
- labels (list[str]): Labels to add to the issue
188
- platform (str): Platform of the issue
189
- business_impact (str): Business impact of the issue
190
- team_to_be_routed (str): Team to be routed
191
- area (str): Area of the issue
192
- zendesk_ticket_id (str): Zendesk ticket id
194
+ issue_data (CustomerIssueData): Container with issue parameters
193
195
  """
194
196
  issue = jira_client.create_issue(
195
197
  issuetype="Incident",
@@ -197,13 +199,13 @@ def create_issue_customer(
197
199
  description=description,
198
200
  assignee=None,
199
201
  reporter=reporter,
200
- priority=priority,
201
- labels=labels,
202
- platform=platform,
203
- business_impact=business_impact,
204
- suggested_team_routing=team_to_be_routed,
205
- area=area,
206
- zendesk_ticket_id=zendesk_ticket_id,
202
+ priority=issue_data.priority,
203
+ labels=issue_data.labels,
204
+ platform=issue_data.platform,
205
+ business_impact=issue_data.business_impact,
206
+ suggested_team_routing=issue_data.team_to_be_routed,
207
+ zendesk_ticket_id=issue_data.zendesk_ticket_id,
208
+ incident_category=issue_data.incident_category,
207
209
  )
208
210
  check_issue_id(issue, title=title, reporter=reporter)
209
211
  return issue
@@ -218,7 +220,7 @@ def create_issue_seller( # noqa: PLR0913, PLR0917
218
220
  platform: str,
219
221
  business_impact: str | None,
220
222
  team_to_be_routed: str | None,
221
- area: str | None,
223
+ incident_category: str | None,
222
224
  seller_contract_id: str | None,
223
225
  is_key_account: bool | None, # noqa: FBT001
224
226
  is_seller_in_golden_list: bool | None, # noqa: FBT001
@@ -235,7 +237,7 @@ def create_issue_seller( # noqa: PLR0913, PLR0917
235
237
  platform (str): Platform of the issue
236
238
  business_impact (str): Business impact of the issue
237
239
  team_to_be_routed (str): Team to be routed
238
- area (str): Area of the issue
240
+ incident_category (str): Incident category of the issue
239
241
  seller_contract_id (str): Seller contract id
240
242
  is_key_account (bool): Is key account
241
243
  is_seller_in_golden_list (bool): Is seller in golden list
@@ -252,11 +254,11 @@ def create_issue_seller( # noqa: PLR0913, PLR0917
252
254
  platform=platform,
253
255
  business_impact=business_impact,
254
256
  suggested_team_routing=team_to_be_routed,
255
- area=area,
256
257
  seller_contract_id=seller_contract_id,
257
258
  is_key_account=is_key_account,
258
259
  is_seller_in_golden_list=is_seller_in_golden_list,
259
260
  zoho_desk_ticket_id=zoho_desk_ticket_id,
261
+ incident_category=incident_category,
260
262
  )
261
263
  check_issue_id(issue, title=title, reporter=reporter)
262
264
  return issue
@@ -1,13 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import TYPE_CHECKING, Any, Never
4
+ from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from django.conf import settings
7
7
  from django.dispatch.dispatcher import receiver
8
8
 
9
9
  from firefighter.jira_app.client import JiraAPIError, JiraUserNotFoundError
10
10
  from firefighter.raid.client import client
11
+ from firefighter.raid.forms import prepare_jira_fields
11
12
  from firefighter.raid.models import JiraTicket
12
13
  from firefighter.raid.service import get_jira_user_from_user
13
14
  from firefighter.slack.messages.slack_messages import (
@@ -26,29 +27,55 @@ APP_DISPLAY_NAME: str = settings.APP_DISPLAY_NAME
26
27
 
27
28
  @receiver(signal=incident_channel_done)
28
29
  def create_ticket(
29
- sender: Any, incident: Incident, channel: IncidentChannel, **kwargs: Never
30
+ sender: Any, incident: Incident, channel: IncidentChannel, **kwargs: Any
30
31
  ) -> JiraTicket:
31
32
  # pylint: disable=unused-argument
32
33
 
34
+ # Extract jira_extra_fields and impacts_data from kwargs (passed from unified incident form)
35
+ jira_extra_fields = kwargs.get("jira_extra_fields", {})
36
+ impacts_data = kwargs.get("impacts_data", {})
37
+ logger.info(f"CREATE_TICKET - kwargs keys: {list(kwargs.keys())}")
38
+ logger.info(f"CREATE_TICKET - jira_extra_fields received: {jira_extra_fields}")
39
+
33
40
  jira_user = get_jira_user_from_user(incident.created_by)
34
41
  account_id = jira_user.id
35
- # XXX Better description
36
- # XXX Custom field with FireFighter ID/link?
37
- # XXX Set affected environment custom field
38
- # XXX Set custom field impacted area to group/domain?
39
- priority: int = incident.priority.value if 1 <= incident.priority.value <= 4 else 1
40
- issue = client.create_issue(
41
- issuetype="Incident",
42
- summary=incident.title,
43
- description=f"""{incident.description}\n
42
+
43
+ # Map Impact priority (1-5) to JIRA priority (1-5), fallback to P1 for invalid values
44
+ priority: int = incident.priority.value if 1 <= incident.priority.value <= 5 else 1
45
+
46
+ # Build enhanced description with incident metadata
47
+ description = f"""{incident.description}\n
44
48
  \n
45
49
  🧯 This incident has been created for a critical incident. Links below to Slack and {APP_DISPLAY_NAME}.\n
46
- 📦 Issue category: {incident.component.name} ({incident.component.group.name})\n
47
- {incident.priority.emoji} Priority: {incident.priority.name}\n""",
48
- assignee=None,
49
- reporter=account_id,
50
+ 📦 Incident category: {incident.incident_category.name} ({incident.incident_category.group.name})\n
51
+ {incident.priority.emoji} Priority: {incident.priority.name}\n"""
52
+
53
+ # Prepare all Jira fields using the common function
54
+ # P1-P3 use first environment only (for backward compatibility)
55
+ environments = jira_extra_fields.get("environments", [incident.environment.value])
56
+ platforms = jira_extra_fields.get("platforms", ["platform-All"])
57
+
58
+ jira_fields = prepare_jira_fields(
59
+ title=incident.title,
60
+ description=description,
50
61
  priority=priority,
62
+ reporter=account_id,
63
+ incident_category=incident.incident_category.name,
64
+ environments=[environments[0]] if environments else [incident.environment.value], # P1-P3: first only
65
+ platforms=platforms,
66
+ impacts_data=impacts_data,
67
+ optional_fields={
68
+ "zendesk_ticket_id": jira_extra_fields.get("zendesk_ticket_id", ""),
69
+ "seller_contract_id": jira_extra_fields.get("seller_contract_id", ""),
70
+ "zoho_desk_ticket_id": jira_extra_fields.get("zoho_desk_ticket_id", ""),
71
+ "is_key_account": jira_extra_fields.get("is_key_account"),
72
+ "is_seller_in_golden_list": jira_extra_fields.get("is_seller_in_golden_list"),
73
+ "suggested_team_routing": jira_extra_fields.get("suggested_team_routing"),
74
+ },
51
75
  )
76
+
77
+ # Create Jira issue with all prepared fields
78
+ issue = client.create_issue(**jira_fields)
52
79
  issue_id = issue.get("id")
53
80
  if issue_id is None:
54
81
  logger.error(f"Could not create Jira ticket for incident {incident.id}")
@@ -24,10 +24,11 @@ def incident_updated_close_ticket_when_mitigated_or_postmortem(
24
24
  updated_fields: list[str],
25
25
  **kwargs: Any,
26
26
  ) -> None:
27
- # Close Jira ticket if mitigated or postmortem
27
+ # Close Jira ticket if mitigated, postmortem, or closed
28
28
  if "_status" in updated_fields and incident_update.status in {
29
- IncidentStatus.FIXED,
29
+ IncidentStatus.MITIGATED,
30
30
  IncidentStatus.POST_MORTEM,
31
+ IncidentStatus.CLOSED,
31
32
  }:
32
33
  if not hasattr(incident, "jira_ticket") or incident.jira_ticket is None:
33
34
  logger.warning(
firefighter/raid/utils.py CHANGED
@@ -33,7 +33,7 @@ def get_domain_from_email(email: str) -> str:
33
33
  if email.count("@") != 1:
34
34
  msg = f"Invalid email: {email}"
35
35
  raise ValueError(msg)
36
- domain = email.split("@")[-1]
36
+ domain = email.rsplit("@", maxsplit=1)[-1]
37
37
  if not domain:
38
38
  msg = f"Invalid email: {email}"
39
39
  raise ValueError(msg)
@@ -37,7 +37,7 @@ if TYPE_CHECKING:
37
37
  "zoho": "https://crmplus.zoho.eu/mycrmlink/index.do/cxapp/agent/mycompany/all/tickets/details/123456789",
38
38
  "platform": "FR",
39
39
  "reporter_email": "john.doe@mycompany.com",
40
- "impacted_area": "Sellers>MF Stock Replenishment",
40
+ "incident_category": "Payment Processing",
41
41
  "project": "SBI",
42
42
  "labels": ["originBot", "ProductsMerge"],
43
43
  "environments": ["PRD", "STG"],