Feat: Moving Migrations from boboko to core
This commit is contained in:
@@ -101,7 +101,7 @@ Add the repository to your project's `composer.json`:
|
|||||||
Then run:
|
Then run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
composer require radical-elements/core
|
composer require boboko/core
|
||||||
php artisan vendor:publish --tag=core-assets
|
php artisan vendor:publish --tag=core-assets
|
||||||
php artisan migrate
|
php artisan migrate
|
||||||
```
|
```
|
||||||
|
|||||||
+36
-15
@@ -1,18 +1,39 @@
|
|||||||
{
|
{
|
||||||
"name": "boboko/core",
|
"name": "boboko/core",
|
||||||
"description": "Core module — authentication and shared panel behaviour",
|
"description": "Core module — authentication and shared panel behaviour",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Modules\\Core\\": "src/"
|
"Modules\\Core\\": "src/"
|
||||||
}
|
|
||||||
},
|
|
||||||
"extra": {
|
|
||||||
"laravel": {
|
|
||||||
"providers": [
|
|
||||||
"Modules\\Core\\Providers\\CoreServiceProvider"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.5",
|
||||||
|
"lunarphp/lunar": "1.3.0",
|
||||||
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/tinker": "^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/envoy": "^2.12",
|
||||||
|
"laravel/pail": "^1.2.5",
|
||||||
|
"laravel/pint": "^1.27",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"pestphp/pest": "^4.6",
|
||||||
|
"pestphp/pest-plugin-laravel": "^4.1"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Modules\\Core\\Providers\\CoreServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('users', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->unique();
|
||||||
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
|
$table->string('password');
|
||||||
|
$table->rememberToken();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('email')->primary();
|
||||||
|
$table->string('token');
|
||||||
|
$table->timestamp('created_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('sessions', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
Schema::dropIfExists('password_reset_tokens');
|
||||||
|
Schema::dropIfExists('sessions');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->mediumText('value');
|
||||||
|
$table->bigInteger('expiration')->index();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->string('owner');
|
||||||
|
$table->bigInteger('expiration')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('cache');
|
||||||
|
Schema::dropIfExists('cache_locks');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('queue')->index();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->unsignedTinyInteger('attempts');
|
||||||
|
$table->unsignedInteger('reserved_at')->nullable();
|
||||||
|
$table->unsignedInteger('available_at');
|
||||||
|
$table->unsignedInteger('created_at');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('job_batches', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->string('name');
|
||||||
|
$table->integer('total_jobs');
|
||||||
|
$table->integer('pending_jobs');
|
||||||
|
$table->integer('failed_jobs');
|
||||||
|
$table->longText('failed_job_ids');
|
||||||
|
$table->mediumText('options')->nullable();
|
||||||
|
$table->integer('cancelled_at')->nullable();
|
||||||
|
$table->integer('created_at');
|
||||||
|
$table->integer('finished_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('uuid')->unique();
|
||||||
|
$table->text('connection');
|
||||||
|
$table->text('queue');
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->longText('exception');
|
||||||
|
$table->timestamp('failed_at')->useCurrent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('jobs');
|
||||||
|
Schema::dropIfExists('job_batches');
|
||||||
|
Schema::dropIfExists('failed_jobs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$old = \Lunar\Admin\Models\Staff::class;
|
||||||
|
$new = 'staff';
|
||||||
|
|
||||||
|
DB::table('model_has_roles')
|
||||||
|
->where('model_type', $old)
|
||||||
|
->update(['model_type' => $new]);
|
||||||
|
|
||||||
|
DB::table('model_has_permissions')
|
||||||
|
->where('model_type', $old)
|
||||||
|
->update(['model_type' => $new]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$old = \Lunar\Admin\Models\Staff::class;
|
||||||
|
$new = 'staff';
|
||||||
|
|
||||||
|
DB::table('model_has_roles')
|
||||||
|
->where('model_type', $new)
|
||||||
|
->update(['model_type' => $old]);
|
||||||
|
|
||||||
|
DB::table('model_has_permissions')
|
||||||
|
->where('model_type', $new)
|
||||||
|
->update(['model_type' => $old]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Notifications
|
||||||
|
|
||||||
|
Notifications are driven by `NotificationRegistry` in `Modules\Core\Notification`. Each notification class declares what event it listens to and who to notify, and the registry wires up the event listener automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Async delivery
|
||||||
|
|
||||||
|
All notification classes extend `BaseNotification`, which implements `ShouldQueue` and uses the `Queueable` trait. This means **every notification is delivered asynchronously** via the queue — the HTTP request returns immediately and the email is sent by a queue worker.
|
||||||
|
|
||||||
|
In local development or environments without a queue worker, set `QUEUE_CONNECTION=sync` in `.env` to send emails inline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key naming convention
|
||||||
|
|
||||||
|
Notification keys follow the pattern `module.event.refersTo.via`:
|
||||||
|
|
||||||
|
| Segment | Description | Example |
|
||||||
|
|---|---|---|
|
||||||
|
| `module` | The module the notification belongs to | `appointment` |
|
||||||
|
| `event` | The event that triggers it | `booked`, `cancelled`, `rescheduled` |
|
||||||
|
| `refersTo` | Who the notification is about | `customer`, `staff` |
|
||||||
|
| `via` | The delivery channel | `mail`, `slack` |
|
||||||
|
|
||||||
|
Example: `appointment.cancelled.customer.mail`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Built-in notifications
|
||||||
|
|
||||||
|
| Key | Class | Event | Channel |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `appointment.booked.customer.mail` | `AppointmentBookedCustomerNotification` | `AppointmentBooked` | mail |
|
||||||
|
| `appointment.booked.staff.mail` | `AppointmentBookedStaffNotification` | `AppointmentBooked` | mail |
|
||||||
|
| `appointment.cancelled.customer.mail` | `AppointmentCancelledCustomerNotification` | `AppointmentCancelled` | mail |
|
||||||
|
| `appointment.rescheduled.customer.mail` | `AppointmentRescheduledCustomerNotification` | `AppointmentRescheduled` | mail |
|
||||||
|
|
||||||
|
These are registered in `AppointmentsServiceProvider::boot()`.
|
||||||
|
|
||||||
|
### When AppointmentBooked fires
|
||||||
|
|
||||||
|
`AppointmentBooked` is **not** fired directly from `book()` or `reassign()`. It is fired by `CreateGoogleMeetLinkListener` after the Meet link has been created (or attempted). This means notifications always go out with the Meet link already in the event — if the link couldn't be created, `meetUrl` is `null` and the email templates handle that gracefully.
|
||||||
|
|
||||||
|
### Resend behaviour
|
||||||
|
|
||||||
|
`AppointmentBooked` carries an `isResend: bool` flag (default `false`). When `true`:
|
||||||
|
- `AppointmentBookedStaffNotification` returns an empty `via()` — no staff email is sent
|
||||||
|
- `N8nWebhookListener` short-circuits — no webhook is fired
|
||||||
|
|
||||||
|
This flag is set by `AppointmentService::resendConfirmation()` so only the customer notification goes out on a manual resend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating a notification
|
||||||
|
|
||||||
|
Extend `BaseNotification`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Illuminate\Support\Facades\Notification as NotificationFacade;
|
||||||
|
use Modules\Core\Notification\BaseNotification;
|
||||||
|
|
||||||
|
class AppointmentBookedAdminNotification extends BaseNotification
|
||||||
|
{
|
||||||
|
public function __construct(private readonly AppointmentBooked $event) {}
|
||||||
|
|
||||||
|
public static function getKey(): string
|
||||||
|
{
|
||||||
|
return 'appointment.booked.admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function listensTo(): string
|
||||||
|
{
|
||||||
|
return AppointmentBooked::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return ['mail'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notifiable(): \Illuminate\Notifications\AnonymousNotifiable
|
||||||
|
{
|
||||||
|
return NotificationFacade::route('mail', 'admin@example.com');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then register it in your service provider:
|
||||||
|
|
||||||
|
```php
|
||||||
|
NotificationRegistry::get()->register([AppointmentBookedAdminNotification::class]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notifiable: anonymous vs model
|
||||||
|
|
||||||
|
Return an `AnonymousNotifiable` (via `Notification::route()`) when there is no Eloquent model involved — e.g. a fixed email address or Slack webhook pulled from the event or config.
|
||||||
|
|
||||||
|
Return an Eloquent model when you have one. The model must implement `routeNotificationFor` for the relevant channel (Laravel does this automatically for `mail` via the `Notifiable` trait).
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function notifiable(): User
|
||||||
|
{
|
||||||
|
return User::find($this->event->userId);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Replacing a built-in notification
|
||||||
|
|
||||||
|
Register a new class with the same key. The registry replaces the previous class under that key, so the existing event listener will dispatch the new class instead.
|
||||||
|
|
||||||
|
```php
|
||||||
|
NotificationRegistry::get()->register([MyCustomMailNotification::class]); // getKey() returns 'appointment.booked.customer.mail'
|
||||||
|
```
|
||||||
|
|
||||||
|
If overriding a module notification from `AppServiceProvider`, do it inside `booted()` to ensure module providers have already run:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->app->booted(function () {
|
||||||
|
NotificationRegistry::get()->register([MyCustomSlackNotification::class]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Removing a notification
|
||||||
|
|
||||||
|
```php
|
||||||
|
NotificationRegistry::get()->unregister('appointment.booked.customer.mail');
|
||||||
|
```
|
||||||
|
|
||||||
|
The event listener remains registered but short-circuits without dispatching anything.
|
||||||
|
|
||||||
|
Again, call this inside `booted()` when doing it from `AppServiceProvider`.
|
||||||
Reference in New Issue
Block a user