Files
etalon 4e01b64bbd feat: ticketing sync workers with idempotent import and orphan flagging (phase 6)
- 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>
2026-06-12 19:03:13 +02:00

115 lines
4.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}