Scrum4Me/prisma/seed.ts
Janpeter Visser a268df3680
feat(PBI-59): Sprint.code (SP-N sequentieel per product) (#153)
Voegt een verplicht code-veld toe aan Sprint, sequentieel per product
(consistent met PBI-N, ST-NNN, T-N).

- **Schema** — `Sprint.code String @db.VarChar(30)` + `@@unique([product_id, code])`
- **Migratie** — voegt kolom toe als nullable, backfillt bestaande sprints
  via `ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY created_at)`
  als `SP-N`, en zet daarna NOT NULL + UNIQUE.
- **Generator** — `generateNextSprintCode(productId)` in lib/code-server.ts
  volgt het patroon van story/pbi/task; createSprintAction gebruikt
  `createWithCodeRetry` voor race-bescherming.
- **Seed** — sprint-counter per product (`SP-1`, `SP-2`, ...).

Zichtbaar in:
- Sprint-header (`Product › Sprint actief · SP-3`)
- JobCard + JobDetailPane voor SPRINT_IMPLEMENTATION jobs
- Insights: VelocityChart x-axis (compacter dan goal-truncated),
  AlignmentTrend tooltip, SprintInfoStrip
- actions/jobs-page.ts: `sprintCode` is weer een echte code i.p.v. null

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:10:16 +02:00

243 lines
7.4 KiB
TypeScript

// Seed bron: docs/scrum4me-backlog.md → prisma/seed-data/parse-backlog.ts.
// Pas alléén de backlog-markdown aan als je seed-data wilt wijzigen, niet deze file.
import { PrismaClient } from '@prisma/client'
import * as dotenv from 'dotenv'
import * as path from 'path'
import * as bcrypt from 'bcryptjs'
import { loadBacklog } from './seed-data/parse-backlog'
// Load env from project root
const root = path.resolve(__dirname, '..')
dotenv.config({ path: path.join(root, '.env.local'), override: true })
dotenv.config({ path: path.join(root, '.env') })
let prisma: PrismaClient
async function main() {
const url = process.env.DATABASE_URL
if (!url) throw new Error('DATABASE_URL is not set. Check .env.local')
const { Pool } = await import('pg')
const { PrismaPg } = await import('@prisma/adapter-pg')
const pool = new Pool({ connectionString: url })
const adapter = new PrismaPg(pool)
prisma = new PrismaClient({ adapter })
console.log('Seeding database...')
// Create main demo user
const demoHash = await bcrypt.hash('demo1234', 12)
const demo = await prisma.user.upsert({
where: { username: 'demo' },
update: {},
create: {
username: 'demo',
password_hash: demoHash,
is_demo: true,
roles: {
create: [
{ role: 'PRODUCT_OWNER' },
{ role: 'DEVELOPER' },
],
},
},
})
console.log(`Demo user: ${demo.username} (id: ${demo.id})`)
// Create seed user for the product
const userHash = await bcrypt.hash('scrum4me123', 12)
const user = await prisma.user.upsert({
where: { username: 'lars' },
update: {},
create: {
username: 'lars',
password_hash: userHash,
is_demo: false,
roles: {
create: [
{ role: 'PRODUCT_OWNER' },
{ role: 'SCRUM_MASTER' },
{ role: 'DEVELOPER' },
],
},
},
})
console.log(`Main user: ${user.username} (id: ${user.id})`)
// Create tester user for cross-user isolation tests (no products)
const testerHash = await bcrypt.hash('tester123', 12)
const tester = await prisma.user.upsert({
where: { username: 'tester' },
update: {},
create: {
username: 'tester',
password_hash: testerHash,
is_demo: false,
roles: {
create: [
{ role: 'PRODUCT_OWNER' },
{ role: 'DEVELOPER' },
],
},
},
})
console.log(`Tester user: ${tester.username} (id: ${tester.id})`)
// Reset demo product data — delete in dependency order to avoid FK violations
const existingProducts = await prisma.product.findMany({ where: { user_id: demo.id }, select: { id: true } })
for (const p of existingProducts) {
// Stories reference product_id directly (no cascade), so delete PBIs first (cascades to stories via pbi_id)
const existingPbis = await prisma.pbi.findMany({ where: { product_id: p.id }, select: { id: true } })
for (const pbi of existingPbis) {
await prisma.story.deleteMany({ where: { pbi_id: pbi.id } })
}
await prisma.pbi.deleteMany({ where: { product_id: p.id } })
await prisma.sprint.deleteMany({ where: { product_id: p.id } })
}
await prisma.product.deleteMany({ where: { user_id: demo.id } })
const product = await prisma.product.create({
data: {
user_id: demo.id,
name: 'Scrum4Me',
code: 'SCRUM4ME',
description:
'Een desktop-first fullstack webapplicatie voor solo developers en kleine Scrum Teams die meerdere softwareprojecten parallel beheren.',
repo_url: 'https://github.com/madhura68/Scrum4Me',
definition_of_done:
'Code is geïmplementeerd, type-checked, getest, gedocumenteerd in code en docs, en gedeployed naar Vercel zonder regressies.',
archived: false,
},
})
console.log(`Product created: ${product.name} (id: ${product.id}, owner: demo)`)
await prisma.productMember.create({
data: { product_id: product.id, user_id: user.id },
})
console.log(` Added ${user.username} as member of ${product.name}`)
const milestones = await loadBacklog(root)
console.log(`Loaded backlog: ${milestones.length} milestones, ${milestones.reduce((acc, m) => acc + m.stories.length, 0)} stories`)
let productTaskCounter = 0
let sprintCounter = 0
for (const ms of milestones) {
const pbi = await prisma.pbi.create({
data: {
product_id: product.id,
code: ms.key,
title: ms.title,
description: ms.goal,
priority: ms.priority,
sort_order: ms.sort_order,
},
})
const sprint = await prisma.sprint.create({
data: {
product_id: product.id,
code: `SP-${++sprintCounter}`,
sprint_goal: `${ms.key}${ms.goal}`,
status: ms.sprint_status,
},
})
console.log(
` PBI ${pbi.title} (priority ${pbi.priority}) + sprint ${ms.sprint_status}`,
)
// M3.5 = de huidige sprint die nog moet beginnen — alle stories en taken
// worden geforceerd op niet-uitgevoerd, ongeacht de checkbox in de backlog.
const forceOpen = ms.key === 'M3.5'
for (const s of ms.stories) {
const isActive = ms.sprint_status === 'ACTIVE'
const effectivelyDone = !forceOpen && s.status === 'DONE'
const inSprint = isActive || effectivelyDone
const storyStatus = effectivelyDone ? 'DONE' : isActive ? 'IN_SPRINT' : 'OPEN'
const storySummary = s.tasks.map((t) => t.title).join('; ')
const story = await prisma.story.create({
data: {
pbi_id: pbi.id,
product_id: product.id,
sprint_id: inSprint ? sprint.id : null,
code: s.ref,
title: s.title,
description: storySummary,
acceptance_criteria: s.acceptance_criteria,
priority: ms.priority,
sort_order: s.sort_order,
status: storyStatus,
},
})
for (const t of s.tasks) {
productTaskCounter += 1
await prisma.task.create({
data: {
story_id: story.id,
product_id: product.id,
sprint_id: inSprint ? sprint.id : null,
code: `T-${productTaskCounter}`,
title: t.title,
description: t.description,
priority: ms.priority,
sort_order: t.sort_order,
status: effectivelyDone ? 'DONE' : 'TO_DO',
},
})
}
}
}
const modelPrices = [
{
model_id: 'claude-opus-4-7',
input_price_per_1m: 15.0,
output_price_per_1m: 75.0,
cache_read_price_per_1m: 1.5,
cache_write_price_per_1m: 18.75,
},
{
model_id: 'claude-sonnet-4-6',
input_price_per_1m: 3.0,
output_price_per_1m: 15.0,
cache_read_price_per_1m: 0.3,
cache_write_price_per_1m: 3.75,
},
{
model_id: 'claude-haiku-4-5-20251001',
input_price_per_1m: 0.8,
output_price_per_1m: 4.0,
cache_read_price_per_1m: 0.08,
cache_write_price_per_1m: 1.0,
},
]
for (const mp of modelPrices) {
await prisma.modelPrice.upsert({
where: { model_id: mp.model_id },
update: mp,
create: mp,
})
console.log(` ModelPrice upserted: ${mp.model_id}`)
}
console.log('\nSeeding complete!')
console.log('Demo user: username=demo password=demo1234')
console.log('Main user: username=lars password=scrum4me123')
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma?.$disconnect()
})