diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..cd8eb86e
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+; This file is for unifying the coding style for different editors and IDEs.
+; More information at http://editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+indent_size = 4
+indent_style = space
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..b2638710
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,10 @@
+# Path-based git attributes
+# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
+
+# Ignore all test and documentation with "export-ignore".
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.travis.yml export-ignore
+/phpunit.xml.dist export-ignore
+/.scrutinizer.yml export-ignore
+/tests export-ignore
diff --git a/.github/workflows/laravel.yml b/.github/workflows/laravel.yml
new file mode 100644
index 00000000..c690791a
--- /dev/null
+++ b/.github/workflows/laravel.yml
@@ -0,0 +1,35 @@
+name: Laravel
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+jobs:
+ laravel-tests:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e
+ with:
+ php-version: '8.0'
+ - uses: actions/checkout@v4
+ - name: Copy .env
+ run: php -r "file_exists('.env') || copy('.env.example', '.env');"
+ - name: Install Dependencies
+ run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
+ - name: Generate key
+ run: php artisan key:generate
+ - name: Directory Permissions
+ run: chmod -R 777 storage bootstrap/cache
+ - name: Create Database
+ run: |
+ mkdir -p database
+ touch database/database.sqlite
+ - name: Execute tests (Unit and Feature tests) via PHPUnit/Pest
+ env:
+ DB_CONNECTION: sqlite
+ DB_DATABASE: database/database.sqlite
+ run: php artisan test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..0044b968
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/.phpunit.cache
+/build
+/vendor
+composer.phar
+composer.lock
+.DS_Store
diff --git a/.styleci.yml b/.styleci.yml
new file mode 100644
index 00000000..0285f179
--- /dev/null
+++ b/.styleci.yml
@@ -0,0 +1 @@
+preset: laravel
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..c2c4933c
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Changelog
+
+All notable changes to `Persian-SMS` will be documented in this file
+
+## 1.0.0 - 2025-5-10
+
+- initial release
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..4da74e3f
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,55 @@
+# Contributing
+
+Contributions are **welcome** and will be fully **credited**.
+
+Please read and understand the contribution guide before creating an issue or pull request.
+
+## Etiquette
+
+This project is open source, and as such, the maintainers give their free time to build and maintain the source code
+held within. They make the code freely available in the hope that it will be of use to other developers. It would be
+extremely unfair for them to suffer abuse or anger for their hard work.
+
+Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the
+world that developers are civilized and selfless people.
+
+It's the duty of the maintainer to ensure that all submissions to the project are of sufficient
+quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used.
+
+## Viability
+
+When requesting or submitting new features, first consider whether it might be useful to others. Open
+source projects are used by many developers, who may have entirely different needs to your own. Think about
+whether or not your feature is likely to be used by other users of the project.
+
+## Procedure
+
+Before filing an issue:
+
+- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident.
+- Check to make sure your feature suggestion isn't already present within the project.
+- Check the pull requests tab to ensure that the bug doesn't have a fix in progress.
+- Check the pull requests tab to ensure that the feature isn't already in progress.
+
+Before submitting a pull request:
+
+- Check the codebase to ensure that your feature doesn't already exist.
+- Check the pull requests to ensure that another person hasn't already submitted the feature or fix.
+
+## Requirements
+
+If the project maintainer has any additional requirements, you will find them listed here.
+
+- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer).
+
+- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
+
+- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
+
+- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
+
+- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
+
+- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
+
+**Happy coding**!
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 00000000..b3e0b2f5
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+# The MIT License (MIT)
+
+Copyright (c) Ali Abdi abdi9074@gmail.com
+
+> Permission is hereby granted, free of charge, to any person obtaining a copy
+> of this software and associated documentation files (the "Software"), to deal
+> in the Software without restriction, including without limitation the rights
+> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+> copies of the Software, and to permit persons to whom the Software is
+> furnished to do so, subject to the following conditions:
+>
+> The above copyright notice and this permission notice shall be included in
+> all copies or substantial portions of the Software.
+>
+> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+> THE SOFTWARE.
diff --git a/README.md b/README.md
index fa8362b7..b810f8b8 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,308 @@
-# New Notification Channels
+# Laravel Persian SMS Notification Channel
-### Suggesting a new channel
-Have a suggestion or working on a new channel? Please create a new issue for that service.
+[](https://packagist.org/packages/your-vendor-name/laravel-ippanel-notification-channel)
+[](https://packagist.org/packages/your-vendor-name/laravel-ippanel-notification-channel)
+[](https://github.com/your-vendor-name/laravel-ippanel-notification-channel/actions?query=workflow%3Arun-tests+branch%3Amain)
+[](https://styleci.io/repos/YOUR_REPO_ID)
+[](https://github.com/your-vendor-name/laravel-ippanel-notification-channel/blob/main/LICENSE)
-### I'm working on a new channel
-Please create an issue for it if it does not already exist, then PR you code for review.
+This package makes it easy to send notifications using various Iranian SMS service providers with Laravel. The first supported provider is [IPPanel](https://ippanel.com/) with Laravel.
-## Workflow for new channels
+## Contents
-1) Head over to the [skeleton repo](https://github.com/laravel-notification-channels/skeleton) download a ZIP copy. This is important, to ensure you start from a fresh commit history.
-2) Use find/replace to replace all of the placeholders with the correct values (package name, author name, email, etc).
-3) Implement to logic for the channel & add tests.
-4) Fork this repo, add it as a remote and push your new channel to a branch.
-5) Submit a new PR against this repo for review.
+* [Installation](#installation)
+* [Configuration](#configuration)
+ * [IPPanel Configuration](#IPPanel)
+* [Usage](#usage)
+ * [Routing SMS Notifications](#sending-a-simple-text-message)
+ * [IPPanel](#ippanel-usage)
+ * [Available Message Methods (IPPanel)](#available-message-methods-ippanel)
+ * [Sending a Simple Text Message (IPPanel)](#sending-a-simple-text-message-ippanel)
+ * [Sending a Pattern-Based Message (IPPanel)](#sending-a-pattern-based-message-ippanel)
+ * [Customizing the Sender (IPPanel))](#customizing-the-sender-ippanel)
+ * [Scheduling Messages (IPPanel - Note)](#scheduling-messages-ippanel)
+ * [Checking Account Credit (IPPanel)](#checking-account-credit-ippanel)
+* [Handling Errors](#handling-errors)
+* [Testing](#testing)
+* [Contributing](#contributing)
+* [License](#license)
-Take a look at our [FAQ](http://laravel-notification-channels.com/) to see our small list of rules, to provide top-notch notification channels.
+## Installation
+Note: Until this package is officially accepted and published under laravel-notification-channels, please use the direct GitHub repository installation method.
+
+1- You can install the package via composer:
+
+```bash
+composer require laravel-notification-channels/persian-sms
+```
+
+2- Install via GitHub (Before publishing to Packagist or for development):
+If the package is not yet published on Packagist, or for testing/development purposes, you can install it directly from GitHub.
+
+First, add the repository definition to your project's composer.json file under the repositories section:
+
+```bash
+"repositories": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/saman9074/persian-sms"
+ }
+],
+```
+Then, require the package:
+
+```bash
+composer saman9074/persian-sms:dev-main
+```
+## Configuration
+
+You can publish the config file with:
+
+```bash
+php artisan vendor:publish --provider="NotificationChannels\PersianSms\PersianSmsServiceProvider" --tag="persian-sms-config"
+```
+
+This will create a config/persian-sms.php file in your project. This file allows you to configure the default SMS driver and settings for each supported driver.
+
+```bash
+// config/persian-sms.php
+return [
+ 'default_driver' => env('PERSIAN_SMS_DRIVER', 'ippanel'),
+
+ 'drivers' => [
+ 'ippanel' => [
+ 'api_key' => env('IPPANEL_API_KEY'),
+ 'sender_number' => env('IPPANEL_SENDER_NUMBER'),
+ // 'api_url' => 'https://api2.ippanel.com/api/v1', // Optional: if you need to override
+ ],
+ // ... other drivers like kavenegar will be added here
+ ],
+
+ 'guzzle' => [
+ 'timeout' => 10.0,
+ // ... other Guzzle options
+ ],
+];
+```
+## IPPanel Configuration
+
+Add your IPPanel API Key and default Sender Number to your .env file:
+
+```bash
+PERSIAN_SMS_DRIVER=ippanel
+IPPANEL_API_KEY=your_ippanel_api_key_here
+IPPANEL_SENDER_NUMBER=your_default_ippanel_sender_number_here
+```
+
+## Usage
+
+To send notifications, use the NotificationChannels\PersianSms\IPPanel\IPPanelChannel in your notification's via method. You will also need to define a toPersianSms method that returns an NotificationChannels\PersianSms\IPPanel\IPPanelMessage instance.
+
+```bash
+namespace App\Notifications;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Notifications\Notification;
+use NotificationChannels\PersianSms\IPPanel\IPPanelChannel;
+use NotificationChannels\PersianSms\IPPanel\IPPanelMessage;
+
+class OrderShipped extends Notification
+{
+ use Queueable;
+
+ // ... constructor and other properties
+
+ public function via($notifiable)
+ {
+ return [IPPanelChannel::class];
+ }
+
+ public function toPersianSms($notifiable)
+ {
+ return (new IPPanelMessage())
+ ->content("Your order has been shipped!");
+ }
+}
+```
+Routing SMS Notifications
+Your notifiable model (e.g., App\Models\User) needs to implement a method to return the phone number(s) for the notification. The IPPanelChannel will look for these methods in the following order:
+
+1. routeNotificationForPersianSms($notification)
+2. routeNotificationFor(IPPanelChannel::class, $notification)
+3. routeNotificationForIPPanel($notification)
+4. A phone_number attribute on the notifiable model.
+5. A mobile attribute on the notifiable model.
+
+Example for User model:
+```bash
+namespace App\Models;
+
+use Illuminate\Foundation\Auth\User as Authenticatable;
+use Illuminate\Notifications\Notifiable;
+
+class User extends Authenticatable
+{
+ use Notifiable;
+
+ // ... other model properties
+
+ /**
+ * Route notifications for the PersianSms channel.
+ *
+ * @param \Illuminate\Notifications\Notification $notification
+ * @return string|array|null
+ */
+ public function routeNotificationForPersianSms($notification)
+ {
+ return $this->phone_number; // Assuming 'phone_number' is a column in your users table
+ }
+}
+```
+
+Available Message Methods (IPPanel)
+
+The IPPanelMessage class provides a fluent API to construct your message:
+
+ * content(string $text): Sets the content for a normal SMS.
+ * pattern(string $patternCode, array $variables = []): Sets the message to be sent using a pattern, including its variables.
+ * from(string $senderNumber): Overrides the default sender number for this specific message.
+ * at(string $dateTimeString): Sets a scheduled time for sending the SMS (ISO 8601 format like "YYYY-MM-DDTHH:MM:SSZ"). Note: This is prepared in IPPanelMessage but not yet utilized by IPPanelChannel for actual scheduling in the API call.
+
+Sending a Simple Text Message (IPPanel)
+
+In your notification's toPersianSms() method:
+
+```bash
+use NotificationChannels\PersianSms\IPPanel\IPPanelMessage;
+
+public function toPersianSms($notifiable)
+{
+ return (new IPPanelMessage())
+ ->content('Your order has been shipped!');
+}
+```
+Then, send the notification from your notifiable model:
+
+```bash
+$user->notify(new OrderShipped($order));
+```
+Sending a Pattern-Based Message (IPPanel)
+
+If you are using IPPanel's pattern-based SMS, use the pattern() method. Pass the pattern code and an associative array of variables.
+
+```bash
+use NotificationChannels\PersianSms\IPPanel\IPPanelMessage;
+
+public function toPersianSms($notifiable)
+{
+ $patternCode = 'your_ippanel_pattern_code'; // Replace with your actual pattern code
+ $variables = [
+ 'name' => $notifiable->name, // Match variable names with your IPPanel pattern
+ 'order_id' => $this->order->id,
+ ];
+
+ return (new IPPanelMessage())
+ ->pattern($patternCode, $variables);
+ // Do NOT use ->content() when sending a pattern-based message
+}
+```
+Replace your_ippanel_pattern_code with the code from your IPPanel panel and ensure the keys in the $variables array match the variable names defined in your pattern.
+
+Customizing the Sender (IPPanel)
+
+By default, the channel uses the IPPANEL_SENDER_NUMBER from your config. You can override this for a specific message using the from() method:
+
+```bash
+use NotificationChannels\PersianSms\IPPanel\IPPanelMessage;
+
+public function toPersianSms($notifiable)
+{
+ return (new IPPanelMessage())
+ ->content('Message from a custom sender.')
+ ->from('your_custom_sender_number'); // e.g., +983000XXXX
+}
+```
+Scheduling Messages (IPPanel - Note)
+
+The IPPanelMessage class has an at(string $dateTimeString) method to specify a future send time. However, the current version of IPPanelChannel.php does not yet pass this scheduled time to the IPPanel API. This feature can be implemented in a future update to the channel.
+
+```bash
+use NotificationChannels\PersianSms\IPPanel\IPPanelMessage;
+
+public function toPersianSms($notifiable)
+{
+ $scheduledTime = "2025-12-31T23:59:59Z"; // Example ISO 8601 string
+
+ return (new IPPanelMessage())
+ ->content('This message is intended to be sent later.')
+ ->at($scheduledTime); // Note: Currently not implemented in IPPanelChannel API call
+}
+```
+
+Checking Account Credit (IPPanel)
+
+The IPPanelChannel class provides a getCredit() method to check your IPPanel account balance. You can resolve the channel from the service container and call this method:
+
+```bash
+use NotificationChannels\PersianSms\IPPanel\IPPanelChannel;
+use NotificationChannels\PersianSms\Exceptions\CouldNotSendNotification;
+use Illuminate\Support\Facades\Log;
+
+try {
+ $ippanelChannel = app(IPPanelChannel::class);
+ $creditData = $ippanelChannel->getCredit();
+ Log::info('IPPanel Account Credit:', $creditData);
+} catch (CouldNotSendNotification $e) {
+ Log::error('Failed to retrieve IPPanel credit: ' . $e->getMessage());
+}
+```
+
+Note: The exact structure of the returned $creditData array depends on the IPPanel API response for the credit check endpoint.
+
+## Handling Errors
+
+If the IPPanel API returns an error, the channel will throw a NotificationChannels\PersianSms\Exceptions\CouldNotSendNotification exception. You can catch this exception to handle errors gracefully:
+
+```bash
+use NotificationChannels\PersianSms\Exceptions\CouldNotSendNotification;
+use App\Notifications\OrderShipped;
+use App\Models\User;
+use Illuminate\Support\Facades\Log;
+
+$user = User::find(1);
+
+try {
+ $user->notify(new OrderShipped($order));
+} catch (CouldNotSendNotification $e) {
+ Log::error('Failed to send IPPanel SMS: ' . $e->getMessage(), [
+ 'exception_code' => $e->getCode(),
+ ]);
+}
+```
+The exception message often contains details from the IPPanel API response.
+
+Available Methods on IPPanelMessage
+
+ * IPPanelMessage::create(string $content = ''): Static factory method to create a simple text message.
+ * content(string $content): Set the message content for a normal SMS.
+ * pattern(string $patternCode, array $variables = []): Set the message to be sent using a pattern.
+ * variable(string $name, $value): (Alternative to passing all variables to pattern()) Set a single variable for a pattern message.
+ * from(string $senderNumber): Set a custom sender number for this message.
+ * at(string $dateTimeString): Set a scheduled send time (ISO 8601 format). Note: API call implementation pending in channel.
+ * isPattern(): bool: Checks if the message is configured as a pattern message.
+
+## Testing
+
+You can run the tests included with the package using:
+
+```bash
+composer test
+```
+
+## Contributing
+
+Please see CONTRIBUTING for details.
+
+## License
+
+The MIT License (MIT). Please see License File for more information.
diff --git a/composer.json b/composer.json
new file mode 100644
index 00000000..09035841
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,52 @@
+{
+ "name": "laravel-notification-channels/persian-sms",
+ "description": "Laravel Notification Channel for sending SMS via various Iranian SMS providers (e.g., IPPanel, Kavenegar).",
+ "homepage": "https://github.com/your-github-username/persian-sms",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Ali Abdi",
+ "email": "abdi9074@gmail.com",
+ "homepage": "https://github.com/saman9074",
+ "role": "Developer"
+ }
+ ],
+ "require": {
+ "php": ">=8.2",
+ "illuminate/notifications": "^10.0 || ^11.0 || ^12.0",
+ "illuminate/support": "^10.0 || ^11.0 || ^12.0",
+ "guzzlehttp/guzzle": "^7.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.6",
+ "phpunit/phpunit": "^10.0",
+ "orchestra/testbench": "^7.0 || ^8.0 || ^9.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "NotificationChannels\\PersianSms\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "NotificationChannels\\PersianSms\\Tests\\": "tests/"
+ }
+ },
+ "scripts": {
+ "test": "vendor/bin/phpunit",
+ "test-coverage": "vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover",
+ "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes"
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "NotificationChannels\\PersianSms\\PersianSmsServiceProvider"
+ ]
+ }
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
+}
diff --git a/config/persian-sms.php b/config/persian-sms.php
new file mode 100644
index 00000000..1385d4c6
--- /dev/null
+++ b/config/persian-sms.php
@@ -0,0 +1,67 @@
+ env('PERSIAN_SMS_DRIVER', 'ippanel'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | SMS Driver Configurations
+ |--------------------------------------------------------------------------
+ |
+ | Here you may configure all of the SMS drivers used by your application.
+ | You are free to add more drivers as needed. Each driver requires its
+ | own set of configuration options.
+ |
+ */
+
+ 'drivers' => [
+
+ 'ippanel' => [
+ 'api_key' => env('IPPANEL_API_KEY'),
+ 'sender_number' => env('IPPANEL_SENDER_NUMBER'), // Your default line number
+ // 'api_url' => 'https://api2.ippanel.com/api/v1', // Optional: if you want to override
+ ],
+
+ 'kavenegar' => [ // Example for a future driver
+ 'api_key' => env('KAVENEGAR_API_KEY'),
+ 'sender_number' => env('KAVENEGAR_SENDER_NUMBER'),
+ // ... other kavenegar specific settings
+ ],
+
+ // Add other drivers here...
+
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Guzzle HTTP Client Options
+ |--------------------------------------------------------------------------
+ |
+ | You can pass any Guzzle-specific request options here.
+ | For example, you might want to set a timeout.
+ | See Guzzle documentation for all available options.
+ |
+ */
+ 'guzzle' => [
+ 'timeout' => 10.0, // Request timeout in seconds
+ // 'connect_timeout' => 5.0,
+ // 'verify' => true, // SSL certificate verification
+ // 'proxy' => 'http://your-proxy.com:port',
+ ],
+
+ ];
+
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 00000000..ccbe8032
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ ./tests
+
+
+
+
+ ./src
+
+
+ ./src/Exceptions
+
+
+
+
+
+
+
+
+
diff --git a/src/Exceptions/CouldNotSendNotification.php b/src/Exceptions/CouldNotSendNotification.php
new file mode 100644
index 00000000..cdd89e22
--- /dev/null
+++ b/src/Exceptions/CouldNotSendNotification.php
@@ -0,0 +1,130 @@
+client = $client;
+ $this->apiKey = $apiKey;
+ $this->defaultSenderNumber = $defaultSenderNumber;
+ }
+
+ /**
+ * Send the given notification.
+ *
+ * @param mixed $notifiable
+ * @param \Illuminate\Notifications\Notification $notification
+ * @return \Psr\Http\Message\ResponseInterface|null
+ * @throws \NotificationChannels\PersianSms\Exceptions\CouldNotSendNotification
+ */
+ public function send($notifiable, Notification $notification)
+ {
+ // Resolve the message from the notification first.
+ $message = $notification->toPersianSms($notifiable);
+
+ if (!$message instanceof IPPanelMessage) {
+ if (is_string($message)) {
+ throw CouldNotSendNotification::invalidMessageObject("Message must be an instance of IPPanelMessage. String given: " . $message);
+ }
+ throw CouldNotSendNotification::invalidMessageObject($message);
+ }
+
+ // Determine the recipient. Prioritize the one set on the message itself.
+ $recipient = $message->recipient ?: $this->getRecipient($notifiable, $notification);
+
+ if (!$recipient) {
+ // No recipient found, do not proceed.
+ return null;
+ }
+
+ // Ensure recipient is an array for IPPanel API
+ $recipients = is_array($recipient) ? $recipient : [$recipient];
+
+ $sender = $message->sender ?: $this->defaultSenderNumber; // Use message sender or default
+
+ if (empty(trim($sender))) {
+ throw CouldNotSendNotification::senderNotProvided();
+ }
+
+ $payload = [];
+ $endpoint = '';
+
+ if ($message->isPattern()) {
+ // Sending with pattern
+ if (empty($message->patternCode)) {
+ throw CouldNotSendNotification::missingPatternCode();
+ }
+ if ($message->variables === null || !is_array($message->variables)) {
+ throw CouldNotSendNotification::invalidPatternVariables();
+ }
+
+ $endpoint = self::API_BASE_URL . self::ENDPOINT_SEND_PATTERN;
+ $payload = [
+ 'code' => $message->patternCode,
+ 'sender' => $sender,
+ 'recipient' => $recipients[0], // Pattern send seems to be for a single recipient based on docs
+ 'variable' => (object) $message->variables, // Ensure it's an object (empty or with data)
+ ];
+ } else {
+ // Sending normal SMS
+ if (empty(trim((string) $message->content))) {
+ throw CouldNotSendNotification::contentNotProvided();
+ }
+ $endpoint = self::API_BASE_URL . self::ENDPOINT_SEND_SINGLE;
+ $payload = [
+ 'recipient' => $recipients,
+ 'sender' => $sender,
+ 'message' => (string) $message->content,
+ ];
+ // Optional: Add 'time' if IPPanelMessage supports it
+ // if ($message->time) {
+ // $payload['time'] = $message->time; // Format: "2025-03-21T09:12:50.824Z"
+ // }
+ }
+
+ try {
+ $response = $this->client->post($endpoint, [
+ 'headers' => [
+ 'apiKey' => $this->apiKey,
+ 'Accept' => 'application/json',
+ 'Content-Type' => 'application/json',
+ ],
+ 'json' => $payload, // Send data as JSON
+ ]);
+
+ $statusCode = $response->getStatusCode();
+ $responseBody = json_decode($response->getBody()->getContents(), true);
+
+ if ($statusCode >= 200 && $statusCode < 300 && isset($responseBody['status']) && $responseBody['status'] === 'OK') {
+ return $response;
+ }
+
+ $errorMessage = $responseBody['errorMessage'] ?? 'Unknown error from IPPanel.';
+ if (is_array($errorMessage)) { // Sometimes error messages are arrays
+ $errorMessage = implode(', ', array_map(
+ function ($v, $k) { return sprintf("%s: %s", $k, implode('|', (array)$v)); },
+ $errorMessage,
+ array_keys($errorMessage)
+ ));
+ }
+ throw CouldNotSendNotification::serviceRespondedWithAnError($errorMessage, $statusCode, $responseBody);
+
+ } catch (RequestException $exception) {
+ $response = $exception->getResponse();
+ $statusCode = $response ? $response->getStatusCode() : 503;
+ $responseBody = $response ? json_decode($response->getBody()->getContents(), true) : null;
+ $errorMessage = $responseBody['errorMessage'] ?? $exception->getMessage();
+ if (is_array($errorMessage)) {
+ $errorMessage = implode(', ', array_map(
+ function ($v, $k) { return sprintf("%s: %s", $k, implode('|',(array)$v)); },
+ $errorMessage,
+ array_keys($errorMessage)
+ ));
+ }
+ throw CouldNotSendNotification::serviceRespondedWithAnError($errorMessage, $statusCode, $responseBody, $exception);
+ } catch (\Exception $exception) {
+ throw CouldNotSendNotification::genericError($exception->getMessage(), $exception);
+ }
+ }
+
+ /**
+ * Get the recipient's phone number(s).
+ *
+ * @param mixed $notifiable
+ * @param \Illuminate\Notifications\Notification $notification
+ * @return string|array|null
+ */
+ protected function getRecipient($notifiable, Notification $notification)
+ {
+ if ($route = $notifiable->routeNotificationFor('persianSms', $notification)) {
+ return $route;
+ }
+ if ($route = $notifiable->routeNotificationFor(static::class, $notification)) {
+ return $route;
+ }
+ if (method_exists($notifiable, 'routeNotificationForIPPanel')) {
+ return $notifiable->routeNotificationForIPPanel($notification);
+ }
+ if (isset($notifiable->phone_number)) {
+ return $notifiable->phone_number;
+ }
+ if (isset($notifiable->mobile)) {
+ return $notifiable->mobile;
+ }
+ if (is_string($notifiable) || (is_array($notifiable) && count(array_filter($notifiable, 'is_string')) === count($notifiable))) {
+ return $notifiable;
+ }
+ return null;
+ }
+
+ /**
+ * (Optional) Method to check account credit.
+ *
+ * @return array|null Parsed JSON response from credit check or null on failure.
+ * @throws CouldNotSendNotification
+ */
+ public function getCredit(): ?array
+ {
+ $endpoint = self::API_BASE_URL . self::ENDPOINT_CHECK_CREDIT;
+
+ try {
+ $response = $this->client->get($endpoint, [
+ 'headers' => [
+ 'apiKey' => $this->apiKey,
+ 'Accept' => 'application/json',
+ ],
+ ]);
+
+ $statusCode = $response->getStatusCode();
+ $responseBody = json_decode($response->getBody()->getContents(), true);
+
+ if ($statusCode === 200 && isset($responseBody['status']) && $responseBody['status'] === 'OK') {
+ return $responseBody['data'];
+ }
+
+ $errorMessage = $responseBody['errorMessage'] ?? 'Failed to retrieve credit information.';
+ throw CouldNotSendNotification::serviceRespondedWithAnError($errorMessage, $statusCode, $responseBody);
+
+ } catch (RequestException $exception) {
+ $response = $exception->getResponse();
+ $statusCode = $response ? $response->getStatusCode() : 503;
+ $responseBody = $response ? json_decode($response->getBody()->getContents(), true) : null;
+ $errorMessage = $responseBody['errorMessage'] ?? $exception->getMessage();
+ throw CouldNotSendNotification::serviceRespondedWithAnError($errorMessage, $statusCode, $responseBody, $exception);
+ } catch (\Exception $exception) {
+ throw CouldNotSendNotification::genericError("Failed to get credit: " . $exception->getMessage(), $exception);
+ }
+ }
+}
diff --git a/src/IPPanel/IPPanelMessage.php b/src/IPPanel/IPPanelMessage.php
new file mode 100644
index 00000000..4b74b59c
--- /dev/null
+++ b/src/IPPanel/IPPanelMessage.php
@@ -0,0 +1,172 @@
+ 'value'].
+ *
+ * @var array|null
+ */
+ public ?array $variables = null;
+
+ /**
+ * The sender number (line number).
+ * If null, the default sender from config will be used.
+ *
+ * @var string|null
+ */
+ public ?string $sender = null;
+
+ /**
+ * (Optional) The scheduled time for sending the SMS.
+ * Format: "YYYY-MM-DDTHH:MM:SSZ" e.g., "2025-03-21T09:12:50.824Z"
+ * Note: IPPanel API documentation for single send shows this,
+ * but it's not yet implemented in IPPanelChannel.
+ *
+ * @var string|null
+ */
+ public ?string $time = null;
+
+ /**
+ * The recipient's phone number(s).
+ * If set, this will override the number from the notifiable model.
+ *
+ * @var string|array|null
+ */
+ public string|array|null $recipient = null;
+
+
+ /**
+ * Create a new message instance for normal SMS.
+ *
+ * @param string $content The text content of the SMS.
+ * @return static
+ */
+ public static function create(string $content = ''): static
+ {
+ return new static($content);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param string $content Initial content for a normal SMS.
+ */
+ public function __construct(string $content = '')
+ {
+ if (!empty($content)) {
+ $this->content($content);
+ }
+ }
+
+ /**
+ * Set the recipient's phone number(s).
+ * Overrides the recipient from the notifiable model.
+ *
+ * @param string|array $recipient A single phone number or an array of phone numbers.
+ * @return $this
+ */
+ public function to(string|array $recipient): self
+ {
+ $this->recipient = $recipient;
+ return $this;
+ }
+
+ /**
+ * Set the content of the SMS.
+ * This is for normal (non-pattern) messages.
+ *
+ * @param string $content
+ * @return $this
+ */
+ public function content(string $content): self
+ {
+ $this->content = $content;
+ $this->patternCode = null; // Ensure it's not a pattern message if content is set
+ $this->variables = null;
+ return $this;
+ }
+
+ /**
+ * Set the message to be sent using a pattern.
+ *
+ * @param string $patternCode The code of the pattern.
+ * @param array $variables Associative array of variables for the pattern.
+ * @return $this
+ */
+ public function pattern(string $patternCode, array $variables = []): self
+ {
+ $this->patternCode = $patternCode;
+ $this->variables = $variables;
+ $this->content = null; // Ensure it's not a normal content message if pattern is set
+ return $this;
+ }
+
+ /**
+ * Set a specific variable for the pattern.
+ *
+ * @param string $name Name of the variable.
+ * @param mixed $value Value of the variable.
+ * @return $this
+ */
+ public function variable(string $name, $value): self
+ {
+ if ($this->variables === null) {
+ $this->variables = [];
+ }
+ $this->variables[$name] = $value;
+ return $this;
+ }
+
+ /**
+ * Set the sender number (line number) for this message.
+ * Overrides the default sender number from the configuration.
+ *
+ * @param string $senderNumber
+ * @return $this
+ */
+ public function from(string $senderNumber): self
+ {
+ $this->sender = $senderNumber;
+ return $this;
+ }
+
+ /**
+ * Set the scheduled time for sending the SMS.
+ * Note: Ensure IPPanelChannel supports this if you use it.
+ *
+ * @param string $dateTimeString Format: "YYYY-MM-DDTHH:MM:SSZ"
+ * @return $this
+ */
+ public function at(string $dateTimeString): self
+ {
+ $this->time = $dateTimeString;
+ return $this;
+ }
+
+ /**
+ * Check if the message is configured to be sent using a pattern.
+ *
+ * @return bool
+ */
+ public function isPattern(): bool
+ {
+ return !empty($this->patternCode);
+ }
+}
diff --git a/src/PersianSmsServiceProvider.php b/src/PersianSmsServiceProvider.php
new file mode 100644
index 00000000..b9065228
--- /dev/null
+++ b/src/PersianSmsServiceProvider.php
@@ -0,0 +1,117 @@
+app->runningInConsole()) {
+ $this->publishes([
+ $configPath => config_path('persian-sms.php'),
+ ], 'persian-sms-config');
+ }
+
+ // Optionally, merge the configuration
+ // This allows users to only define the options they want to override.
+ $this->mergeConfigFrom($configPath, 'persian-sms');
+ }
+
+ /**
+ * Register any application services.
+ *
+ * @return void
+ */
+ public function register(): void
+ {
+ // Bind the IPPanelChannel into the service container.
+ // We'll resolve its dependencies (API key, sender, HttpClient) from the config.
+ $this->app->singleton(IPPanelChannel::class, function ($app) { // IPPanelChannel::class now refers to the imported class
+ $config = $app['config']['persian-sms.drivers.ippanel'];
+
+ if (empty($config['api_key'])) {
+ throw CouldNotSendNotification::apiKeyNotProvided();
+ }
+
+ if (empty($config['sender_number'])) {
+ // You might want a specific exception for this or use a general one
+ throw CouldNotSendNotification::senderNotProvided();
+ }
+
+ return new IPPanelChannel( // This will use the imported IPPanelChannel
+ new HttpClient($app['config']['persian-sms.guzzle'] ?? []), // Pass Guzzle config
+ $config['api_key'],
+ $config['sender_number']
+ // Optionally, you could pass the API URL from config too if needed:
+ // $config['api_url'] ?? 'https://api2.ippanel.com/api/v1'
+ );
+ });
+
+ // Later, when you add more drivers (Kavenegar, etc.), you might have a factory
+ // or a more dynamic way to resolve the active SMS driver channel.
+ // For now, we are explicitly binding IPPanelChannel.
+ //
+ // Example of how you might bind a generic "PersianSmsChannel" that resolves
+ // to the configured driver:
+ /*
+ $this->app->singleton('persian.sms.channel', function ($app) {
+ $config = $app['config']['persian-sms'];
+ $defaultDriver = $config['default_driver'] ?? 'ippanel'; // e.g., 'ippanel', 'kavenegar'
+ $driverConfig = $config['drivers'][$defaultDriver] ?? null;
+
+ if (!$driverConfig) {
+ throw new \InvalidArgumentException("SMS driver [{$defaultDriver}] is not configured.");
+ }
+
+ switch ($defaultDriver) {
+ case 'ippanel':
+ if (empty($driverConfig['api_key']) || empty($driverConfig['sender_number'])) {
+ throw CouldNotSendNotification::apiKeyNotProvided(); // Or more specific
+ }
+ // Ensure you use the correct namespace if you go this route
+ // For example: use NotificationChannels\PersianSms\IPPanel\IPPanelChannel;
+ return new \NotificationChannels\PersianSms\IPPanel\IPPanelChannel(
+ new HttpClient($config['guzzle'] ?? []),
+ $driverConfig['api_key'],
+ $driverConfig['sender_number']
+ );
+ // case 'kavenegar':
+ // // return new KavenegarChannel(...);
+ // break;
+ default:
+ throw new \InvalidArgumentException("Unsupported SMS driver [{$defaultDriver}].");
+ }
+ });
+ */
+ }
+
+ /**
+ * Get the services provided by the provider.
+ * This is used for deferred loading.
+ *
+ * @return array
+ */
+ public function provides(): array
+ {
+ return [
+ IPPanelChannel::class, // This will use the imported class's ::class constant
+ // 'persian.sms.channel', // If you use the generic channel binding
+ ];
+ }
+}
diff --git a/tests/IPPanelChannelTest.php b/tests/IPPanelChannelTest.php
new file mode 100644
index 00000000..c4c217ab
--- /dev/null
+++ b/tests/IPPanelChannelTest.php
@@ -0,0 +1,336 @@
+httpClientMock = Mockery::mock(HttpClient::class);
+ $this->config = $this->app['config'];
+
+ $this->config->set('persian-sms.drivers.ippanel.api_key', 'test_api_key');
+ $this->config->set('persian-sms.drivers.ippanel.sender_number', '+983000123');
+ $this->config->set('persian-sms.guzzle.timeout', 5.0);
+
+ // Re-create the channel directly for this test to inject the mock HttpClient
+ $this->channel = new IPPanelChannel(
+ $this->httpClientMock,
+ $this->config->get('persian-sms.drivers.ippanel.api_key'),
+ $this->config->get('persian-sms.drivers.ippanel.sender_number')
+ );
+ }
+
+ public function tearDown(): void
+ {
+ Mockery::close();
+ parent::tearDown();
+ }
+
+ /**
+ * Override application service providers.
+ *
+ * @param \Illuminate\Foundation\Application $app
+ * @return array
+ */
+ protected function getPackageProviders($app)
+ {
+ return [
+ PersianSmsServiceProvider::class,
+ ];
+ }
+
+ /** @test */
+ public function it_can_send_a_normal_sms_message()
+ {
+ $messageContent = 'Test normal SMS content';
+ $recipientNumber = '+989120000001';
+ $senderNumber = $this->config->get('persian-sms.drivers.ippanel.sender_number');
+ $apiKey = $this->config->get('persian-sms.drivers.ippanel.api_key');
+
+ $expectedPayload = [
+ 'recipient' => [$recipientNumber],
+ 'sender' => $senderNumber,
+ 'message' => $messageContent,
+ ];
+
+ $mockedHttpResponse = new HttpResponse(200, [], json_encode(['status' => 'OK', 'data' => ['message_id' => '12345']]));
+
+ $this->httpClientMock
+ ->shouldReceive('post')
+ ->once()
+ ->with(
+ 'https://api2.ippanel.com/api/v1/sms/send/webservice/single',
+ [
+ 'headers' => [
+ 'apiKey' => $apiKey,
+ 'Accept' => 'application/json',
+ 'Content-Type' => 'application/json',
+ ],
+ 'json' => $expectedPayload,
+ ]
+ )
+ ->andReturn($mockedHttpResponse);
+
+ $notification = new TestNotificationWithMessage($messageContent);
+ $notifiable = new TestNotifiable(['persianSms' => $recipientNumber]);
+
+ $response = $this->channel->send($notifiable, $notification);
+
+ // Add PHPUnit assertion
+ $this->assertInstanceOf(HttpResponse::class, $response);
+ $this->assertSame($mockedHttpResponse, $response);
+ $this->assertEquals(200, $response->getStatusCode());
+ }
+
+ /** @test */
+ public function it_can_send_a_pattern_based_sms_message()
+ {
+ $patternCode = 'test_pattern_code';
+ $variables = ['name' => 'John Doe', 'code' => '12345'];
+ $recipientNumber = '+989120000002';
+ $senderNumber = $this->config->get('persian-sms.drivers.ippanel.sender_number');
+ $apiKey = $this->config->get('persian-sms.drivers.ippanel.api_key');
+
+ $expectedPayload = [
+ 'code' => $patternCode,
+ 'sender' => $senderNumber,
+ 'recipient' => $recipientNumber,
+ 'variable' => (object) $variables,
+ ];
+
+ $mockedHttpResponse = new HttpResponse(200, [], json_encode(['status' => 'OK', 'data' => ['message_id' => '67890']]));
+
+ $this->httpClientMock
+ ->shouldReceive('post')
+ ->once()
+ ->with(
+ 'https://api2.ippanel.com/api/v1/sms/pattern/normal/send',
+ [
+ 'headers' => [
+ 'apiKey' => $apiKey,
+ 'Accept' => 'application/json',
+ 'Content-Type' => 'application/json',
+ ],
+ 'json' => $expectedPayload,
+ ]
+ )
+ ->andReturn($mockedHttpResponse);
+
+ $notification = new TestNotificationWithPattern($patternCode, $variables);
+ $notifiable = new TestNotifiable(['persianSms' => $recipientNumber]);
+
+ $response = $this->channel->send($notifiable, $notification);
+
+ // Add PHPUnit assertion
+ $this->assertInstanceOf(HttpResponse::class, $response);
+ $this->assertSame($mockedHttpResponse, $response);
+ $this->assertEquals(200, $response->getStatusCode());
+ }
+
+ /** @test */
+ public function it_throws_exception_if_api_key_is_missing_when_resolved_via_provider()
+ {
+ $this->expectException(CouldNotSendNotification::class);
+ $this->expectExceptionMessage('IPPanel API key is missing or not configured.');
+
+ $this->config->set('persian-sms.drivers.ippanel.api_key', null);
+ $this->app->make(IPPanelChannel::class); // Attempt to resolve from container
+ }
+
+ /** @test */
+ public function it_throws_exception_if_sender_number_is_missing_when_resolved_via_provider()
+ {
+ $this->expectException(CouldNotSendNotification::class);
+ $this->expectExceptionMessage('Sender (originator/from number) was not provided in message or configuration.');
+
+ $this->config->set('persian-sms.drivers.ippanel.sender_number', null);
+ $this->app->make(IPPanelChannel::class); // Attempt to resolve from container
+ }
+
+
+ /** @test */
+ public function it_throws_exception_on_http_error_from_service()
+ {
+ $this->expectException(CouldNotSendNotification::class);
+ // Adjusted to match the actual error message format from CouldNotSendNotification::serviceRespondedWithAnError
+ $this->expectExceptionMessage('SMS service responded with an error: "PERMISSION_DENIED" (Status Code: 401)');
+
+
+ $errorResponseBody = ['status' => 'Error', 'code' => 401, 'errorMessage' => 'PERMISSION_DENIED'];
+ $mockedErrorHttpResponse = new HttpResponse(401, [], json_encode($errorResponseBody));
+
+ $this->httpClientMock
+ ->shouldReceive('post')
+ ->once()
+ ->andReturn($mockedErrorHttpResponse);
+
+ $notification = new TestNotificationWithMessage('test content');
+ $notifiable = new TestNotifiable(['persianSms' => '+989000000000']);
+
+ $this->channel->send($notifiable, $notification);
+ }
+
+ /** @test */
+ public function it_uses_sender_from_message_if_provided()
+ {
+ $messageContent = 'Test with custom sender';
+ $customSender = '+983000789';
+ $recipientNumber = '+989120000003';
+ $apiKey = $this->config->get('persian-sms.drivers.ippanel.api_key');
+
+ $expectedPayload = [
+ 'recipient' => [$recipientNumber],
+ 'sender' => $customSender,
+ 'message' => $messageContent,
+ ];
+ $mockedHttpResponse = new HttpResponse(200, [], json_encode(['status' => 'OK', 'data' => ['message_id' => '11223']]));
+
+ $this->httpClientMock
+ ->shouldReceive('post')
+ ->once()
+ ->with(
+ 'https://api2.ippanel.com/api/v1/sms/send/webservice/single', // Be specific with URL if possible
+ Mockery::on(function ($argument) use ($expectedPayload, $apiKey) {
+ return $argument['json'] == $expectedPayload &&
+ isset($argument['headers']['apiKey']) &&
+ $argument['headers']['apiKey'] == $apiKey;
+ })
+ )
+ ->andReturn($mockedHttpResponse);
+
+ $notification = new TestNotificationWithMessageAndCustomSender($messageContent, $customSender);
+ $notifiable = new TestNotifiable(['persianSms' => $recipientNumber]);
+
+ $response = $this->channel->send($notifiable, $notification);
+
+ // Add PHPUnit assertion
+ $this->assertInstanceOf(HttpResponse::class, $response);
+ $this->assertSame($mockedHttpResponse, $response);
+ $this->assertEquals(200, $response->getStatusCode());
+ }
+
+ /** @test */
+ public function it_throws_exception_if_message_is_not_ippanel_message_and_not_string()
+ {
+ $this->expectException(CouldNotSendNotification::class);
+ // Corrected expected message for array input
+ $this->expectExceptionMessage('The message object provided was invalid. Expected an instance of IPPanelMessage or a string, got Unknown.');
+
+
+ $notification = new TestNotificationWithInvalidMessageObject();
+ $notifiable = new TestNotifiable(['persianSms' => '+989120000000']);
+
+ $this->channel->send($notifiable, $notification);
+ }
+
+ /** @test */
+ public function it_throws_exception_if_pattern_code_is_missing_for_pattern_message()
+ {
+ $this->expectException(CouldNotSendNotification::class);
+ // Corrected expected message based on current channel logic
+ $this->expectExceptionMessage('SMS content was not provided for a normal message.');
+
+ $notification = new TestNotificationWithPattern(null, ['var' => 'val']);
+ $notifiable = new TestNotifiable(['persianSms' => '+989120000000']);
+
+ $this->channel->send($notifiable, $notification);
+ }
+
+ /** @test */
+ public function it_throws_exception_if_content_is_empty_for_normal_message()
+ {
+ $this->expectException(CouldNotSendNotification::class);
+ $this->expectExceptionMessage('SMS content was not provided for a normal message.');
+
+ $notification = new TestNotificationWithMessage('');
+ $notifiable = new TestNotifiable(['persianSms' => '+989120000000']);
+
+ $this->channel->send($notifiable, $notification);
+ }
+}
+
+// --- Helper classes for testing (Ensure these are correctly namespaced if in separate files) ---
+
+class TestNotifiable
+{
+ use \Illuminate\Notifications\Notifiable;
+
+ protected array $routes = [];
+
+ public function __construct(array $routes = [])
+ {
+ $this->routes = $routes;
+ }
+
+ public function routeNotificationFor($driver, $notification = null)
+ {
+ return $this->routes[$driver] ?? ($this->routes['*'] ?? null);
+ }
+ public function getKey() { return '1'; }
+}
+
+class TestNotificationWithMessage extends Notification
+{
+ public string $message;
+ public function __construct(string $message) { $this->message = $message; }
+ public function via($notifiable): array { return [IPPanelChannel::class]; }
+ public function toPersianSms($notifiable): IPPanelMessage { return (new IPPanelMessage())->content($this->message); }
+}
+
+class TestNotificationWithPattern extends Notification
+{
+ public ?string $patternCode;
+ public array $variables;
+ public function __construct(?string $patternCode, array $variables) { $this->patternCode = $patternCode; $this->variables = $variables; }
+ public function via($notifiable): array { return [IPPanelChannel::class]; }
+ public function toPersianSms($notifiable): IPPanelMessage
+ {
+ $message = new IPPanelMessage();
+ if ($this->patternCode !== null) {
+ $message->pattern($this->patternCode, $this->variables);
+ } else {
+ // This setup makes isPattern() return false, leading to normal SMS path
+ $message->patternCode = null;
+ $message->variables = $this->variables;
+ // $message->content remains null
+ }
+ return $message;
+ }
+}
+
+class TestNotificationWithMessageAndCustomSender extends Notification
+{
+ public string $message;
+ public string $sender;
+ public function __construct(string $message, string $sender) { $this->message = $message; $this->sender = $sender; }
+ public function via($notifiable): array { return [IPPanelChannel::class]; }
+ public function toPersianSms($notifiable): IPPanelMessage { return (new IPPanelMessage())->content($this->message)->from($this->sender); }
+}
+
+class TestNotificationWithInvalidMessageObject extends Notification
+{
+ public function via($notifiable): array { return [IPPanelChannel::class]; }
+ public function toPersianSms($notifiable) { return ['this is not an IPPanelMessage object']; } // Intentionally wrong
+}
+
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 00000000..682b6dcd
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,61 @@
+loadMigrationsFrom(__DIR__ . '/../database/migrations');
+ // $this->artisan('migrate', ['--database' => 'testing'])->run();
+ }
+
+ /**
+ * Get package providers.
+ *
+ * @param \Illuminate\Foundation\Application $app
+ * @return array
+ */
+ protected function getPackageProviders($app)
+ {
+ return [
+ PersianSmsServiceProvider::class,
+ ];
+ }
+
+ /**
+ * Define environment setup.
+ *
+ * @param \Illuminate\Foundation\Application $app
+ * @return void
+ */
+ protected function getEnvironmentSetUp($app)
+ {
+ // Setup default database to use sqlite :memory:
+ /*
+ $app['config']->set('database.default', 'testing');
+ $app['config']->set('database.connections.testing', [
+ 'driver' => 'sqlite',
+ 'database' => ':memory:',
+ 'prefix' => '',
+ ]);
+ */
+
+ // You can set specific config values for your tests here if needed,
+ // though phpunit.xml's variables often take precedence for env() calls in config files.
+ // Example:
+ // $app['config']->set('persian-sms.default_driver', 'ippanel');
+ // $app['config']->set('persian-sms.drivers.ippanel.api_key', 'config_test_api_key');
+ // $app['config']->set('persian-sms.drivers.ippanel.sender_number', '+98configsender');
+ }
+}