Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea/
/vendor
build
composer.phar
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

All notable changes to `CMSMS` will be documented in this file

## [4.0.0] - 2025-02-??
#### Changed
- Moved from XML to JSON for the request body
- Changed CM endpoint
#### Added
- Added config value for encoding detection type
- Two events for success and failure: `SMSSentSuccessfullyEvent` and `SMSSendingFailedEvent`

## [3.3.0] - 2024-03-22
#### Added
- Laravel 11 support

## [3.3.0] - 2024-03-22
#### Changed
- Update CM endpoint by @marventhieme

## [3.2.0] - 2023-03-29
#### Changed
- Added support for Laravel 10.0 (#17) by @charleskoko
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,14 @@ Add your CMSMS Product Token and default originator (name or number of sender) t
'cmsms' => [
'product_token' => env('CMSMS_PRODUCT_TOKEN'),
'originator' => env('CMSMS_ORIGINATOR'),
'encoding_detection_type' => env('CMSMS_ENCODING_DETECTION_TYPE', 'AUTO'),
],
...
```

Notice: The originator can contain a maximum of 11 alphanumeric characters.
Notice:
- The originator can contain a maximum of 11 alphanumeric characters.
- Read about encoding detection here: https://developers.cm.com/messaging/docs/sms#auto-detect-encoding

## Usage

Expand Down Expand Up @@ -105,6 +108,11 @@ public function routeNotificationForCmsms()
- `tariff()`: Accepts a integer value for your message tariff. The unit is eurocent. Requires the `originator` to be set to a specific value. Contact CM for this tariff value. CM also must enable this feature for your contract manually.
- `multipart($minimum, $maximum)`: Accepts a 0 to 8 integer range which allows multipart messages. See the [documentation from CM](https://dashboard.onlinesmsgateway.com/docs#send-a-message-multipart) for more information.

### Available events
- `SMSSentSuccessfullyEvent`: This event will be fired after the message was sent. The event will contain the payload we have sent to CM.
- `SMSSendingFailedEvent`: This event will be fired if the message was not sent. The event will contain the response body we received from CM.


## Changelog

Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
Expand Down
85 changes: 66 additions & 19 deletions src/CmsmsClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,105 @@
namespace NotificationChannels\Cmsms;

use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Arr;
use NotificationChannels\Cmsms\Events\SMSSendingFailedEvent;
use NotificationChannels\Cmsms\Events\SMSSentSuccessfullyEvent;
use NotificationChannels\Cmsms\Exceptions\CouldNotSendNotification;
use SimpleXMLElement;
use NotificationChannels\Cmsms\Exceptions\InvalidMessage;

class CmsmsClient
{
public const GATEWAY_URL = 'https://gw.messaging.cm.com/gateway.ashx';
public const GATEWAY_URL = 'https://gw.cmtelecom.com/v1.0/message';

public function __construct(
protected GuzzleClient $client,
protected string $productToken,
) {
}

/**
* @throws InvalidMessage
* @throws GuzzleException
* @throws CouldNotSendNotification
*/
public function send(CmsmsMessage $message, string $recipient): void
{
if (is_null(Arr::get($message->toXmlArray(), 'FROM'))) {
if (empty($message->getOriginator())) {
$message->originator(config('services.cmsms.originator'));
}

$payload = $this->buildMessageJson($message, $recipient);

$response = $this->client->request('POST', static::GATEWAY_URL, [
'body' => $this->buildMessageXml($message, $recipient),
'body' => $payload,
'headers' => [
'Content-Type' => 'application/xml',
'accept' => 'application/json',
'content-type' => 'application/json',
],
]);

// API returns an empty string on success
// On failure, only the error string is passed
/**
* If error code is 0, the message was sent successfully.
*/
$body = $response->getBody()->getContents();
if (! empty($body)) {
$errorCode = Arr::get(json_decode($body, true), 'errorCode');
if ($errorCode !== 0) {
SMSSendingFailedEvent::dispatch($body);

throw CouldNotSendNotification::serviceRespondedWithAnError($body);
}

SMSSentSuccessfullyEvent::dispatch($payload);
}

public function buildMessageXml(CmsmsMessage $message, string $recipient): string
/**
* See: https://developers.cm.com/messaging/reference/messages_sendmessage-1
*/
public function buildMessageJson(CmsmsMessage $message, string $recipient): string
{
$xml = new SimpleXMLElement('<MESSAGES/>');
$encodingDetectionType = config('services.cmsms.encoding_detection_type', 'AUTO');

$xml->addChild('AUTHENTICATION')
->addChild('PRODUCTTOKEN', $this->productToken);
$body = [];
$body['content'] = $message->getBody();
if (strtoupper($encodingDetectionType) === 'AUTO') {
$body['type'] = 'AUTO';
}

if ($tariff = $message->getTariff()) {
$xml->addChild('TARIFF', (string) $tariff);
$minimumNumberOfMessageParts = [];
if ($message->getMinimumNumberOfMessageParts() !== null) {
$minimumNumberOfMessageParts['minimumNumberOfMessageParts'] = $message->getMinimumNumberOfMessageParts();
}
$maximumNumberOfMessageParts = [];
if ($message->getMaximumNumberOfMessageParts() !== null) {
$maximumNumberOfMessageParts['maximumNumberOfMessageParts'] = $message->getMaximumNumberOfMessageParts();
}

$msg = $xml->addChild('MSG');
foreach ($message->toXmlArray() as $name => $value) {
$msg->addChild($name, (string) $value);
$reference = [];
if ($message->getReference() !== null) {
$reference['reference'] = $message->getReference();
}
$msg->addChild('TO', $recipient);

return $xml->asXML();
$json = [
'messages' => [
'authentication' => [
'productToken' => $this->productToken,
],
'tariff' => $message->getTariff(),
'msg' => [[
'body' => $body,
'to' => [[
'number' => $recipient,
]],
'dcs' => strtoupper($encodingDetectionType) === 'AUTO' ? 0 : $encodingDetectionType,
'from' => $message->getOriginator(),
...$minimumNumberOfMessageParts,
...$maximumNumberOfMessageParts,
...$reference,
]],
],
];

return json_encode($json);
}
}
30 changes: 22 additions & 8 deletions src/CmsmsMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public function body(string $body): self
return $this;
}

public function getBody(): string
{
return $this->body;
}

public function originator(string|int $originator): self
{
if (empty($originator) || strlen($originator) > 11) {
Expand All @@ -42,6 +47,11 @@ public function originator(string|int $originator): self
return $this;
}

public function getOriginator(): string
{
return $this->originator;
}

public function reference(string $reference): self
{
if (empty($reference) || strlen($reference) > 32 || ! ctype_alnum($reference)) {
Expand All @@ -53,6 +63,11 @@ public function reference(string $reference): self
return $this;
}

public function getReference(): string
{
return $this->reference;
}

public function tariff(int $tariff): self
{
$this->tariff = $tariff;
Expand All @@ -77,15 +92,14 @@ public function multipart(int $minimum, int $maximum): self
return $this;
}

public function toXmlArray(): array
public function getMinimumNumberOfMessageParts(): ?int
{
return $this->minimumNumberOfMessageParts;
}

public function getMaximumNumberOfMessageParts(): ?int
{
return array_filter([
'BODY' => $this->body,
'FROM' => $this->originator,
'REFERENCE' => $this->reference,
'MINIMUMNUMBEROFMESSAGEPARTS' => $this->minimumNumberOfMessageParts,
'MAXIMUMNUMBEROFMESSAGEPARTS' => $this->maximumNumberOfMessageParts,
]);
return $this->maximumNumberOfMessageParts;
}

public static function create(string $body = ''): self
Expand Down
14 changes: 14 additions & 0 deletions src/Events/SMSSendingFailedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace NotificationChannels\Cmsms\Events;

use Illuminate\Foundation\Events\Dispatchable;

class SMSSendingFailedEvent
{
use Dispatchable;

public function __construct(public string $response)
{
}
}
14 changes: 14 additions & 0 deletions src/Events/SMSSentSuccessfullyEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace NotificationChannels\Cmsms\Events;

use Illuminate\Foundation\Events\Dispatchable;

class SMSSentSuccessfullyEvent
{
use Dispatchable;

public function __construct(public string $payload)
{
}
}
84 changes: 76 additions & 8 deletions tests/CmsmsClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Illuminate\Support\Facades\Event;
use Mockery;
use NotificationChannels\Cmsms\CmsmsClient;
use NotificationChannels\Cmsms\CmsmsMessage;
use NotificationChannels\Cmsms\Events\SMSSendingFailedEvent;
use NotificationChannels\Cmsms\Events\SMSSentSuccessfullyEvent;
use NotificationChannels\Cmsms\Exceptions\CouldNotSendNotification;
use Orchestra\Testbench\TestCase;

Expand Down Expand Up @@ -43,7 +46,7 @@ public function it_can_send_message()
$this->guzzle
->shouldReceive('request')
->once()
->andReturn(new Response(200, [], ''));
->andReturn(new Response(200, [], '{"details": "Created 1 message(s)", "errorCode": 0}'));

$this->client->send($this->message, '00301234');
}
Expand All @@ -62,7 +65,7 @@ public function it_sets_a_default_originator_if_none_is_set()
$this->guzzle
->shouldReceive('request')
->once()
->andReturn(new Response(200, [], ''));
->andReturn(new Response(200, [], '{"details": "Created 1 message(s)", "errorCode": 0}'));

$this->client->send($message, '00301234');
}
Expand All @@ -75,21 +78,86 @@ public function it_throws_exception_on_error_response()
$this->guzzle
->shouldReceive('request')
->once()
->andReturn(new Response(200, [], 'error'));
->andReturn(new Response(200, [], '{"details": "Some error message", "errorCode": 1}'));

$this->client->send($this->message, '00301234');
}

/** @test */
public function it_includes_tariff_in_xml()
public function it_includes_tariff_data()
{
$message = clone $this->message;
$message->tariff(20);

$messageXml = $this->client->buildMessageXml($message, '00301234');
$parsedXml = simplexml_load_string($messageXml);
$messageJson = $this->client->buildMessageJson($message, '00301234');

$this->assertFalse(empty($parsedXml->TARIFF));
$this->assertEquals(20, (int) $parsedXml->TARIFF);
$messageJsonObject = json_decode($messageJson);

$this->assertTrue(isset($messageJsonObject->messages->tariff));
$this->assertEquals(20, $messageJsonObject->messages->tariff);
}

/** @test */
public function it_includes_multipart_data()
{
$message = clone $this->message;
$message->multipart(2, 6);

$messageJson = $this->client->buildMessageJson($message, '00301234');

$messageJsonObject = json_decode($messageJson);

$this->assertTrue(isset($messageJsonObject->messages->msg[0]->minimumNumberOfMessageParts));
$this->assertEquals(2, $messageJsonObject->messages->msg[0]->minimumNumberOfMessageParts);
$this->assertTrue(isset($messageJsonObject->messages->msg[0]->minimumNumberOfMessageParts));
$this->assertEquals(6, $messageJsonObject->messages->msg[0]->maximumNumberOfMessageParts);
}

/** @test */
public function it_includes_reference_data()
{
$message = clone $this->message;
$message->reference('ABC');

$messageJson = $this->client->buildMessageJson($message, '00301234');

$messageJsonObject = json_decode($messageJson);

$this->assertTrue(isset($messageJsonObject->messages->msg[0]->reference));
$this->assertEquals('ABC', $messageJsonObject->messages->msg[0]->reference);
}

/** @test */
public function it_dispatches_a_success_event()
{
Event::fake();

$this->guzzle
->shouldReceive('request')
->once()
->andReturn(new Response(200, [], '{"details": "Created 1 message(s)", "errorCode": 0}'));

$this->client->send($this->message, '00301234');

Event::assertDispatched(SMSSentSuccessfullyEvent::class);
}

/** @test */
public function it_dispatches_a_failure_event()
{
Event::fake();

$this->guzzle
->shouldReceive('request')
->once()
->andReturn(new Response(200, [], '{"details": "Some error message", "errorCode": 1}'));

try {
$this->client->send($this->message, '00301234');
} catch (CouldNotSendNotification $e) {
// Do nothing, we know about the exception
}

Event::assertDispatched(SMSSendingFailedEvent::class);
}
}
Loading