Conversation
… into feature/comprehensive-testing
There was a problem hiding this comment.
Pull request overview
This PR introduces WhatsApp as a first-class notification channel and expands the package documentation/API surface to support WhatsApp delivery (including a Twilio-based sender and a mock sender), alongside extensive inline documentation updates across core and Nest integration.
Changes:
- Add
NotificationChannel.WHATSAPPplus Twilio and mock WhatsApp sender implementations, and export them via infra index barrels. - Expand NestJS integration docs and provider/module factory comments, and update README + add a template configuration guide.
- Update core layer documentation/comments across ports, DTOs, errors, and service.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| src/nest/providers.ts | Adds extensive provider-factory documentation; adjusts provider definitions for defaults/dynamic imports. |
| src/nest/module.ts | Expands module documentation and refactors registerAsync() provider setup. |
| src/nest/index.ts | Adds public API documentation and keeps Nest integration barrel exports. |
| src/infra/senders/whatsapp/twilio-whatsapp.sender.ts | New Twilio-based WhatsApp sender (media + template support). |
| src/infra/senders/whatsapp/mock-whatsapp.sender.ts | New mock WhatsApp sender for development/testing. |
| src/infra/senders/whatsapp/index.ts | Barrel export for WhatsApp senders. |
| src/infra/senders/index.ts | Exports WhatsApp senders from the infra senders index. |
| src/infra/senders/email/nodemailer.sender.ts | Adds extensive documentation/comments to the Nodemailer sender. |
| src/index.ts | Adds package-level entrypoint documentation; continues exporting core/infra/nest. |
| src/core/types.ts | Adds WHATSAPP channel and expands inline documentation. |
| src/core/ports.ts | Expands inline documentation for ports/interfaces. |
| src/core/notification.service.ts | Adds extensive documentation/comments describing service behavior. |
| src/core/index.ts | Adds documentation and clarifies export intent for the core layer. |
| src/core/errors.ts | Expands error documentation and examples. |
| src/core/dtos.ts | Expands DTO/schema documentation; keeps validation logic. |
| docs/TEMPLATE_CONFIGURATION.md | Adds a comprehensive template configuration guide (provider vs global templates). |
| README.md | Updates positioning/features and adds WhatsApp + template/content guidance sections. |
Comments suppressed due to low confidence (2)
src/core/dtos.ts:156
CreateNotificationDtoSchema’s cross-field validation doesn’t include the newWHATSAPPchannel. As a result, DTO validation can accept WhatsApp notifications withoutrecipient.phoneeven though the docs/state that phone is required. Extend the refine logic to requirerecipient.phoneforNotificationChannel.WHATSAPP(and consider aligning the earlier comment block too).
// Cross-field validation: Ensure recipient has required contact info for channel
// Email channel requires email address
if (data.channel === NotificationChannel.EMAIL && !data.recipient.email) {
return false;
}
// SMS channel requires phone number
if (data.channel === NotificationChannel.SMS && !data.recipient.phone) {
return false;
}
// Push channel requires device token
if (data.channel === NotificationChannel.PUSH && !data.recipient.deviceToken) {
src/nest/providers.ts:33
- Unused imports
DateTimeProvider as _DateTimeProviderandUuidGenerator as _UuidGeneratorwill fail lint/typecheck (and are not needed since the implementation uses dynamic imports). Remove them.
import { NotificationService } from "../core/notification.service";
import { DateTimeProvider as _DateTimeProvider } from "../infra/providers/datetime.provider";
import { UuidGenerator as _UuidGenerator } from "../infra/providers/id-generator.provider";
| // Create a factory provider that creates NotificationKit providers | ||
| // once the module options are available | ||
| const providersFactory: Provider = { | ||
| provide: "NOTIFICATION_PROVIDERS", | ||
| useFactory: (moduleOptions: NotificationKitModuleOptions) => { | ||
| return createNotificationKitProviders(moduleOptions); | ||
| }, | ||
| inject: [NOTIFICATION_KIT_OPTIONS], | ||
| inject: [NOTIFICATION_KIT_OPTIONS], // Wait for options to be resolved | ||
| }; |
There was a problem hiding this comment.
registerAsync() currently registers a provider ("NOTIFICATION_PROVIDERS") whose value is an array of providers returned by createNotificationKitProviders(). Nest will not treat that returned array as providers, so NOTIFICATION_SERVICE (and related tokens) will not actually be registered in async mode. Refactor async registration to register the real providers themselves (e.g., create providers that inject: [NOTIFICATION_KIT_OPTIONS] and read from resolved options, or add an async-aware createNotificationKitProviders variant that builds providers around the options token).
| static registerAsync(options: NotificationKitModuleAsyncOptions): DynamicModule { | ||
| // Create provider that resolves module options asynchronously | ||
| const asyncOptionsProvider = this.createAsyncOptionsProvider(options); | ||
|
|
||
| // Create any additional async providers (useClass providers) | ||
| const asyncProviders = this.createAsyncProviders(options); | ||
|
|
||
| // We can't conditionally load controllers in async mode without the options | ||
| // So we'll need to always include them and they can handle being disabled internally | ||
| // Or we can create a factory provider that returns empty array | ||
| // Create a factory provider that creates NotificationKit providers | ||
| // once the module options are available | ||
| const providersFactory: Provider = { | ||
| provide: "NOTIFICATION_PROVIDERS", | ||
| useFactory: (moduleOptions: NotificationKitModuleOptions) => { | ||
| return createNotificationKitProviders(moduleOptions); | ||
| }, | ||
| inject: [NOTIFICATION_KIT_OPTIONS], | ||
| inject: [NOTIFICATION_KIT_OPTIONS], // Wait for options to be resolved | ||
| }; | ||
|
|
||
| // Combine all providers | ||
| const allProviders = [asyncOptionsProvider, ...asyncProviders, providersFactory]; | ||
|
|
||
| // Export providers for injection | ||
| const exports = allProviders.map((p) => | ||
| typeof p === "object" && "provide" in p ? p.provide : p, | ||
| ); | ||
|
|
||
| return { | ||
| global: true, | ||
| module: NotificationKitModule, | ||
| imports: options.imports || [], | ||
| global: true, // Module is global | ||
| module: NotificationKitModule, // This module class | ||
| imports: options.imports || [], // Import dependencies (ConfigModule, etc.) | ||
| controllers: [], // Controllers disabled in async mode for simplicity | ||
| providers: allProviders, | ||
| exports, | ||
| providers: allProviders, // Async providers + factory | ||
| exports, // Make providers available for injection | ||
| }; |
There was a problem hiding this comment.
There is no test asserting that registerAsync() exposes NOTIFICATION_SERVICE / functional providers (the current tests only check NOTIFICATION_KIT_OPTIONS). Add a module test that moduleRef.get(NOTIFICATION_SERVICE) works in async mode to prevent regressions like the current provider-factory issue.
| * Usage with NotificationKit: | ||
| * ```typescript | ||
| * NotificationKitModule.forRoot({ | ||
| * senders: [whatsappSender], | ||
| * // ... other config | ||
| * }); | ||
| * ``` |
There was a problem hiding this comment.
The usage example references NotificationKitModule.forRoot(...), but the module API is NotificationKitModule.register(...) / registerAsync(...). Update the example to match the actual public API.
| ```typescript | ||
| await notificationService.send({ | ||
| channel: NotificationChannel.WHATSAPP, | ||
| recipient: { phone: "+14155551234" }, | ||
| content: { | ||
| templateId: "orderShipped", // Maps to 'order_shipped_v1' | ||
| templateVars: { | ||
| orderNumber: "12345", | ||
| trackingUrl: "https://track.com/12345", | ||
| }, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
This doc example is missing required fields according to the package’s types/DTOs: recipient.id is required, and content currently requires title and body (even when using templates). Update the example (or relax the DTO/types) so it reflects what actually validates and runs.
| return { | ||
| success: true, | ||
| notificationId: _recipient.id, | ||
| providerMessageId: `mock-whatsapp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, |
There was a problem hiding this comment.
Math.random().toString(36).substr(...) uses the legacy substr() API. Use slice()/substring() instead to avoid deprecated APIs and potential lint failures.
| providerMessageId: `mock-whatsapp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, | |
| providerMessageId: `mock-whatsapp-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, |
| error: errorCode ? `[${errorCode}] ${errorMessage}` : errorMessage, | ||
| metadata: { | ||
| errorCode, | ||
| rawError: error, |
There was a problem hiding this comment.
On failure you return rawError: error in metadata. Twilio errors can include request/response details and other sensitive information; returning the full object can leak secrets/PII to callers or logs. Prefer returning only a sanitized subset (e.g., code, message, moreInfo, status) and omit the raw error object.
| rawError: error, | |
| message: errorMessage, | |
| status: error?.status, | |
| moreInfo: error?.moreInfo, |
| * Usage with NotificationKit: | ||
| * ```typescript | ||
| * NotificationKitModule.forRoot({ | ||
| * senders: [mockSender], | ||
| * // ... other config | ||
| * }); | ||
| * ``` |
There was a problem hiding this comment.
The usage example references NotificationKitModule.forRoot(...), but the module API is NotificationKitModule.register(...) / registerAsync(...). Update the example to match the actual public API.
| import { NotificationKitModule } from '@ciscode/notification-kit'; | ||
| import { SimpleTemplateEngine } from '@ciscode/notification-kit/infra'; |
There was a problem hiding this comment.
Examples import from @ciscode/notification-kit/infra, but package.json currently exports only the root entry (.). These subpath imports will fail for consumers unless you add subpath exports. Prefer importing infrastructure exports from @ciscode/notification-kit (or update exports).
| import { NotificationKitModule } from '@ciscode/notification-kit'; | |
| import { SimpleTemplateEngine } from '@ciscode/notification-kit/infra'; | |
| import { NotificationKitModule, SimpleTemplateEngine } from '@ciscode/notification-kit'; |
| * HTTP Status: 503 Service Unavailable | ||
| * | ||
| * Common causes: | ||
| * - Forgot to register sender: NotificationKitModule.forRoot({ senders: [emailSender] }) |
There was a problem hiding this comment.
Documentation mentions NotificationKitModule.forRoot(...), but the module API in this package is NotificationKitModule.register(...) / registerAsync(...). Update this reference to the correct method name.
| * - Forgot to register sender: NotificationKitModule.forRoot({ senders: [emailSender] }) | |
| * - Forgot to register sender: NotificationKitModule.register({ senders: [emailSender] }) |
| * NotificationKit NestJS Integration - Public API | ||
| * | ||
| * This file exports all public NestJS integration components for NotificationKit. | ||
| * Import from '@ciscode/notification-kit/nest' to use NotificationKit with NestJS. | ||
| * |
There was a problem hiding this comment.
This file’s JSDoc suggests consumers should import from @ciscode/notification-kit/nest, but package.json exports only the root entry (exports: {".": ...}), so that subpath import will fail in Node/bundlers. Update the docs/examples to import from @ciscode/notification-kit (or add the /nest subpath export if that’s intended).
|


Summary
Why
Checklist
npm run lintpassesnpm run typecheckpassesnpm testpassesnpm run buildpassesnpx changeset) if this affects consumersNotes