From ccf8b9033ce6738b1393d4b92ae909ca097fb8c4 Mon Sep 17 00:00:00 2001 From: Kolibri <66674482+Kolibri1990@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:39:12 +0100 Subject: [PATCH 1/3] feat(webhook): add a retry webhook strategy --- migration/1768855049768-AddNextTryDate.js | 26 ++++ .../webhook/webhook-notification.service.ts | 118 ++++++++++++------ .../user/services/webhook/webhook.entity.ts | 3 + 3 files changed, 108 insertions(+), 39 deletions(-) create mode 100644 migration/1768855049768-AddNextTryDate.js diff --git a/migration/1768855049768-AddNextTryDate.js b/migration/1768855049768-AddNextTryDate.js new file mode 100644 index 0000000000..fe8ebfcf44 --- /dev/null +++ b/migration/1768855049768-AddNextTryDate.js @@ -0,0 +1,26 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class AddNextTryDate1768855049768 { + name = 'AddNextTryDate1768855049768' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "webhook" ADD "nextTryDate" datetime2`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "webhook" DROP COLUMN "nextTryDate"`); + } +} diff --git a/src/subdomains/generic/user/services/webhook/webhook-notification.service.ts b/src/subdomains/generic/user/services/webhook/webhook-notification.service.ts index 2c0570d69a..cab18dbfe2 100644 --- a/src/subdomains/generic/user/services/webhook/webhook-notification.service.ts +++ b/src/subdomains/generic/user/services/webhook/webhook-notification.service.ts @@ -4,9 +4,10 @@ import { DfxLogger } from 'src/shared/services/dfx-logger'; import { HttpService } from 'src/shared/services/http.service'; import { Process } from 'src/shared/services/process.service'; import { DfxCron } from 'src/shared/utils/cron'; +import { Util } from 'src/shared/utils/util'; import { MailContext, MailType } from 'src/subdomains/supporting/notification/enums'; import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service'; -import { IsNull } from 'typeorm'; +import { LessThanOrEqual } from 'typeorm'; import { KycWebhookData } from './dto/kyc-webhook.dto'; import { PaymentWebhookData } from './dto/payment-webhook.dto'; import { WebhookDto } from './dto/webhook.dto'; @@ -29,56 +30,95 @@ export class WebhookNotificationService { } async sendOpenWebhooks(): Promise { - const entities = await this.webhookRepo.find({ where: { lastTryDate: IsNull() } }); + const now = new Date(); - if (entities.length > 0) this.logger.verbose(`Sending ${entities.length} 'webhooks`); + const entities = await this.webhookRepo.find({ + where: { + nextTryDate: LessThanOrEqual(now), + }, + relations: ['wallet', 'user', 'userData'], + }); + + if (entities.length > 0) this.logger.verbose(`Sending ${entities.length} webhooks`); for (const entity of entities) { - try { - const result = await this.triggerWebhook(entity); - await this.webhookRepo.update(...entity.sentWebhook(result)); - } catch (e) { - this.logger.error(`Failed to send webhook ${entity.id}:`, e); - } + await this.handleSingleWebhook(entity); } } - // --- HELPER METHODS --- // + private async handleSingleWebhook(webhook: Webhook): Promise { + const now = new Date(); - async triggerWebhook(webhook: Webhook): Promise { try { - if (!webhook.wallet.apiUrl) - throw new Error(`API URL for wallet ${webhook.wallet.name} not available anymore in webhook ${webhook.id}`); - - const webhookDto: WebhookDto = { - accountId: webhook.userData.id, - id: webhook.user?.address, - type: webhook.type, - data: JSON.parse(webhook.data), - reason: webhook.reason, - }; - - await this.http.post(webhook.wallet.apiUrl, webhookDto, { - headers: { 'x-api-key': webhook.wallet.apiKey }, - retryDelay: 5000, - tryCount: 3, - }); + const result = await this.triggerWebhook(webhook); + + await this.webhookRepo.update( + { id: webhook.id }, + { + ...webhook.sentWebhook(result), + lastTryDate: now, + nextTryDate: null, // Erfolg -> keine Retries mehr + }, + ); } catch (error) { - const id = webhook.id ?? webhook.identifier; - const errMessage = `Exception during webhook for user data ${webhook.userData.id} and wallet ${webhook.wallet.name} (webhook ${id}):`; + const nextTryDate = this.getNextTryDateOrNull(webhook); - this.logger.error(errMessage, error); + if (!nextTryDate) { + await this.sendManualCheckMail(webhook, error); + } - await this.notificationService.sendMail({ - type: MailType.ERROR_MONITORING, - context: MailContext.WEBHOOK, - input: { - subject: `Webhook ${id} failed`, - errors: [errMessage, error], + await this.webhookRepo.update( + { id: webhook.id }, + { + lastTryDate: now, + nextTryDate, }, - }); - - return error.message; + ); } } + + // --- HELPER METHODS --- // + + async triggerWebhook(webhook: Webhook): Promise { + if (!webhook.wallet.apiUrl) + throw new Error(`API URL for wallet ${webhook.wallet.name} not available anymore in webhook ${webhook.id}`); + + const webhookDto: WebhookDto = { + accountId: webhook.userData.id, + id: webhook.user?.address, + type: webhook.type, + data: JSON.parse(webhook.data), + reason: webhook.reason, + }; + + await this.http.post(webhook.wallet.apiUrl, webhookDto, { + headers: { 'x-api-key': webhook.wallet.apiKey }, + retryDelay: 5000, + tryCount: 3, + }); + + return undefined; + } + + private async sendManualCheckMail(webhook: Webhook, error: any): Promise { + const id = webhook.id ?? webhook.identifier; + const errMessage = `Webhook for user data ${webhook.userData.id} and wallet ${webhook.wallet.name} (webhook ${id}) failed for more than 24h.`; + + await this.notificationService.sendMail({ + type: MailType.ERROR_MONITORING, + context: MailContext.WEBHOOK, + input: { + subject: `Webhook ${id} requires manual check`, + errors: [errMessage, error], + }, + }); + } + + private getNextTryDateOrNull(webhook: Webhook): Date | null { + const ageMinutes = (new Date().getTime() - webhook.created.getTime()) / 60000; + if (ageMinutes >= 24 * 60) return null; + + const delayMinutes = ageMinutes < 60 ? 5 : 60; + return Util.minutesAfter(delayMinutes); + } } diff --git a/src/subdomains/generic/user/services/webhook/webhook.entity.ts b/src/subdomains/generic/user/services/webhook/webhook.entity.ts index 01e34724ca..726e2f2c4b 100644 --- a/src/subdomains/generic/user/services/webhook/webhook.entity.ts +++ b/src/subdomains/generic/user/services/webhook/webhook.entity.ts @@ -22,6 +22,9 @@ export class Webhook extends IEntity { @Column({ type: 'datetime2', nullable: true }) lastTryDate?: Date; + @Column({ type: 'datetime2', nullable: true }) + nextTryDate?: Date; + @Column({ length: 'MAX', nullable: true }) error?: string; From 57d8fbe412f26bee7a20bc5e148ae6753d24a68f Mon Sep 17 00:00:00 2001 From: Kolibri <66674482+Kolibri1990@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:40:48 +0100 Subject: [PATCH 2/3] feat(webhook): refactoring --- .../user/services/webhook/webhook-notification.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subdomains/generic/user/services/webhook/webhook-notification.service.ts b/src/subdomains/generic/user/services/webhook/webhook-notification.service.ts index cab18dbfe2..aeafb8b165 100644 --- a/src/subdomains/generic/user/services/webhook/webhook-notification.service.ts +++ b/src/subdomains/generic/user/services/webhook/webhook-notification.service.ts @@ -57,7 +57,7 @@ export class WebhookNotificationService { { ...webhook.sentWebhook(result), lastTryDate: now, - nextTryDate: null, // Erfolg -> keine Retries mehr + nextTryDate: null, }, ); } catch (error) { From 9568a4fd1eac5099f263f4be7c5f049bf2050e04 Mon Sep 17 00:00:00 2001 From: Kolibri <66674482+Kolibri1990@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:32:24 +0100 Subject: [PATCH 3/3] feat(urble): Add or condition to get the open webhooks --- .../user/services/webhook/webhook-notification.service.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/subdomains/generic/user/services/webhook/webhook-notification.service.ts b/src/subdomains/generic/user/services/webhook/webhook-notification.service.ts index aeafb8b165..2585c03d9c 100644 --- a/src/subdomains/generic/user/services/webhook/webhook-notification.service.ts +++ b/src/subdomains/generic/user/services/webhook/webhook-notification.service.ts @@ -7,7 +7,7 @@ import { DfxCron } from 'src/shared/utils/cron'; import { Util } from 'src/shared/utils/util'; import { MailContext, MailType } from 'src/subdomains/supporting/notification/enums'; import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service'; -import { LessThanOrEqual } from 'typeorm'; +import { IsNull, LessThanOrEqual } from 'typeorm'; import { KycWebhookData } from './dto/kyc-webhook.dto'; import { PaymentWebhookData } from './dto/payment-webhook.dto'; import { WebhookDto } from './dto/webhook.dto'; @@ -33,9 +33,7 @@ export class WebhookNotificationService { const now = new Date(); const entities = await this.webhookRepo.find({ - where: { - nextTryDate: LessThanOrEqual(now), - }, + where: [{ nextTryDate: LessThanOrEqual(now) }, { nextTryDate: IsNull(), lastTryDate: IsNull() }], relations: ['wallet', 'user', 'userData'], });