Skip to content

Commit

Permalink
Merge pull request #10 from skoro/4-external-services-notifications
Browse files Browse the repository at this point in the history
Subscribers and notifications
  • Loading branch information
skoro authored Sep 4, 2024
2 parents ef7de59 + 56edf49 commit cd83cda
Show file tree
Hide file tree
Showing 53 changed files with 1,972 additions and 32 deletions.
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,45 @@ _TBD: make docker installation_
```

## API documentation
Two end-points are available for getting the API documentation:
Two end-points are available for getting the API documentation:
- `/api/doc` swagger ui.
- `/api/doc.json`
as above but in Json format
for consuming by [Postman](https://www.postman.com/product/what-is-postman/), for example.

## Order status notifications
A subscription allows you to get notifications when the order status has been changed.

### Channels and messages
A notification sends via channel. By default, `http` channel is available.
`http` does a POST/PUT/PATCH request to a predefined url with json payload.

A message is used to represent data in the channel. There is `simple` message type, which
will turn an order and payment gateway response into a plain array.

To get a list of available channels and messages, use `subscribe:channels` console command.

If you want to develop your own channel and/or message, add tag `app.subscriber.channel`
for your custom channel and `app.subscriber.message` for a custom message (take a look
at `services.yml`).

### Subscribe example
This is how to subscribe your external service when order status is changed to `payment_received`.
```shell
console subscriber:add-http --order-status payment_received --channel-message simple https://backend.my-service.com/api/order-payment
```

When the order gets `payment_received` status due to payment gateway response,
a POST http request will be sent to https://backend.my-service.com/api/order-payment end point.
The request will contain json data like this:
```json
{
"order_num": "1234567890",
"order_status": "payment_received",
"success": true,
"transaction_id": "1234567890",
"response": {
// original response from the payment gateway.
}
}
```
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
}
},
"require-dev": {
"dama/doctrine-test-bundle": "^8.2",
"doctrine/doctrine-fixtures-bundle": "^3.6",
"fakerphp/faker": "^1.23",
"phpunit/phpunit": "^9.5",
Expand Down
69 changes: 68 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions config/bundles.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
];
5 changes: 5 additions & 0 deletions config/packages/dama_doctrine_test_bundle.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
when@test:
dama_doctrine_test:
enable_static_connection: true
enable_static_meta_data_cache: true
enable_static_query_cache: true
17 changes: 9 additions & 8 deletions config/packages/messenger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ framework:

transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
# async: '%env(MESSENGER_TRANSPORT_DSN)%'
async: '%env(MESSENGER_TRANSPORT_DSN)%'
# failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'

routing:
# Route your messages to the transports
App\Message\NotifySubscriber: async

# when@test:
# framework:
# messenger:
# transports:
# # replace with your transport name here (e.g., my_transport: 'in-memory://')
# # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test
# async: 'in-memory://'
when@test:
framework:
messenger:
transports:
# replace with your transport name here (e.g., my_transport: 'in-memory://')
# For more Messenger testing tools, see https://github.com/zenstruck/messenger-test
async: 'in-memory://'
8 changes: 8 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ services:
App\Order\OrderTotalAmountCalculator:
arguments:
$currencyCode: 'UAH'

App\Subscriber\Channel\HttpNotificationChannel:
tags:
- { name: 'app.subscriber.channel', type: 'http' }

App\Subscriber\Channel\SimpleArrayChannelMessage:
tags:
- { name: 'app.subscriber.message', type: 'simple' }
44 changes: 44 additions & 0 deletions migrations/Version20240901095458.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240901095458 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE subscribers (
id INT AUTO_INCREMENT NOT NULL,
uuid BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\',
hash VARCHAR(255) NOT NULL,
channel_type VARCHAR(255) NOT NULL,
order_status VARCHAR(255) NOT NULL,
channel_message VARCHAR(255) NOT NULL,
params JSON NOT NULL,
created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
UNIQUE INDEX UNIQ_2FCD16ACD17F50A6 (uuid),
UNIQUE INDEX UNIQ_2FCD16ACD1B862B8 (hash),
INDEX IDX_2FCD16ACB88F75C9 (order_status),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE subscribers');
}
}
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@
</listeners>

<extensions>
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" />
</extensions>
</phpunit>
2 changes: 1 addition & 1 deletion src/Command/PaymentGatewaysCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
name: 'payment:gateways',
description: 'List of available payment gateways',
)]
class PaymentGatewaysCommand extends Command
final class PaymentGatewaysCommand extends Command
{
public function __construct(
private readonly PaymentGatewayRegistryInterface $paymentGateways,
Expand Down
88 changes: 88 additions & 0 deletions src/Command/SubscriberAddHttpCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace App\Command;

use App\Entity\OrderStatus;
use App\Subscriber\Action\AddHttpSubscriberAction;
use App\Subscriber\Channel\HttpNotificationChannel;
use Exception;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
name: 'subscriber:add-http',
description: 'Add a subscriber of HTTP channel notification',
)]
final class SubscriberAddHttpCommand extends Command
{
public function __construct(
private readonly AddHttpSubscriberAction $addHttpSubscriberAction,
) {
parent::__construct();
}

protected function configure(): void
{
$this
->addArgument('url', InputArgument::REQUIRED, 'Url to call.')
->addOption(
'http-method',
'X',
InputOption::VALUE_OPTIONAL,
'Http request method',
HttpNotificationChannel::DEFAULT_HTTP_METHOD,
)
->addOption(
'order-status',
's',
InputOption::VALUE_REQUIRED,
'Expected order status: ' . OrderStatus::formattedString(),
''
)
->addOption(
'channel-message',
'm',
InputOption::VALUE_REQUIRED,
'Channel message type (see output of "subscriber:channels" for available message types)',
''
)
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$url = $input->getArgument('url');
$httpMethod = $input->getOption('http-method');
$orderStatusValue = $input->getOption('order-status');
$channelMessage = $input->getOption('channel-message');

try {
if (! ($orderStatus = OrderStatus::tryFrom($orderStatusValue))) {
throw new Exception(
sprintf('Invalid order status: "%s". Expected: %s.',
$orderStatusValue,
OrderStatus::formattedString()
)
);
}

$subscriber = $this->addHttpSubscriberAction->add($orderStatus, $url, $channelMessage, $httpMethod);

$io->success("Subscriber \"{$subscriber->getUuid()}\" has been added.");
} catch (\Throwable $e) {
$io->error($e->getMessage());
return Command::FAILURE;
}

return Command::SUCCESS;
}
}
49 changes: 49 additions & 0 deletions src/Command/SubscriberChannelsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace App\Command;

use App\Subscriber\Channel\NotificationChannelCollection;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
name: 'subscriber:channels',
description: 'Show a list of registered subscriber notification channels and messages',
)]
final class SubscriberChannelsCommand extends Command
{
public function __construct(
private readonly NotificationChannelCollection $channelCollection,
) {
parent::__construct();
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$channelTypes = $this->channelCollection->getNotificationChannelTypes();
$messageTypes = $this->channelCollection->getMessageTypes();

if ($channelTypes) {
$io->title('Notification channels:');
$io->block($channelTypes);
} else {
$io->warning('No subscriber notification channels found.');
}

if ($messageTypes) {
$io->title('Channel messages:');
$io->block($messageTypes);
} else {
$io->warning('No channel messages found.');
}

return Command::SUCCESS;
}
}
Loading

0 comments on commit cd83cda

Please sign in to comment.