Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/acp 3507/acp 3746 persist webhooks and allow to run them later #7

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0481050
ACP-3746 Added persistence for WebhookRequests.
stereomon Aug 21, 2024
69dac60
ACP-3764 Added strict flag to transfer definition.
stereomon Aug 21, 2024
048da60
ACP-3746 Only delete webhook inbox entity when webhook is successful.
stereomon Aug 21, 2024
369ad69
Merge branch 'feature/acp-3507/acp-3746-persist-webhooks-and-allow-to…
stereomon Aug 21, 2024
1e62e66
ACP-3746 Removed log file.
stereomon Aug 22, 2024
bdfc991
ACP-3746 Added log file to ignore.
stereomon Aug 22, 2024
68679c1
ACP-3746 Cache file.
stereomon Aug 22, 2024
153dcf3
ACP-3746 Added cache to ignore.
stereomon Aug 22, 2024
35163b3
ACP-3507 SprykerCV fixes.
stereomon Sep 11, 2024
3021698
ACP-3507 CI Fixes.
stereomon Sep 11, 2024
265c54c
ACP-3507 Applied CR fixes.
stereomon Sep 11, 2024
abd8277
ACP-3636 Added docs.
stereomon Sep 11, 2024
c457da4
ACP-3507 Added logger.
stereomon Sep 16, 2024
f7a6dc1
ACP-3839 Added Facade method to be able to delete persisted webhooks.
stereomon Sep 17, 2024
f017410
ACP-3507 Fixes after CR.
stereomon Sep 18, 2024
bf228d4
ACP-3507 Removed commented line.
stereomon Sep 25, 2024
bd21b6a
ACP-3507 Updated min PHP Version.
stereomon Sep 25, 2024
57cfacd
ACP-3507 Updated batch in README.md
stereomon Sep 26, 2024
56fe060
Update README.md
matweew Sep 30, 2024
358ed7c
ACP-3507 Updated README.md
stereomon Sep 30, 2024
8cf6ed2
Merge branch 'feature/acp-3507/acp-3746-persist-webhooks-and-allow-to…
stereomon Sep 30, 2024
65afb78
Merge branch 'feature/acp-3507/acp-3636-add-update-documentations' of…
stereomon Sep 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ jobs:
- name: Composer install
run: composer install --optimize-autoloader

- name: Setup Database
run: |
mkdir -p tests/_data
touch tests/_data/app_webhook_db

- name: Setup AppWebhook
run: composer setup

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ composer.lock
src/Generated
src/Orm
tests/_data/app_webhook_db
data/DE/logs/ZED/propel.log

data/cache/

# tests
tests/**/_generated/
Expand Down
71 changes: 68 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# AppWebhook Package
# AppWebhook Module
[![Latest Stable Version](https://poser.pugx.org/spryker/app-webhook/v/stable.svg)](https://packagist.org/packages/spryker/app-webhook)
[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.1-8892BF.svg)](https://php.net/)
[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.2-8892BF.svg)](https://php.net/)

Provides SyncAPI and AsyncAPI schema files and the needed code to make the Mini-Framework an App.
Provides SyncAPI and AsyncAPI schema files and the needed code to make the Mini-Framework an App with Webhook capabilities.

## Installation

Expand Down Expand Up @@ -36,3 +36,68 @@ With these commands you've set up the AppWebhook and can start the tests
vendor/bin/codecept build
vendor/bin/codecept run
```

### Documentation

### Webhook handling

This package is responsible to receive and handle webhooks. The package provides a controller that can be used to handle incoming webhooks.

The API endpoint is `/webhooks` and the controller is `WebhooksController` inside the Glue Application. This package is not handling webhooks on its own, you must implement the logic to handle the webhooks via the provided `\Spryker\Zed\AppWebhook\Dependency\Plugin\WebhookHandlerPluginInterface`, see the description down below.

#### Process in a Nutshell

- A webhook is received by the `WebhooksController` and the `WebhookRequestTransfer` is created.
- The webhook content is mapped to a `WebhookRequestTransfer`.
- The `WebhookRequestTransfer` is passed together with a `WebhookResponseTransfer` to the `\Spryker\Glue\AppWebhookBackendApi\Dependency\Facade\AppWebhookBackendApiToAppWebhookFacadeInterface::handleWebhook()` method.
- The `handleWebhook` method does:
- Creates an identifier for this specific webhook.
- When it is not a retried webhook the webhook will be persisted in the database.
- In case of a retried webhook the webhook will be fetched from the database.
- When the number of retries exceeds the configurable allowed number of retries the webhook will be removed from the database and an exception will be thrown.
- Find the correct handler for the webhook and call the `handle` method of the handler.
- When the handler returns a failed `WebhookResponseTransfer` the response will be persisted in the database.
- When the handler returns a not handled `WebhookResponseTransfer` the response will be persisted in the database with a message that was provided by the implementation of the plugin.
- When the handler throws an exception the exception message will be persisted in the database.
- When the handler returns a successful `WebhookResponseTransfer` the webhook will be removed from the database.
- It returns the `WebhookResponseTransfer` to the controller.
- The controller formats the `WebhookResponseTransfer` into a Glue response which will be either:
- 200 OK in case everything went well.
- 400 BAD REQUEST in case of a failed response.

#### Retry Mechanism

In a case when a webhook can not be handled it is persisted in the database and will be retried with the next incoming webhook. The number of retries is configurable and can be set in the `AppWebhookConfig::getAllowedNumberOfRetries()` method.

There are numerous reasons why a webhook may fail. An exception is thrown, the plugin implementation returns a failed response or the plugin implementation returns a not handled response.

Another case could be an event is sent to the application before it is ready to handle it. For example, in the PreOrder payment of a PSP the order has not persisted yet and has no order-reference, but the PSP sends a webhook request about a payment state, in this case, the system has to wait until it can process the webhook.

##### Future improvements for the Retry mechanism

It may be helpful in the future to provide a console command that can be used to retry failed webhooks. This command can be used to retry all failed webhooks or only a specific webhook.

### Configuration

Currently only the number of allowed retries can be configured. The configuration can be found in the `AppWebhookConfig` class.

### Plugins

### GlueApplication

#### \Spryker\Glue\AppWebhookBackendApi\Plugin\GlueApplication\AppWebhookBackendApiRouteProviderPlugin

This plugin provides the routes for the AppWebhookBackendApi module.

### Extensions

#### \Spryker\Zed\AppWebhook\Dependency\Plugin\WebhookHandlerPluginInterface

This plugin can be implemented by any other module and has two methods:

- `\Spryker\Zed\AppWebhook\Dependency\Plugin\WebhookHandlerPluginInterface::canHandle(WebhookRequestTransfer $webhookRequestTransfer): bool`
- `\Spryker\Zed\AppWebhook\Dependency\Plugin\WebhookHandlerPluginInterface::handle(WebhookRequestTransfer $webhookRequestTransfer): WebhookResponseTransfer`

The `canHandle()` method is used to check if a webhook can be handled by a specific module. F.e. you have two handlers one for `order.created` and one for `order.updated` you can check in the `canHandle()` method if the webhook can be handled by the module and return true or false.

The `handle()` method is used to handle the webhook. The method is called if the `canHandle()` method returns true. The method should return a `WebhookResponseTransfer` with the status of the webhook handling.
1 change: 1 addition & 0 deletions architecture-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
1 change: 1 addition & 0 deletions codeception.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace: SprykerTest

include:
- tests/SprykerTest/Glue/AppWebhookBackendApi/
- tests/SprykerTest/Zed/AppWebhook/

bootstrap: _data/Environment.php

Expand Down
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "AppWebhook module",
"license": "proprietary",
"require": {
"php": ">=8.1",
"php": ">=8.2",
"spryker/glue-application-extension": "^1.0.0",
"spryker/kernel": "^3.30.0",
"spryker/log": "^3.0.0",
Expand All @@ -27,7 +27,7 @@
"spryker/app-kernel": "^1.0.0",
"spryker/code-sniffer": "*",
"spryker/development": "^3.34.0",
"spryker/glue-application": "^1.64",
"spryker/glue-application": "^1.64.0",
"spryker/message-broker-aws": "^1.7.0",
"spryker/propel": "*",
"spryker/testify": "*",
Expand All @@ -52,12 +52,13 @@
"scripts": {
"cs-check": "phpcs -p src/ tests/",
"cs-fix": "phpcbf -p src/ tests/",
"setup": "tests/bin/console transfer:generate && tests/bin/console transfer:databuilder:generate && tests/bin/console dev:ide-auto-completion:zed:generate && tests/bin/console dev:ide-auto-completion:glue:generate",
"setup": "tests/bin/console app-webhook:setup && tests/bin/console transfer:generate && tests/bin/console transfer:databuilder:generate && tests/bin/console dev:ide-auto-completion:zed:generate && tests/bin/console dev:ide-auto-completion:glue:generate && tests/bin/console dev:ide-auto-completion:glue-backend:generate && tests/bin/console propel:install && vendor/bin/codecept build",
"stan": "phpstan analyze src/Spryker/",
"test": "codecept build && codecept run",
"test-strict": "vendor/bin/infection --threads=max --min-msi=100 --min-covered-msi=100",
"test-strict-ci": "vendor/bin/infection --threads=max --logger-github=true --min-msi=100 --min-covered-msi=100",
"test-cover": "codecept build && codecept run --coverage-xml",
"test-cover-html": "codecept build && codecept run --coverage-html",
"rector": "vendor/bin/rector process src/Spryker/ --config rector.php --ansi",
"rector-ci": "vendor/bin/rector process src/Spryker/ --config rector.php --ansi --dry-run",
"local-ci": "composer cs-fix && composer cs-check && composer stan && composer rector-ci && composer test"
Expand Down
55 changes: 55 additions & 0 deletions config/Shared/config_default.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,61 @@
use Spryker\Shared\ErrorHandler\ErrorHandlerConstants;
use Spryker\Shared\Kernel\KernelConstants;
use Spryker\Shared\Log\LogConstants;
use Spryker\Shared\Propel\PropelConstants;
use Spryker\Zed\PropelOrm\Business\Builder\ExtensionObjectBuilder;
use Spryker\Zed\PropelOrm\Business\Builder\ExtensionQueryBuilder;
use Spryker\Zed\PropelOrm\Business\Builder\ObjectBuilder;
use Spryker\Zed\PropelOrm\Business\Builder\QueryBuilder;

$connections = [
'mysql' => [
'adapter' => 'sqlite',
'dsn' => 'sqlite:tests/_data/app_webhook_db',
'user' => '',
'password' => '',
'settings' => [],
],
];

$config[PropelConstants::PROPEL] = [
'database' => [
'connections' => [],
],
'runtime' => [
'defaultConnection' => 'default',
'connections' => ['default', 'zed'],
],
'generator' => [
'defaultConnection' => 'default',
'connections' => ['default', 'zed'],
'objectModel' => [
'defaultKeyType' => 'fieldName',
'builders' => [
// If you need full entity logging on Create/Update/Delete, then switch to
// Spryker\Zed\PropelOrm\Business\Builder\ObjectBuilderWithLogger instead.
'object' => ObjectBuilder::class,
'objectstub' => ExtensionObjectBuilder::class,
'query' => QueryBuilder::class,
'querystub' => ExtensionQueryBuilder::class,
],
],
],
'paths' => [
'phpDir' => APPLICATION_ROOT_DIR,
'sqlDir' => APPLICATION_SOURCE_DIR . '/Orm/Propel/Sql/',
'migrationDir' => APPLICATION_SOURCE_DIR . '/Orm/Propel/Migration_SQLite/',
'schemaDir' => APPLICATION_SOURCE_DIR . '/Orm/Propel/Schema/',
],
];

$config[PropelConstants::ZED_DB_ENGINE] = 'mysql';
$config[PropelConstants::ZED_DB_HOST] = 'localhost';
$config[PropelConstants::ZED_DB_PORT] = 1234;
$config[PropelConstants::ZED_DB_USERNAME] = 'catface';
$config[PropelConstants::ZED_DB_PASSWORD] = 'catface';

$config[PropelConstants::PROPEL]['database']['connections']['default'] = $connections['mysql'];
$config[PropelConstants::PROPEL]['database']['connections']['zed'] = $connections['mysql'];

$config[KernelConstants::PROJECT_NAMESPACE] = 'Spryker';
$config[KernelConstants::PROJECT_NAMESPACES] = ['Spryker'];
Expand Down
5 changes: 5 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,9 @@
<exclude name="SlevomatCodingStandard.Classes.DisallowConstructorPropertyPromotion.DisallowedConstructorPropertyPromotion"/>
</rule>

<!-- PHP 8.0 we will allow the null safe operator -->
<rule ref="SlevomatCodingStandard.ControlStructures.DisallowNullSafeObjectOperator">
<exclude name="SlevomatCodingStandard.ControlStructures.DisallowNullSafeObjectOperator"/>
</rule>

</ruleset>
4 changes: 4 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
parameters:
level: 8
ignoreErrors:
- '#Call to an undefined method Propel\\Runtime\\Collection\\Collection\:\:delete\(\).#'
6 changes: 6 additions & 0 deletions psalm-report.json

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

10 changes: 10 additions & 0 deletions src/Spryker/Shared/AppWebhook/Transfer/app_webhook.transfer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@
<transfer name="WebhookRequest" strict="true">
<property name="content" type="string"/>
<property name="mode" type="string"/>
<property name="identifier" type="string" description="This is the default identifier set for internal processing."/>
<property name="isRetry" type="bool" description="When this WebhookRequest was tried to be processed before we set this to not persist it again in the database."/>
<property name="retries" type="int" description="The number of how often this webhook was retried."/>
</transfer>

<transfer name="WebhookResponse" strict="true">
<property name="isSuccessful" type="bool"/>
<property name="isHandled" type="bool" description="A Webhook can be accepted for later processing and setting this to false will keep it persisted. When true or null it will be removed from persistence."/>
<property name="identifier" type="string" description="The identifier will be set from outside and used to find previously unprocessed webhooks and execute them when possible."/>
<property name="sequenceNumber" type="int" description="This number will be set from outside. When there were already unprocessed webhooks for the same identifier this number will be increased."/>
<property name="content" type="string"/>
<property name="message" type="string"/>
</transfer>

<transfer name="WebhookInboxCriteria" strict="true">
<property name="identifiers" type="array" singular="identifier"/>
</transfer>

<transfer name="GlueError">
<property name="message" type="string"/>
</transfer>
Expand Down
25 changes: 25 additions & 0 deletions src/Spryker/Zed/AppWebhook/AppWebhookConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/**
* Copyright © 2016-present Spryker Systems GmbH. All rights reserved.
* Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
*/

namespace Spryker\Zed\AppWebhook;

use Spryker\Zed\Kernel\AbstractBundleConfig;

class AppWebhookConfig extends AbstractBundleConfig
{
/**
* @api
*
* @var int
*/
protected const NUMBER_OF_ALLOWED_RETRIES = 9;

public function getAllowedNumberOfWebhookRetries(): int
{
return static::NUMBER_OF_ALLOWED_RETRIES;
}
}
3 changes: 3 additions & 0 deletions src/Spryker/Zed/AppWebhook/AppWebhookDependencyProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
use Spryker\Zed\Kernel\AbstractBundleDependencyProvider;
use Spryker\Zed\Kernel\Container;

/**
* @method \Spryker\Zed\AppWebhook\AppWebhookConfig getConfig()
*/
class AppWebhookDependencyProvider extends AbstractBundleDependencyProvider
{
/**
Expand Down
25 changes: 25 additions & 0 deletions src/Spryker/Zed/AppWebhook/Business/AppWebhookBusinessFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,43 @@
namespace Spryker\Zed\AppWebhook\Business;

use Spryker\Zed\AppWebhook\AppWebhookDependencyProvider;
use Spryker\Zed\AppWebhook\Business\Identifier\IdentifierBuilder;
use Spryker\Zed\AppWebhook\Business\Identifier\IdentifierBuilderInterface;
use Spryker\Zed\AppWebhook\Business\WebhookHandler\WebhookHandler;
use Spryker\Zed\AppWebhook\Business\WebhookProcessor\WebhookProcessor;
use Spryker\Zed\Kernel\Business\AbstractBusinessFactory;

/**
* @method \Spryker\Zed\AppWebhook\Persistence\AppWebhookEntityManagerInterface getEntityManager()
* @method \Spryker\Zed\AppWebhook\Persistence\AppWebhookRepositoryInterface getRepository()
* @method \Spryker\Zed\AppWebhook\AppWebhookConfig getConfig()
*/
class AppWebhookBusinessFactory extends AbstractBusinessFactory
{
public function createWebhookHandler(): WebhookHandler
{
return new WebhookHandler(
$this->getAppWebhookHandlerPlugins(),
$this->getConfig(),
$this->getRepository(),
$this->getEntityManager(),
$this->createIdentifierBuilder(),
);
}

public function createWebhookProcessor(): WebhookProcessor
{
return new WebhookProcessor(
$this->createWebhookHandler(),
$this->getRepository(),
);
}

public function createIdentifierBuilder(): IdentifierBuilderInterface
{
return new IdentifierBuilder();
}

/**
* @return array<\Spryker\Zed\AppWebhook\Dependency\Plugin\WebhookHandlerPluginInterface>
*/
Expand Down
23 changes: 23 additions & 0 deletions src/Spryker/Zed/AppWebhook/Business/AppWebhookFacade.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@

namespace Spryker\Zed\AppWebhook\Business;

use Generated\Shared\Transfer\WebhookInboxCriteriaTransfer;
use Generated\Shared\Transfer\WebhookRequestTransfer;
use Generated\Shared\Transfer\WebhookResponseTransfer;
use Spryker\Zed\Kernel\Business\AbstractFacade;

/**
* @method \Spryker\Zed\AppWebhook\Business\AppWebhookBusinessFactory getFactory()
* @method \Spryker\Zed\AppWebhook\Persistence\AppWebhookEntityManagerInterface getEntityManager()
* @method \Spryker\Zed\AppWebhook\Persistence\AppWebhookRepositoryInterface getRepository()
*/
class AppWebhookFacade extends AbstractFacade implements AppWebhookFacadeInterface
{
Expand All @@ -25,4 +28,24 @@ public function handleWebhook(WebhookRequestTransfer $webhookRequestTransfer, We
{
return $this->getFactory()->createWebhookHandler()->handleWebhook($webhookRequestTransfer, $webhookResponseTransfer);
}

/**
* @api
*
* @inheritDoc
*/
public function processUnprocessedWebhooks(WebhookInboxCriteriaTransfer $webhookInboxCriteriaTransfer): void
{
$this->getFactory()->createWebhookProcessor()->processUnprocessedWebhooks($webhookInboxCriteriaTransfer);
}

/**
* @api
*
* @inheritDoc
*/
public function deleteWebhooks(WebhookInboxCriteriaTransfer $webhookInboxCriteriaTransfer): void
{
$this->getEntityManager()->deleteWebhookRequests($webhookInboxCriteriaTransfer);
}
}
Loading