// Command seed populates a demo environment (§11.11): one demo customer // (offline ticketing type), 1 admin, 2 consultants, 6 developers, pool // memberships, and sample conversations. Idempotent: existing users (by // email) and customers (by name) are reused, not duplicated. // // MONGO_SEED_URI=mongodb://127.0.0.1:27017 go run ./scripts package main import ( "context" "fmt" "log" "os" "time" "go.mongodb.org/mongo-driver/v2/bson" "bountyboard/internal/auth" "bountyboard/internal/config" "bountyboard/internal/domain" "bountyboard/internal/store" "bountyboard/internal/ulid" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) const demoPassword = "demo-pass-123" func main() { _ = config.LoadDotEnv(".env") uri := os.Getenv("MONGO_SEED_URI") if uri == "" { uri = "mongodb://127.0.0.1:27017" } dbName := os.Getenv("MONGO_DB") if dbName == "" { dbName = "bountyboard" } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() client, err := mongo.Connect(options.Client().ApplyURI(uri). SetServerSelectionTimeout(5 * time.Second)) if err != nil { log.Fatalf("connect: %v", err) } defer client.Disconnect(context.Background()) st := &store.Store{Client: client, DB: client.Database(dbName)} if err := store.EnsureIndexes(ctx, st.DB); err != nil { log.Fatalf("indexes: %v", err) } user := func(email, name string, roles domain.Roles) *domain.User { if u, err := st.UserByEmail(ctx, email); err == nil { fmt.Printf(" exists: %s\n", email) return u } hash, err := auth.HashPassword(demoPassword) if err != nil { log.Fatal(err) } u := &domain.User{ ID: ulid.New(), Email: email, Name: name, Roles: roles, Auth: domain.Auth{Local: &domain.LocalAuth{PasswordHash: hash}}, Settings: domain.UserSettings{Theme: "light", Notifications: domain.NotificationPrefs{Email: true, InApp: true}}, Bio: "Seeded demo account.", } if err := st.CreateUser(ctx, u); err != nil { log.Fatalf("create %s: %v", email, err) } fmt.Printf(" created: %s (password %q)\n", email, demoPassword) return u } fmt.Println("Seeding users…") user("seed-admin@example.com", "Seed Admin", domain.Roles{Admin: true}) clara := user("clara@example.com", "Clara Consultant", domain.Roles{Consultant: true}) carlos := user("carlos@example.com", "Carlos Consultant", domain.Roles{Consultant: true}) devs := []*domain.User{} devNames := []string{"Dana", "Devon", "Dimitri", "Daria", "Dylan", "Drew"} for i, n := range devNames { devs = append(devs, user(fmt.Sprintf("dev%d@example.com", i+1), n+" Developer", domain.Roles{Developer: true})) } fmt.Println("Seeding pools…") addPool := func(c, d *domain.User) { if err := st.AddToPool(ctx, c.ID, d.ID); err != nil && err != store.ErrDuplicate { log.Fatalf("pool: %v", err) } } for _, d := range devs[:4] { addPool(clara, d) } for _, d := range devs[2:] { addPool(carlos, d) } fmt.Println("Seeding demo customer…") customers, err := st.ListCustomers(ctx, "", true) if err != nil { log.Fatal(err) } var demoCustomer *domain.Customer for i := range customers { if customers[i].Name == "ACME Demo" { demoCustomer = &customers[i] } } if demoCustomer == nil { demoCustomer = &domain.Customer{ Name: "ACME Demo", Ticketing: domain.Ticketing{ Type: domain.TicketingDemo, ProjectKey: "ACME", PollIntervalSec: 30, }, ConsultantIDs: []string{clara.ID, carlos.ID}, DefaultBudget: 1500, } if err := st.CreateCustomer(ctx, demoCustomer); err != nil { log.Fatal(err) } fmt.Println(" created: ACME Demo (demo ticketing — tickets appear within one poll)") } else { fmt.Println(" exists: ACME Demo") } fmt.Println("Seeding conversations…") dm, err := st.FindOrCreateDM(ctx, clara.ID, devs[0].ID) if err != nil { log.Fatal(err) } n, err := st.DB.Collection("messages").CountDocuments(ctx, bson.M{"conversationId": dm.ID}) if err != nil { log.Fatal(err) } if n == 0 { msgs := []struct { from *domain.User body string }{ {clara, "

Hi Dana — I just added you to my pool. The ACME board fills up shortly.

"}, {devs[0], "

Great, I will grab the CSV importer once it is published.

"}, {clara, "

Perfect. Ping me here if any acceptance criteria are unclear.

"}, } for _, m := range msgs { if err := st.InsertMessage(ctx, &store.Message{ ConversationID: dm.ID, SenderID: m.from.ID, Body: m.body, }); err != nil { log.Fatal(err) } } fmt.Println(" created: DM Clara ↔ Dana with 3 messages") } groups, _ := st.ListConversations(ctx, clara.ID) hasGroup := false for _, g := range groups { if g.Kind == "group" && g.Title == "ACME standup" { hasGroup = true } } if !hasGroup { grp := &store.Conversation{ Kind: "group", Title: "ACME standup", ParticipantIDs: []string{clara.ID, carlos.ID, devs[0].ID, devs[1].ID, devs[2].ID}, } if err := st.CreateConversation(ctx, grp); err != nil { log.Fatal(err) } if err := st.InsertMessage(ctx, &store.Message{ ConversationID: grp.ID, SenderID: carlos.ID, Body: "

Welcome to the ACME standup channel. Daily updates here, please.

", }); err != nil { log.Fatal(err) } fmt.Println(" created: group 'ACME standup'") } fmt.Println("\nSeed complete.") fmt.Printf("Demo accounts (password %q):\n", demoPassword) fmt.Println(" seed-admin@example.com (admin)") fmt.Println(" clara@example.com, carlos@example.com (consultants)") for i := range devNames { fmt.Printf(" dev%d@example.com (developer)\n", i+1) } }