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>
This commit is contained in:
etalon
2026-06-12 19:03:13 +02:00
parent 458ba6a7ee
commit 4e01b64bbd
12 changed files with 1461 additions and 1 deletions
+17
View File
@@ -115,6 +115,23 @@ func (s *Store) LinkOIDC(ctx context.Context, userID, issuer, subject string) er
return nil
}
// ListAdminIDs returns ids of all active admins (system notifications).
func (s *Store) ListAdminIDs(ctx context.Context) ([]string, error) {
cur, err := s.users().Find(ctx, bson.M{"roles.admin": true, "disabled": false})
if err != nil {
return nil, fmt.Errorf("list admins: %w", err)
}
var users []domain.User
if err := cur.All(ctx, &users); err != nil {
return nil, fmt.Errorf("decode admins: %w", err)
}
ids := make([]string, len(users))
for i, u := range users {
ids[i] = u.ID
}
return ids, nil
}
// TouchLastSeen updates lastSeenAt without bumping version (pure telemetry).
func (s *Store) TouchLastSeen(ctx context.Context, userID string) error {
_, err := s.users().UpdateOne(ctx, bson.M{"_id": userID},