70a813edfa
- whitelist HTML sanitizer (stdlib tokenizer) with XSS vector tests - developer board: pool-scoped visibility, customer/search/minBounty/sort filters, stale-task age badges, competing-claims visibility setting, saved filters in profile extras - claims: request/withdraw (developer), approve/decline (consultant) with notifications to winners and losers; unassign/abandon back to board - work tracking: start, sanitized comments with @mention notifications, time logging, submit for review - review queue + review with per-AC checklist stored on the timeline; approve writes the immutable bountyAwards row (human assignees only) - assign-to-AI: §5.2 job submission, HMAC-verified callback endpoint, idempotent by jobId, artifacts downloaded into GridFS, failure path keeps the task assigned with timeline + notification - notifications API + bell with unread badge, dropdown, page, WS toasts - pages: bounty board, my-tasks kanban, task detail (role-driven actions, review dialog, AI dialog), review queue, developer pool Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
78 lines
2.3 KiB
Go
78 lines
2.3 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
|
|
|
"bountyboard/internal/ulid"
|
|
)
|
|
|
|
// PoolEntry is a developer-pool membership row (§4.3).
|
|
type PoolEntry struct {
|
|
ID string `bson:"_id" json:"id"`
|
|
ConsultantID string `bson:"consultantId" json:"consultantId"`
|
|
DeveloperID string `bson:"developerId" json:"developerId"`
|
|
AddedAt time.Time `bson:"addedAt" json:"addedAt"`
|
|
}
|
|
|
|
func (s *Store) AddToPool(ctx context.Context, consultantID, developerID string) error {
|
|
_, err := s.DB.Collection("pools").InsertOne(ctx, PoolEntry{
|
|
ID: ulid.New(), ConsultantID: consultantID, DeveloperID: developerID,
|
|
AddedAt: time.Now().UTC(),
|
|
})
|
|
if err != nil {
|
|
if mongo.IsDuplicateKeyError(err) {
|
|
return ErrDuplicate
|
|
}
|
|
return fmt.Errorf("add to pool: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) RemoveFromPool(ctx context.Context, consultantID, developerID string) error {
|
|
res, err := s.DB.Collection("pools").DeleteOne(ctx,
|
|
bson.M{"consultantId": consultantID, "developerId": developerID})
|
|
if err != nil {
|
|
return fmt.Errorf("remove from pool: %w", err)
|
|
}
|
|
if res.DeletedCount == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PoolDeveloperIDs lists developers in one consultant's pool.
|
|
func (s *Store) PoolDeveloperIDs(ctx context.Context, consultantID string) ([]string, error) {
|
|
return s.poolSide(ctx, bson.M{"consultantId": consultantID}, "developerId")
|
|
}
|
|
|
|
// PoolConsultantIDs lists consultants who selected the developer — drives
|
|
// the developer's bounty-board visibility (§3).
|
|
func (s *Store) PoolConsultantIDs(ctx context.Context, developerID string) ([]string, error) {
|
|
return s.poolSide(ctx, bson.M{"developerId": developerID}, "consultantId")
|
|
}
|
|
|
|
func (s *Store) poolSide(ctx context.Context, filter bson.M, field string) ([]string, error) {
|
|
cur, err := s.DB.Collection("pools").Find(ctx, filter)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list pool: %w", err)
|
|
}
|
|
var rows []PoolEntry
|
|
if err := cur.All(ctx, &rows); err != nil {
|
|
return nil, fmt.Errorf("decode pool: %w", err)
|
|
}
|
|
out := make([]string, 0, len(rows))
|
|
for _, r := range rows {
|
|
if field == "developerId" {
|
|
out = append(out, r.DeveloperID)
|
|
} else {
|
|
out = append(out, r.ConsultantID)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|