🔥 Add initial database migrations, seeders, and factories

🎨 Refactor `Lecturer` model to include new fields and factory usage
🔧 Update `DatabaseSeeder` to handle default seeds
🛠️ Enhance `einundzwanzig` database configuration for SQLite compatibility
This commit is contained in:
BT
2026-05-02 17:17:13 +01:00
parent 04abf231bd
commit cb61d9d543
54 changed files with 1975 additions and 417 deletions
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace Database\Factories;
use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Category>
*/
class CategoryFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->unique()->randomElement([
'Grundlagen',
'Self Custody',
'Lightning',
'Nostr',
'Privacy',
'Mining',
'Wirtschaft',
'Recht',
'Technik',
]),
];
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace Database\Factories;
use App\Models\City;
use App\Models\Country;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<City>
*/
class CityFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'country_id' => Country::factory(),
'name' => fake()->city(),
'latitude' => fake()->latitude(47, 55),
'longitude' => fake()->longitude(6, 16),
'osm_relation' => null,
'simplified_geojson' => null,
];
}
public function vienna(): static
{
return $this->state(fn () => [
'name' => 'Wien',
'latitude' => 48.2082,
'longitude' => 16.3738,
]);
}
public function berlin(): static
{
return $this->state(fn () => [
'name' => 'Berlin',
'latitude' => 52.5200,
'longitude' => 13.4050,
]);
}
public function munich(): static
{
return $this->state(fn () => [
'name' => 'München',
'latitude' => 48.1351,
'longitude' => 11.5820,
]);
}
public function zurich(): static
{
return $this->state(fn () => [
'name' => 'Zürich',
'latitude' => 47.3769,
'longitude' => 8.5417,
]);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace Database\Factories;
use App\Models\Country;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Country>
*/
class CountryFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->unique()->country(),
'code' => strtoupper(fake()->unique()->lexify('??')),
'language_codes' => ['de'],
];
}
public function germany(): static
{
return $this->state(fn () => [
'name' => 'Deutschland',
'code' => 'DE',
'language_codes' => ['de'],
]);
}
public function austria(): static
{
return $this->state(fn () => [
'name' => 'Österreich',
'code' => 'AT',
'language_codes' => ['de'],
]);
}
public function switzerland(): static
{
return $this->state(fn () => [
'name' => 'Schweiz',
'code' => 'CH',
'language_codes' => ['de', 'fr', 'it'],
]);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace Database\Factories;
use App\Models\Course;
use App\Models\CourseEvent;
use App\Models\Venue;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<CourseEvent>
*/
class CourseEventFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
$from = fake()->dateTimeBetween('+1 day', '+90 days');
$to = (clone $from)->modify('+2 hours');
return [
'course_id' => Course::factory(),
'venue_id' => Venue::factory(),
'from' => $from,
'to' => $to,
];
}
public function past(): static
{
return $this->state(function () {
$from = fake()->dateTimeBetween('-90 days', '-1 day');
return [
'from' => $from,
'to' => (clone $from)->modify('+2 hours'),
];
});
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use App\Models\Course;
use App\Models\Lecturer;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Course>
*/
class CourseFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
$titles = [
'Bitcoin Basics',
'Self Custody und Hardware Wallets',
'Lightning Network 101',
'Nostr Einführung',
'Bitcoin für Unternehmer',
'Privacy & Coinjoin',
'Running Your Own Node',
];
return [
'lecturer_id' => Lecturer::factory(),
'name' => fake()->randomElement($titles),
'description' => fake()->paragraphs(2, true),
'duration_minutes' => fake()->randomElement([60, 90, 120, 180]),
];
}
public function bitcoinBasics(): static
{
return $this->state(fn () => [
'name' => 'Bitcoin Basics',
'description' => 'Eine umfassende Einführung in Bitcoin: Was ist Bitcoin, wie funktioniert die Blockchain, warum ist es revolutionär. Perfekt für Einsteiger.',
'duration_minutes' => 90,
]);
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace Database\Factories;
use App\Models\Event;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Event>
*/
class EventFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
$eventId = bin2hex(random_bytes(32));
$pubkey = bin2hex(random_bytes(32));
$createdAt = fake()->dateTimeBetween('-1 year')->getTimestamp();
return [
'event_id' => $eventId,
'parent_event_id' => null,
'pubkey' => $pubkey,
'type' => fake()->randomElement(['note', 'long-form', 'reaction']),
'json' => json_encode([
'id' => $eventId,
'pubkey' => $pubkey,
'created_at' => $createdAt,
'kind' => 1,
'tags' => [],
'content' => fake()->paragraph(),
'sig' => bin2hex(random_bytes(64)),
], JSON_THROW_ON_ERROR),
];
}
public function fromMarkusTurm(): static
{
return $this->state(function () {
$eventId = bin2hex(random_bytes(32));
$pubkey = 'f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba';
$createdAt = fake()->dateTimeBetween('-30 days')->getTimestamp();
return [
'event_id' => $eventId,
'pubkey' => $pubkey,
'type' => 'note',
'json' => json_encode([
'id' => $eventId,
'pubkey' => $pubkey,
'created_at' => $createdAt,
'kind' => 1,
'tags' => [['t', 'bitcoin'], ['t', 'einundzwanzig']],
'content' => 'Bitcoin fixes this. #bitcoin #einundzwanzig',
'sig' => bin2hex(random_bytes(64)),
], JSON_THROW_ON_ERROR),
];
});
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace Database\Factories;
use App\Models\Lecturer;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Lecturer>
*/
class LecturerFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'bio' => fake()->paragraph(),
'pubkey' => bin2hex(random_bytes(32)),
'website' => fake()->url(),
'active' => true,
];
}
public function markusTurm(): static
{
return $this->state(fn () => [
'name' => 'Markus Turm',
'bio' => 'Hobby Hedge Fund Manager. Bitcoin, Austrian Economics, Laissez-Faire Radical.',
'npub' => 'npub17fqtu2mgf7zueq2kdusgzwr2lqwhgfl2scjsez77ddag2qx8vxaq3vnr8y',
'pubkey' => 'f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba',
'website' => 'https://einundzwanzig.space',
'active' => true,
]);
}
public function inactive(): static
{
return $this->state(fn () => ['active' => false]);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace Database\Factories;
use App\Models\Meetup;
use App\Models\MeetupEvent;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<MeetupEvent>
*/
class MeetupEventFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'meetup_id' => Meetup::factory(),
'start' => fake()->dateTimeBetween('+1 day', '+90 days'),
'location' => fake()->company().', '.fake()->streetAddress(),
'description' => fake()->paragraph(),
'link' => fake()->url(),
'attendees' => [
'f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba',
bin2hex(random_bytes(32)),
],
'might_attendees' => [bin2hex(random_bytes(32))],
'nostr_status' => 'Sent event '.bin2hex(random_bytes(8)).' to wss://simple-relay.codingarena.top',
];
}
public function past(): static
{
return $this->state(fn () => [
'start' => fake()->dateTimeBetween('-90 days', '-1 day'),
]);
}
public function upcoming(): static
{
return $this->state(fn () => [
'start' => fake()->dateTimeBetween('+1 day', '+30 days'),
]);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace Database\Factories;
use App\Models\City;
use App\Models\Meetup;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Meetup>
*/
class MeetupFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
$name = 'Einundzwanzig '.fake()->city();
return [
'city_id' => City::factory(),
'name' => $name,
'description' => fake()->paragraph(),
'website' => 'https://einundzwanzig.space/meetups/'.str()->slug($name),
'nostr_pubkey' => bin2hex(random_bytes(32)),
'github_data' => [
'repo' => 'einundzwanzig-portal',
'path' => 'meetups/'.str()->slug($name).'.json',
],
'simplified_geojson' => null,
];
}
public function vienna(): static
{
return $this->state(fn () => [
'name' => 'Einundzwanzig Wien',
'description' => 'Das Bitcoin-only Meetup in Wien. Jeden ersten Donnerstag im Monat.',
'website' => 'https://einundzwanzig.space/meetups/wien',
'nostr_pubkey' => 'f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba',
]);
}
public function berlin(): static
{
return $this->state(fn () => [
'name' => 'Einundzwanzig Berlin',
'description' => 'Bitcoin Meetup in der Hauptstadt. Plebs willkommen.',
'website' => 'https://einundzwanzig.space/meetups/berlin',
]);
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace Database\Factories;
use App\Models\Profile;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Profile>
*/
class ProfileFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
$name = fake()->userName();
return [
'pubkey' => bin2hex(random_bytes(32)),
'name' => $name,
'display_name' => fake()->name(),
'picture' => 'https://image.nostr.build/'.fake()->uuid().'.jpg',
'banner' => null,
'website' => fake()->url(),
'about' => fake()->sentence(12),
'nip05' => $name.'@einundzwanzig.space',
'lud16' => $name.'@walletofsatoshi.com',
'lud06' => null,
'deleted' => false,
];
}
public function markusTurm(): static
{
return $this->state(fn (array $attributes) => [
'pubkey' => 'f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba',
'name' => 'markusturm',
'display_name' => 'Markus Turm',
'picture' => 'https://m.primal.net/HQqf.jpg',
'banner' => 'https://m.primal.net/HQqg.jpg',
'website' => 'https://einundzwanzig.space',
'about' => '#Bitcoin | Austrian Economics | Laissez-Faire Radical | @_einundzwanzig_',
'nip05' => 'markusturm@einundzwanzig.space',
'lud16' => 'markusturm@walletofsatoshi.com',
'deleted' => false,
]);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Database\Factories;
use App\Models\Event;
use App\Models\RenderedEvent;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<RenderedEvent>
*/
class RenderedEventFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'event_id' => Event::factory()->create()->event_id,
'html' => '<p>'.fake()->paragraph().'</p>',
'profile_image' => 'https://m.primal.net/'.fake()->uuid().'.jpg',
'profile_name' => fake()->userName(),
];
}
}
@@ -0,0 +1,49 @@
<?php
namespace Database\Factories;
use App\Models\SecurityAttempt;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Validation\ValidationException;
/**
* @extends Factory<SecurityAttempt>
*/
class SecurityAttemptFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'ip_address' => fake()->ipv4(),
'user_agent' => fake()->userAgent(),
'method' => fake()->randomElement(['GET', 'POST']),
'url' => fake()->url(),
'route_name' => fake()->randomElement(['association.profile', 'association.elections', 'association.projectSupport']),
'exception_class' => fake()->randomElement([
ValidationException::class,
AuthenticationException::class,
AuthorizationException::class,
]),
'exception_message' => fake()->sentence(),
'component_name' => fake()->randomElement(['association.profile', 'association.election.show']),
'target_property' => fake()->randomElement(['name', 'email', 'npub']),
'payload' => ['attempt' => fake()->word()],
'severity' => fake()->randomElement(['low', 'medium', 'high', 'critical']),
];
}
public function high(): static
{
return $this->state(fn () => ['severity' => 'high']);
}
public function critical(): static
{
return $this->state(fn () => ['severity' => 'critical']);
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace Database\Factories;
use App\Models\City;
use App\Models\Venue;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Venue>
*/
class VenueFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'city_id' => City::factory(),
'name' => fake()->randomElement(['Bitcoin Bar', 'Plebsfreundlicher Coworking', 'Hodler Hangout', 'Lightning Lounge']).' '.fake()->lastName(),
'description' => fake()->paragraph(),
'address' => fake()->streetAddress(),
'website' => fake()->url(),
];
}
public function bitcoinBarVienna(): static
{
return $this->state(fn () => [
'name' => 'Bitcoin Bar Wien',
'description' => 'Die erste Bitcoin-only Bar im 7. Bezirk.',
'address' => 'Neubaugasse 21, 1070 Wien',
'website' => 'https://bitcoin-bar.at',
]);
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace Database\Factories;
use App\Models\EinundzwanzigPleb;
use App\Models\ProjectProposal;
use App\Models\Vote;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Vote>
*/
class VoteFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'einundzwanzig_pleb_id' => EinundzwanzigPleb::factory(),
'project_proposal_id' => ProjectProposal::factory(),
'value' => fake()->boolean(70),
'reason' => fake()->optional(0.3)->sentence(),
];
}
public function approve(): static
{
return $this->state(fn () => ['value' => true]);
}
public function reject(): static
{
return $this->state(fn () => ['value' => false]);
}
}
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('countries', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('code', 8)->unique();
$table->json('language_codes')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('countries');
}
};
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('cities', function (Blueprint $table) {
$table->id();
$table->foreignId('country_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('slug')->nullable()->index();
$table->decimal('latitude', 10, 7)->nullable();
$table->decimal('longitude', 10, 7)->nullable();
$table->json('osm_relation')->nullable();
$table->json('simplified_geojson')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('cities');
}
};
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('lecturers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->nullable()->index();
$table->text('bio')->nullable();
$table->string('npub', 63)->nullable()->index();
$table->string('pubkey', 64)->nullable()->index();
$table->string('website')->nullable();
$table->boolean('active')->default(true);
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('lecturers');
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('categories');
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('meetups', function (Blueprint $table) {
$table->id();
$table->foreignId('city_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('slug')->nullable()->index();
$table->text('description')->nullable();
$table->string('website')->nullable();
$table->string('nostr_pubkey', 64)->nullable();
$table->json('github_data')->nullable();
$table->json('simplified_geojson')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('meetups');
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('meetup_events', function (Blueprint $table) {
$table->id();
$table->foreignId('meetup_id')->constrained()->cascadeOnDelete();
$table->timestamp('start');
$table->string('location')->nullable();
$table->text('description')->nullable();
$table->string('link')->nullable();
$table->json('attendees')->nullable();
$table->json('might_attendees')->nullable();
$table->string('nostr_status')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('meetup_events');
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('venues', function (Blueprint $table) {
$table->id();
$table->foreignId('city_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('slug')->nullable()->index();
$table->text('description')->nullable();
$table->string('address')->nullable();
$table->string('website')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('venues');
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('courses', function (Blueprint $table) {
$table->id();
$table->foreignId('lecturer_id')->nullable()->constrained()->nullOnDelete();
$table->string('name');
$table->string('slug')->nullable()->index();
$table->text('description')->nullable();
$table->unsignedInteger('duration_minutes')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('courses');
}
};
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('course_events', function (Blueprint $table) {
$table->id();
$table->foreignId('course_id')->constrained()->cascadeOnDelete();
$table->foreignId('venue_id')->nullable()->constrained()->nullOnDelete();
$table->timestamp('from');
$table->timestamp('to')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('course_events');
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('category_course', function (Blueprint $table) {
$table->foreignId('category_id')->constrained()->cascadeOnDelete();
$table->foreignId('course_id')->constrained()->cascadeOnDelete();
$table->primary(['category_id', 'course_id']);
});
}
public function down(): void
{
Schema::dropIfExists('category_course');
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('meetup_user', function (Blueprint $table) {
$table->foreignId('meetup_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->primary(['meetup_id', 'user_id']);
});
}
public function down(): void
{
Schema::dropIfExists('meetup_user');
}
};
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace Database\Seeders;
use App\Models\Category;
use App\Models\Course;
use App\Models\CourseEvent;
use App\Models\Lecturer;
use App\Models\Venue;
use Illuminate\Database\Seeder;
class CourseSeeder extends Seeder
{
public function run(): void
{
$categories = collect([
'Grundlagen',
'Self Custody',
'Lightning',
'Nostr',
'Privacy',
'Wirtschaft',
])->map(fn (string $name) => Category::query()->create(['name' => $name]));
$markus = Lecturer::factory()->markusTurm()->create();
$otherLecturers = Lecturer::factory()->count(3)->create();
$bitcoinBasics = Course::factory()->bitcoinBasics()->for($markus)->create();
$bitcoinBasics->categories()->attach([
$categories->firstWhere('name', 'Grundlagen')->id,
$categories->firstWhere('name', 'Wirtschaft')->id,
]);
$lightningCourse = Course::factory()
->state(['name' => 'Lightning Network 101'])
->for($markus)
->create();
$lightningCourse->categories()->attach($categories->firstWhere('name', 'Lightning')->id);
foreach ($otherLecturers as $lecturer) {
$course = Course::factory()->for($lecturer)->create();
$course->categories()->attach($categories->random(rand(1, 3))->pluck('id'));
}
$venues = Venue::query()->take(3)->get();
if ($venues->isEmpty()) {
return;
}
foreach (Course::query()->get() as $course) {
CourseEvent::factory()->for($course)->for($venues->random())->past()->create();
CourseEvent::factory()->for($course)->for($venues->random())->create();
}
}
}
+13 -8
View File
@@ -3,21 +3,26 @@
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::query()->updateOrCreate(
['email' => 'test@example.com'],
['name' => 'Test User', 'password' => bcrypt('password')]
);
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
$this->call([
PlebSeeder::class,
ProjectProposalSeeder::class,
ElectionSeeder::class,
NostrSeeder::class,
MeetupSeeder::class,
CourseSeeder::class,
NotificationSeeder::class,
SecuritySeeder::class,
]);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Database\Seeders;
use App\Models\Election;
use Illuminate\Database\Seeder;
class ElectionSeeder extends Seeder
{
public function run(): void
{
$candidates = config('einundzwanzig.config.current_board');
Election::query()->updateOrCreate(
['year' => 2025],
[
'candidates' => $candidates,
'end_time' => now()->setDate(2025, 4, 15),
]
);
Election::query()->updateOrCreate(
['year' => 2026],
[
'candidates' => $candidates,
'end_time' => now()->addMonths(2),
]
);
Election::query()->updateOrCreate(
['year' => 2027],
[
'candidates' => [],
'end_time' => now()->addYear()->addMonths(3),
]
);
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace Database\Seeders;
use App\Models\City;
use App\Models\Country;
use App\Models\Meetup;
use App\Models\MeetupEvent;
use App\Models\Venue;
use Illuminate\Database\Seeder;
class MeetupSeeder extends Seeder
{
public function run(): void
{
$de = Country::factory()->germany()->create();
$at = Country::factory()->austria()->create();
$ch = Country::factory()->switzerland()->create();
$vienna = City::factory()->vienna()->for($at)->create();
$berlin = City::factory()->berlin()->for($de)->create();
$munich = City::factory()->munich()->for($de)->create();
$zurich = City::factory()->zurich()->for($ch)->create();
$viennaMeetup = Meetup::factory()->vienna()->for($vienna)->create();
$berlinMeetup = Meetup::factory()->berlin()->for($berlin)->create();
$munichMeetup = Meetup::factory()->state(['name' => 'Einundzwanzig München'])->for($munich)->create();
$zurichMeetup = Meetup::factory()->state(['name' => 'Einundzwanzig Zürich'])->for($zurich)->create();
Venue::factory()->bitcoinBarVienna()->for($vienna)->create();
Venue::factory()->count(2)->for($berlin)->create();
Venue::factory()->count(2)->for($munich)->create();
Venue::factory()->count(1)->for($zurich)->create();
foreach ([$viennaMeetup, $berlinMeetup, $munichMeetup, $zurichMeetup] as $meetup) {
MeetupEvent::factory()->past()->for($meetup)->count(2)->create();
MeetupEvent::factory()->upcoming()->for($meetup)->count(2)->create();
}
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace Database\Seeders;
use App\Models\Event;
use App\Models\Profile;
use App\Models\RenderedEvent;
use Illuminate\Database\Seeder;
class NostrSeeder extends Seeder
{
public function run(): void
{
Profile::factory()->markusTurm()->create();
$boardProfiles = [
[
'pubkey' => '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
'name' => 'pleb1',
'display_name' => 'Vorstandsmitglied 1',
],
[
'pubkey' => '430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
'name' => 'pleb2',
'display_name' => 'Vorstandsmitglied 2',
],
[
'pubkey' => '7acf30cf60b85c62b8f654556cc21e4016df8f5604b3b6892794f88bb80d7a1d',
'name' => 'pleb3',
'display_name' => 'Vorstandsmitglied 3',
],
[
'pubkey' => '19e358b8011f5f4fc653c565c6d4c2f33f32661f4f90982c9eedc292a8774ec3',
'name' => 'pleb4',
'display_name' => 'Vorstandsmitglied 4',
],
];
foreach ($boardProfiles as $data) {
Profile::query()->create([
...$data,
'about' => 'Vorstand bei Einundzwanzig. Bitcoin only.',
'nip05' => $data['name'].'@einundzwanzig.space',
'lud16' => $data['name'].'@walletofsatoshi.com',
'website' => 'https://einundzwanzig.space',
'picture' => 'https://m.primal.net/'.fake()->uuid().'.jpg',
'deleted' => false,
]);
}
Event::factory()
->fromMarkusTurm()
->count(5)
->create()
->each(function (Event $event): void {
RenderedEvent::query()->create([
'event_id' => $event->event_id,
'html' => '<div class="prose"><p>'.fake()->paragraph().'</p></div>',
'profile_image' => 'https://m.primal.net/HQqf.jpg',
'profile_name' => 'markusturm',
]);
});
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace Database\Seeders;
use App\Enums\NewsCategory;
use App\Models\EinundzwanzigPleb;
use App\Models\Notification;
use Illuminate\Database\Seeder;
class NotificationSeeder extends Seeder
{
public function run(): void
{
$markus = EinundzwanzigPleb::query()
->where('npub', 'npub17fqtu2mgf7zueq2kdusgzwr2lqwhgfl2scjsez77ddag2qx8vxaq3vnr8y')
->first() ?? EinundzwanzigPleb::query()->first();
if (! $markus) {
return;
}
$news = [
[
'name' => 'Generalversammlung 2026 - Save the Date',
'description' => "Die nächste Generalversammlung findet am 21. Juni 2026 in Wien statt. Alle Mitglieder sind herzlich eingeladen.\n\nAgenda:\n- Bericht des Vorstands\n- Wahl des neuen Vorstands\n- Project Support Abstimmungen",
'category' => NewsCategory::Veranstaltungen,
],
[
'name' => 'Neuer Lightning Watchtower verfügbar',
'description' => 'Mitglieder können ab sofort unseren Watchtower nutzen. Details zur Konfiguration im Mitgliederbereich.',
'category' => NewsCategory::Bitcoin,
],
[
'name' => 'Meetup-Welle im Sommer 2026',
'description' => 'Über 30 Einundzwanzig Meetups im DACH-Raum geplant. Termine im Portal.',
'category' => NewsCategory::Meetups,
],
[
'name' => 'Q1 2026 Finanzbericht',
'description' => 'Der Finanzbericht für das erste Quartal 2026 ist im Mitgliederbereich abrufbar.',
'category' => NewsCategory::Finanzen,
],
[
'name' => 'Neue Bildungsinitiative gestartet',
'description' => 'Die Einundzwanzig Bitcoin Schule startet im September. Anmeldung ab sofort möglich.',
'category' => NewsCategory::Bildung,
],
];
foreach ($news as $item) {
Notification::query()->create([
...$item,
'einundzwanzig_pleb_id' => $markus->id,
]);
}
}
}
+111
View File
@@ -0,0 +1,111 @@
<?php
namespace Database\Seeders;
use App\Enums\AssociationStatus;
use App\Models\EinundzwanzigPleb;
use App\Models\PaymentEvent;
use Illuminate\Database\Seeder;
class PlebSeeder extends Seeder
{
/**
* @var array<int, array{npub:string, pubkey:string, email:string, nip05:string, status:AssociationStatus, paid_years:array<int>}>
*/
private array $boardMembers = [
[
'npub' => 'npub17fqtu2mgf7zueq2kdusgzwr2lqwhgfl2scjsez77ddag2qx8vxaq3vnr8y',
'pubkey' => 'f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba',
'email' => 'markus@einundzwanzig.space',
'nip05' => 'markusturm',
'status' => AssociationStatus::HONORARY,
'paid_years' => [2024, 2025, 2026],
'name' => 'Markus Turm',
],
[
'npub' => 'npub1pt0kw36ue3w2g4haxq3wgm6a2fhtptmzsjlc2j2vphtcgle72qesgpjyc6',
'pubkey' => '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033',
'email' => 'board1@einundzwanzig.space',
'nip05' => 'pleb1',
'status' => AssociationStatus::HONORARY,
'paid_years' => [2024, 2025, 2026],
'name' => 'Vorstand 1',
],
[
'npub' => 'npub1gvqkjccl9urg93svaw60jqkk3ux8r3ycl5t3rlvc9uzjeu0agfuss8x8qy',
'pubkey' => '430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279',
'email' => 'board2@einundzwanzig.space',
'nip05' => 'pleb2',
'status' => AssociationStatus::HONORARY,
'paid_years' => [2025, 2026],
'name' => 'Vorstand 2',
],
[
'npub' => 'npub10t8npnmqhpwx9w8k232kess7gqtdlr6kqjemdzf8jnughwqd0gwsez0924',
'pubkey' => '7acf30cf60b85c62b8f654556cc21e4016df8f5604b3b6892794f88bb80d7a1d',
'email' => 'board3@einundzwanzig.space',
'nip05' => 'pleb3',
'status' => AssociationStatus::HONORARY,
'paid_years' => [2025, 2026],
'name' => 'Vorstand 3',
],
[
'npub' => 'npub1r8343wqpra05l3jnc4jud4xz7vlnyeslf7gfsty7ahpf92rhfmpsmqwym8',
'pubkey' => '19e358b8011f5f4fc653c565c6d4c2f33f32661f4f90982c9eedc292a8774ec3',
'email' => 'board4@einundzwanzig.space',
'nip05' => 'pleb4',
'status' => AssociationStatus::HONORARY,
'paid_years' => [2026],
'name' => 'Vorstand 4',
],
];
public function run(): void
{
foreach ($this->boardMembers as $member) {
$pleb = EinundzwanzigPleb::query()->create([
'npub' => $member['npub'],
'pubkey' => $member['pubkey'],
'email' => $member['email'],
'nip05_handle' => $member['nip05'],
'association_status' => $member['status'],
'application_text' => 'Ich bin Teil des Einundzwanzig Vorstands und unterstütze die Mission, Bitcoin in den deutschsprachigen Raum zu bringen.',
]);
foreach ($member['paid_years'] as $year) {
PaymentEvent::query()->create([
'einundzwanzig_pleb_id' => $pleb->id,
'year' => $year,
'amount' => 21000,
'paid' => true,
'event_id' => 'seed_'.bin2hex(random_bytes(16)),
]);
}
}
EinundzwanzigPleb::factory()
->count(8)
->active()
->create()
->each(function (EinundzwanzigPleb $pleb): void {
PaymentEvent::factory()
->paid()
->withYear((int) date('Y'))
->for($pleb, 'pleb')
->create();
});
EinundzwanzigPleb::factory()
->count(5)
->state(['association_status' => AssociationStatus::PASSIVE])
->create();
EinundzwanzigPleb::factory()
->count(3)
->state([
'association_status' => AssociationStatus::DEFAULT,
'application_text' => 'Ich möchte Mitglied bei Einundzwanzig werden und die Bitcoin-Community im deutschsprachigen Raum mitgestalten.',
])
->create();
}
}
@@ -0,0 +1,91 @@
<?php
namespace Database\Seeders;
use App\Models\EinundzwanzigPleb;
use App\Models\ProjectProposal;
use App\Models\Vote;
use Illuminate\Database\Seeder;
class ProjectProposalSeeder extends Seeder
{
/**
* @var array<int, array{name:string, description:string, support_in_sats:int, website:string, accepted:bool}>
*/
private array $proposals = [
[
'name' => 'Einundzwanzig Portal Refactoring',
'description' => "Das Einundzwanzig Portal benötigt ein größeres Refactoring, um die Performance zu verbessern und neue Features wie Meetup-Anmeldung über Nostr DMs zu ermöglichen.\n\n**Geplante Änderungen:**\n- Migration auf Livewire 4\n- Nostr Login per NIP-46\n- Meetup Kalender als ICS-Feed\n- API für externe Tools",
'support_in_sats' => 2_100_000,
'website' => 'https://github.com/einundzwanzig-portal/einundzwanzig-portal',
'accepted' => true,
],
[
'name' => 'Bitcoin Schule für Plebs',
'description' => "Curriculum für eine deutschsprachige Bitcoin-Schule mit modular aufgebauten Kursen für Einsteiger und Fortgeschrittene.\n\n- Onboarding für totale Beginner\n- Self Custody Workshops\n- Lightning Network Hands-on\n- Privacy & Coinjoin Module",
'support_in_sats' => 1_500_000,
'website' => 'https://einundzwanzig.school',
'accepted' => true,
],
[
'name' => 'Lightning Watchtower für Mitglieder',
'description' => 'Hosting eines Lightning Watchtower Service exklusiv für Einundzwanzig Mitglieder, um Channel-Verlust durch böswillige Counterparties zu verhindern.',
'support_in_sats' => 500_000,
'website' => 'https://einundzwanzig.space/benefits',
'accepted' => false,
],
[
'name' => 'Nostr Relay Hosting',
'description' => 'Betrieb eines schnellen Nostr Relay (`wss://simple-relay.codingarena.top`) für die Einundzwanzig Community. Optimiert für deutschsprachige Inhalte und Meetup-Events.',
'support_in_sats' => 800_000,
'website' => 'https://simple-relay.codingarena.top',
'accepted' => true,
],
[
'name' => 'Meetup Sticker Druck Q3 2026',
'description' => 'Bestellung von 5000 Einundzwanzig Stickern für die Meetups im DACH-Raum. Verteilung über die lokalen Meetup Organisatoren.',
'support_in_sats' => 210_000,
'website' => 'https://einundzwanzig.space',
'accepted' => false,
],
];
public function run(): void
{
$plebs = EinundzwanzigPleb::query()->get();
if ($plebs->isEmpty()) {
return;
}
$markus = EinundzwanzigPleb::query()
->where('npub', 'npub17fqtu2mgf7zueq2kdusgzwr2lqwhgfl2scjsez77ddag2qx8vxaq3vnr8y')
->first();
foreach ($this->proposals as $index => $data) {
$pleb = $markus && $index < 2 ? $markus : $plebs->random();
$proposal = ProjectProposal::query()->create([
'einundzwanzig_pleb_id' => $pleb->id,
'name' => $data['name'],
'description' => $data['description'],
'support_in_sats' => $data['support_in_sats'],
'website' => $data['website'],
'accepted' => $data['accepted'],
'sats_paid' => $data['accepted'] ? $data['support_in_sats'] : null,
]);
foreach ($plebs->random(min($plebs->count(), 6)) as $voter) {
Vote::query()->updateOrCreate(
[
'einundzwanzig_pleb_id' => $voter->id,
'project_proposal_id' => $proposal->id,
],
[
'value' => fake()->boolean(70),
'reason' => fake()->optional(0.4)->sentence(),
]
);
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Database\Seeders;
use App\Models\SecurityAttempt;
use Illuminate\Database\Seeder;
class SecuritySeeder extends Seeder
{
public function run(): void
{
SecurityAttempt::factory()->count(15)->create();
SecurityAttempt::factory()->high()->count(4)->create();
SecurityAttempt::factory()->critical()->count(1)->create();
}
}