Skip to content

Webhook controller dependency injection & modularity#127

Merged
respp merged 17 commits into
PayStell:mainfrom
DioChuks:refactor/webhook-controller
Oct 6, 2025
Merged

Webhook controller dependency injection & modularity#127
respp merged 17 commits into
PayStell:mainfrom
DioChuks:refactor/webhook-controller

Conversation

@DioChuks

@DioChuks DioChuks commented Sep 29, 2025

Copy link
Copy Markdown
Contributor

Pull Request Overview

📝 Summary

"Refactor webhook controller and related services to use manual dependency injection for improved modularity and testability."

🔗 Related Issues

🔄 Changes Made

  • Refactored controllers and services to use manual dependency injection (no DI framework)
  • Defined and used interfaces for all dependencies
  • Updated instantiation in route/entry files to pass dependencies explicitly
  • Updated or added documentation (see docs/MANUAL_DEPENDENCY_INJECTION.md)
  • Updated and fixed tests to use interface-based mocks

🖼️ Current Output

image

🧪 Testing

  • Test performed was a unit test.
image
  • test file updated: webhook.controller.test.ts

📚 Documentation

💬 Comments

Summary by CodeRabbit

  • New Features

    • Configuration getter now returns string | number | boolean | object | null.
  • Bug Fixes

    • Webhook API validates inactive merchants (404 MERCHANT_INACTIVE).
    • Standardized error codes and consistent signature header handling.
  • Documentation

    • Added "Manual Dependency Injection" to README and a detailed manual DI guide with examples.
  • Refactor

    • Webhook components converted to explicit, interface-based dependency injection.
  • Tests

    • Unit tests updated to use interface-based/plain-object mocks and expanded mock data.

@coderabbitai

coderabbitai Bot commented Sep 29, 2025

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@DioChuks has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 1 minutes and 36 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4f1afc4 and 3b3f0af.

📒 Files selected for processing (3)
  • src/controllers/webhook.controller.ts (6 hunks)
  • src/tests/controllers/webhook.controller.test.ts (5 hunks)
  • src/validators/webhook.validators.ts (2 hunks)

Walkthrough

Adds manual dependency-injection documentation and interface contracts; refactors WebhookController and WebhookNotificationService to accept injected interfaces; wires concrete instances in routes; updates request config getter typing; and adapts webhook controller tests and signature/merchant-active validation. No behavioral control-flow changes beyond added validations.

Changes

Cohort / File(s) Summary
Documentation (Manual DI)
README.md, docs/MANUAL_DEPENDENCY_INJECTION.md
Adds README section and a new guide describing manual dependency injection patterns, examples, and wiring steps.
Interfaces for DI contracts
src/interfaces/IBookingService.ts, src/interfaces/ICryptoGeneratorService.ts, src/interfaces/IMerchantAuthService.ts, src/interfaces/IPaymentService.ts, src/interfaces/IWebhookNotificationService.ts, src/interfaces/IWebhookService.ts
Adds exported interfaces used as DI contracts (placeholders or single-method interfaces) for webhook and related services.
Webhook controller + routes DI wiring
src/controllers/webhook.controller.ts, src/routes/webhook.routes.ts
WebhookController now requires IWebhookService, IMerchantAuthService, IWebhookNotificationService via constructor; routes instantiate concrete services and inject them; controller adds merchant active-state check and standardized header/signature handling and error codes.
Service constructor strict injection
src/services/webhookNotification.service.ts
Makes merchantAuthService and cryptoGeneratorService required constructor parameters; removes fallback/default instance creation.
Middleware typing update
src/middlewares/configurationMiddleware.ts
Changes req.config.get return type from Promise<string> to `Promise<string
Tests updated for DI
src/tests/controllers/webhook.controller.test.ts
Replaces class mocks with interface-typed plain object mocks, updates fixtures (merchant fields, headers, params), and adjusts typings to reflect injected interfaces.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant Route as "Express Route\n(/webhook/:merchantId)"
  participant Controller as "WebhookController\n(uses interfaces)"
  participant Auth as "MerchantAuthService\n(IMerchantAuthService)"
  participant WS as "WebhookService\n(IWebhookService)"
  participant Notif as "WebhookNotificationService\n(IWebhookNotificationService)"
  participant Crypto as "CryptoGeneratorService\n(ICryptoGeneratorService)"

  Client->>Route: POST /webhook/:merchantId (body + headers.signature)
  Route->>Controller: handleWebhook(req, res)
  Controller->>Auth: getMerchantById(merchantId)
  alt merchant not found
    Controller-->>Client: 404 MERCHANT_NOT_FOUND
  else merchant inactive
    Controller-->>Client: 404 MERCHANT_INACTIVE
  else signature missing/invalid
    Controller-->>Client: 400/401 MISSING_PARAMETERS / INVALID_SIGNATURE
  else valid
    Controller->>WS: getMerchantWebhook(merchantId)
    alt webhook not found
      Controller-->>Client: 404 WEBHOOK_NOT_FOUND
    else found
      Controller->>Notif: send/notify(payload)
      Notif->>Crypto: generate/verify signature (as needed)
      Notif-->>Controller: Promise<boolean>
      Controller-->>Client: 200 OK
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • PayStell/paystell-backend#105 — Implements the same manual constructor-based DI refactor for webhook.controller.ts (interfaces, constructor injection, wiring), closely related to these changes.

Possibly related PRs

Suggested reviewers

  • MPSxDev

Poem

A rabbit hopped through wires and lanes,
Untied tight knots for softer chains.
Controllers sip dependencies neat—
Tests now mock with simpler feet.
Hip-hop DI, the code feels light 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning The PR successfully defines interfaces for all dependencies, refactors the webhook controller and related services to use manual constructor injection, updates the application entry point with explicit instantiation, and revises unit tests to use interface-based mocks, aligning with core part of issue #125. However, there are no integration tests for the webhook flow or explicit tests for edge cases such as inactive merchants or signature failures as required in the acceptance criteria. This omission of integration tests and coverage validation means not all coding objectives from the linked issue are met. Integration tests covering both successful and error webhook scenarios need to be added and edge case handling (e.g., inactive merchants, invalid signatures) should be verified to fully satisfy the acceptance criteria of issue #125.
Out of Scope Changes Check ⚠️ Warning The PR includes a change to src/middlewares/configurationMiddleware.ts that expands the return type of req.config.get with union types, which is unrelated to the manual DI refactor for the webhook controller defined in issue #125. This update falls outside the stated objectives of decoupling and dependency injection for webhook processing. The configuration middleware update should be extracted into a separate PR or removed from this changeset to keep the refactor focused on the webhook controller dependency injection scope.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title clearly summarizes the main change, which is refactoring the webhook controller to use dependency injection and improve modularity, matching the core objective of the changeset. It is concise, specific to the controller refactor, and avoids extraneous details or vague terms. Peers can understand the primary focus at a glance without needing to read through file lists or minor updates.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@DioChuks DioChuks marked this pull request as ready for review September 30, 2025 20:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/middlewares/configurationMiddleware.ts (1)

18-20: Add explicit return type annotation for consistency.

The get function here should have an explicit return type annotation matching the one added in configCacheMiddleware (lines 178-181). Currently, this relies on type inference, which may not align with the explicit union type defined later.

Apply this diff to add the explicit return type:

-      get: async (key: string, defaultValue?: string) => {
+      get: async (
+        key: string,
+        defaultValue?: string
+      ): Promise<string | number | boolean | Record<string, unknown> | null> => {
         return await configurationService.getConfig(key, defaultValue);
       },
src/controllers/webhook.controller.ts (1)

24-71: Signature isn’t verified; external delivery is misused as “verification” (regression + security risk).

  • You read the signature (Line 26) but never verify it.
  • You call sendWebhookNotification and treat its boolean as “isValid” signature (Lines 66-71), returning INVALID_SIGNATURE on failure. That couples inbound auth to outbound delivery and can cause false 401s on network hiccups. It also triggers side effects during auth checks.

Action (essential):

  • Introduce a dedicated signature-verification dependency and verify before any outbound calls.
  • Separate “signature invalid” from “delivery failed.”

Proposed patch (adds a verifier dep and fixes flow):

-import { IWebhookNotificationService } from "../interfaces/IWebhookNotificationService";
+import { IWebhookNotificationService } from "../interfaces/IWebhookNotificationService";
+import { IWebhookSignatureVerifier } from "../interfaces/IWebhookSignatureVerifier";
@@
-export class WebhookController {
+export class WebhookController {
   private webhookService: IWebhookService;
   private webhookNotificationService: IWebhookNotificationService;
   private merchantAuthService: IMerchantAuthService;
+  private signatureVerifier: IWebhookSignatureVerifier;
@@
-  constructor(
-    webhookService: IWebhookService,
-    merchantAuthService: IMerchantAuthService,
-    webhookNotificationService: IWebhookNotificationService,
-  ) {
+  constructor(
+    webhookService: IWebhookService,
+    merchantAuthService: IMerchantAuthService,
+    webhookNotificationService: IWebhookNotificationService,
+    signatureVerifier: IWebhookSignatureVerifier,
+  ) {
     this.webhookService = webhookService;
     this.merchantAuthService = merchantAuthService;
     this.webhookNotificationService = webhookNotificationService;
+    this.signatureVerifier = signatureVerifier;
   }
@@
-      // Verify signature
-      const payload = req.body as WebhookPayload;
-      const isValid = await this.webhookNotificationService.sendWebhookNotification(
-        webhook.url,
-        payload,
-        merchantId,
-      );
-
-      if (!isValid) {
-        return res.status(401).json({
-          status: "error",
-          code: "INVALID_SIGNATURE",
-          message: "Invalid webhook signature",
-        });
-      }
+      // Verify signature before any side-effects
+      const raw = (req as any).rawBody ?? JSON.stringify(req.body);
+      const verified = await this.signatureVerifier.verify(
+        String(sig),
+        raw,
+        merchant.secret
+      );
+      if (!verified) {
+        return res.status(401).json({
+          status: "error",
+          code: "INVALID_SIGNATURE",
+          message: "Invalid webhook signature",
+        });
+      }
+
+      // Normalize payload and deliver
+      const normalized: WebhookPayload = {
+        transactionId: req.body?.payload?.transaction?.id,
+        transactionType: req.body?.payload?.transaction?.type,
+        status: req.body?.payload?.transaction?.status,
+        amount: req.body?.payload?.transaction?.amount_in?.amount,
+        asset: req.body?.payload?.transaction?.amount_in?.asset,
+        merchantId: req.body?.payload?.customer?.id ?? merchantId,
+        timestamp: new Date().toISOString(),
+        eventType: `${req.body?.payload?.transaction?.type}.${req.body?.payload?.transaction?.status}`,
+        reqMethod: req.method,
+      };
+      const delivered = await this.webhookNotificationService.sendWebhookNotification(
+        webhook.url,
+        normalized,
+        merchantId,
+      );
+      if (!delivered) {
+        return res.status(502).json({
+          status: "error",
+          code: "DELIVERY_FAILED",
+          message: "Failed to deliver webhook to merchant",
+        });
+      }
src/tests/controllers/webhook.controller.test.ts (1)

123-134: Assert that delivery is invoked with the normalized payload.

You build _expectedWebhookPayload but never assert its use. Add a call-assertion to ensure the controller maps and forwards correctly.

       expect(responseStatus).toHaveBeenCalledWith(200);
       expect(responseJson).toHaveBeenCalledWith({
         message: "Webhook processed successfully",
         status: "success",
       });
+      expect(webhookNotificationService.sendWebhookNotification).toHaveBeenCalledWith(
+        mockMerchantWebhook.url,
+        expect.objectContaining({
+          transactionId: _expectedWebhookPayload.transactionId,
+          transactionType: _expectedWebhookPayload.transactionType,
+          status: _expectedWebhookPayload.status,
+          amount: _expectedWebhookPayload.amount,
+          asset: _expectedWebhookPayload.asset,
+          merchantId: _expectedWebhookPayload.merchantId,
+          eventType: _expectedWebhookPayload.eventType,
+          reqMethod: "POST",
+        }),
+        "merchant-123"
+      );

Note: This assumes the controller performs normalization (see controller review).

🧹 Nitpick comments (10)
src/middlewares/configurationMiddleware.ts (1)

178-181: Consider broadening the defaultValue parameter type.

The defaultValue parameter is typed as string, but the return type now includes number | boolean | Record<string, unknown> | null. This creates a type mismatch where a string default might be provided when the actual config value is expected to be a different type.

Consider updating the parameter type to match the return type:

       req.config.get = async (
         key: string,
-        defaultValue?: string
+        defaultValue?: string | number | boolean | Record<string, unknown> | null
       ): Promise<string | number | boolean | Record<string, unknown> | null> => {

This same change should be applied to the base middleware at line 18 and to the configurationService.getConfig method signature if not already done.

src/interfaces/IMerchantAuthService.ts (1)

1-3: Tighten the return type.

Using Promise<any> forfeits type safety across the DI boundary. Please surface the actual merchant shape (e.g., Promise<Merchant | null>) so the controller and tests benefit from compile‑time checks.

src/controllers/webhook.controller.ts (4)

26-33: Header handling: use a configured header name and normalize access.

Accessing req.headers.signature assumes a nonstandard header and exact casing. Prefer a constant (e.g., WEBHOOK_SIGNATURE_HEADER) and normalize:

-const sig = req.headers?.signature;
+const sigHeader = process.env.WEBHOOK_SIGNATURE_HEADER ?? "x-stellar-signature";
+const sig = req.headers?.[sigHeader] as string | string[] | undefined;

Also include sigHeader in the 400 error context for easier debugging (but don’t echo values).


85-91: Don’t leak raw error messages to clients.

Returning (error as Error).message can disclose internals. Return a generic string and log details server-side.

-      return res.status(500).json({
-        status: "error",
-        code: "INTERNAL_ERROR",
-        message: (error as Error).message,
-      });
+      return res.status(500).json({
+        status: "error",
+        code: "INTERNAL_ERROR",
+        message: "An unexpected error occurred.",
+      });

Same for testWebhook’s catch block.

Also applies to: 165-170


94-121: Optional: mirror inactive-merchant guard for testWebhook.

If inactive merchants shouldn’t send test events, add the isActive guard here as well for consistency.


1-4: Interface return types are too loose (any).

IMerchantAuthService.getMerchantById and IWebhookService.getMerchantWebhook return Promise. Tighten to Merchant and MerchantWebhook to regain type safety and prevent the unsafe cast above. Based on relevant snippets.

src/tests/controllers/webhook.controller.test.ts (4)

150-169: Reset mocks inside the status loop to avoid cross-iteration bleed.

Without clearing, earlier calls can satisfy later expectations.

-      for (const status of statuses) {
+      for (const status of statuses) {
+        responseStatus.mockClear();
+        responseJson.mockClear();
+        (webhookNotificationService.sendWebhookNotification as jest.Mock).mockClear();
         mockRequest.body = {

171-187: Add negative test: missing parameters (signature or merchantId).

Cover the 400 MISSING_PARAMETERS branch.

     it("should return 404 when merchant is not found", async () => {
       (merchantAuthService.getMerchantById as jest.Mock).mockResolvedValue(
         null,
       );
@@
       expect(responseJson).toHaveBeenCalledWith({
         code: "MERCHANT_NOT_FOUND",
         message: "Merchant not found",
         status: "error",
       });
     });
+
+    it("should return 400 when signature header is missing", async () => {
+      mockRequest.headers = { } as any;
+      responseStatus.mockClear(); responseJson.mockClear();
+      await webhookController.handleWebhook(mockRequest as Request, mockResponse as Response);
+      expect(responseStatus).toHaveBeenCalledWith(400);
+      expect(responseJson).toHaveBeenCalledWith({
+        status: "error",
+        code: "MISSING_PARAMETERS",
+        message: "Missing required parameters",
+      });
+    });

205-217: Assert explicit error code for inactive merchants.

Strengthen the check to ensure the API contract.

       await webhookController.handleWebhook(
         mockRequest as Request,
         mockResponse as Response,
       );
 
       expect(responseStatus).toHaveBeenCalledWith(404);
+      expect(responseJson).toHaveBeenCalledWith({
+        status: "error",
+        code: "MERCHANT_INACTIVE",
+        message: "Merchant is inactive",
+      });

116-140: Add delivery-failure (not signature) test case.

If delivery fails, expect 502 DELIVERY_FAILED (per suggested controller fix).

   describe("handleWebhook", () => {
     it("should successfully process a valid webhook request", async () => {
@@
     });
+
+    it("should return 502 when delivery to merchant webhook fails", async () => {
+      (webhookNotificationService.sendWebhookNotification as jest.Mock).mockResolvedValue(false);
+      responseStatus.mockClear(); responseJson.mockClear();
+      await webhookController.handleWebhook(mockRequest as Request, mockResponse as Response);
+      expect(responseStatus).toHaveBeenCalledWith(502);
+      expect(responseJson).toHaveBeenCalledWith({
+        status: "error",
+        code: "DELIVERY_FAILED",
+        message: "Failed to deliver webhook to merchant",
+      });
+    });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f63dd29 and 3b16136.

📒 Files selected for processing (13)
  • README.md (1 hunks)
  • docs/MANUAL_DEPENDENCY_INJECTION.md (1 hunks)
  • src/controllers/webhook.controller.ts (6 hunks)
  • src/interfaces/IBookingService.ts (1 hunks)
  • src/interfaces/ICryptoGeneratorService.ts (1 hunks)
  • src/interfaces/IMerchantAuthService.ts (1 hunks)
  • src/interfaces/IPaymentService.ts (1 hunks)
  • src/interfaces/IWebhookNotificationService.ts (1 hunks)
  • src/interfaces/IWebhookService.ts (1 hunks)
  • src/middlewares/configurationMiddleware.ts (1 hunks)
  • src/routes/webhook.routes.ts (1 hunks)
  • src/services/webhookNotification.service.ts (1 hunks)
  • src/tests/controllers/webhook.controller.test.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/routes/webhook.routes.ts (5)
src/services/webhook.service.ts (1)
  • WebhookService (7-85)
src/services/merchant.service.ts (1)
  • MerchantAuthService (13-309)
src/services/cryptoGenerator.service.ts (1)
  • CryptoGeneratorService (4-48)
src/services/webhookNotification.service.ts (1)
  • WebhookNotificationService (10-84)
src/controllers/webhook.controller.ts (1)
  • WebhookController (9-172)
src/services/webhookNotification.service.ts (2)
src/services/merchant.service.ts (1)
  • MerchantAuthService (13-309)
src/services/cryptoGenerator.service.ts (1)
  • CryptoGeneratorService (4-48)
src/tests/controllers/webhook.controller.test.ts (4)
src/controllers/webhook.controller.ts (1)
  • WebhookController (9-172)
src/interfaces/IMerchantAuthService.ts (1)
  • IMerchantAuthService (1-3)
src/interfaces/IWebhookService.ts (1)
  • IWebhookService (1-3)
src/interfaces/IWebhookNotificationService.ts (1)
  • IWebhookNotificationService (3-14)
src/interfaces/IWebhookNotificationService.ts (1)
src/interfaces/webhook.interfaces.ts (1)
  • MerchantWebhook (18-25)
src/controllers/webhook.controller.ts (3)
src/interfaces/IWebhookService.ts (1)
  • IWebhookService (1-3)
src/interfaces/IWebhookNotificationService.ts (1)
  • IWebhookNotificationService (3-14)
src/interfaces/IMerchantAuthService.ts (1)
  • IMerchantAuthService (1-3)
🪛 Biome (2.1.2)
src/interfaces/IBookingService.ts

[error] 1-3: An empty interface is equivalent to {}.

Safe fix: Use a type alias instead.

(lint/suspicious/noEmptyInterface)

src/interfaces/ICryptoGeneratorService.ts

[error] 1-3: An empty interface is equivalent to {}.

Safe fix: Use a type alias instead.

(lint/suspicious/noEmptyInterface)

🔇 Additional comments (8)
src/middlewares/configurationMiddleware.ts (1)

184-184: Type assertion safety verified.

No other writes to requestCache detected; cached values originate solely from originalGet.

src/routes/webhook.routes.ts (1)

9-28: LGTM! Dependency wiring is correct.

The manual dependency injection is properly implemented:

  • Services are instantiated in correct order (no circular dependencies)
  • WebhookNotificationService receives required dependencies (merchantAuthService, cryptoGeneratorService)
  • WebhookController receives all three required dependencies
  • Aligns with the DI pattern documented in the PR
docs/MANUAL_DEPENDENCY_INJECTION.md (1)

1-79: Excellent documentation!

The manual DI documentation is clear, comprehensive, and includes practical examples. The step-by-step guide will help developers understand and maintain the DI pattern consistently across the codebase.

src/interfaces/IWebhookNotificationService.ts (1)

1-14: LGTM! Interface definition is correct.

The interface properly defines the contract for webhook notification services:

  • Method signatures match the concrete implementation
  • Appropriate use of Omit<WebhookPayload, "timestamp"> for notifyPaymentUpdate
  • Imports necessary types correctly
src/services/webhookNotification.service.ts (1)

14-20: LGTM! Constructor properly enforces dependency injection.

The constructor has been correctly refactored to require explicit dependencies:

  • Removed optional parameters and default instantiation
  • Dependencies must be injected at construction time
  • Aligns with the manual DI pattern implemented across the PR
src/controllers/webhook.controller.ts (2)

14-22: DI refactor looks good.

Constructor now cleanly injects dependencies via interfaces; this unblocks unit testing and route-level wiring.


47-53: Inactive merchant status code—confirm 404 vs 403 policy.

Returning 404 hides existence (ok for anti-enumeration). If product policy favors explicit auth semantics, consider 403 Forbidden with code MERCHANT_INACTIVE. Please confirm desired behavior.

src/tests/controllers/webhook.controller.test.ts (1)

14-24: Nice shift to interface-typed mocks.

Dropping jest.mock of classes and using jest.Mocked interfaces aligns with the new manual DI and keeps tests lightweight.

Comment thread src/controllers/webhook.controller.ts Outdated
Comment thread src/interfaces/IBookingService.ts Outdated
Comment thread src/interfaces/ICryptoGeneratorService.ts Outdated
Comment thread src/interfaces/IWebhookService.ts
Comment thread src/middlewares/configurationMiddleware.ts
@DioChuks

DioChuks commented Oct 2, 2025

Copy link
Copy Markdown
Contributor Author

@respp pls review thanks

@respp respp merged commit 3b3f0af into PayStell:main Oct 6, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor Webhook Controller to Use Dependency Injection

2 participants