# Ultra Message Package — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build `promoseven/ultra-message`, a reusable Laravel package that wraps the UltraMSG WhatsApp API, then integrate it into OperationModule with a database-backed Settings UI and per-event notification classes. **Architecture:** Three phases. Phase 1 builds the standalone Composer package (separate directory/repo). Phase 2 installs it into OperationModule and wires up the Settings UI + dynamic config. Phase 3 wires individual Laravel Notification classes to ERP events. **Tech Stack:** PHP 8.2, Laravel 12, UltraMSG REST API, Orchestra Testbench 10 (package tests), PHPUnit 11, SQLite (OperationModule DB), Tailwind CSS + Alpine.js (Settings UI) --- ## File Map ### Phase 1 — Package (new directory: `../ultra-message/` — sibling to OperationModule) | Action | Path | Responsibility | |--------|------|----------------| | Create | `composer.json` | Package metadata, autoloading, Laravel discovery | | Create | `config/ultra-message.php` | Default config values | | Create | `src/UltraMessageServiceProvider.php` | Register client singleton, load config/routes | | Create | `src/UltraMessageClient.php` | All HTTP calls to UltraMSG API | | Create | `src/UltraMessageMessage.php` | Fluent DTO for outbound messages | | Create | `src/UltraMessageChannel.php` | Laravel Notification channel | | Create | `src/UltraMessageFake.php` | Test double — records sends, no HTTP | | Create | `src/UltraMessageException.php` | Single exception type for all API errors | | Create | `src/Facades/UltraMessage.php` | Laravel Facade → UltraMessageClient | | Create | `src/Events/UltraMessageWebhookReceived.php` | Event fired on incoming webhook | | Create | `src/Http/Controllers/WebhookController.php` | Handles POST to webhook route | | Create | `routes/webhook.php` | Registers webhook POST route | | Create | `tests/UltraMessageClientTest.php` | Unit tests for the client | | Create | `tests/UltraMessageChannelTest.php` | Unit tests for the channel | | Create | `tests/TestCase.php` | Orchestra Testbench base test case | ### Phase 2 — OperationModule Integration | Action | Path | Responsibility | |--------|------|----------------| | Modify | `composer.json` | Add VCS repository + require package | | Create | `database/migrations/xxxx_create_settings_table.php` | `settings` key/value table | | Create | `database/migrations/xxxx_add_whatsapp_number_to_suppliers.php` | `whatsapp_number` column | | Create | `database/migrations/xxxx_add_whatsapp_number_to_customers.php` | `whatsapp_number` column | | Create | `database/migrations/xxxx_add_whatsapp_number_to_users.php` | `whatsapp_number` column | | Create | `app/Models/Setting.php` | Key/value model with static get/set helpers | | Modify | `app/Models/Supplier.php` | Add `routeNotificationFor('ultra_message')` | | Modify | `app/Models/Customer.php` | Add `routeNotificationFor('ultra_message')` | | Modify | `app/Models/User.php` | Add `routeNotificationFor('ultra_message')` | | Modify | `app/Providers/AppServiceProvider.php` | Boot dynamic config resolver | | Modify | `bootstrap/app.php` | Exclude webhook path from CSRF | | Create | `app/Http/Controllers/SettingsController.php` | Show/update integration settings | | Create | `resources/views/settings/integrations.blade.php` | WhatsApp settings form | | Modify | `routes/web.php` | Add settings routes | | Modify | `resources/views/layouts/navigation.blade.php` | Add Settings sidebar link (Admin only) | | Modify | `resources/views/purchase/suppliers/create.blade.php` | Add whatsapp_number field | | Modify | `resources/views/purchase/suppliers/edit.blade.php` | Add whatsapp_number field | | Modify | `resources/views/sales/customers/create.blade.php` | Add whatsapp_number field | | Modify | `resources/views/sales/customers/edit.blade.php` | Add whatsapp_number field | | Modify | `app/Http/Controllers/Purchase/SupplierController.php` | Store/update whatsapp_number | | Modify | `app/Http/Controllers/Sales/CustomerController.php` | Store/update whatsapp_number | ### Phase 3 — Notifications | Action | Path | Responsibility | |--------|------|----------------| | Create | `app/Notifications/Purchase/PurchaseOrderConfirmedNotification.php` | Notify supplier on PO confirm | | Create | `app/Notifications/Purchase/GoodsReceiptConfirmedNotification.php` | Notify store manager on GRN confirm | | Create | `app/Notifications/Sales/SalesOrderConfirmedNotification.php` | Notify customer on SO confirm | | Create | `app/Notifications/Sales/InvoiceCreatedNotification.php` | Notify customer on invoice creation | | Create | `app/Notifications/Sales/DeliveryDispatchedNotification.php` | Notify customer on dispatch | | Create | `app/Notifications/Inventory/LowStockAlertNotification.php` | Notify store manager on low stock | | Create | `app/Notifications/Production/ProductionOrderCompletedNotification.php` | Notify production manager on completion | | Modify | `app/Http/Controllers/Purchase/PurchaseOrderController.php` | Trigger PO confirmed notification | | Modify | `app/Http/Controllers/Purchase/GoodsReceiptNoteController.php` | Trigger GRN confirmed notification | | Modify | `app/Http/Controllers/Sales/SalesOrderController.php` | Trigger SO confirmed notification | | Modify | `app/Http/Controllers/Sales/SalesInvoiceController.php` | Trigger invoice created notification | | Modify | `app/Http/Controllers/Sales/DeliveryNoteController.php` | Trigger dispatch notification | | Modify | `app/Http/Controllers/Inventory/StockMovementController.php` | Trigger low stock check + notification | | Modify | `app/Http/Controllers/Production/ProductionOrderController.php` | Trigger production complete notification | --- ## PHASE 1 — The Package > Work in a new directory: create `ultra-message/` as a sibling to `OperationModule/` (e.g. `C:\Users\IT Department\Desktop\ultra-message\`). --- ### Task 1: Package scaffold — composer.json and directory structure **Files:** - Create: `ultra-message/composer.json` - Create: `ultra-message/src/` (empty dir placeholder) - Create: `ultra-message/config/ultra-message.php` - Create: `ultra-message/routes/webhook.php` - Create: `ultra-message/tests/TestCase.php` - [ ] **Step 1: Create the package directory and composer.json** ```bash mkdir -p ultra-message/src/Facades ultra-message/src/Events ultra-message/src/Http/Controllers ultra-message/config ultra-message/routes ultra-message/tests ``` Create `ultra-message/composer.json`: ```json { "name": "promoseven/ultra-message", "description": "Laravel WhatsApp integration via UltraMSG API", "type": "library", "license": "MIT", "require": { "php": "^8.2", "illuminate/support": "^11.0|^12.0", "illuminate/http": "^11.0|^12.0", "illuminate/notifications": "^11.0|^12.0", "illuminate/routing": "^11.0|^12.0" }, "require-dev": { "phpunit/phpunit": "^11.0", "orchestra/testbench": "^10.0" }, "autoload": { "psr-4": { "PromoSeven\\UltraMessage\\": "src/" } }, "autoload-dev": { "psr-4": { "PromoSeven\\UltraMessage\\Tests\\": "tests/" } }, "extra": { "laravel": { "providers": [ "PromoSeven\\UltraMessage\\UltraMessageServiceProvider" ], "aliases": { "UltraMessage": "PromoSeven\\UltraMessage\\Facades\\UltraMessage" } } }, "minimum-stability": "stable", "prefer-stable": true } ``` - [ ] **Step 2: Create the default config file** Create `ultra-message/config/ultra-message.php`: ```php env('ULTRAMSG_INSTANCE_ID'), 'token' => env('ULTRAMSG_TOKEN'), 'webhook_secret' => env('ULTRAMSG_WEBHOOK_SECRET', null), 'webhook_path' => env('ULTRAMSG_WEBHOOK_PATH', 'ultra-message/webhook'), 'timeout' => env('ULTRAMSG_TIMEOUT', 30), 'enabled' => env('ULTRAMSG_ENABLED', true), ]; ``` - [ ] **Step 3: Create the Orchestra Testbench base test case** Create `ultra-message/tests/TestCase.php`: ```php set('ultra-message.instance_id', 'instance123'); $app['config']->set('ultra-message.token', 'test-token'); $app['config']->set('ultra-message.enabled', true); } } ``` - [ ] **Step 4: Run composer install** ```bash cd ultra-message && composer install ``` Expected: Vendor directory created, no errors. - [ ] **Step 5: Commit** ```bash cd ultra-message git init git add . git commit -m "feat: scaffold ultra-message package" ``` --- ### Task 2: UltraMessageException **Files:** - Create: `ultra-message/src/UltraMessageException.php` - [ ] **Step 1: Create the exception class** Create `ultra-message/src/UltraMessageException.php`: ```php client = new UltraMessageClient([ 'instance_id' => 'instance123', 'token' => 'test-token', 'timeout' => 30, 'enabled' => true, ]); } public function test_send_text_posts_correct_payload(): void { Http::fake([ 'api.ultramsg.com/*' => Http::response(['sent' => 'true', 'id' => 'msg1'], 200), ]); $result = $this->client->sendText('+971501234567', 'Hello World'); Http::assertSent(function ($request) { return str_contains($request->url(), 'instance123/messages/chat') && $request['to'] === '+971501234567' && $request['body'] === 'Hello World' && $request['token'] === 'test-token'; }); $this->assertEquals(['sent' => 'true', 'id' => 'msg1'], $result); } public function test_send_text_throws_on_api_error(): void { Http::fake([ 'api.ultramsg.com/*' => Http::response(['error' => 'invalid token'], 200), ]); $this->expectException(UltraMessageException::class); $this->expectExceptionMessage('invalid token'); $this->client->sendText('+971501234567', 'Hello'); } public function test_send_text_throws_on_http_failure(): void { Http::fake([ 'api.ultramsg.com/*' => Http::response([], 500), ]); $this->expectException(UltraMessageException::class); $this->client->sendText('+971501234567', 'Hello'); } public function test_send_returns_early_when_disabled(): void { Http::fake(); $client = new UltraMessageClient([ 'instance_id' => 'instance123', 'token' => 'test-token', 'timeout' => 30, 'enabled' => false, ]); $result = $client->sendText('+971501234567', 'Hello'); Http::assertNothingSent(); $this->assertEquals([], $result); } public function test_send_image_posts_correct_payload(): void { Http::fake([ 'api.ultramsg.com/*' => Http::response(['sent' => 'true'], 200), ]); $this->client->sendImage('+971501234567', 'https://example.com/img.jpg', 'Caption'); Http::assertSent(function ($request) { return str_contains($request->url(), 'instance123/messages/image') && $request['image'] === 'https://example.com/img.jpg' && $request['caption'] === 'Caption'; }); } public function test_send_document_posts_correct_payload(): void { Http::fake([ 'api.ultramsg.com/*' => Http::response(['sent' => 'true'], 200), ]); $this->client->sendDocument('+971501234567', 'https://example.com/file.pdf', 'invoice.pdf', 'Your invoice'); Http::assertSent(function ($request) { return str_contains($request->url(), 'instance123/messages/document') && $request['document'] === 'https://example.com/file.pdf' && $request['filename'] === 'invoice.pdf' && $request['caption'] === 'Your invoice'; }); } public function test_send_location_posts_correct_payload(): void { Http::fake([ 'api.ultramsg.com/*' => Http::response(['sent' => 'true'], 200), ]); $this->client->sendLocation('+971501234567', 25.197197, 55.2721877, 'Dubai, UAE'); Http::assertSent(function ($request) { return str_contains($request->url(), 'instance123/messages/location') && $request['lat'] == 25.197197 && $request['lng'] == 55.2721877 && $request['address'] === 'Dubai, UAE'; }); } } ``` - [ ] **Step 2: Run tests to confirm they fail** ```bash cd ultra-message && ./vendor/bin/phpunit tests/UltraMessageClientTest.php ``` Expected: ERRORS — `UltraMessageClient` class not found. - [ ] **Step 3: Implement UltraMessageClient** Create `ultra-message/src/UltraMessageClient.php`: ```php instanceId = $config['instance_id'] ?? ''; $this->token = $config['token'] ?? ''; $this->timeout = $config['timeout'] ?? 30; $this->enabled = $config['enabled'] ?? true; } protected function post(string $endpoint, array $data): array { if (!$this->enabled) { return []; } $response = Http::timeout($this->timeout) ->asForm() ->post(self::BASE_URL . "/{$this->instanceId}/{$endpoint}", array_merge($data, [ 'token' => $this->token, ])); if ($response->failed()) { throw new UltraMessageException("UltraMSG HTTP error: {$response->status()}"); } $body = $response->json() ?? []; if (isset($body['error'])) { throw new UltraMessageException($body['error']); } return $body; } protected function get(string $endpoint, array $query = []): array { if (!$this->enabled) { return []; } $response = Http::timeout($this->timeout) ->get(self::BASE_URL . "/{$this->instanceId}/{$endpoint}", array_merge($query, [ 'token' => $this->token, ])); if ($response->failed()) { throw new UltraMessageException("UltraMSG HTTP error: {$response->status()}"); } $body = $response->json() ?? []; if (isset($body['error'])) { throw new UltraMessageException($body['error']); } return $body; } public function sendText(string $to, string $message, ?string $replyId = null): array { $data = ['to' => $to, 'body' => $message]; if ($replyId !== null) { $data['quoted_id'] = $replyId; } return $this->post('messages/chat', $data); } public function sendImage(string $to, string $imageUrl, string $caption = ''): array { return $this->post('messages/image', [ 'to' => $to, 'image' => $imageUrl, 'caption' => $caption, ]); } public function sendDocument(string $to, string $fileUrl, string $filename, string $caption = ''): array { return $this->post('messages/document', [ 'to' => $to, 'document' => $fileUrl, 'filename' => $filename, 'caption' => $caption, ]); } public function sendAudio(string $to, string $audioUrl): array { return $this->post('messages/audio', [ 'to' => $to, 'audio' => $audioUrl, ]); } public function sendVoice(string $to, string $audioUrl): array { return $this->post('messages/voice', [ 'to' => $to, 'audio' => $audioUrl, ]); } public function sendVideo(string $to, string $videoUrl, string $caption = ''): array { return $this->post('messages/video', [ 'to' => $to, 'video' => $videoUrl, 'caption' => $caption, ]); } public function sendSticker(string $to, string $stickerUrl): array { return $this->post('messages/sticker', [ 'to' => $to, 'sticker' => $stickerUrl, ]); } public function sendContact(string $to, string $contactId): array { return $this->post('messages/contact', [ 'to' => $to, 'contact' => $contactId, ]); } public function sendLocation(string $to, float $lat, float $lng, string $address = ''): array { return $this->post('messages/location', [ 'to' => $to, 'lat' => $lat, 'lng' => $lng, 'address' => $address, ]); } public function sendReaction(string $to, string $messageId, string $emoji): array { return $this->post('messages/reaction', [ 'to' => $to, 'msgId' => $messageId, 'emoji' => $emoji, ]); } public function deleteMessage(string $messageId): array { return $this->post('messages/delete', [ 'msgId' => $messageId, ]); } public function getInstanceStatus(): array { return $this->get('instance/status'); } public function getChats(): array { return $this->get('chats/'); } public function getContacts(): array { return $this->get('contacts/'); } public function getGroups(): array { return $this->get('groups/'); } } ``` - [ ] **Step 4: Run tests — confirm they pass** ```bash ./vendor/bin/phpunit tests/UltraMessageClientTest.php ``` Expected: 5 tests, 5 assertions, PASS. - [ ] **Step 5: Commit** ```bash git add src/UltraMessageClient.php tests/UltraMessageClientTest.php git commit -m "feat: implement UltraMessageClient with all send methods" ``` --- ### Task 4: UltraMessageMessage DTO **Files:** - Create: `ultra-message/src/UltraMessageMessage.php` - [ ] **Step 1: Create the DTO** Create `ultra-message/src/UltraMessageMessage.php`: ```php type = $type; $this->payload = $payload; } public static function text(string $message, ?string $replyId = null): self { return new self('text', ['body' => $message, 'quoted_id' => $replyId]); } public static function image(string $url, string $caption = ''): self { return new self('image', ['image' => $url, 'caption' => $caption]); } public static function document(string $url, string $filename, string $caption = ''): self { return new self('document', ['document' => $url, 'filename' => $filename, 'caption' => $caption]); } public static function audio(string $url): self { return new self('audio', ['audio' => $url]); } public static function voice(string $url): self { return new self('voice', ['audio' => $url]); } public static function video(string $url, string $caption = ''): self { return new self('video', ['video' => $url, 'caption' => $caption]); } public static function sticker(string $url): self { return new self('sticker', ['sticker' => $url]); } public static function contact(string $contactId): self { return new self('contact', ['contact' => $contactId]); } public static function location(float $lat, float $lng, string $address = ''): self { return new self('location', ['lat' => $lat, 'lng' => $lng, 'address' => $address]); } public function to(string $number): self { $this->to = $number; return $this; } } ``` - [ ] **Step 2: Commit** ```bash git add src/UltraMessageMessage.php git commit -m "feat: add UltraMessageMessage DTO" ``` --- ### Task 5: UltraMessageChannel **Files:** - Create: `ultra-message/src/UltraMessageChannel.php` - Create: `ultra-message/tests/UltraMessageChannelTest.php` - [ ] **Step 1: Write the failing tests** Create `ultra-message/tests/UltraMessageChannelTest.php`: ```php Http::response(['sent' => 'true'], 200)]); $client = new UltraMessageClient([ 'instance_id' => 'instance123', 'token' => 'test-token', 'timeout' => 30, 'enabled' => true, ]); $channel = new UltraMessageChannel($client); $notifiable = new class { public string $whatsapp_number = '+971501234567'; public function routeNotificationFor(string $channel, $notification = null): string { return $this->whatsapp_number; } }; $notification = new class extends Notification { public function toUltraMessage($notifiable): UltraMessageMessage { return UltraMessageMessage::text('Test message'); } }; $channel->send($notifiable, $notification); Http::assertSent(function ($request) { return str_contains($request->url(), 'messages/chat') && $request['body'] === 'Test message' && $request['to'] === '+971501234567'; }); } public function test_channel_uses_message_to_over_notifiable_route(): void { Http::fake(['api.ultramsg.com/*' => Http::response(['sent' => 'true'], 200)]); $client = new UltraMessageClient([ 'instance_id' => 'instance123', 'token' => 'test-token', 'timeout' => 30, 'enabled' => true, ]); $channel = new UltraMessageChannel($client); $notifiable = new class { public function routeNotificationFor(string $channel, $notification = null): string { return '+9710000000'; } }; $notification = new class extends Notification { public function toUltraMessage($notifiable): UltraMessageMessage { return UltraMessageMessage::text('Override test')->to('+971999999'); } }; $channel->send($notifiable, $notification); Http::assertSent(fn($r) => $r['to'] === '+971999999'); } public function test_channel_skips_when_no_recipient(): void { Http::fake(); $client = new UltraMessageClient([ 'instance_id' => 'instance123', 'token' => 'test-token', 'timeout' => 30, 'enabled' => true, ]); $channel = new UltraMessageChannel($client); $notifiable = new class { public function routeNotificationFor(string $channel, $notification = null): ?string { return null; } }; $notification = new class extends Notification { public function toUltraMessage($notifiable): UltraMessageMessage { return UltraMessageMessage::text('No recipient'); } }; $channel->send($notifiable, $notification); Http::assertNothingSent(); } } ``` - [ ] **Step 2: Run tests — confirm they fail** ```bash ./vendor/bin/phpunit tests/UltraMessageChannelTest.php ``` Expected: ERRORS — `UltraMessageChannel` not found. - [ ] **Step 3: Implement UltraMessageChannel** Create `ultra-message/src/UltraMessageChannel.php`: ```php toUltraMessage($notifiable); $to = $message->to ?: $notifiable->routeNotificationFor('ultra_message', $notification); if (!$to) { return; } match ($message->type) { 'text' => $this->client->sendText($to, $message->payload['body'], $message->payload['quoted_id'] ?? null), 'image' => $this->client->sendImage($to, $message->payload['image'], $message->payload['caption'] ?? ''), 'document' => $this->client->sendDocument($to, $message->payload['document'], $message->payload['filename'], $message->payload['caption'] ?? ''), 'audio' => $this->client->sendAudio($to, $message->payload['audio']), 'voice' => $this->client->sendVoice($to, $message->payload['audio']), 'video' => $this->client->sendVideo($to, $message->payload['video'], $message->payload['caption'] ?? ''), 'sticker' => $this->client->sendSticker($to, $message->payload['sticker']), 'contact' => $this->client->sendContact($to, $message->payload['contact']), 'location' => $this->client->sendLocation($to, $message->payload['lat'], $message->payload['lng'], $message->payload['address'] ?? ''), default => throw new UltraMessageException("Unknown message type: {$message->type}"), }; } } ``` - [ ] **Step 4: Run tests — confirm they pass** ```bash ./vendor/bin/phpunit tests/UltraMessageChannelTest.php ``` Expected: 3 tests, PASS. - [ ] **Step 5: Commit** ```bash git add src/UltraMessageChannel.php tests/UltraMessageChannelTest.php git commit -m "feat: implement UltraMessageChannel for Laravel Notifications" ``` --- ### Task 6: UltraMessageFake (test double) **Files:** - Create: `ultra-message/src/UltraMessageFake.php` - [ ] **Step 1: Create the fake** Create `ultra-message/src/UltraMessageFake.php`: ```php sent[] = ['endpoint' => $endpoint, 'data' => $data]; return ['sent' => 'ok']; } protected function get(string $endpoint, array $query = []): array { return []; } public function assertSent(callable $callback): void { Assert::assertTrue( collect($this->sent)->contains($callback), 'Expected UltraMessage was not sent.' ); } public function assertNotSent(): void { Assert::assertEmpty($this->sent, 'Unexpected UltraMessage messages were sent.'); } public function assertSentCount(int $count): void { Assert::assertCount($count, $this->sent, "Expected {$count} messages sent, got " . count($this->sent)); } public function getSent(): array { return $this->sent; } } ``` - [ ] **Step 2: Commit** ```bash git add src/UltraMessageFake.php git commit -m "feat: add UltraMessageFake for test support" ``` --- ### Task 7: Facade + ServiceProvider **Files:** - Create: `ultra-message/src/Facades/UltraMessage.php` - Create: `ultra-message/src/UltraMessageServiceProvider.php` - [ ] **Step 1: Create the Facade** Create `ultra-message/src/Facades/UltraMessage.php`: ```php instance('ultra-message.config-resolver', $resolver); app()->forgetInstance(UltraMessageClient::class); } } ``` - [ ] **Step 2: Create the ServiceProvider** Create `ultra-message/src/UltraMessageServiceProvider.php`: ```php mergeConfigFrom(__DIR__ . '/../config/ultra-message.php', 'ultra-message'); $this->app->singleton('ultra-message.config-resolver', fn() => null); $this->app->singleton(UltraMessageClient::class, function ($app) { $resolver = $app->make('ultra-message.config-resolver'); $config = $resolver ? call_user_func($resolver) : config('ultra-message'); return new UltraMessageClient($config); }); $this->app->singleton(UltraMessageChannel::class, function ($app) { return new UltraMessageChannel($app->make(UltraMessageClient::class)); }); } public function boot(): void { if ($this->app->runningInConsole()) { $this->publishes([ __DIR__ . '/../config/ultra-message.php' => config_path('ultra-message.php'), ], 'ultra-message-config'); } $this->loadRoutesFrom(__DIR__ . '/../routes/webhook.php'); } } ``` - [ ] **Step 3: Commit** ```bash git add src/Facades/UltraMessage.php src/UltraMessageServiceProvider.php git commit -m "feat: add Facade and ServiceProvider" ``` --- ### Task 8: Webhook route + controller + event **Files:** - Create: `ultra-message/src/Events/UltraMessageWebhookReceived.php` - Create: `ultra-message/src/Http/Controllers/WebhookController.php` - Create: `ultra-message/routes/webhook.php` - [ ] **Step 1: Create the event** Create `ultra-message/src/Events/UltraMessageWebhookReceived.php`: ```php header('X-Hub-Signature-256', ''); $expected = 'sha256=' . hash_hmac('sha256', $request->getContent(), $secret); if (!hash_equals($expected, $signature)) { abort(403, 'Invalid webhook signature.'); } } event(new UltraMessageWebhookReceived($request->all())); return response('OK', 200); } } ``` - [ ] **Step 3: Create the webhook route file** Create `ultra-message/routes/webhook.php`: ```php name('ultra-message.webhook'); ``` - [ ] **Step 4: Run the full test suite** ```bash ./vendor/bin/phpunit ``` Expected: All tests pass (8 tests minimum). - [ ] **Step 5: Commit** ```bash git add src/Events/ src/Http/ routes/ git commit -m "feat: add webhook route, controller, and event" ``` --- ### Task 9: Push package to GitHub > Do this step when the user provides GitHub access. - [ ] **Step 1: Create the GitHub repository named `ultra-message` under the Promoseven org** - [ ] **Step 2: Push** ```bash git remote add origin git@github.com:promoseven/ultra-message.git git branch -M main git push -u origin main ``` - [ ] **Step 3: Note the repo URL** — needed for OperationModule `composer.json`. --- ## PHASE 2 — OperationModule Integration > All remaining steps are inside `C:\Users\IT Department\Desktop\OperationModule\`. --- ### Task 10: Add package to OperationModule via Composer **Files:** - Modify: `composer.json` - [ ] **Step 1: Add VCS repository + require entry to composer.json** In `composer.json`, add inside `"repositories"` (create the key if missing) and update `"require"`: ```json { "repositories": [ { "type": "vcs", "url": "https://github.com/promoseven/ultra-message" } ], "require": { "php": "^8.2", "barryvdh/laravel-dompdf": "^3.1", "laravel/framework": "^12.0", "laravel/tinker": "^2.10.1", "phpoffice/phpspreadsheet": "^5.7", "promoseven/ultra-message": "dev-main", "spatie/laravel-permission": "^6.25" } } ``` > **Until GitHub is set up:** use a local path repository instead: > ```json > { > "repositories": [ > { > "type": "path", > "url": "../ultra-message" > } > ], > "require": { > "promoseven/ultra-message": "*" > } > } > ``` - [ ] **Step 2: Install** ```bash composer require promoseven/ultra-message ``` Expected: Package installed, service provider auto-discovered, no errors. - [ ] **Step 3: Publish config** ```bash php artisan vendor:publish --tag=ultra-message-config ``` Expected: `config/ultra-message.php` created in OperationModule. - [ ] **Step 4: Commit** ```bash git add composer.json composer.lock config/ultra-message.php git commit -m "feat: install ultra-message package" ``` --- ### Task 11: Settings table migration + Setting model **Files:** - Create: `database/migrations/xxxx_create_settings_table.php` - Create: `app/Models/Setting.php` - [ ] **Step 1: Create the migration** ```bash php artisan make:migration create_settings_table ``` Edit the generated file in `database/migrations/`: ```php id(); $table->string('key')->unique(); $table->text('value')->nullable(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('settings'); } }; ``` - [ ] **Step 2: Run migration** ```bash php artisan migrate ``` Expected: `settings` table created, no errors. - [ ] **Step 3: Create the Setting model** Create `app/Models/Setting.php`: ```php first(); return $setting ? $setting->value : $default; } public static function set(string $key, mixed $value): void { static::updateOrCreate(['key' => $key], ['value' => $value]); } } ``` - [ ] **Step 4: Commit** ```bash git add database/migrations/ app/Models/Setting.php git commit -m "feat: add settings table and Setting model" ``` --- ### Task 12: Add whatsapp_number to suppliers, customers, and users **Files:** - Create: 3 migration files - Modify: `app/Models/Supplier.php` - Modify: `app/Models/Customer.php` - Modify: `app/Models/User.php` - [ ] **Step 1: Create the migrations** ```bash php artisan make:migration add_whatsapp_number_to_suppliers_table php artisan make:migration add_whatsapp_number_to_customers_table php artisan make:migration add_whatsapp_number_to_users_table ``` For each, edit the generated file: **`add_whatsapp_number_to_suppliers_table`:** ```php public function up(): void { Schema::table('suppliers', function (Blueprint $table) { $table->string('whatsapp_number')->nullable()->after('phone'); }); } public function down(): void { Schema::table('suppliers', function (Blueprint $table) { $table->dropColumn('whatsapp_number'); }); } ``` **`add_whatsapp_number_to_customers_table`:** ```php public function up(): void { Schema::table('customers', function (Blueprint $table) { $table->string('whatsapp_number')->nullable()->after('phone'); }); } public function down(): void { Schema::table('customers', function (Blueprint $table) { $table->dropColumn('whatsapp_number'); }); } ``` **`add_whatsapp_number_to_users_table`:** ```php public function up(): void { Schema::table('users', function (Blueprint $table) { $table->string('whatsapp_number')->nullable()->after('email'); }); } public function down(): void { Schema::table('users', function (Blueprint $table) { $table->dropColumn('whatsapp_number'); }); } ``` - [ ] **Step 2: Run migrations** ```bash php artisan migrate ``` Expected: 3 columns added, no errors. - [ ] **Step 3: Add routeNotificationFor to Supplier** Read `app/Models/Supplier.php` first. Add this method to the class body: ```php public function routeNotificationFor(string $channel, mixed $notification = null): ?string { return $this->whatsapp_number; } ``` Also add `'whatsapp_number'` to the `$fillable` array. - [ ] **Step 4: Add routeNotificationFor to Customer** Read `app/Models/Customer.php` first. Add to the class body: ```php public function routeNotificationFor(string $channel, mixed $notification = null): ?string { return $this->whatsapp_number; } ``` Also add `'whatsapp_number'` to `$fillable`. - [ ] **Step 5: Add routeNotificationFor to User** Read `app/Models/User.php` first. Add to the class body: ```php public function routeNotificationFor(string $channel, mixed $notification = null): ?string { return $this->whatsapp_number; } ``` Also add `'whatsapp_number'` to `$fillable`. - [ ] **Step 6: Commit** ```bash git add database/migrations/ app/Models/Supplier.php app/Models/Customer.php app/Models/User.php git commit -m "feat: add whatsapp_number field to suppliers, customers, users" ``` --- ### Task 13: Dynamic config + CSRF exclusion in OperationModule **Files:** - Modify: `app/Providers/AppServiceProvider.php` - Modify: `bootstrap/app.php` - [ ] **Step 1: Boot dynamic config resolver in AppServiceProvider** Read `app/Providers/AppServiceProvider.php`. In the `boot()` method, add: ```php use App\Models\Setting; use PromoSeven\UltraMessage\Facades\UltraMessage; // Inside boot(): UltraMessage::configUsing(function () { return [ 'instance_id' => Setting::get('ultramsg_instance_id', config('ultra-message.instance_id')), 'token' => Setting::get('ultramsg_token', config('ultra-message.token')), 'webhook_secret' => Setting::get('ultramsg_webhook_secret', config('ultra-message.webhook_secret')), 'webhook_path' => Setting::get('ultramsg_webhook_path', config('ultra-message.webhook_path', 'ultra-message/webhook')), 'timeout' => config('ultra-message.timeout', 30), 'enabled' => (bool) Setting::get('ultramsg_enabled', config('ultra-message.enabled', true)), ]; }); ``` - [ ] **Step 2: Exclude webhook path from CSRF in bootstrap/app.php** Read `bootstrap/app.php`. Inside the `->withMiddleware(function (Middleware $middleware) {` block, add: ```php $middleware->validateCsrfTokens(except: [ config('ultra-message.webhook_path', 'ultra-message/webhook'), 'ultra-message/*', ]); ``` - [ ] **Step 3: Commit** ```bash git add app/Providers/AppServiceProvider.php bootstrap/app.php git commit -m "feat: wire dynamic config resolver and exclude webhook from CSRF" ``` --- ### Task 14: SettingsController + routes **Files:** - Create: `app/Http/Controllers/SettingsController.php` - Modify: `routes/web.php` - [ ] **Step 1: Create SettingsController** Create `app/Http/Controllers/SettingsController.php`: ```php Setting::get('ultramsg_enabled', false), 'instance_id' => Setting::get('ultramsg_instance_id', ''), 'token' => Setting::get('ultramsg_token', ''), 'webhook_secret' => Setting::get('ultramsg_webhook_secret', ''), 'webhook_path' => Setting::get('ultramsg_webhook_path', 'ultra-message/webhook'), ]; return view('settings.integrations', compact('settings')); } public function updateWhatsapp(Request $request): RedirectResponse { $request->validate([ 'instance_id' => ['required', 'string', 'max:100'], 'token' => ['required', 'string', 'max:255'], 'webhook_secret' => ['nullable', 'string', 'max:255'], 'webhook_path' => ['required', 'string', 'max:100'], ]); Setting::set('ultramsg_enabled', $request->boolean('enabled') ? '1' : '0'); Setting::set('ultramsg_instance_id', $request->instance_id); Setting::set('ultramsg_token', $request->token); Setting::set('ultramsg_webhook_secret', $request->webhook_secret ?? ''); Setting::set('ultramsg_webhook_path', $request->webhook_path); return redirect()->route('settings.integrations')->with('success', 'WhatsApp settings saved.'); } public function testWhatsappConnection(): \Illuminate\Http\JsonResponse { try { $status = UltraMessage::getInstanceStatus(); return response()->json(['success' => true, 'status' => $status]); } catch (UltraMessageException $e) { return response()->json(['success' => false, 'message' => $e->getMessage()]); } } } ``` - [ ] **Step 2: Add routes to routes/web.php** Read `routes/web.php`. Inside the `auth` + `verified` middleware group, add at the end before the closing brace: ```php // Settings Route::middleware('role:Admin')->group(function () { Route::get('settings/integrations', [SettingsController::class, 'integrations'])->name('settings.integrations'); Route::post('settings/integrations/whatsapp', [SettingsController::class, 'updateWhatsapp'])->name('settings.integrations.whatsapp'); Route::get('settings/integrations/test-whatsapp', [SettingsController::class, 'testWhatsappConnection'])->name('settings.integrations.test-whatsapp'); }); ``` Also add the import at the top of the file: ```php use App\Http\Controllers\SettingsController; ``` - [ ] **Step 3: Commit** ```bash git add app/Http/Controllers/SettingsController.php routes/web.php git commit -m "feat: add SettingsController and settings routes" ``` --- ### Task 15: Settings integrations view **Files:** - Create: `resources/views/settings/integrations.blade.php` - [ ] **Step 1: Create the view directory** ```bash mkdir -p resources/views/settings ``` - [ ] **Step 2: Create the view** Create `resources/views/settings/integrations.blade.php`: ```blade

Settings — Integrations

{{-- WhatsApp / UltraMSG Section --}}

WhatsApp (UltraMSG)

@csrf {{-- Enable toggle --}}

Enable WhatsApp Notifications

When disabled, no messages will be sent regardless of other settings.


{{-- Instance ID --}}
@error('instance_id')

{{ $message }}

@enderror
{{-- API Token --}}
@error('token')

{{ $message }}

@enderror
{{-- Webhook Secret --}}
{{-- Webhook Path --}}
{{ url('/') }}/

Full URL: {{ url('/') }}/{{ $settings['webhook_path'] }} — paste this in your UltraMSG dashboard.

@error('webhook_path')

{{ $message }}

@enderror
{{-- Actions --}}
``` - [ ] **Step 3: Commit** ```bash git add resources/views/settings/ git commit -m "feat: add WhatsApp integration settings view" ``` --- ### Task 16: Add Settings to sidebar (Admin only) **Files:** - Modify: `resources/views/layouts/navigation.blade.php` - [ ] **Step 1: Read the current navigation file** Read `resources/views/layouts/navigation.blade.php` and find where sidebar navigation links are listed. - [ ] **Step 2: Add Settings link visible to Admin role only** Find the last navigation `` or nav item group in the sidebar and add after it: ```blade @role('Admin') Settings @endrole ``` - [ ] **Step 3: Commit** ```bash git add resources/views/layouts/navigation.blade.php git commit -m "feat: add Settings sidebar link for Admin role" ``` --- ### Task 17: Add whatsapp_number field to Supplier and Customer forms **Files:** - Modify: `resources/views/purchase/suppliers/create.blade.php` - Modify: `resources/views/purchase/suppliers/edit.blade.php` - Modify: `resources/views/sales/customers/create.blade.php` - Modify: `resources/views/sales/customers/edit.blade.php` - Modify: `app/Http/Controllers/Purchase/SupplierController.php` - Modify: `app/Http/Controllers/Sales/CustomerController.php` - [ ] **Step 1: Read both supplier form views** Read `resources/views/purchase/suppliers/create.blade.php` and `edit.blade.php`. Find where the `phone` field is rendered. After the phone field, add: ```blade

International format. Used for WhatsApp notifications.

``` For `create.blade.php`, use `old('whatsapp_number', '')`. - [ ] **Step 2: Read both customer form views** Same treatment for `resources/views/sales/customers/create.blade.php` and `edit.blade.php` — add after the phone field. - [ ] **Step 3: Update SupplierController store/update** Read `app/Http/Controllers/Purchase/SupplierController.php`. In `store()` and `update()` methods, add `'whatsapp_number'` to the validated fields and the `$supplier->fill()` / `Supplier::create()` call. Add to the validation rules: ```php 'whatsapp_number' => ['nullable', 'string', 'max:20'], ``` - [ ] **Step 4: Update CustomerController store/update** Read `app/Http/Controllers/Sales/CustomerController.php`. Same as above — add `whatsapp_number` to validation and assignment. - [ ] **Step 5: Commit** ```bash git add resources/views/purchase/suppliers/ resources/views/sales/customers/ \ app/Http/Controllers/Purchase/SupplierController.php \ app/Http/Controllers/Sales/CustomerController.php git commit -m "feat: add whatsapp_number field to supplier and customer forms" ``` --- ## PHASE 3 — Notifications > All files in `C:\Users\IT Department\Desktop\OperationModule\`. --- ### Task 18: PurchaseOrderConfirmedNotification **Files:** - Create: `app/Notifications/Purchase/PurchaseOrderConfirmedNotification.php` - Modify: `app/Http/Controllers/Purchase/PurchaseOrderController.php` - [ ] **Step 1: Create the notification** ```bash mkdir -p app/Notifications/Purchase ``` Create `app/Notifications/Purchase/PurchaseOrderConfirmedNotification.php`: ```php name},\n\nPurchase Order *#{$this->order->number}* has been confirmed.\n\nTotal: {$this->order->currency} {$this->order->total}\nExpected delivery: {$this->order->expected_date}\n\nThank you." ); } } ``` - [ ] **Step 2: Trigger notification in PurchaseOrderController** Read `app/Http/Controllers/Purchase/PurchaseOrderController.php`. Find the method that confirms/stores a PO (likely `store()` or a `confirm()` action). After the PO is saved/confirmed, add: ```php use App\Notifications\Purchase\PurchaseOrderConfirmedNotification; use Illuminate\Support\Facades\Notification; // After PO confirmed: if ($order->supplier && $order->supplier->whatsapp_number) { $order->supplier->notify(new PurchaseOrderConfirmedNotification($order)); } ``` - [ ] **Step 3: Commit** ```bash git add app/Notifications/Purchase/PurchaseOrderConfirmedNotification.php \ app/Http/Controllers/Purchase/PurchaseOrderController.php git commit -m "feat: send WhatsApp notification on PO confirmed" ``` --- ### Task 19: GoodsReceiptConfirmedNotification **Files:** - Create: `app/Notifications/Purchase/GoodsReceiptConfirmedNotification.php` - Modify: `app/Http/Controllers/Purchase/GoodsReceiptNoteController.php` - [ ] **Step 1: Create the notification** Create `app/Notifications/Purchase/GoodsReceiptConfirmedNotification.php`: ```php grn->number}* has been confirmed and goods received.\n\nPO Reference: {$this->grn->purchaseOrder->number}\nDate: {$this->grn->date}" ); } } ``` - [ ] **Step 2: Trigger in GoodsReceiptNoteController confirm method** Read `app/Http/Controllers/Purchase/GoodsReceiptNoteController.php`. In the `confirm()` method, after confirming the GRN, add: ```php use App\Notifications\Purchase\GoodsReceiptConfirmedNotification; // Notify the store manager(s) $storeManagers = \App\Models\User::role('Store Manager')->whereNotNull('whatsapp_number')->get(); \Illuminate\Support\Facades\Notification::send($storeManagers, new GoodsReceiptConfirmedNotification($grn)); ``` - [ ] **Step 3: Commit** ```bash git add app/Notifications/Purchase/GoodsReceiptConfirmedNotification.php \ app/Http/Controllers/Purchase/GoodsReceiptNoteController.php git commit -m "feat: send WhatsApp notification on GRN confirmed" ``` --- ### Task 20: SalesOrderConfirmedNotification **Files:** - Create: `app/Notifications/Sales/SalesOrderConfirmedNotification.php` - Modify: `app/Http/Controllers/Sales/SalesOrderController.php` - [ ] **Step 1: Create the notification** ```bash mkdir -p app/Notifications/Sales ``` Create `app/Notifications/Sales/SalesOrderConfirmedNotification.php`: ```php name},\n\nYour order *#{$this->order->number}* has been confirmed.\n\nTotal: {$this->order->currency} {$this->order->total}\n\nWe will keep you updated on the delivery status. Thank you for your business." ); } } ``` - [ ] **Step 2: Trigger in SalesOrderController confirm method** Read `app/Http/Controllers/Sales/SalesOrderController.php`. In the `confirm()` method, after confirmation, add: ```php use App\Notifications\Sales\SalesOrderConfirmedNotification; if ($order->customer && $order->customer->whatsapp_number) { $order->customer->notify(new SalesOrderConfirmedNotification($order)); } ``` - [ ] **Step 3: Commit** ```bash git add app/Notifications/Sales/SalesOrderConfirmedNotification.php \ app/Http/Controllers/Sales/SalesOrderController.php git commit -m "feat: send WhatsApp notification on sales order confirmed" ``` --- ### Task 21: InvoiceCreatedNotification **Files:** - Create: `app/Notifications/Sales/InvoiceCreatedNotification.php` - Modify: `app/Http/Controllers/Sales/SalesInvoiceController.php` - [ ] **Step 1: Create the notification** Create `app/Notifications/Sales/InvoiceCreatedNotification.php`: ```php name},\n\nInvoice *#{$this->invoice->number}* is ready.\n\nAmount Due: {$this->invoice->currency} {$this->invoice->total}\nDue Date: {$this->invoice->due_date}\n\nPlease arrange payment at your earliest convenience. Thank you." ); } } ``` - [ ] **Step 2: Trigger in SalesInvoiceController store method** Read `app/Http/Controllers/Sales/SalesInvoiceController.php`. In `store()`, after the invoice is saved, add: ```php use App\Notifications\Sales\InvoiceCreatedNotification; if ($invoice->salesOrder?->customer && $invoice->salesOrder->customer->whatsapp_number) { $invoice->salesOrder->customer->notify(new InvoiceCreatedNotification($invoice)); } ``` - [ ] **Step 3: Commit** ```bash git add app/Notifications/Sales/InvoiceCreatedNotification.php \ app/Http/Controllers/Sales/SalesInvoiceController.php git commit -m "feat: send WhatsApp notification on sales invoice created" ``` --- ### Task 22: DeliveryDispatchedNotification **Files:** - Create: `app/Notifications/Sales/DeliveryDispatchedNotification.php` - Modify: `app/Http/Controllers/Sales/DeliveryNoteController.php` - [ ] **Step 1: Create the notification** Create `app/Notifications/Sales/DeliveryDispatchedNotification.php`: ```php name},\n\nYour delivery *#{$this->delivery->number}* has been dispatched and is on its way.\n\nOrder Reference: {$this->delivery->salesOrder->number}\nDispatch Date: {$this->delivery->dispatch_date}\n\nThank you for your business." ); } } ``` - [ ] **Step 2: Trigger in DeliveryNoteController dispatch method** Read `app/Http/Controllers/Sales/DeliveryNoteController.php`. In the `dispatch()` method, after dispatch, add: ```php use App\Notifications\Sales\DeliveryDispatchedNotification; if ($delivery->salesOrder?->customer && $delivery->salesOrder->customer->whatsapp_number) { $delivery->salesOrder->customer->notify(new DeliveryDispatchedNotification($delivery)); } ``` - [ ] **Step 3: Commit** ```bash git add app/Notifications/Sales/DeliveryDispatchedNotification.php \ app/Http/Controllers/Sales/DeliveryNoteController.php git commit -m "feat: send WhatsApp notification on delivery dispatched" ``` --- ### Task 23: LowStockAlertNotification **Files:** - Create: `app/Notifications/Inventory/LowStockAlertNotification.php` - Modify: `app/Http/Controllers/Inventory/StockMovementController.php` - [ ] **Step 1: Create the notification** ```bash mkdir -p app/Notifications/Inventory ``` Create `app/Notifications/Inventory/LowStockAlertNotification.php`: ```php item->name}* ({$this->item->code})\nCurrent Stock: {$this->stockLevel->quantity} {$this->item->unit}\nReorder Level: {$this->item->reorder_level} {$this->item->unit}\n\nPlease raise a purchase request." ); } } ``` - [ ] **Step 2: Trigger in StockMovementController after stock is reduced** Read `app/Http/Controllers/Inventory/StockMovementController.php`. In the `store()` method, after the stock movement is recorded, add a check: ```php use App\Notifications\Inventory\LowStockAlertNotification; // After stock movement saved — check if item is now below reorder level $stockLevel = \App\Models\StockLevel::where('item_id', $movement->item_id) ->where('warehouse_id', $movement->warehouse_id) ->first(); if ($stockLevel && $movement->item->reorder_level && $stockLevel->quantity <= $movement->item->reorder_level) { $storeManagers = \App\Models\User::role('Store Manager')->whereNotNull('whatsapp_number')->get(); \Illuminate\Support\Facades\Notification::send( $storeManagers, new LowStockAlertNotification($movement->item, $stockLevel) ); } ``` - [ ] **Step 3: Commit** ```bash git add app/Notifications/Inventory/LowStockAlertNotification.php \ app/Http/Controllers/Inventory/StockMovementController.php git commit -m "feat: send WhatsApp low stock alert to store managers" ``` --- ### Task 24: ProductionOrderCompletedNotification **Files:** - Create: `app/Notifications/Production/ProductionOrderCompletedNotification.php` - Modify: `app/Http/Controllers/Production/ProductionOrderController.php` - [ ] **Step 1: Create the notification** ```bash mkdir -p app/Notifications/Production ``` Create `app/Notifications/Production/ProductionOrderCompletedNotification.php`: ```php order->number}* has been completed.\n\nProduct: {$this->order->product->name}\nQuantity: {$this->order->quantity}\nCompleted: " . now()->format('d M Y') ); } } ``` - [ ] **Step 2: Trigger in ProductionOrderController complete method** Read `app/Http/Controllers/Production/ProductionOrderController.php`. In the `complete()` method, after completion, add: ```php use App\Notifications\Production\ProductionOrderCompletedNotification; $productionManagers = \App\Models\User::role('Production Manager')->whereNotNull('whatsapp_number')->get(); \Illuminate\Support\Facades\Notification::send( $productionManagers, new ProductionOrderCompletedNotification($order) ); ``` - [ ] **Step 3: Commit** ```bash git add app/Notifications/Production/ProductionOrderCompletedNotification.php \ app/Http/Controllers/Production/ProductionOrderController.php git commit -m "feat: send WhatsApp notification on production order completed" ``` --- ## Self-Review Checklist **Spec coverage:** - [x] Package structure (Tasks 1–8) - [x] Config + dynamic config resolver (Tasks 7, 13) - [x] UltraMessageClient — all 16 send/info methods (Task 3) - [x] UltraMessageMessage DTO — all types (Task 4) - [x] UltraMessageChannel (Task 5) - [x] UltraMessageFake + assertSent/assertNotSent/assertSentCount (Task 6) - [x] Facade with fake() and configUsing() (Task 7) - [x] Webhook route + HMAC verification + event fire (Task 8) - [x] GitHub push (Task 9) - [x] Settings table + Setting model (Task 11) - [x] whatsapp_number on Supplier/Customer/User + routeNotificationFor (Task 12) - [x] CSRF exclusion for webhook (Task 13) - [x] SettingsController with test connection (Task 14) - [x] Settings UI view with masked fields + test button (Task 15) - [x] Sidebar link (Admin only) (Task 16) - [x] WhatsApp field on Supplier + Customer forms (Task 17) - [x] All 7 notification classes (Tasks 18–24) - [x] All 7 notification triggers in controllers (Tasks 18–24) **No placeholders found.** **Type consistency:** `UltraMessageMessage::text/image/document/...` constructors defined in Task 4, used identically in Tasks 5 and 18–24. `UltraMessageChannel` + `UltraMessageClient` method names match throughout.