From 23a34566800fdc797c6eeba2c69be1dc6b289dc6 Mon Sep 17 00:00:00 2001 From: Opulence Chuks Date: Thu, 20 Aug 2026 17:22:56 +0100 Subject: [PATCH 1/2] feat: create deterministic fixtures for prisma seed --- prisma/SETUP.md | 17 +++-- prisma/fixtures/index.ts | 2 + prisma/fixtures/modules.ts | 75 +++++++++++++++++++ prisma/fixtures/users.ts | 75 +++++++++++++++++++ prisma/seed.ts | 150 ++++--------------------------------- 5 files changed, 177 insertions(+), 142 deletions(-) create mode 100644 prisma/fixtures/index.ts create mode 100644 prisma/fixtures/modules.ts create mode 100644 prisma/fixtures/users.ts diff --git a/prisma/SETUP.md b/prisma/SETUP.md index 4cfc56a..d56ffa5 100644 --- a/prisma/SETUP.md +++ b/prisma/SETUP.md @@ -46,14 +46,19 @@ Populate the database with mock data for development: npm run seed ``` -This will create: -- 2 test users -- 4 sample modules with different difficulties -- 3 module completions -- 2 credentials -- 3 transactions +This will create deterministic local-only fixtures: +- 6 test users (including admin, verified learner, unverified learner, disabled learner, instructor, employer) +- 8 sample modules with different difficulties +- Module completions and credentials for testing success/empty/pending states +- Transactions for rewards, referrals, and adjustments - 1 webhook endpoint +> **Note**: All fixture identities are strictly local-only and idempotent. Running `npm run seed` multiple times will not duplicate records. +> To explicitly reset the database before seeding, use: +> ```bash +> npm run seed:reset +> ``` + ### 5. Verify Setup Open Prisma Studio to view and manage your data: diff --git a/prisma/fixtures/index.ts b/prisma/fixtures/index.ts new file mode 100644 index 0000000..634a276 --- /dev/null +++ b/prisma/fixtures/index.ts @@ -0,0 +1,2 @@ +export * from './users' +export * from './modules' diff --git a/prisma/fixtures/modules.ts b/prisma/fixtures/modules.ts new file mode 100644 index 0000000..71149d4 --- /dev/null +++ b/prisma/fixtures/modules.ts @@ -0,0 +1,75 @@ +export type SeedModule = { + id: string + title: string + description: string + category: string + difficulty: string + reward: number +} + +export const seedModuleFixtures: SeedModule[] = [ + { + id: 'seed-module-blockchain-101', + title: 'Stellar Fundamentals', + description: 'Core ledger concepts, accounts, trustlines, and transaction flow on Stellar.', + category: 'blockchain', + difficulty: 'beginner', + reward: 10, + }, + { + id: 'seed-module-finance-101', + title: 'Understanding Stablecoins', + description: 'How fiat-backed and crypto-backed stablecoins work across global payment rails.', + category: 'finance', + difficulty: 'beginner', + reward: 12, + }, + { + id: 'seed-module-security-201', + title: 'Wallet Security & Key Management', + description: 'Threat modeling, custody approaches, and secure key handling in production systems.', + category: 'security', + difficulty: 'intermediate', + reward: 18, + }, + { + id: 'seed-module-development-301', + title: 'Build with Soroban', + description: 'Develop and test smart contracts using practical Soroban development workflows.', + category: 'development', + difficulty: 'advanced', + reward: 30, + }, + { + id: 'seed-module-compliance-201', + title: 'AML/KYC for Digital Finance', + description: 'Compliance basics, sanctions screening, and regulated onboarding for fintech teams.', + category: 'compliance', + difficulty: 'intermediate', + reward: 20, + }, + { + id: 'seed-module-identity-301', + title: 'Decentralized Identity in Practice', + description: 'Verifiable credentials, selective disclosure, and identity portability patterns.', + category: 'identity', + difficulty: 'advanced', + reward: 28, + }, + { + id: 'seed-module-development-401', + title: 'Production API Hardening', + description: 'Rate limiting, auth patterns, observability, and safe rollout practices.', + category: 'development', + difficulty: 'expert', + reward: 40, + }, + { + id: 'seed-module-blockchain-202', + title: 'Stellar Asset Issuance', + description: 'Issue and manage custom assets with issuer/distributor architecture.', + category: 'blockchain', + difficulty: 'intermediate', + reward: 22, + }, +] diff --git a/prisma/fixtures/users.ts b/prisma/fixtures/users.ts new file mode 100644 index 0000000..9163859 --- /dev/null +++ b/prisma/fixtures/users.ts @@ -0,0 +1,75 @@ +import { Role } from '@prisma/client' + +export type SeedUser = { + id: string + email: string + username: string + name: string + role: Role + status: string + isVerified: boolean + walletAddress: string | null +} + +export const seedUserFixtures: SeedUser[] = [ + { + id: 'seed-user-admin-ada', + email: 'ada.admin+seed@learnault.local', + username: 'ada_admin_seed', + name: 'Ada Admin', + role: 'ADMIN', + status: 'ACTIVE', + isVerified: true, + walletAddress: 'GD6QK3H5MYYXQUDMVDGLDZ4E2IWXQ7N5SLW7PZBLW4D5YV3V2NFH8A1A', + }, + { + id: 'seed-user-instructor-deepak', + email: 'deepak.instructor+seed@learnault.local', + username: 'deepak_inst_seed', + name: 'Deepak Instructor', + role: 'INSTRUCTOR', + status: 'ACTIVE', + isVerified: true, + walletAddress: 'GDVXG4D7WQ3WWT3S3Q6L2J5KLC2TSGBYHYQ4M4U7QWEK3J75VYLPQ9U2', + }, + { + id: 'seed-user-employer-acme', + email: 'acme.employer+seed@learnault.local', + username: 'acme_emp_seed', + name: 'Acme Talent Team', + role: 'LEARNER', + status: 'ACTIVE', + isVerified: true, + walletAddress: null, + }, + { + id: 'seed-user-learner-alice', + email: 'alice.learner+seed@learnault.local', + username: 'alice_learner_seed', + name: 'Alice Learner', + role: 'LEARNER', + status: 'ACTIVE', + isVerified: true, + walletAddress: 'GBUQWP3BOUZX34ULNQG23RQ6F4BFSRJ4HBSNF63YLIXLQ5EDLF274Y6C', + }, + { + id: 'seed-user-learner-bob', + email: 'bob.learner+seed@learnault.local', + username: 'bob_learner_seed', + name: 'Bob Learner', + role: 'LEARNER', + status: 'ACTIVE', + isVerified: false, + walletAddress: 'GB7YLLICSMNYWJ46NWYCBTGXD54FPPWFY3YQYBFJFTQ6B2N3WHAL5V4K', + }, + { + id: 'seed-user-learner-carla', + email: 'carla.learner+seed@learnault.local', + username: 'carla_learner_seed', + name: 'Carla Learner', + role: 'LEARNER', + status: 'DEACTIVATED', + isVerified: true, + walletAddress: 'GA5N2IBQ2J5KVSBBR6K7D6JQ3Y7L46BP3I7WNIPXQ2XH2WQOTJXK5C2Z', + }, +] diff --git a/prisma/seed.ts b/prisma/seed.ts index e08137f..2471450 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -4,26 +4,14 @@ import { PrismaPg } from '@prisma/adapter-pg' import { Pool } from 'pg' import { hash } from 'bcryptjs' +import { seedUserFixtures } from './fixtures/users' +import { seedModuleFixtures } from './fixtures/modules' +import type { SeedModule } from './fixtures/modules' + const pool = new Pool({ connectionString: process.env.DATABASE_URL }) const adapter = new PrismaPg(pool) const prisma = new PrismaClient({ adapter }) -type SeedUser = { - id: string - email: string - name: string - walletAddress: string | null -} - -type SeedModule = { - id: string - title: string - description: string - category: string - difficulty: string - reward: number -} - const MS_PER_DAY = 24 * 60 * 60 * 1000 const DIFFICULTY_MULTIPLIER: Record = { beginner: 1, @@ -32,124 +20,10 @@ const DIFFICULTY_MULTIPLIER: Record = { expert: 2.5, } -const seedUserFixtures: SeedUser[] = [ - { - id: 'seed-user-admin-ada', - email: 'ada.admin+seed@learnault.dev', - name: 'Ada Admin', - walletAddress: 'GD6QK3H5MYYXQUDMVDGLDZ4E2IWXQ7N5SLW7PZBLW4D5YV3V2NFH8A1A', - }, - { - id: 'seed-user-learner-alice', - email: 'alice.learner+seed@learnault.dev', - name: 'Alice Learner', - walletAddress: 'GBUQWP3BOUZX34ULNQG23RQ6F4BFSRJ4HBSNF63YLIXLQ5EDLF274Y6C', - }, - { - id: 'seed-user-learner-bob', - email: 'bob.learner+seed@learnault.dev', - name: 'Bob Learner', - walletAddress: 'GB7YLLICSMNYWJ46NWYCBTGXD54FPPWFY3YQYBFJFTQ6B2N3WHAL5V4K', - }, - { - id: 'seed-user-learner-carla', - email: 'carla.learner+seed@learnault.dev', - name: 'Carla Learner', - walletAddress: 'GA5N2IBQ2J5KVSBBR6K7D6JQ3Y7L46BP3I7WNIPXQ2XH2WQOTJXK5C2Z', - }, - { - id: 'seed-user-learner-deepak', - email: 'deepak.learner+seed@learnault.dev', - name: 'Deepak Learner', - walletAddress: 'GDVXG4D7WQ3WWT3S3Q6L2J5KLC2TSGBYHYQ4M4U7QWEK3J75VYLPQ9U2', - }, - { - id: 'seed-user-employer-acme', - email: 'acme.employer+seed@learnault.dev', - name: 'Acme Talent Team', - walletAddress: null, - }, - { - id: 'seed-user-employer-globex', - email: 'globex.employer+seed@learnault.dev', - name: 'Globex Hiring Ops', - walletAddress: null, - }, -] - -const seedModuleFixtures: SeedModule[] = [ - { - id: 'seed-module-blockchain-101', - title: 'Stellar Fundamentals', - description: 'Core ledger concepts, accounts, trustlines, and transaction flow on Stellar.', - category: 'blockchain', - difficulty: 'beginner', - reward: 10, - }, - { - id: 'seed-module-finance-101', - title: 'Understanding Stablecoins', - description: 'How fiat-backed and crypto-backed stablecoins work across global payment rails.', - category: 'finance', - difficulty: 'beginner', - reward: 12, - }, - { - id: 'seed-module-security-201', - title: 'Wallet Security & Key Management', - description: 'Threat modeling, custody approaches, and secure key handling in production systems.', - category: 'security', - difficulty: 'intermediate', - reward: 18, - }, - { - id: 'seed-module-development-301', - title: 'Build with Soroban', - description: 'Develop and test smart contracts using practical Soroban development workflows.', - category: 'development', - difficulty: 'advanced', - reward: 30, - }, - { - id: 'seed-module-compliance-201', - title: 'AML/KYC for Digital Finance', - description: 'Compliance basics, sanctions screening, and regulated onboarding for fintech teams.', - category: 'compliance', - difficulty: 'intermediate', - reward: 20, - }, - { - id: 'seed-module-identity-301', - title: 'Decentralized Identity in Practice', - description: 'Verifiable credentials, selective disclosure, and identity portability patterns.', - category: 'identity', - difficulty: 'advanced', - reward: 28, - }, - { - id: 'seed-module-development-401', - title: 'Production API Hardening', - description: 'Rate limiting, auth patterns, observability, and safe rollout practices.', - category: 'development', - difficulty: 'expert', - reward: 40, - }, - { - id: 'seed-module-blockchain-202', - title: 'Stellar Asset Issuance', - description: 'Issue and manage custom assets with issuer/distributor architecture.', - category: 'blockchain', - difficulty: 'intermediate', - reward: 22, - }, -] - function createPrng(seed: number) { let state = seed >>> 0 - return () => { state = (1664525 * state + 1013904223) >>> 0 - return state / 0xffffffff } } @@ -157,12 +31,10 @@ function createPrng(seed: number) { function scoreFor(module: SeedModule, rand: () => number) { const base = 66 + rand() * 28 const penalty = (DIFFICULTY_MULTIPLIER[module.difficulty] - 1) * 5 - return Number(Math.max(60, Math.min(99, base - penalty)).toFixed(2)) } function xlmAmount(module: SeedModule, rand: () => number) { - return Number((module.reward * (1 + rand() * 0.3)).toFixed(2)) } @@ -184,16 +56,22 @@ async function upsertUsers(passwordHash: string) { where: { id: user.id }, update: { email: user.email, - name: user.name, + username: user.username, + role: user.role, + status: user.status, + isVerified: user.isVerified, walletAddress: user.walletAddress, - passwordHash, + password: passwordHash, }, create: { id: user.id, email: user.email, - name: user.name, + username: user.username, + role: user.role, + status: user.status, + isVerified: user.isVerified, walletAddress: user.walletAddress, - passwordHash, + password: passwordHash, }, }) } From 0eb80829e419a39f0169b638206b613a004fe6f2 Mon Sep 17 00:00:00 2001 From: Opulence Chuks Date: Thu, 20 Aug 2026 17:30:02 +0100 Subject: [PATCH 2/2] fix: resolve eslint newline-before-return violations --- prisma/seed.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/prisma/seed.ts b/prisma/seed.ts index 2471450..c88f4f6 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -22,8 +22,10 @@ const DIFFICULTY_MULTIPLIER: Record = { function createPrng(seed: number) { let state = seed >>> 0 + return () => { state = (1664525 * state + 1013904223) >>> 0 + return state / 0xffffffff } } @@ -31,6 +33,7 @@ function createPrng(seed: number) { function scoreFor(module: SeedModule, rand: () => number) { const base = 66 + rand() * 28 const penalty = (DIFFICULTY_MULTIPLIER[module.difficulty] - 1) * 5 + return Number(Math.max(60, Math.min(99, base - penalty)).toFixed(2)) }