From cbe046ba07d70e2405bc273c856b6d1662336849 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 2 Dec 2020 06:54:13 +0100 Subject: [PATCH] Can generate webhook messages for creating transactions. --- app/Events/DestroyedTransactionGroup.php | 50 +++++ app/Events/StoredTransactionGroup.php | 6 +- .../Webhook/WebhookMessageGenerator.php | 182 ++++++++++++++++++ .../Events/StoredGroupEventHandler.php | 21 ++ app/Models/Webhook.php | 30 ++- app/Models/WebhookMessage.php | 76 ++++++++ app/Providers/EventServiceProvider.php | 6 + app/Transformers/AccountTransformer.php | 4 - .../TransactionGroupTransformer.php | 4 - config/firefly.php | 4 +- .../2020_11_12_070604_changes_for_v550.php | 1 + 11 files changed, 363 insertions(+), 21 deletions(-) create mode 100644 app/Events/DestroyedTransactionGroup.php create mode 100644 app/Generator/Webhook/WebhookMessageGenerator.php create mode 100644 app/Models/WebhookMessage.php diff --git a/app/Events/DestroyedTransactionGroup.php b/app/Events/DestroyedTransactionGroup.php new file mode 100644 index 0000000000..68d3f6a941 --- /dev/null +++ b/app/Events/DestroyedTransactionGroup.php @@ -0,0 +1,50 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Events; + +use FireflyIII\Models\TransactionGroup; +use Illuminate\Queue\SerializesModels; + +/** + * Class DestroyedTransactionGroup. + * + * @codeCoverageIgnore + */ +class DestroyedTransactionGroup extends Event +{ + use SerializesModels; + public TransactionGroup $transactionGroup; + + + /** + * Create a new event instance. + * + * @param TransactionGroup $transactionGroup + */ + public function __construct(TransactionGroup $transactionGroup) + { + $this->transactionGroup = $transactionGroup; + } +} diff --git a/app/Events/StoredTransactionGroup.php b/app/Events/StoredTransactionGroup.php index 391610e0ef..95fe3c92da 100644 --- a/app/Events/StoredTransactionGroup.php +++ b/app/Events/StoredTransactionGroup.php @@ -36,10 +36,8 @@ class StoredTransactionGroup extends Event { use SerializesModels; - /** @var bool */ - public $applyRules; - /** @var TransactionGroup The group that was stored. */ - public $transactionGroup; + public bool $applyRules; + public TransactionGroup $transactionGroup; /** diff --git a/app/Generator/Webhook/WebhookMessageGenerator.php b/app/Generator/Webhook/WebhookMessageGenerator.php new file mode 100644 index 0000000000..5fd19816fa --- /dev/null +++ b/app/Generator/Webhook/WebhookMessageGenerator.php @@ -0,0 +1,182 @@ +. + */ + +namespace FireflyIII\Generator\Webhook; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionGroup; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\Webhook; +use FireflyIII\Models\WebhookMessage; +use FireflyIII\Transformers\AccountTransformer; +use FireflyIII\Transformers\TransactionGroupTransformer; +use FireflyIII\User; +use Illuminate\Support\Collection; +use Log; +use Ramsey\Uuid\Uuid; +use Symfony\Component\HttpFoundation\ParameterBag; + +/** + * Class WebhookMessageGenerator + */ +class WebhookMessageGenerator +{ + private User $user; + private Collection $transactionGroups; + private int $trigger; + private Collection $webhooks; + + /** + * + */ + public function generateMessages(): void + { + $this->webhooks = $this->getWebhooks(); + Log::debug(sprintf('Generate messages for %d group(s) and %d webhook(s).', $this->transactionGroups->count(), $this->webhooks->count())); + $this->run(); + } + + /** + * @param User $user + */ + public function setUser(User $user): void + { + $this->user = $user; + } + + /** + * @param Collection $transactionGroups + */ + public function setTransactionGroups(Collection $transactionGroups): void + { + $this->transactionGroups = $transactionGroups; + } + + /** + * @param int $trigger + */ + public function setTrigger(int $trigger): void + { + $this->trigger = $trigger; + } + + /** + * @return Collection + */ + private function getWebhooks(): Collection + { + return $this->user->webhooks()->where('trigger', $this->trigger)->get(['webhooks.*']); + } + + private function run(): void + { + /** @var Webhook $webhook */ + foreach ($this->webhooks as $webhook) { + $this->runWebhook($webhook); + } + } + + /** + * @param Webhook $webhook + */ + private function runWebhook(Webhook $webhook): void + { + /** @var TransactionGroup $transactionGroup */ + foreach ($this->transactionGroups as $transactionGroup) { + $this->generateMessage($webhook, $transactionGroup); + } + } + + /** + * @param Webhook $webhook + * @param TransactionGroup $transactionGroup + */ + private function generateMessage(Webhook $webhook, TransactionGroup $transactionGroup): void + { + Log::debug(sprintf('Generating message for webhook #%d and transaction group #%d.', $webhook->id, $transactionGroup->id)); + + // message depends on what the webhook sets. + $uuid = Uuid::uuid4(); + $message = [ + 'user_id' => $transactionGroup->user->id, + 'trigger' => config('firefly.webhooks.triggers')[$webhook->trigger], + 'url' => $webhook->url, + 'uuid' => $uuid->toString(), + 'response' => config('firefly.webhooks.responses')[$webhook->response], + 'content' => [], + ]; + + switch ($webhook->response) { + default: + throw new FireflyException(sprintf('Cannot handle this webhook response (%d)', $webhook->response)); + case Webhook::RESPONSE_TRANSACTIONS: + $transformer = new TransactionGroupTransformer; + $message['content'] = $transformer->transformObject($transactionGroup); + break; + case Webhook::RESPONSE_ACCOUNTS: + $accounts = $this->collectAccounts($transactionGroup); + foreach ($accounts as $account) { + $transformer = new AccountTransformer; + $transformer->setParameters(new ParameterBag); + $message['content'][] = $transformer->transform($account); + } + } + $this->storeMessage($webhook, $message); + } + + /** + * @param TransactionGroup $transactionGroup + * + * @return Collection + */ + private function collectAccounts(TransactionGroup $transactionGroup): Collection + { + $accounts = new Collection; + /** @var TransactionJournal $journal */ + foreach ($transactionGroup->transactionJournals as $journal) { + /** @var Transaction $transaction */ + foreach ($journal->transactions as $transaction) { + $accounts->push($transaction->account); + } + } + + return $accounts->unique(); + } + + /** + * @param Webhook $webhook + * @param array $message + */ + private function storeMessage(Webhook $webhook, array $message): void + { + $webhookMessage = new WebhookMessage; + $webhookMessage->webhook()->associate($webhook); + $webhookMessage->sent = false; + $webhookMessage->errored = false; + $webhookMessage->uuid = $message['uuid']; + $webhookMessage->message = $message; + $webhookMessage->logs = null; + $webhookMessage->save(); + } + + +} \ No newline at end of file diff --git a/app/Handlers/Events/StoredGroupEventHandler.php b/app/Handlers/Events/StoredGroupEventHandler.php index 05b86d5fe4..555fbb6f97 100644 --- a/app/Handlers/Events/StoredGroupEventHandler.php +++ b/app/Handlers/Events/StoredGroupEventHandler.php @@ -23,10 +23,13 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Events; use FireflyIII\Events\StoredTransactionGroup; +use FireflyIII\Generator\Webhook\WebhookMessageGenerator; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\Webhook; use FireflyIII\Repositories\Rule\RuleRepositoryInterface; use FireflyIII\TransactionRules\Engine\RuleEngine; use FireflyIII\TransactionRules\Engine\RuleEngineInterface; +use Illuminate\Support\Collection; use Log; /** @@ -70,4 +73,22 @@ class StoredGroupEventHandler $newRuleEngine->fire(); } + /** + * This method processes all webhooks that respond to the "stored transaction group" trigger (100) + * + * @param StoredTransactionGroup $storedGroupEvent + */ + public function triggerWebhooks(StoredTransactionGroup $storedGroupEvent): void + { + Log::debug('StoredTransactionGroup:triggerWebhooks'); + $group = $storedGroupEvent->transactionGroup; + $user = $group->user; + $engine = new WebhookMessageGenerator; + $engine->setUser($user); + $engine->setTransactionGroups(new Collection([$group])); + $engine->setTrigger(Webhook::TRIGGER_STORE_TRANSACTION); + + $messages= $engine->generateMessages(); + } + } diff --git a/app/Models/Webhook.php b/app/Models/Webhook.php index f1cdc919e0..780b1939e2 100644 --- a/app/Models/Webhook.php +++ b/app/Models/Webhook.php @@ -25,6 +25,7 @@ namespace FireflyIII\Models; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -56,19 +57,25 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * @method static \Illuminate\Database\Eloquent\Builder|Webhook whereUrl($value) * @method static \Illuminate\Database\Eloquent\Builder|Webhook whereUserId($value) * @mixin \Eloquent + * @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\WebhookMessage[] $webhookMessages + * @property-read int|null $webhook_messages_count + * @method static \Illuminate\Database\Query\Builder|Webhook onlyTrashed() + * @method static \Illuminate\Database\Query\Builder|Webhook withTrashed() + * @method static \Illuminate\Database\Query\Builder|Webhook withoutTrashed() */ class Webhook extends Model { use SoftDeletes; + // dont forget to update the config in firefly.php // triggers - public const TRIGGER_CREATE_TRANSACTION = 100; - public const TRIGGER_UPDATE_TRANSACTION = 110; - public const TRIGGER_DELETE_TRANSACTION = 120; + public const TRIGGER_STORE_TRANSACTION = 100; + public const TRIGGER_UPDATE_TRANSACTION = 110; + public const TRIGGER_DESTROY_TRANSACTION = 120; // actions - public const MESSAGE_TRANSACTIONS = 200; - public const MESSAGE_ACCOUNTS = 210; + public const RESPONSE_TRANSACTIONS = 200; + public const RESPONSE_ACCOUNTS = 210; // delivery public const DELIVERY_JSON = 300; @@ -88,13 +95,13 @@ class Webhook extends Model * * @param string $value * - * @throws NotFoundHttpException * @return Webhook + * @throws NotFoundHttpException */ public static function routeBinder(string $value): Webhook { if (auth()->check()) { - $budgetId = (int) $value; + $budgetId = (int)$value; /** @var User $user */ $user = auth()->user(); /** @var Webhook $webhook */ @@ -115,4 +122,13 @@ class Webhook extends Model return $this->belongsTo(User::class); } + /** + * @codeCoverageIgnore + * @return HasMany + */ + public function webhookMessages(): HasMany + { + return $this->hasMany(WebhookMessage::class); + } + } \ No newline at end of file diff --git a/app/Models/WebhookMessage.php b/app/Models/WebhookMessage.php new file mode 100644 index 0000000000..0c8862b90d --- /dev/null +++ b/app/Models/WebhookMessage.php @@ -0,0 +1,76 @@ +. + */ + +namespace FireflyIII\Models; + + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + +/** + * Class WebhookMessage + * + * @property int $id + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property string|null $deleted_at + * @property int $webhook_id + * @property int $sent + * @property int $errored + * @property string $uuid + * @property string $message + * @property-read \FireflyIII\Models\Webhook $webhook + * @method static \Illuminate\Database\Eloquent\Builder|WebhookMessage newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|WebhookMessage newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|WebhookMessage query() + * @method static \Illuminate\Database\Eloquent\Builder|WebhookMessage whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebhookMessage whereDeletedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebhookMessage whereErrored($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebhookMessage whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebhookMessage whereMessage($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebhookMessage whereSent($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebhookMessage whereUpdatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebhookMessage whereUuid($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebhookMessage whereWebhookId($value) + * @mixin \Eloquent + */ +class WebhookMessage extends Model +{ + + protected $casts + = [ + 'sent' => 'boolean', + 'errored' => 'boolean', + 'uuid' => 'string', + 'message' => 'json', + ]; + + /** + * @codeCoverageIgnore + * @return BelongsTo + */ + public function webhook(): BelongsTo + { + return $this->belongsTo(Webhook::class); + } + + +} \ No newline at end of file diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 4a898d45e1..eae72ec7b1 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -24,6 +24,7 @@ namespace FireflyIII\Providers; use Exception; use FireflyIII\Events\AdminRequestedTestMessage; +use FireflyIII\Events\DestroyedTransactionGroup; use FireflyIII\Events\DetectedNewIPAddress; use FireflyIII\Events\RegisteredUser; use FireflyIII\Events\RequestedNewPassword; @@ -96,11 +97,16 @@ class EventServiceProvider extends ServiceProvider // is a Transaction Journal related event. StoredTransactionGroup::class => [ 'FireflyIII\Handlers\Events\StoredGroupEventHandler@processRules', + 'FireflyIII\Handlers\Events\StoredGroupEventHandler@triggerWebhooks', ], // is a Transaction Journal related event. UpdatedTransactionGroup::class => [ 'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@unifyAccounts', 'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@processRules', + 'FireflyIII\Handlers\Events\UpdatedGroupEventHandler@triggerWebhooks', + ], + DestroyedTransactionGroup::class => [ + 'FireflyIII\Handlers\Events\DestroyedGroupEventHandler@triggerWebhooks', ], // API related events: AccessTokenCreated::class => [ diff --git a/app/Transformers/AccountTransformer.php b/app/Transformers/AccountTransformer.php index a207f20b4e..c6c2d61bf2 100644 --- a/app/Transformers/AccountTransformer.php +++ b/app/Transformers/AccountTransformer.php @@ -45,10 +45,6 @@ class AccountTransformer extends AbstractTransformer */ public function __construct() { - if ('testing' === config('app.env')) { - Log::warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this))); - } - $this->repository = app(AccountRepositoryInterface::class); } diff --git a/app/Transformers/TransactionGroupTransformer.php b/app/Transformers/TransactionGroupTransformer.php index bb8726ac8e..52eb1e5e66 100644 --- a/app/Transformers/TransactionGroupTransformer.php +++ b/app/Transformers/TransactionGroupTransformer.php @@ -61,10 +61,6 @@ class TransactionGroupTransformer extends AbstractTransformer 'recurrence_count', 'recurrence_total', ]; $this->metaDateFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date']; - - if ('testing' === config('app.env')) { - app('log')->warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this))); - } } /** diff --git a/config/firefly.php b/config/firefly.php index 540fe6e837..9ba834d4da 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -824,9 +824,9 @@ return [ ], 'webhooks' => [ 'triggers' => [ - 100 => 'TRIGGER_CREATE_TRANSACTION', + 100 => 'TRIGGER_STORE_TRANSACTION', 110 => 'TRIGGER_UPDATE_TRANSACTION', - 120 => 'TRIGGER_DELETE_TRANSACTION', + 120 => 'TRIGGER_DESTROY_TRANSACTION', ], 'responses' => [ 200 => 'RESPONSE_TRANSACTIONS', diff --git a/database/migrations/2020_11_12_070604_changes_for_v550.php b/database/migrations/2020_11_12_070604_changes_for_v550.php index e0bcdf6f68..18f4d9ef49 100644 --- a/database/migrations/2020_11_12_070604_changes_for_v550.php +++ b/database/migrations/2020_11_12_070604_changes_for_v550.php @@ -140,6 +140,7 @@ class ChangesForV550 extends Migration $table->boolean('errored')->default(false); $table->string('uuid',64); $table->longText('message'); + $table->longText('logs')->nullable(); $table->foreign('webhook_id')->references('id')->on('webhooks')->onDelete('cascade'); } );