4e01b64bbd
- task domain model + exhaustive §4.4 status machine tests (single source of truth) - per-customer polling workers reconciled every 30s from the customers collection; panic-safe runs; manual sync-now trigger; status provider for the admin panel - §5.3 upsert keyed on (system, key, customerId): refreshes content while status=imported, records upstream_changed timeline + notification after - attachments cached into GridFS via connector-authorized downloads - orphaned tickets flagged with timeline + consultant notification, never deleted; idempotent across polls - consultant identity from users.extra.ticketingIdentities per system - notifications store (insert/list/unread-count/mark-read) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
115 lines
4.7 KiB
Go
115 lines
4.7 KiB
Go
package domain
|
||
|
||
import (
|
||
"math"
|
||
"time"
|
||
)
|
||
|
||
type TaskOrigin string
|
||
|
||
const (
|
||
OriginImported TaskOrigin = "imported"
|
||
OriginSubdivided TaskOrigin = "subdivided"
|
||
OriginExtended TaskOrigin = "extended"
|
||
)
|
||
|
||
type External struct {
|
||
System string `bson:"system" json:"system"`
|
||
Key string `bson:"key" json:"key"`
|
||
URL string `bson:"url" json:"url"`
|
||
Type string `bson:"type" json:"type"` // epic | story | task (icon only)
|
||
Raw map[string]any `bson:"raw" json:"-"`
|
||
Orphaned bool `bson:"orphaned" json:"orphaned"`
|
||
ContentHash string `bson:"contentHash" json:"-"` // change detection for upstream edits
|
||
}
|
||
|
||
type TaskAttachment struct {
|
||
Name string `bson:"name" json:"name"`
|
||
URL string `bson:"url" json:"url"`
|
||
MimeType string `bson:"mimeType" json:"mimeType"`
|
||
FileID string `bson:"fileId,omitempty" json:"fileId,omitempty"`
|
||
}
|
||
|
||
type Assignee struct {
|
||
Kind string `bson:"kind" json:"kind"` // human | ai
|
||
UserID string `bson:"userId,omitempty" json:"userId,omitempty"`
|
||
JobID string `bson:"jobId,omitempty" json:"jobId,omitempty"`
|
||
}
|
||
|
||
type ClaimRequest struct {
|
||
DeveloperID string `bson:"developerId" json:"developerId"`
|
||
Note string `bson:"note" json:"note"`
|
||
At time.Time `bson:"at" json:"at"`
|
||
}
|
||
|
||
type TimelineEntry struct {
|
||
At time.Time `bson:"at" json:"at"`
|
||
ActorID string `bson:"actorId" json:"actorId"` // user id or "system"
|
||
Event string `bson:"event" json:"event"`
|
||
Data map[string]any `bson:"data,omitempty" json:"data,omitempty"`
|
||
}
|
||
|
||
type Comment struct {
|
||
ID string `bson:"_id" json:"id"`
|
||
AuthorID string `bson:"authorId" json:"authorId"`
|
||
Body string `bson:"body" json:"body"` // sanitized HTML
|
||
At time.Time `bson:"at" json:"at"`
|
||
}
|
||
|
||
type TimeLogEntry struct {
|
||
DeveloperID string `bson:"developerId" json:"developerId"`
|
||
Minutes int `bson:"minutes" json:"minutes"`
|
||
Note string `bson:"note" json:"note"`
|
||
At time.Time `bson:"at" json:"at"`
|
||
}
|
||
|
||
// Task is the central work item (§4.4).
|
||
type Task struct {
|
||
ID string `bson:"_id" json:"id"`
|
||
CustomerID string `bson:"customerId" json:"customerId"`
|
||
ConsultantID string `bson:"consultantId" json:"consultantId"`
|
||
Origin TaskOrigin `bson:"origin" json:"origin"`
|
||
ParentID string `bson:"parentId,omitempty" json:"parentId,omitempty"`
|
||
RootID string `bson:"rootId" json:"rootId"`
|
||
External *External `bson:"external,omitempty" json:"external,omitempty"`
|
||
Title string `bson:"title" json:"title"`
|
||
Description string `bson:"description" json:"description"`
|
||
AcceptanceCriteria []string `bson:"acceptanceCriteria" json:"acceptanceCriteria"`
|
||
Attachments []TaskAttachment `bson:"attachments" json:"attachments"`
|
||
Links []string `bson:"links" json:"links"`
|
||
EffortCoefficient float64 `bson:"effortCoefficient" json:"effortCoefficient"`
|
||
Budget float64 `bson:"budget" json:"budget"`
|
||
Bounty float64 `bson:"bounty" json:"bounty"`
|
||
Status TaskStatus `bson:"status" json:"status"`
|
||
Assignee *Assignee `bson:"assignee,omitempty" json:"assignee,omitempty"`
|
||
ClaimRequests []ClaimRequest `bson:"claimRequests" json:"claimRequests"`
|
||
AtomizationNote string `bson:"atomizationNote,omitempty" json:"atomizationNote,omitempty"`
|
||
Timeline []TimelineEntry `bson:"timeline" json:"timeline"`
|
||
Comments []Comment `bson:"comments" json:"comments"`
|
||
TimeLog []TimeLogEntry `bson:"timeLog" json:"timeLog"`
|
||
PublishedAt time.Time `bson:"publishedAt,omitempty" json:"publishedAt,omitempty"` // stale-task aging (§11.19)
|
||
CreatedAt time.Time `bson:"createdAt" json:"createdAt"`
|
||
UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"`
|
||
Version int64 `bson:"version" json:"version"`
|
||
}
|
||
|
||
// ComputeBounty implements §6.1: round(coefficient × budget, 2).
|
||
func ComputeBounty(coefficient, budget float64) float64 {
|
||
return math.Round(coefficient*budget*100) / 100
|
||
}
|
||
|
||
// IsAssignedTo reports whether the human developer holds the task.
|
||
func (t *Task) IsAssignedTo(userID string) bool {
|
||
return t.Assignee != nil && t.Assignee.Kind == "human" && t.Assignee.UserID == userID
|
||
}
|
||
|
||
// HasClaimFrom reports an open claim request by the developer.
|
||
func (t *Task) HasClaimFrom(developerID string) bool {
|
||
for _, cr := range t.ClaimRequests {
|
||
if cr.DeveloperID == developerID {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|