Skip to content

Commit 5e96ec7

Browse files
committed
feat(poam): enhance milestone model with full status set and additional fields
- Expand MilestoneStatus: open, in-progress, completed, cancelled (replaces the minimal planned/completed-only set from Phase 1) - Rename ScheduledCompletionDate -> PlannedCompletionDate on PoamItemMilestone model, service params, and handler structs to align with the UI contract and the PoamItem field naming convention - Add ResponsibleParty and Remarks fields to PoamItemMilestone, enabling accountability tracking and audit notes per milestone - Default milestone status is now 'open' (was 'planned') - Fix worker_enabled env var binding in cmd/root.go so that CCF_WORKER_ENABLED=false is correctly honoured by Viper - Update integration tests to use new status values and field names These changes align the backend milestone API contract with the Vue components built for the POAM Items UI feature.
1 parent f3dce82 commit 5e96ec7

File tree

5 files changed

+133
-100
lines changed

5 files changed

+133
-100
lines changed

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func bindEnvironmentVariables() {
5353
viper.MustBindEnv("evidence_default_expiry_months")
5454
viper.MustBindEnv("digest_enabled")
5555
viper.MustBindEnv("digest_schedule")
56+
viper.MustBindEnv("worker_enabled")
5657
}
5758

5859
func init() {

internal/api/handler/poam_items.go

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -109,21 +109,25 @@ type updatePoamItemRequest struct {
109109
}
110110

111111
type createMilestoneRequest struct {
112-
Title string `json:"title" validate:"required"`
113-
Description string `json:"description"`
114-
Status string `json:"status"`
115-
ScheduledCompletionDate *time.Time `json:"scheduledCompletionDate"`
112+
Title string `json:"title" validate:"required"`
113+
Description string `json:"description"`
114+
Status string `json:"status"`
115+
PlannedCompletionDate *time.Time `json:"plannedCompletionDate"`
116+
ResponsibleParty *string `json:"responsibleParty"`
117+
Remarks *string `json:"remarks"`
116118
// OrderIndex is a pointer so that clients can explicitly set 0 without it
117119
// being indistinguishable from an omitted field.
118120
OrderIndex *int `json:"orderIndex"`
119121
}
120122

121123
type updateMilestoneRequest struct {
122-
Title *string `json:"title"`
123-
Description *string `json:"description"`
124-
Status *string `json:"status"`
125-
ScheduledCompletionDate *time.Time `json:"scheduledCompletionDate"`
126-
OrderIndex *int `json:"orderIndex"`
124+
Title *string `json:"title"`
125+
Description *string `json:"description"`
126+
Status *string `json:"status"`
127+
PlannedCompletionDate *time.Time `json:"plannedCompletionDate"`
128+
ResponsibleParty *string `json:"responsibleParty"`
129+
Remarks *string `json:"remarks"`
130+
OrderIndex *int `json:"orderIndex"`
127131
}
128132

129133
type addLinkRequest struct {
@@ -185,16 +189,18 @@ type poamItemResponse struct {
185189
}
186190

187191
type milestoneResponse struct {
188-
ID uuid.UUID `json:"id"`
189-
PoamItemID uuid.UUID `json:"poamItemId"`
190-
Title string `json:"title"`
191-
Description string `json:"description"`
192-
Status string `json:"status"`
193-
ScheduledCompletionDate *time.Time `json:"scheduledCompletionDate,omitempty"`
194-
CompletionDate *time.Time `json:"completionDate,omitempty"`
195-
OrderIndex int `json:"orderIndex"`
196-
CreatedAt time.Time `json:"createdAt"`
197-
UpdatedAt time.Time `json:"updatedAt"`
192+
ID uuid.UUID `json:"id"`
193+
PoamItemID uuid.UUID `json:"poamItemId"`
194+
Title string `json:"title"`
195+
Description string `json:"description"`
196+
Status string `json:"status"`
197+
PlannedCompletionDate *time.Time `json:"plannedCompletionDate,omitempty"`
198+
CompletionDate *time.Time `json:"completionDate,omitempty"`
199+
ResponsibleParty *string `json:"responsibleParty,omitempty"`
200+
Remarks *string `json:"remarks,omitempty"`
201+
OrderIndex int `json:"orderIndex"`
202+
CreatedAt time.Time `json:"createdAt"`
203+
UpdatedAt time.Time `json:"updatedAt"`
198204
}
199205

200206
func toPoamItemResponse(item *poamsvc.PoamItem) poamItemResponse {
@@ -251,16 +257,18 @@ func toPoamItemResponse(item *poamsvc.PoamItem) poamItemResponse {
251257

252258
func toMilestoneResponse(m *poamsvc.PoamItemMilestone) milestoneResponse {
253259
return milestoneResponse{
254-
ID: m.ID,
255-
PoamItemID: m.PoamItemID,
256-
Title: m.Title,
257-
Description: m.Description,
258-
Status: m.Status,
259-
ScheduledCompletionDate: m.ScheduledCompletionDate,
260-
CompletionDate: m.CompletionDate,
261-
OrderIndex: m.OrderIndex,
262-
CreatedAt: m.CreatedAt,
263-
UpdatedAt: m.UpdatedAt,
260+
ID: m.ID,
261+
PoamItemID: m.PoamItemID,
262+
Title: m.Title,
263+
Description: m.Description,
264+
Status: m.Status,
265+
PlannedCompletionDate: m.PlannedCompletionDate,
266+
CompletionDate: m.CompletionDate,
267+
ResponsibleParty: m.ResponsibleParty,
268+
Remarks: m.Remarks,
269+
OrderIndex: m.OrderIndex,
270+
CreatedAt: m.CreatedAt,
271+
UpdatedAt: m.UpdatedAt,
264272
}
265273
}
266274

@@ -416,11 +424,13 @@ func (h *PoamItemsHandler) Create(c echo.Context) error {
416424
msOrderIdx = *mr.OrderIndex
417425
}
418426
params.Milestones = append(params.Milestones, poamsvc.CreateMilestoneParams{
419-
Title: mr.Title,
420-
Description: mr.Description,
421-
Status: mr.Status,
422-
ScheduledCompletionDate: mr.ScheduledCompletionDate,
423-
OrderIndex: msOrderIdx,
427+
Title: mr.Title,
428+
Description: mr.Description,
429+
Status: mr.Status,
430+
PlannedCompletionDate: mr.PlannedCompletionDate,
431+
ResponsibleParty: mr.ResponsibleParty,
432+
Remarks: mr.Remarks,
433+
OrderIndex: msOrderIdx,
424434
})
425435
}
426436

@@ -663,11 +673,13 @@ func (h *PoamItemsHandler) AddMilestone(c echo.Context) error {
663673
orderIdx = *in.OrderIndex
664674
}
665675
m, err := h.poamService.AddMilestone(id, poamsvc.CreateMilestoneParams{
666-
Title: in.Title,
667-
Description: in.Description,
668-
Status: in.Status,
669-
ScheduledCompletionDate: in.ScheduledCompletionDate,
670-
OrderIndex: orderIdx,
676+
Title: in.Title,
677+
Description: in.Description,
678+
Status: in.Status,
679+
PlannedCompletionDate: in.PlannedCompletionDate,
680+
ResponsibleParty: in.ResponsibleParty,
681+
Remarks: in.Remarks,
682+
OrderIndex: orderIdx,
671683
})
672684
if err != nil {
673685
return h.internalError(c, "failed to add milestone", err)
@@ -707,11 +719,13 @@ func (h *PoamItemsHandler) UpdateMilestone(c echo.Context) error {
707719
return c.JSON(http.StatusBadRequest, api.NewError(fmt.Errorf("invalid milestone status: %s", *in.Status)))
708720
}
709721
m, err := h.poamService.UpdateMilestone(id, milestoneID, poamsvc.UpdateMilestoneParams{
710-
Title: in.Title,
711-
Description: in.Description,
712-
Status: in.Status,
713-
ScheduledCompletionDate: in.ScheduledCompletionDate,
714-
OrderIndex: in.OrderIndex,
722+
Title: in.Title,
723+
Description: in.Description,
724+
Status: in.Status,
725+
PlannedCompletionDate: in.PlannedCompletionDate,
726+
ResponsibleParty: in.ResponsibleParty,
727+
Remarks: in.Remarks,
728+
OrderIndex: in.OrderIndex,
715729
})
716730
if err != nil {
717731
if errors.Is(err, gorm.ErrRecordNotFound) {

internal/api/handler/poam_items_integration_test.go

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ func (suite *PoamItemsApiIntegrationSuite) TestCreate_WithMilestonesAndLinks() {
140140
Status: "open",
141141
SourceType: "risk-promotion",
142142
Milestones: []createMilestoneRequest{
143-
{Title: "Patch staging", Status: "planned", ScheduledCompletionDate: &due, OrderIndex: intPtr(0)},
144-
{Title: "Patch production", Status: "planned", OrderIndex: intPtr(1)},
143+
{Title: "Patch staging", Status: "open", PlannedCompletionDate: &due, OrderIndex: intPtr(0)},
144+
{Title: "Patch production", Status: "open", OrderIndex: intPtr(1)},
145145
},
146146
}
147147
raw, _ := json.Marshal(body)
@@ -390,8 +390,8 @@ func (suite *PoamItemsApiIntegrationSuite) TestGet_Exists() {
390390
suite.Require().NoError(suite.Migrator.Refresh())
391391
sspID := uuid.New()
392392
item := suite.seedItem(sspID, "Get test item", "open")
393-
suite.seedMilestone(item.ID, "Milestone A", "planned", 0)
394-
suite.seedMilestone(item.ID, "Milestone B", "planned", 1)
393+
suite.seedMilestone(item.ID, "Milestone A", "open", 0)
394+
suite.seedMilestone(item.ID, "Milestone B", "open", 1)
395395
rec, req := suite.authedReq(http.MethodGet, fmt.Sprintf("/api/poam-items/%s", item.ID), nil)
396396
suite.newServer().E().ServeHTTP(rec, req)
397397
assert.Equal(suite.T(), http.StatusOK, rec.Code)
@@ -512,7 +512,7 @@ func (suite *PoamItemsApiIntegrationSuite) TestDelete_CascadesAllLinks() {
512512
suite.Require().NoError(suite.Migrator.Refresh())
513513
sspID := uuid.New()
514514
item := suite.seedItem(sspID, "To delete", "open")
515-
suite.seedMilestone(item.ID, "MS1", "planned", 0)
515+
suite.seedMilestone(item.ID, "MS1", "open", 0)
516516
riskID := uuid.New()
517517
suite.DB.Create(&poamsvc.PoamItemRiskLink{PoamItemID: item.ID, RiskID: riskID})
518518
evidenceID := uuid.New()
@@ -546,9 +546,9 @@ func (suite *PoamItemsApiIntegrationSuite) TestListMilestones_OrderedByIndex() {
546546
suite.Require().NoError(suite.Migrator.Refresh())
547547
sspID := uuid.New()
548548
item := suite.seedItem(sspID, "MS order test", "open")
549-
suite.seedMilestone(item.ID, "Third", "planned", 2)
550-
suite.seedMilestone(item.ID, "First", "planned", 0)
551-
suite.seedMilestone(item.ID, "Second", "planned", 1)
549+
suite.seedMilestone(item.ID, "Third", "open", 2)
550+
suite.seedMilestone(item.ID, "First", "open", 0)
551+
suite.seedMilestone(item.ID, "Second", "open", 1)
552552
rec, req := suite.authedReq(http.MethodGet, fmt.Sprintf("/api/poam-items/%s/milestones", item.ID), nil)
553553
suite.newServer().E().ServeHTTP(rec, req)
554554
assert.Equal(suite.T(), http.StatusOK, rec.Code)
@@ -577,11 +577,11 @@ func (suite *PoamItemsApiIntegrationSuite) TestAddMilestone() {
577577
item := suite.seedItem(sspID, "Add milestone test", "open")
578578
due := time.Now().Add(7 * 24 * time.Hour).UTC().Truncate(time.Second)
579579
body := createMilestoneRequest{
580-
Title: "Deploy to staging",
581-
Description: "Deploy patched version to staging",
582-
Status: "planned",
583-
ScheduledCompletionDate: &due,
584-
OrderIndex: intPtr(0),
580+
Title: "Deploy to staging",
581+
Description: "Deploy patched version to staging",
582+
Status: "open",
583+
PlannedCompletionDate: &due,
584+
OrderIndex: intPtr(0),
585585
}
586586
raw, _ := json.Marshal(body)
587587
rec, req := suite.authedReq(http.MethodPost, fmt.Sprintf("/api/poam-items/%s/milestones", item.ID), raw)
@@ -590,13 +590,13 @@ func (suite *PoamItemsApiIntegrationSuite) TestAddMilestone() {
590590
var resp GenericDataResponse[milestoneResponse]
591591
suite.Require().NoError(json.Unmarshal(rec.Body.Bytes(), &resp))
592592
assert.Equal(suite.T(), "Deploy to staging", resp.Data.Title)
593-
assert.Equal(suite.T(), "planned", resp.Data.Status)
593+
assert.Equal(suite.T(), "open", resp.Data.Status)
594594
assert.Equal(suite.T(), item.ID, resp.Data.PoamItemID)
595595
}
596596

597597
func (suite *PoamItemsApiIntegrationSuite) TestAddMilestone_ParentNotFound() {
598598
suite.Require().NoError(suite.Migrator.Refresh())
599-
body := createMilestoneRequest{Title: "Ghost MS", Status: "planned"}
599+
body := createMilestoneRequest{Title: "Ghost MS", Status: "open"}
600600
raw, _ := json.Marshal(body)
601601
rec, req := suite.authedReq(http.MethodPost, fmt.Sprintf("/api/poam-items/%s/milestones", uuid.New()), raw)
602602
suite.newServer().E().ServeHTTP(rec, req)
@@ -611,7 +611,7 @@ func (suite *PoamItemsApiIntegrationSuite) TestUpdateMilestone_MarkCompleted_Set
611611
suite.Require().NoError(suite.Migrator.Refresh())
612612
sspID := uuid.New()
613613
item := suite.seedItem(sspID, "Milestone complete test", "open")
614-
ms := suite.seedMilestone(item.ID, "Enable scanning", "planned", 0)
614+
ms := suite.seedMilestone(item.ID, "Enable scanning", "open", 0)
615615
newStatus := "completed"
616616
body := updateMilestoneRequest{Status: &newStatus}
617617
raw, _ := json.Marshal(body)
@@ -632,7 +632,7 @@ func (suite *PoamItemsApiIntegrationSuite) TestUpdateMilestone_UpdateTitle() {
632632
suite.Require().NoError(suite.Migrator.Refresh())
633633
sspID := uuid.New()
634634
item := suite.seedItem(sspID, "MS title update", "open")
635-
ms := suite.seedMilestone(item.ID, "Old title", "planned", 0)
635+
ms := suite.seedMilestone(item.ID, "Old title", "open", 0)
636636
newTitle := "New title"
637637
body := updateMilestoneRequest{Title: &newTitle}
638638
raw, _ := json.Marshal(body)
@@ -652,7 +652,7 @@ func (suite *PoamItemsApiIntegrationSuite) TestUpdateMilestone_UpdateOrderIndex(
652652
suite.Require().NoError(suite.Migrator.Refresh())
653653
sspID := uuid.New()
654654
item := suite.seedItem(sspID, "MS order update", "open")
655-
ms := suite.seedMilestone(item.ID, "Reorder me", "planned", 0)
655+
ms := suite.seedMilestone(item.ID, "Reorder me", "open", 0)
656656
newOrder := 5
657657
body := updateMilestoneRequest{OrderIndex: &newOrder}
658658
raw, _ := json.Marshal(body)
@@ -692,7 +692,7 @@ func (suite *PoamItemsApiIntegrationSuite) TestDeleteMilestone() {
692692
suite.Require().NoError(suite.Migrator.Refresh())
693693
sspID := uuid.New()
694694
item := suite.seedItem(sspID, "Delete MS test", "open")
695-
ms := suite.seedMilestone(item.ID, "To delete", "planned", 0)
695+
ms := suite.seedMilestone(item.ID, "To delete", "open", 0)
696696
rec, req := suite.authedReq(
697697
http.MethodDelete,
698698
fmt.Sprintf("/api/poam-items/%s/milestones/%s", item.ID, ms.ID),

internal/service/relational/poam/models.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,16 @@ func (s PoamItemSourceType) IsValid() bool {
4949
type MilestoneStatus string
5050

5151
const (
52-
MilestoneStatusPlanned MilestoneStatus = "planned"
52+
MilestoneStatusOpen MilestoneStatus = "open"
53+
MilestoneStatusInProgress MilestoneStatus = "in-progress"
5354
MilestoneStatusCompleted MilestoneStatus = "completed"
55+
MilestoneStatusCancelled MilestoneStatus = "cancelled"
5456
)
5557

5658
// IsValid reports whether the milestone status is one of the defined constants.
5759
func (s MilestoneStatus) IsValid() bool {
5860
switch s {
59-
case MilestoneStatusPlanned, MilestoneStatusCompleted:
61+
case MilestoneStatusOpen, MilestoneStatusInProgress, MilestoneStatusCompleted, MilestoneStatusCancelled:
6062
return true
6163
}
6264
return false
@@ -117,16 +119,18 @@ func (p *PoamItem) BeforeCreate(_ *gorm.DB) error {
117119
// PoamItemMilestone is a strong-typed milestone entry for a PoamItem.
118120
// Field names follow the Confluence design doc (v15).
119121
type PoamItemMilestone struct {
120-
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
121-
PoamItemID uuid.UUID `gorm:"type:uuid;index;not null" json:"poamItemId"`
122-
Title string `gorm:"not null" json:"title"`
123-
Description string ` json:"description"`
124-
Status string `gorm:"type:text;not null" json:"status"`
125-
ScheduledCompletionDate *time.Time ` json:"scheduledCompletionDate,omitempty"`
126-
CompletionDate *time.Time ` json:"completionDate,omitempty"`
127-
OrderIndex int `gorm:"not null;default:0" json:"orderIndex"`
128-
CreatedAt time.Time ` json:"createdAt"`
129-
UpdatedAt time.Time ` json:"updatedAt"`
122+
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
123+
PoamItemID uuid.UUID `gorm:"type:uuid;index;not null" json:"poamItemId"`
124+
Title string `gorm:"not null" json:"title"`
125+
Description string ` json:"description"`
126+
Status string `gorm:"type:text;not null" json:"status"`
127+
PlannedCompletionDate *time.Time ` json:"plannedCompletionDate,omitempty"`
128+
CompletionDate *time.Time ` json:"completionDate,omitempty"`
129+
ResponsibleParty *string ` json:"responsibleParty,omitempty"`
130+
Remarks *string ` json:"remarks,omitempty"`
131+
OrderIndex int `gorm:"not null;default:0" json:"orderIndex"`
132+
CreatedAt time.Time ` json:"createdAt"`
133+
UpdatedAt time.Time ` json:"updatedAt"`
130134
}
131135

132136
// TableName returns the physical table name.
@@ -138,7 +142,7 @@ func (m *PoamItemMilestone) BeforeCreate(_ *gorm.DB) error {
138142
m.ID = uuid.New()
139143
}
140144
if m.Status == "" {
141-
m.Status = string(MilestoneStatusPlanned)
145+
m.Status = string(MilestoneStatusOpen)
142146
}
143147
if !MilestoneStatus(m.Status).IsValid() {
144148
return fmt.Errorf("invalid milestone status: %s", m.Status)

0 commit comments

Comments
 (0)