diff --git a/README.md b/README.md
index 35ec5ca..9fc8cd4 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# @ciscode/notification-kit
-> A flexible, type-safe notification system for NestJS applications supporting multiple channels (Email, SMS, Push) with pluggable providers.
+> A lightweight, delivery-focused notification library for NestJS. Send notifications through multiple channels (Email, SMS, Push, WhatsApp) with pluggable providers. **Your app manages content and templates, NotificationKit handles delivery.**
[](https://www.npmjs.com/package/@ciscode/notification-kit)
[](https://opensource.org/licenses/MIT)
@@ -8,17 +8,20 @@
## ⨠Features
-- š **Multi-Channel Support** - Email, SMS, and Push notifications in one unified interface
+- šÆ **Lightweight & Focused** - Does one thing well: delivers notifications. No bloat, no unnecessary dependencies.
+- š **Multi-Channel Support** - Email, SMS, Push, and WhatsApp notifications in one unified interface
- š **Pluggable Providers** - Support for multiple providers (Twilio, AWS SNS, Firebase, Nodemailer, etc.)
+- š± **WhatsApp Support** - Send WhatsApp messages with media support via Twilio API
- šÆ **NestJS First** - Built specifically for NestJS with dependency injection support
- š¦ **Framework Agnostic Core** - Clean architecture with framework-independent domain logic
- š **Retry & Queue Management** - Built-in retry logic and notification state management
- š **Event System** - Track notification lifecycle with event emitters
-- šØ **Template Support** - Handlebars and simple template engines included
- š¾ **Flexible Storage** - MongoDB, PostgreSQL, or custom repository implementations
- ā
**Fully Tested** - Comprehensive test suite with 133+ tests
- š **Type Safe** - Written in TypeScript with full type definitions
+> **š Design Philosophy**: NotificationKit is a **delivery library**, not a content management system. Your application should manage templates, content, and business logic. NotificationKit focuses solely on reliable multi-channel delivery.
+
## š¦ Installation
```bash
@@ -39,6 +42,9 @@ npm install twilio # Twilio
npm install @aws-sdk/client-sns # AWS SNS
npm install @vonage/server-sdk # Vonage
+# For WhatsApp
+npm install twilio # Twilio WhatsApp API
+
# For push notifications (choose one)
npm install firebase-admin # Firebase
npm install @aws-sdk/client-sns # AWS SNS
@@ -148,7 +154,132 @@ POST /notifications/:id/retry
POST /notifications/:id/cancel
```
-## š Documentation
+## ļæ½ WhatsApp Support
+
+NotificationKit now supports WhatsApp messaging via Twilio's WhatsApp API with full media and template support!
+
+### Setup WhatsApp Sender
+
+```typescript
+import { TwilioWhatsAppSender, MockWhatsAppSender } from "@ciscode/notification-kit";
+
+// For production (real Twilio API)
+NotificationKitModule.register({
+ senders: [
+ new TwilioWhatsAppSender({
+ accountSid: process.env.TWILIO_ACCOUNT_SID,
+ authToken: process.env.TWILIO_AUTH_TOKEN,
+ fromNumber: process.env.TWILIO_WHATSAPP_FROM, // e.g., '+14155238886'
+ templates: {
+ orderShipped: "order_shipped_v1",
+ welcomeMessage: "welcome_v2",
+ },
+ }),
+ ],
+ // ... other config
+});
+
+// For development/testing (no credentials needed)
+NotificationKitModule.register({
+ senders: [new MockWhatsAppSender({ logMessages: true })],
+ // ... other config
+});
+```
+
+### Send WhatsApp Messages
+
+#### Basic Text Message
+
+```typescript
+await notificationService.send({
+ channel: NotificationChannel.WHATSAPP,
+ priority: NotificationPriority.HIGH,
+ recipient: {
+ id: "user-123",
+ phone: "+14155551234", // E.164 format required
+ },
+ content: {
+ title: "Order Update",
+ body: "Your order #12345 has been shipped!",
+ },
+});
+```
+
+#### WhatsApp with Media (Images/PDFs/Videos)
+
+```typescript
+await notificationService.send({
+ channel: NotificationChannel.WHATSAPP,
+ recipient: {
+ id: "user-456",
+ phone: "+447911123456",
+ },
+ content: {
+ title: "Invoice Ready",
+ body: "Your invoice is attached",
+ data: {
+ mediaUrl: "https://example.com/invoice.pdf",
+ },
+ },
+});
+```
+
+#### WhatsApp with Templates
+
+```typescript
+await notificationService.send({
+ channel: NotificationChannel.WHATSAPP,
+ recipient: {
+ id: "user-789",
+ phone: "+212612345678",
+ },
+ content: {
+ title: "OTP Code",
+ body: "Your verification code is {{code}}",
+ templateId: "otp_verification",
+ templateVars: {
+ code: "123456",
+ expiryMinutes: "5",
+ },
+ },
+});
+```
+
+### WhatsApp Requirements
+
+- **Phone Format**: Must be E.164 format (`+[country code][number]`)
+ - ā
Valid: `+14155551234`, `+447911123456`, `+212612345678`
+ - ā Invalid: `4155551234`, `+1-415-555-1234`, `+1 (415) 555-1234`
+- **Twilio Account**: Required for production use
+- **WhatsApp Opt-in**: Recipients must opt-in to receive messages (send "join [code]" to Twilio number)
+- **Media Support**: Images, videos, audio, PDFs (max 16MB for videos, 5MB for images)
+- **Templates**: Some message types require pre-approved WhatsApp templates
+
+### Testing WhatsApp Without Twilio
+
+Use `MockWhatsAppSender` for development:
+
+```typescript
+const mockSender = new MockWhatsAppSender({ logMessages: true });
+
+// Simulates sending and logs to console
+// No actual API calls or credentials needed
+```
+
+Console output example:
+
+```
+āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+š± [MockWhatsApp] Simulating WhatsApp send
+āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+To: +14155551234
+Recipient ID: user-123
+
+š¬ Message: Your order has been shipped!
+āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+```
+
+## ļæ½š Documentation
### Core Concepts
@@ -157,6 +288,7 @@ POST /notifications/:id/cancel
- **EMAIL** - Email notifications via SMTP providers
- **SMS** - Text messages via SMS gateways
- **PUSH** - Mobile push notifications
+- **WHATSAPP** - WhatsApp messages via Twilio or Meta Business API
- **WEBHOOK** - HTTP callbacks (coming soon)
#### Notification Status Lifecycle
@@ -188,6 +320,11 @@ CANCELLED
- **AwsSnsSender** - AWS SNS for SMS
- **VonageSmsSender** - Vonage (formerly Nexmo)
+#### WhatsApp Senders
+
+- **TwilioWhatsAppSender** - Twilio WhatsApp API (supports media & templates)
+- **MockWhatsAppSender** - Mock sender for testing without credentials
+
#### Push Notification Senders
- **FirebasePushSender** - Firebase Cloud Messaging (FCM)
@@ -281,14 +418,7 @@ NotificationKitModule.registerAsync({
}),
],
repository: new MongooseNotificationRepository(/* connection */),
- templateEngine: new HandlebarsTemplateEngine({
- templates: {
- welcome: {
- title: "Welcome {{name}}!",
- body: "Hello {{name}}, thanks for joining {{appName}}!",
- },
- },
- }),
+ // templateEngine: optional - most apps manage templates in backend
eventEmitter: new InMemoryEventEmitter(),
}),
inject: [ConfigService],
@@ -317,37 +447,74 @@ eventEmitter.on("*", (event) => {
});
```
-### Template Rendering
+### Content Management
+
+> ā ļø **Best Practice**: Manage templates and content in your backend application, not in NotificationKit. Your app knows your business logic, user preferences, and localization needs better than a delivery library.
+
+**Recommended Approach** (Render in Your Backend):
```typescript
-import { HandlebarsTemplateEngine } from "@ciscode/notification-kit/infra";
+@Injectable()
+export class NotificationService {
+ constructor(
+ private templateService: TemplateService, // Your template service
+ private notificationKit: NotificationService, // From NotificationKit
+ ) {}
-const templateEngine = new HandlebarsTemplateEngine({
- templates: {
+ async sendWelcomeEmail(user: User) {
+ // 1. Your backend renders the template
+ const content = await this.templateService.render("welcome", {
+ name: user.name,
+ appName: "MyApp",
+ });
+
+ // 2. NotificationKit delivers it
+ await this.notificationKit.send({
+ channel: NotificationChannel.EMAIL,
+ recipient: { id: user.id, email: user.email },
+ content: {
+ title: content.subject,
+ body: content.text,
+ html: content.html,
+ },
+ });
+ }
+}
+```
+
+**Built-in Template Engine** (Optional, for simple use cases):
+
+NotificationKit includes optional template engines for quick prototyping:
+
+```typescript
+import { SimpleTemplateEngine } from "@ciscode/notification-kit/infra";
+
+// Only use for demos/prototyping
+NotificationKitModule.register({
+ templateEngine: new SimpleTemplateEngine({
welcome: {
title: "Welcome {{name}}!",
body: "Hello {{name}}, welcome to {{appName}}!",
- html: "
Welcome {{name}}!
Thanks for joining {{appName}}!
",
},
- },
+ }),
});
-// Use in notification
+// Send using template
await notificationService.send({
- channel: NotificationChannel.EMAIL,
- recipient: { id: "user-123", email: "user@example.com" },
content: {
templateId: "welcome",
- templateVars: {
- name: "John Doe",
- appName: "My App",
- },
+ templateVars: { name: "John", appName: "MyApp" },
},
});
```
### Webhook Handling
+> **Note**: Built-in templates are optional and best suited for prototyping. Production apps should manage templates in the backend for flexibility, versioning, and localization. See [Template Configuration Guide](./docs/TEMPLATE_CONFIGURATION.md) for details.
+
+````
+
+
Enable webhook endpoints to receive delivery notifications from providers:
```typescript
@@ -356,7 +523,7 @@ NotificationKitModule.register({
webhookSecret: process.env.WEBHOOK_SECRET,
// ... other options
});
-```
+````
Webhook endpoint: `POST /notifications/webhook`
diff --git a/docs/TEMPLATE_CONFIGURATION.md b/docs/TEMPLATE_CONFIGURATION.md
new file mode 100644
index 0000000..5c43940
--- /dev/null
+++ b/docs/TEMPLATE_CONFIGURATION.md
@@ -0,0 +1,604 @@
+# Template Configuration Guide
+
+> ā ļø **Important Design Principle**: NotificationKit is a **delivery-focused library**, not a content management system. For production applications, you should manage templates in your backend application where you have full control over content, localization, versioning, and business logic.
+
+This guide explains template configuration in NotificationKit for different scenarios:
+
+- **Provider templates** (required for WhatsApp) - Always needed
+- **Built-in template engine** (optional) - Use only for prototyping
+
+## When to Use Built-in Templates
+
+**ā
Good for:**
+
+- Quick prototypes and demos
+- Simple use cases with static templates
+- Learning and testing NotificationKit
+
+**ā Not recommended for:**
+
+- Production applications
+- Multi-language support
+- Content that changes frequently
+- Complex personalization logic
+- A/B testing content
+- Template versioning and history
+
+## Recommended: Manage Templates in Your Backend
+
+For production apps, manage templates in your backend:
+
+```typescript
+// Your backend handles templates
+@Injectable()
+export class EmailService {
+ async sendWelcome(user: User) {
+ // 1. Load & render template from YOUR system (DB, CMS, files)
+ const template = await this.templateRepo.findByName("welcome");
+ const content = await this.renderEngine.render(template, {
+ name: user.name,
+ locale: user.locale,
+ });
+
+ // 2. NotificationKit delivers pre-rendered content
+ await this.notificationService.send({
+ channel: NotificationChannel.EMAIL,
+ recipient: { id: user.id, email: user.email },
+ content: {
+ title: content.subject,
+ body: content.text,
+ html: content.html,
+ },
+ });
+ }
+}
+```
+
+**Benefits:**
+
+- ā
Update templates without redeploying code
+- ā
Store in database with version history
+- ā
Use any template engine (Handlebars, Pug, EJS, React Email, etc.)
+- ā
Implement i18n properly
+- ā
A/B test different content
+- ā
Separate concerns: content vs delivery
+
+---
+
+## Two Template Systems
+
+NotificationKit provides **two optional template systems** for simple use cases:
+
+1. **Provider-Specific Templates** - Channel-specific templates (e.g., WhatsApp/Twilio templates)
+2. **Global Template Engine** - Cross-channel template system for dynamic content
+
+---
+
+## 1. Provider-Specific Templates
+
+### WhatsApp Templates (Twilio)
+
+WhatsApp Business API requires pre-approved templates. Configure them in the sender:
+
+```typescript
+import { TwilioWhatsAppSender } from "@ciscode/notification-kit";
+
+new TwilioWhatsAppSender({
+ accountSid: process.env.TWILIO_ACCOUNT_SID!,
+ authToken: process.env.TWILIO_AUTH_TOKEN!,
+ fromNumber: "+14155238886",
+
+ // Map your app's template IDs to Twilio's approved templates
+ templates: {
+ orderShipped: "order_shipped_v1",
+ orderConfirmed: "order_confirmed_v2",
+ otp: "otp_verification_v3",
+ welcomeMessage: "welcome_user_v1",
+ },
+});
+```
+
+**Usage:**
+
+```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",
+ },
+ },
+});
+```
+
+**Setup Steps:**
+
+1. Create templates in [Twilio Console](https://console.twilio.com) ā Content API
+2. Get templates approved by Twilio/Meta
+3. Map approved template names in your config
+4. Use `templateId` to reference them
+
+---
+
+## 2. Global Template Engine
+
+Use the global template engine for dynamic content across **all channels** (Email, SMS, Push, WhatsApp).
+
+### Option A: SimpleTemplateEngine
+
+Simple string replacement with `{{variable}}` syntax.
+
+```typescript
+import { NotificationKitModule } from '@ciscode/notification-kit';
+import { SimpleTemplateEngine } from '@ciscode/notification-kit/infra';
+
+NotificationKitModule.register({
+ senders: [/* ... */],
+ repository: /* ... */,
+
+ templateEngine: new SimpleTemplateEngine({
+ // Email templates
+ 'welcome-email': {
+ title: 'Welcome to {{appName}}!',
+ body: 'Hello {{userName}}, welcome to our platform.',
+ html: 'Welcome {{userName}}!
',
+ },
+
+ 'password-reset': {
+ title: 'Reset Your Password',
+ body: 'Use this code: {{code}}',
+ html: 'Code: {{code}}
',
+ },
+
+ // SMS templates
+ 'sms-otp': {
+ title: 'Verification Code',
+ body: 'Your code is {{code}}. Valid for {{minutes}} minutes.',
+ },
+
+ // Push notification templates
+ 'push-new-message': {
+ title: 'New Message from {{senderName}}',
+ body: '{{preview}}',
+ },
+ }),
+});
+```
+
+**Features:**
+
+- ā
Simple `{{variable}}` replacement
+- ā
No dependencies
+- ā
Fast and lightweight
+- ā No conditionals or loops
+
+**Usage:**
+
+```typescript
+// Email
+await notificationService.send({
+ channel: NotificationChannel.EMAIL,
+ recipient: { email: "user@example.com" },
+ content: {
+ templateId: "welcome-email",
+ templateVars: { appName: "MyApp", userName: "John" },
+ },
+});
+
+// SMS
+await notificationService.send({
+ channel: NotificationChannel.SMS,
+ recipient: { phone: "+14155551234" },
+ content: {
+ templateId: "sms-otp",
+ templateVars: { code: "123456", minutes: "5" },
+ },
+});
+```
+
+---
+
+### Option B: HandlebarsTemplateEngine
+
+Advanced templating with conditionals, loops, and helpers.
+
+```typescript
+import { HandlebarsTemplateEngine } from "@ciscode/notification-kit/infra";
+
+NotificationKitModule.register({
+ templateEngine: new HandlebarsTemplateEngine({
+ templates: {
+ "welcome-email": {
+ title: "Welcome {{name}}!",
+ body: `
+ Hello {{name}},
+
+ {{#if isPremium}}
+ Welcome to Premium! š
+ {{else}}
+ Upgrade to Premium for exclusive features.
+ {{/if}}
+
+ Features:
+ {{#each features}}
+ - {{this}}
+ {{/each}}
+ `,
+ html: `
+ Welcome {{name}}!
+ {{#if isPremium}}
+ Premium
+ {{/if}}
+
+ {{#each features}}
+ - {{this}}
+ {{/each}}
+
+ `,
+ },
+
+ "order-summary": {
+ title: "Order #{{orderId}} - ${{total}}",
+ body: `
+ Order Confirmed!
+
+ Items:
+ {{#each items}}
+ - {{name}} x{{qty}} = ${{ price }}
+ {{/each}}
+
+ Subtotal: ${{ subtotal }}
+ Tax: ${{ tax }}
+ Total: ${{ total }}
+
+ {{#if shippingAddress}}
+ Shipping to:
+ {{shippingAddress.street}}
+ {{shippingAddress.city}}, {{shippingAddress.zip}}
+ {{/if}}
+ `,
+ },
+ },
+ }),
+});
+```
+
+**Features:**
+
+- ā
Conditionals: `{{#if}}`, `{{#unless}}`
+- ā
Loops: `{{#each}}`
+- ā
Nested objects: `{{user.name}}`
+- ā
Helpers: Custom functions
+- ā ļø Requires `handlebars` peer dependency
+
+**Installation:**
+
+```bash
+npm install handlebars
+```
+
+**Usage:**
+
+```typescript
+await notificationService.send({
+ channel: NotificationChannel.EMAIL,
+ recipient: { email: "user@example.com" },
+ content: {
+ templateId: "order-summary",
+ templateVars: {
+ orderId: "12345",
+ items: [
+ { name: "Product A", qty: 2, price: 29.99 },
+ { name: "Product B", qty: 1, price: 49.99 },
+ ],
+ subtotal: 109.97,
+ tax: 11.0,
+ total: 120.97,
+ shippingAddress: {
+ street: "123 Main St",
+ city: "San Francisco",
+ zip: "94105",
+ },
+ },
+ },
+});
+```
+
+---
+
+## Complete Configuration Example
+
+Here's a full example using both template systems:
+
+```typescript
+import { Module } from "@nestjs/common";
+import {
+ NotificationKitModule,
+ TwilioWhatsAppSender,
+ NodemailerSender,
+ TwilioSmsSender,
+ HandlebarsTemplateEngine,
+ InMemoryNotificationRepository,
+} from "@ciscode/notification-kit";
+
+@Module({
+ imports: [
+ NotificationKitModule.register({
+ // Configure senders with channel-specific templates
+ senders: [
+ // Email sender (uses global template engine)
+ new NodemailerSender({
+ host: "smtp.gmail.com",
+ port: 587,
+ auth: {
+ user: process.env.EMAIL_USER,
+ pass: process.env.EMAIL_PASS,
+ },
+ from: "noreply@myapp.com",
+ }),
+
+ // SMS sender (uses global template engine)
+ new TwilioSmsSender({
+ accountSid: process.env.TWILIO_ACCOUNT_SID!,
+ authToken: process.env.TWILIO_AUTH_TOKEN!,
+ fromNumber: "+14155551234",
+ }),
+
+ // WhatsApp sender (has its OWN templates)
+ new TwilioWhatsAppSender({
+ accountSid: process.env.TWILIO_ACCOUNT_SID!,
+ authToken: process.env.TWILIO_AUTH_TOKEN!,
+ fromNumber: "+14155238886",
+
+ // WhatsApp-specific templates (pre-approved on Twilio)
+ templates: {
+ orderShipped: "order_shipped_v1",
+ otp: "otp_verification_v2",
+ welcomeMessage: "welcome_user_v1",
+ },
+ }),
+ ],
+
+ // Global template engine for ALL channels
+ templateEngine: new HandlebarsTemplateEngine({
+ templates: {
+ // Email templates
+ "welcome-email": {
+ title: "Welcome to {{appName}}!",
+ body: "Hello {{userName}}, welcome to our platform!",
+ html: "Welcome {{userName}}!
",
+ },
+
+ "password-reset-email": {
+ title: "Reset Your Password",
+ body: "Click here: {{resetLink}}",
+ html: 'Reset Password',
+ },
+
+ // SMS templates
+ "sms-verification": {
+ title: "Verification Code",
+ body: "Your code: {{code}}. Valid {{minutes}}min.",
+ },
+
+ "sms-alert": {
+ title: "Alert",
+ body: "{{message}}",
+ },
+
+ // Push notification templates
+ "push-new-message": {
+ title: "New message from {{senderName}}",
+ body: "{{preview}}",
+ },
+
+ // WhatsApp fallback templates (when not using Twilio templates)
+ "whatsapp-custom": {
+ title: "Order Update",
+ body: "Hi {{name}}, your order #{{id}} is {{status}}",
+ },
+ },
+ }),
+
+ repository: new InMemoryNotificationRepository(),
+ }),
+ ],
+})
+export class AppModule {}
+```
+
+---
+
+## Usage Examples
+
+### Email with Global Template
+
+```typescript
+await notificationService.send({
+ channel: NotificationChannel.EMAIL,
+ recipient: { id: "user-1", email: "john@example.com" },
+ content: {
+ templateId: "welcome-email",
+ templateVars: { appName: "MyApp", userName: "John" },
+ },
+});
+```
+
+### SMS with Global Template
+
+```typescript
+await notificationService.send({
+ channel: NotificationChannel.SMS,
+ recipient: { id: "user-2", phone: "+14155551234" },
+ content: {
+ templateId: "sms-verification",
+ templateVars: { code: "123456", minutes: "5" },
+ },
+});
+```
+
+### WhatsApp with Provider Template
+
+```typescript
+await notificationService.send({
+ channel: NotificationChannel.WHATSAPP,
+ recipient: { id: "user-3", phone: "+447911123456" },
+ content: {
+ templateId: "orderShipped", // Uses Twilio's 'order_shipped_v1'
+ templateVars: {
+ orderNumber: "12345",
+ trackingUrl: "https://track.com/12345",
+ },
+ },
+});
+```
+
+### WhatsApp with Global Template (Fallback)
+
+```typescript
+await notificationService.send({
+ channel: NotificationChannel.WHATSAPP,
+ recipient: { id: "user-4", phone: "+212612345678" },
+ content: {
+ templateId: "whatsapp-custom", // Uses global template engine
+ templateVars: {
+ name: "Alice",
+ id: "67890",
+ status: "shipped",
+ },
+ },
+});
+```
+
+---
+
+## Template Priority
+
+When sending a notification with a `templateId`:
+
+1. **Check provider-specific templates first** (e.g., WhatsApp sender's `templates` map)
+
+### šÆ Primary Recommendation: Backend Template Management
+
+**For production applications**, manage templates in your backend:
+
+```typescript
+// ā
Best: Full control in your backend
+class TemplateService {
+ async render(name: string, vars: any, locale: string) {
+ const template = await this.db.templates.findOne({ name, locale });
+ return this.engine.render(template, vars);
+ }
+}
+```
+
+**Benefits:**
+
+- Update templates without code deployment
+- Store in database with full audit trail
+- Support multiple languages properly
+- A/B test content variations
+- Use any template engine you prefer
+
+2. **Fall back to global template engine** if not found in provider
+
+- **Manage templates in your backend** for production apps (recommended)
+
+3. **Use raw content** if no templates match
+
+---
+
+## Best Practices
+
+### ā
Do:
+
+- Use built-in templates for production (manage in backend instead)
+- Use **provider templates** for WhatsApp (required by Twilio/Meta)
+- Pass **pre-rendered content** to NotificationKit
+- Use built-in templates **only for prototypes/demos**
+- Keep templates **simple and reusable** (if using built-in)
+- Store production templates in NotificationKit config
+- Test templates with **real data**
+- Version your templates (e.g., `welcome_v2`)
+
+### ā Don't:
+
+- Hard-code content in your app logic
+- Mix template systems unnecessarily
+- Forget to handle missing variables
+- Use complex logic in templates (move to app code)
+
+---
+
+## Configuration File Organization
+
+For large apps, organize templates in separate files:
+
+```typescript
+// templates/email.templates.ts
+export const emailTemplates = {
+ 'welcome-email': {
+ title: 'Welcome to {{appName}}!',
+ body: '...',
+ html: '...',
+ },
+ // ... more email templates
+};
+
+// templates/sms.templates.ts
+export const smsTemplates = {
+ 'sms-otp': { ... },
+ 'sms-alert': { ... },
+};
+
+// templates/whatsapp.config.ts
+export const whatsappTemplates = {
+ orderShipped: 'order_shipped_v1',
+ otp: 'otp_verification_v2',
+};
+
+// app.module.ts
+import { emailTemplates } from './templates/email.templates';
+import { smsTemplates } from './templates/sms.templates';
+import { whatsappTemplates } from './templates/whatsapp.config';
+
+NotificationKitModule.register({
+ senders: [
+ new TwilioWhatsAppSender({
+ // ...
+ templates: whatsappTemplates,
+ }),
+ ],
+ templateEngine: new HandlebarsTemplateEngine({
+ templates: {
+ ...emailTemplates,
+ ...smsTemplates,
+ },
+ }),
+});
+```
+
+---
+
+## Summary
+
+| Template Type | Where Configured | Used For | Example |
+| --------------------- | ---------------- | --------------------------- | ----------------------------------------------------- |
+| **Provider-Specific** | Sender config | WhatsApp (Twilio templates) | `new TwilioWhatsAppSender({ templates: {...} })` |
+| **Global Engine** | Module config | Email, SMS, Push, WhatsApp | `templateEngine: new HandlebarsTemplateEngine({...})` |
+
+**Key Difference:**
+
+- **Provider templates** = Pre-approved by provider (Twilio/Meta)
+- **Global templates** = Your own custom templates for any channel
+
+---
+
+## Need Help?
+
+- Check [Core Documentation](../src/core/README.md)
+- See [Infrastructure Providers](../src/infra/README.md)
+- Review [Test Examples](../src/core/notification.service.test.ts)
diff --git a/src/core/dtos.ts b/src/core/dtos.ts
index 8cbfe85..77e2c3e 100644
--- a/src/core/dtos.ts
+++ b/src/core/dtos.ts
@@ -1,45 +1,149 @@
+/**
+ * Data Transfer Objects (DTOs) with Zod Validation
+ *
+ * This file defines all DTOs (Data Transfer Objects) used for validating input
+ * across the NotificationKit API. It uses Zod for runtime type validation and
+ * schema enforcement.
+ *
+ * Why DTOs with Zod?
+ * - Runtime validation: Ensure data is valid before business logic runs
+ * - Type safety: TypeScript types automatically inferred from schemas
+ * - Clear error messages: Detailed validation errors for API consumers
+ * - Schema documentation: Self-documenting API contracts
+ *
+ * DTOs defined here:
+ * 1. CreateNotificationDto - For creating single notifications
+ * 2. SendNotificationDto - For immediate sending (alias of CreateNotificationDto)
+ * 3. QueryNotificationsDto - For searching/filtering notifications
+ * 4. UpdateNotificationStatusDto - For updating notification status
+ * 5. BulkSendNotificationDto - For sending to multiple recipients
+ *
+ * Usage:
+ * ```typescript
+ * // Validate and parse
+ * const dto = validateDto(CreateNotificationDtoSchema, requestBody);
+ *
+ * // Safe validation (doesn't throw)
+ * const result = validateDtoSafe(CreateNotificationDtoSchema, requestBody);
+ * if (result.success) {
+ * // Use result.data
+ * } else {
+ * // Handle result.errors
+ * }
+ * ```
+ */
+
import { z } from "zod";
import { NotificationChannel, NotificationPriority } from "./types";
/**
- * Zod schema for notification recipient
+ * Schema for notification recipient information
+ *
+ * Defines who will receive the notification. Different channels require
+ * different contact information:
+ * - EMAIL channel: requires 'email' field
+ * - SMS channel: requires 'phone' field
+ * - PUSH channel: requires 'deviceToken' field
+ * - IN_APP/WEBHOOK channels: only require 'id'
+ *
+ * Fields:
+ * - id: Unique identifier for the recipient (user ID, customer ID, etc.)
+ * - email: Email address (required for EMAIL channel)
+ * - phone: Phone number in E.164 format (required for SMS channel)
+ * - deviceToken: FCM/APNS device token (required for PUSH channel)
+ * - metadata: Additional recipient data for logging/analytics
*/
export const NotificationRecipientSchema = z.object({
- id: z.string().min(1, "Recipient ID is required"),
- email: z.string().email().optional(),
- phone: z.string().optional(),
- deviceToken: z.string().optional(),
- metadata: z.record(z.unknown()).optional(),
+ id: z.string().min(1, "Recipient ID is required"), // Unique recipient identifier
+ email: z.string().email().optional(), // Email address (for EMAIL channel)
+ phone: z.string().optional(), // Phone number (for SMS channel)
+ deviceToken: z.string().optional(), // FCM/APNS token (for PUSH channel)
+ metadata: z.record(z.unknown()).optional(), // Additional data (user preferences, etc.)
});
/**
- * Zod schema for notification content
+ * Schema for notification content
+ *
+ * Defines what will be sent in the notification. Supports both direct content
+ * and template-based content.
+ *
+ * Direct content mode:
+ * - Provide title, body, and (optionally) html
+ * - Content is used as-is
+ *
+ * Template mode:
+ * - Provide templateId (e.g., "welcome-email", "password-reset")
+ * - Provide templateVars for variable substitution (e.g., { name: "John", code: "123456" })
+ * - Template engine renders title/body/html from template
+ *
+ * Fields:
+ * - title: Notification title/subject (email subject, push title, etc.)
+ * - body: Main notification text content (plain text)
+ * - html: HTML version of body (for email)
+ * - data: Additional structured data (for push notifications, webhooks)
+ * - templateId: ID of template to render (optional)
+ * - templateVars: Variables for template rendering (optional)
*/
export const NotificationContentSchema = z.object({
- title: z.string().min(1, "Title is required"),
- body: z.string().min(1, "Body is required"),
- html: z.string().optional(),
- data: z.record(z.unknown()).optional(),
- templateId: z.string().optional(),
- templateVars: z.record(z.unknown()).optional(),
+ title: z.string().min(1, "Title is required"), // Required: notification title/subject
+ body: z.string().min(1, "Body is required"), // Required: main text content
+ html: z.string().optional(), // Optional: HTML version (for email)
+ data: z.record(z.unknown()).optional(), // Optional: additional data payload
+ templateId: z.string().optional(), // Optional: template to render
+ templateVars: z.record(z.unknown()).optional(), // Optional: variables for template
});
/**
- * DTO for creating a new notification
+ * Schema for creating a new notification
+ *
+ * This is the primary DTO for creating notifications. It includes comprehensive
+ * validation rules:
+ *
+ * 1. Channel validation: Must be a valid NotificationChannel enum value
+ * 2. Priority validation: Must be a valid NotificationPriority (default: NORMAL)
+ * 3. Recipient validation: Must pass NotificationRecipientSchema checks
+ * 4. Content validation: Must pass NotificationContentSchema checks
+ * 5. Schedule validation: If provided, must be valid ISO 8601 datetime
+ * 6. Retry validation: Must be 0-10 (default: 3)
+ * 7. Cross-field validation: Recipient must have appropriate contact info for channel
+ * - EMAIL channel ā recipient.email required
+ * - SMS channel ā recipient.phone required
+ * - PUSH channel ā recipient.deviceToken required
+ *
+ * Example valid DTO:
+ * ```json
+ * {
+ * "channel": "email",
+ * "priority": "high",
+ * "recipient": {
+ * "id": "user-123",
+ * "email": "user@example.com"
+ * },
+ * "content": {
+ * "title": "Welcome!",
+ * "body": "Welcome to our platform",
+ * "html": "Welcome!
"
+ * },
+ * "scheduledFor": "2026-04-01T10:00:00Z",
+ * "maxRetries": 3,
+ * "metadata": { "campaign": "onboarding" }
+ * }
+ * ```
*/
export const CreateNotificationDtoSchema = z
.object({
- channel: z.nativeEnum(NotificationChannel),
- priority: z.nativeEnum(NotificationPriority).default(NotificationPriority.NORMAL),
- recipient: NotificationRecipientSchema,
- content: NotificationContentSchema,
- scheduledFor: z.string().datetime().optional(),
- maxRetries: z.number().int().min(0).max(10).default(3),
- metadata: z.record(z.unknown()).optional(),
+ channel: z.nativeEnum(NotificationChannel), // Which channel to send through
+ priority: z.nativeEnum(NotificationPriority).default(NotificationPriority.NORMAL), // Priority level
+ recipient: NotificationRecipientSchema, // Who receives it
+ content: NotificationContentSchema, // What to send
+ scheduledFor: z.string().datetime().optional(), // When to send (optional, ISO 8601)
+ maxRetries: z.number().int().min(0).max(10).default(3), // Retry limit (0-10, default 3)
+ metadata: z.record(z.unknown()).optional(), // Additional tracking data
})
.refine(
(data) => {
+ // 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;
@@ -62,71 +166,216 @@ export const CreateNotificationDtoSchema = z
export type CreateNotificationDto = z.infer;
/**
- * DTO for sending a notification immediately
+ * Schema for sending a notification immediately
+ *
+ * This is an alias of CreateNotificationDtoSchema. It has the exact same validation
+ * rules, but semantically indicates the notification will be sent immediately
+ * rather than just created.
+ *
+ * Use CreateNotificationDto when you want to create and potentially schedule for later.
+ * Use SendNotificationDto when you want to send immediately (scheduledFor will be ignored).
*/
export const SendNotificationDtoSchema = CreateNotificationDtoSchema;
export type SendNotificationDto = z.infer;
/**
- * DTO for querying notifications
+ * Schema for querying/searching notifications
+ *
+ * This DTO supports flexible filtering and pagination for notification queries.
+ * All filter fields are optional - you can query by any combination.
+ *
+ * Filter fields:
+ * - recipientId: Find all notifications for a specific user
+ * - channel: Filter by channel (email, sms, push, etc.)
+ * - status: Filter by status (pending, sent, failed, etc.)
+ * - priority: Filter by priority level
+ * - fromDate: Find notifications created/sent after this date (ISO 8601)
+ * - toDate: Find notifications created/sent before this date (ISO 8601)
+ *
+ * Pagination fields:
+ * - limit: Maximum results to return (1-100, default: 10)
+ * - offset: Number of results to skip (default: 0)
+ *
+ * Example queries:
+ * ```json
+ * // Get user's failed emails (first 10)
+ * { "recipientId": "user-123", "channel": "email", "status": "failed", "limit": 10, "offset": 0 }
+ *
+ * // Get urgent notifications from last 24 hours
+ * { "priority": "urgent", "fromDate": "2026-03-30T00:00:00Z", "limit": 50 }
+ *
+ * // Get all SMS notifications (paginated)
+ * { "channel": "sms", "limit": 20, "offset": 40 }
+ * ```
*/
export const QueryNotificationsDtoSchema = z.object({
- recipientId: z.string().optional(),
- channel: z.nativeEnum(NotificationChannel).optional(),
- status: z.string().optional(),
- priority: z.nativeEnum(NotificationPriority).optional(),
- fromDate: z.string().datetime().optional(),
- toDate: z.string().datetime().optional(),
- limit: z.number().int().positive().max(100).default(10),
- offset: z.number().int().min(0).default(0),
+ recipientId: z.string().optional(), // Filter by recipient
+ channel: z.nativeEnum(NotificationChannel).optional(), // Filter by channel
+ status: z.string().optional(), // Filter by status
+ priority: z.nativeEnum(NotificationPriority).optional(), // Filter by priority
+ fromDate: z.string().datetime().optional(), // Filter by start date
+ toDate: z.string().datetime().optional(), // Filter by end date
+ limit: z.number().int().positive().max(100).default(10), // Page size (1-100)
+ offset: z.number().int().min(0).default(0), // Pagination offset
});
export type QueryNotificationsDto = z.infer;
/**
- * DTO for updating notification status
+ * Schema for updating notification status
+ *
+ * This DTO is used for webhook callbacks and status updates from notification
+ * providers. When a provider sends a webhook (e.g., "message delivered"), we
+ * use this DTO to update the notification record.
+ *
+ * Fields:
+ * - notificationId: The ID of the notification to update
+ * - status: New status value (e.g., "delivered", "bounced", "failed")
+ * - error: Error message if status is failed/bounced
+ * - providerMessageId: Provider's tracking ID (Twilio SID, SendGrid ID, etc.)
+ * - metadata: Additional provider data (timestamps, costs, etc.)
+ *
+ * Example webhook payload from Twilio:
+ * ```json
+ * {
+ * "notificationId": "notif-123",
+ * "status": "delivered",
+ * "providerMessageId": "SM1234567890abcdef",
+ * "metadata": {
+ * "MessageStatus": "delivered",
+ * "MessageSid": "SM1234567890abcdef",
+ * "To": "+1234567890"
+ * }
+ * }
+ * ```
*/
export const UpdateNotificationStatusDtoSchema = z.object({
- notificationId: z.string().min(1),
- status: z.string().min(1),
- error: z.string().optional(),
- providerMessageId: z.string().optional(),
- metadata: z.record(z.unknown()).optional(),
+ notificationId: z.string().min(1), // Required: notification ID
+ status: z.string().min(1), // Required: new status
+ error: z.string().optional(), // Optional: error message
+ providerMessageId: z.string().optional(), // Optional: provider tracking ID
+ metadata: z.record(z.unknown()).optional(), // Optional: provider data
});
export type UpdateNotificationStatusDto = z.infer;
/**
- * DTO for bulk notification sending
+ * Schema for bulk notification sending
+ *
+ * This DTO allows sending the same notification to multiple recipients in one call.
+ * It's more efficient than making individual API calls for each recipient.
+ *
+ * Key differences from CreateNotificationDto:
+ * - Takes array of 'recipients' instead of single 'recipient'
+ * - Limited to 1000 recipients per bulk send (to prevent timeouts)
+ * - All recipients receive the same content
+ * - Channel must support bulk sending (most do)
+ *
+ * Use cases:
+ * - Marketing campaigns (send to list of customers)
+ * - Alerts/announcements (notify all users of maintenance)
+ * - Batch processing (process notification queue)
+ *
+ * Example:
+ * ```json
+ * {
+ * "channel": "email",
+ * "priority": "normal",
+ * "recipients": [
+ * { "id": "user-1", "email": "user1@example.com" },
+ * { "id": "user-2", "email": "user2@example.com" },
+ * { "id": "user-3", "email": "user3@example.com" }
+ * ],
+ * "content": {
+ * "title": "Weekly Newsletter",
+ * "body": "Here's what's new this week..."
+ * },
+ * "maxRetries": 3
+ * }
+ * ```
+ *
+ * Note: Each recipient will get a separate notification record in the database.
*/
export const BulkSendNotificationDtoSchema = z.object({
- channel: z.nativeEnum(NotificationChannel),
- priority: z.nativeEnum(NotificationPriority).default(NotificationPriority.NORMAL),
- recipients: z.array(NotificationRecipientSchema).min(1).max(1000),
- content: NotificationContentSchema,
- scheduledFor: z.string().datetime().optional(),
- maxRetries: z.number().int().min(0).max(10).default(3),
- metadata: z.record(z.unknown()).optional(),
+ channel: z.nativeEnum(NotificationChannel), // Which channel to send through
+ priority: z.nativeEnum(NotificationPriority).default(NotificationPriority.NORMAL), // Priority level
+ recipients: z.array(NotificationRecipientSchema).min(1).max(1000), // 1-1000 recipients
+ content: NotificationContentSchema, // What to send (same for all)
+ scheduledFor: z.string().datetime().optional(), // When to send (optional)
+ maxRetries: z.number().int().min(0).max(10).default(3), // Retry limit per notification
+ metadata: z.record(z.unknown()).optional(), // Tracking data (applied to all)
});
export type BulkSendNotificationDto = z.infer;
/**
- * Validate and parse a DTO
+ * Validate and parse a DTO (throws on validation error)
+ *
+ * This function validates data against a Zod schema and returns the parsed
+ * (type-safe) result. If validation fails, it throws a ZodError with detailed
+ * error information.
+ *
+ * @param schema - The Zod schema to validate against
+ * @param data - The data to validate (typically from API request body)
+ * @returns The validated and parsed data (type-safe)
+ * @throws ZodError - If validation fails
+ *
+ * Usage:
+ * ```typescript
+ * try {
+ * const dto = validateDto(CreateNotificationDtoSchema, requestBody);
+ * // dto is now type-safe CreateNotificationDto
+ * await notificationService.create(dto);
+ * } catch (error) {
+ * if (error instanceof z.ZodError) {
+ * // Handle validation errors
+ * return { errors: error.errors };
+ * }
+ * throw error;
+ * }
+ * ```
*/
export function validateDto(schema: z.ZodSchema, data: unknown): T {
- return schema.parse(data);
+ return schema.parse(data); // Throws ZodError if invalid
}
/**
- * Safely validate a DTO and return result
+ * Safely validate a DTO without throwing (returns result object)
+ *
+ * This function validates data against a Zod schema but doesn't throw errors.
+ * Instead, it returns a result object indicating success or failure.
+ *
+ * @param schema - The Zod schema to validate against
+ * @param data - The data to validate
+ * @returns Success result { success: true, data: T } or failure result { success: false, errors: ZodError }
+ *
+ * This is useful when:
+ * - You want to handle validation errors without try/catch
+ * - You need to return validation errors to the caller
+ * - You're validating in middleware/interceptors
+ *
+ * Usage:
+ * ```typescript
+ * const result = validateDtoSafe(CreateNotificationDtoSchema, requestBody);
+ * if (result.success) {
+ * // result.data is type-safe CreateNotificationDto
+ * await notificationService.create(result.data);
+ * return { success: true };
+ * } else {
+ * // result.errors contains detailed validation errors
+ * return {
+ * success: false,
+ * errors: result.errors.errors.map(e => ({ field: e.path.join('.'), message: e.message }))
+ * };
+ * }
+ * ```
*/
export function validateDtoSafe(
schema: z.ZodSchema,
data: unknown,
): { success: true; data: T } | { success: false; errors: z.ZodError } {
- const result = schema.safeParse(data);
+ const result = schema.safeParse(data); // Doesn't throw
if (result.success) {
return { success: true, data: result.data };
}
diff --git a/src/core/errors.ts b/src/core/errors.ts
index 8e0f398..64e0561 100644
--- a/src/core/errors.ts
+++ b/src/core/errors.ts
@@ -1,5 +1,69 @@
/**
- * Base error class for notification-related errors
+ * NotificationKit Custom Error Classes
+ *
+ * This file defines all custom error types used throughout the NotificationKit package.
+ * All errors extend from a base NotificationError class, which provides:
+ * - Consistent error structure (message, code, details)
+ * - Proper prototype chain for instanceof checks
+ * - Type-safe error handling
+ *
+ * Error Hierarchy:
+ * ```
+ * Error (built-in)
+ * āāā NotificationError (base class)
+ * āāā ValidationError - Input validation failures
+ * āāā NotificationNotFoundError - Notification doesn't exist
+ * āāā SenderNotAvailableError - No sender configured for channel
+ * āāā SendFailedError - Send operation failed
+ * āāā InvalidRecipientError - Recipient data is invalid
+ * āāā TemplateError - Template rendering failed
+ * āāā MaxRetriesExceededError - Retry limit reached
+ * ```
+ *
+ * Why custom errors?
+ * - Clear error identification: Each error type has a unique code
+ * - Structured details: Attach context data to errors (IDs, counts, etc.)
+ * - Better debugging: Stack traces and error names help troubleshoot
+ * - Type-safe catching: TypeScript can narrow error types in catch blocks
+ * - API consistency: Return standardized error responses to clients
+ *
+ * Usage:
+ * ```typescript
+ * try {
+ * await notificationService.send(dto);
+ * } catch (error) {
+ * if (error instanceof NotificationNotFoundError) {
+ * // Handle missing notification (404)
+ * return { status: 404, message: error.message, code: error.code };
+ * } else if (error instanceof SendFailedError) {
+ * // Handle send failure (500)
+ * return { status: 500, message: error.message, details: error.details };
+ * }
+ * // Handle other errors...
+ * }
+ * ```
+ */
+
+/**
+ * Base error class for all notification-related errors
+ *
+ * All NotificationKit errors extend from this class to provide a consistent
+ * error structure and proper instanceof checks.
+ *
+ * Properties:
+ * - message: Human-readable error description (inherited from Error)
+ * - _code: Machine-readable error code (e.g., "NOTIFICATION_NOT_FOUND")
+ * - _details: Additional context data (e.g., notificationId, retryCount)
+ * - name: Error class name (e.g., "NotificationError")
+ * - stack: Stack trace (inherited from Error)
+ *
+ * Why we use Object.setPrototypeOf?
+ * TypeScript transpiles to ES5 by default, which breaks instanceof checks for
+ * custom errors. Setting the prototype explicitly fixes this.
+ *
+ * @param message - Description of what went wrong
+ * @param _code - Unique error code for programmatic handling
+ * @param _details - Additional structured data about the error
*/
export class NotificationError extends Error {
constructor(
@@ -7,22 +71,49 @@ export class NotificationError extends Error {
public readonly _code: string,
public readonly _details?: Record,
) {
- super(message);
- this.name = "NotificationError";
- Object.setPrototypeOf(this, NotificationError.prototype);
+ super(message); // Call parent Error constructor
+ this.name = "NotificationError"; // Set error name for stack traces
+ Object.setPrototypeOf(this, NotificationError.prototype); // Fix prototype chain for instanceof
}
+ // Getter for error code (makes it readonly to consumers)
get code(): string {
return this._code;
}
+ // Getter for error details (makes it readonly to consumers)
get details(): Record | undefined {
return this._details;
}
}
/**
- * Error thrown when validation fails
+ * Validation error - thrown when input data fails validation
+ *
+ * This error is thrown when:
+ * - DTO validation fails (Zod schema validation)
+ * - Business rule validation fails (e.g., invalid date range)
+ * - Required fields are missing
+ * - Field values are out of acceptable range
+ *
+ * Error Code: "VALIDATION_ERROR"
+ * HTTP Status: 400 Bad Request
+ *
+ * Example usage:
+ * ```typescript
+ * if (!email || !email.includes('@')) {
+ * throw new ValidationError("Invalid email format", { email });
+ * }
+ * ```
+ *
+ * Example error details:
+ * ```json
+ * {
+ * "message": "Recipient email is required for EMAIL channel",
+ * "code": "VALIDATION_ERROR",
+ * "details": { "channel": "email", "recipient": { "id": "user-123" } }
+ * }
+ * ```
*/
export class ValidationError extends NotificationError {
constructor(message: string, details?: Record) {
@@ -33,7 +124,32 @@ export class ValidationError extends NotificationError {
}
/**
- * Error thrown when a notification is not found
+ * Notification not found error - thrown when looking up a notification that doesn't exist
+ *
+ * This error is thrown when:
+ * - Querying by ID and notification doesn't exist in database
+ * - Attempting to update/delete a non-existent notification
+ * - Trying to retry a notification that was already deleted
+ *
+ * Error Code: "NOTIFICATION_NOT_FOUND"
+ * HTTP Status: 404 Not Found
+ *
+ * Example usage:
+ * ```typescript
+ * const notification = await repository.findById(id);
+ * if (!notification) {
+ * throw new NotificationNotFoundError(id);
+ * }
+ * ```
+ *
+ * Example error:
+ * ```json
+ * {
+ * "message": "Notification with ID notif-123 not found",
+ * "code": "NOTIFICATION_NOT_FOUND",
+ * "details": { "notificationId": "notif-123" }
+ * }
+ * ```
*/
export class NotificationNotFoundError extends NotificationError {
constructor(notificationId: string) {
@@ -46,7 +162,37 @@ export class NotificationNotFoundError extends NotificationError {
}
/**
- * Error thrown when a sender is not available for a channel
+ * Sender not available error - thrown when no sender is configured for a channel
+ *
+ * This error is thrown when:
+ * - No sender is registered for the requested channel
+ * - Sender exists but isReady() returns false (not configured/connected)
+ * - Sender was not provided during module initialization
+ *
+ * Error Code: "SENDER_NOT_AVAILABLE"
+ * HTTP Status: 503 Service Unavailable
+ *
+ * Common causes:
+ * - Forgot to register sender: NotificationKitModule.forRoot({ senders: [emailSender] })
+ * - Missing credentials: EMAIL_USER and EMAIL_PASS not set
+ * - Connection failure: SMTP server unreachable
+ *
+ * Example usage:
+ * ```typescript
+ * const sender = this.senders.get(channel);
+ * if (!sender || !(await sender.isReady())) {
+ * throw new SenderNotAvailableError(channel);
+ * }
+ * ```
+ *
+ * Example error:
+ * ```json
+ * {
+ * "message": "No sender available for channel: sms",
+ * "code": "SENDER_NOT_AVAILABLE",
+ * "details": { "channel": "sms" }
+ * }
+ * ```
*/
export class SenderNotAvailableError extends NotificationError {
constructor(channel: string) {
@@ -57,7 +203,43 @@ export class SenderNotAvailableError extends NotificationError {
}
/**
- * Error thrown when sending a notification fails
+ * Send failed error - thrown when notification send operation fails
+ *
+ * This error is thrown when:
+ * - Provider API returns an error (Twilio, SendGrid, Firebase, etc.)
+ * - Network request to provider fails
+ * - Provider rate limit exceeded
+ * - Invalid credentials or authentication failure
+ *
+ * Error Code: "SEND_FAILED"
+ * HTTP Status: 500 Internal Server Error (or 502 Bad Gateway)
+ *
+ * This error includes details from the provider's error response.
+ * The notification will be marked as FAILED and can be retried.
+ *
+ * Example usage:
+ * ```typescript
+ * const result = await sender.send(recipient, content);
+ * if (!result.success) {
+ * throw new SendFailedError(result.error || "Send failed", {
+ * notificationId: notification.id,
+ * providerError: result.error
+ * });
+ * }
+ * ```
+ *
+ * Example error:
+ * ```json
+ * {
+ * "message": "Twilio error: Invalid 'To' Phone Number",
+ * "code": "SEND_FAILED",
+ * "details": {
+ * "notificationId": "notif-456",
+ * "providerError": "21211: Invalid To Phone Number",
+ * "providerCode": 21211
+ * }
+ * }
+ * ```
*/
export class SendFailedError extends NotificationError {
constructor(message: string, details?: Record) {
@@ -68,7 +250,39 @@ export class SendFailedError extends NotificationError {
}
/**
- * Error thrown when recipient validation fails
+ * Invalid recipient error - thrown when recipient data is invalid for the channel
+ *
+ * This error is thrown when:
+ * - EMAIL channel but no email address provided
+ * - SMS channel but no phone number provided
+ * - PUSH channel but no device token provided
+ * - Email address format is invalid
+ * - Phone number format is invalid
+ *
+ * Error Code: "INVALID_RECIPIENT"
+ * HTTP Status: 400 Bad Request
+ *
+ * Example usage:
+ * ```typescript
+ * if (channel === 'email' && !recipient.email) {
+ * throw new InvalidRecipientError(
+ * "Email address is required for EMAIL channel",
+ * { channel, recipient }
+ * );
+ * }
+ * ```
+ *
+ * Example error:
+ * ```json
+ * {
+ * "message": "Phone number is required for SMS channel",
+ * "code": "INVALID_RECIPIENT",
+ * "details": {
+ * "channel": "sms",
+ * "recipient": { "id": "user-789" }
+ * }
+ * }
+ * ```
*/
export class InvalidRecipientError extends NotificationError {
constructor(message: string, details?: Record) {
@@ -79,7 +293,40 @@ export class InvalidRecipientError extends NotificationError {
}
/**
- * Error thrown when template rendering fails
+ * Template error - thrown when template rendering fails
+ *
+ * This error is thrown when:
+ * - Template with specified ID doesn't exist
+ * - Template has syntax errors (invalid Handlebars/Mustache syntax)
+ * - Required template variables are missing
+ * - Template engine is not configured but templateId was provided
+ *
+ * Error Code: "TEMPLATE_ERROR"
+ * HTTP Status: 500 Internal Server Error
+ *
+ * Example usage:
+ * ```typescript
+ * try {
+ * const rendered = await templateEngine.render(templateId, vars);
+ * } catch (error) {
+ * throw new TemplateError(`Failed to render template: ${templateId}`, {
+ * templateId,
+ * error: error.message
+ * });
+ * }
+ * ```
+ *
+ * Example error:
+ * ```json
+ * {
+ * "message": "Failed to render template: welcome-email",
+ * "code": "TEMPLATE_ERROR",
+ * "details": {
+ * "templateId": "welcome-email",
+ * "error": "Missing required variable: userName"
+ * }
+ * }
+ * ```
*/
export class TemplateError extends NotificationError {
constructor(message: string, details?: Record) {
@@ -90,7 +337,38 @@ export class TemplateError extends NotificationError {
}
/**
- * Error thrown when notification has reached max retries
+ * Max retries exceeded error - thrown when notification has been retried too many times
+ *
+ * This error is thrown when:
+ * - Notification.retryCount >= Notification.maxRetries
+ * - Attempting to retry a notification that's already exceeded the limit
+ *
+ * Error Code: "MAX_RETRIES_EXCEEDED"
+ * HTTP Status: 400 Bad Request (if manual retry) or 500 (if auto-retry)
+ *
+ * When this error is thrown:
+ * - Notification status remains FAILED
+ * - No further retry attempts will be made
+ * - Manual intervention required (fix issue, increase maxRetries, or recreate notification)
+ *
+ * Example usage:
+ * ```typescript
+ * if (notification.retryCount >= notification.maxRetries) {
+ * throw new MaxRetriesExceededError(notification.id, notification.retryCount);
+ * }
+ * ```
+ *
+ * Example error:
+ * ```json
+ * {
+ * "message": "Notification notif-999 exceeded max retries (3)",
+ * "code": "MAX_RETRIES_EXCEEDED",
+ * "details": {
+ * "notificationId": "notif-999",
+ * "retryCount": 3
+ * }
+ * }
+ * ```
*/
export class MaxRetriesExceededError extends NotificationError {
constructor(notificationId: string, retryCount: number) {
diff --git a/src/core/index.ts b/src/core/index.ts
index cf1dbac..918a934 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -1,17 +1,30 @@
-// Public exports from core go here.
-// Keep core framework-free (no Nest imports).
+/**
+ * @file Core Layer Export Index
+ *
+ * This file manages all exports from the core domain layer of NotificationKit.
+ * The core layer is completely framework-agnostic and contains:
+ *
+ * - Types & Enums: Domain types, enums for channels, status, and priority
+ * - DTOs: Data Transfer Objects for API operations (validation with Zod)
+ * - Ports: Interface abstractions for senders, repositories, and services
+ * - Errors: Custom error classes for domain-specific exceptions
+ * - Services: Core business logic for managing notifications
+ *
+ * Design Principle: This layer has zero framework dependencies (no NestJS, no Express, etc.)
+ * This ensures the business logic can be reused in any context.
+ */
-// Types and enums
+// Types and enums - Core domain models and enumeration values
export * from "./types";
-// DTOs and schemas
+// DTOs and schemas - Data Transfer Objects with Zod validation
export * from "./dtos";
-// Port interfaces (abstractions)
+// Port interfaces (abstractions) - Contracts that infrastructure must implement
export * from "./ports";
-// Errors
+// Errors - Domain-specific custom exception classes
export * from "./errors";
-// Core service
+// Core service - Main notification business logic
export * from "./notification.service";
diff --git a/src/core/notification.service.ts b/src/core/notification.service.ts
index 9d62e34..6ac9cc5 100644
--- a/src/core/notification.service.ts
+++ b/src/core/notification.service.ts
@@ -1,3 +1,31 @@
+/**
+ * @file Core Notification Service
+ *
+ * This file contains the main NotificationService class which orchestrates all notification operations.
+ * It is the heart of the NotificationKit business logic and handles:
+ *
+ * - Creating notifications (with validation and template rendering)
+ * - Sending notifications through appropriate channels
+ * - Managing notification lifecycle (pending, queued, sending, sent, delivered, failed)
+ * - Handling retries for failed notifications
+ * - Processing scheduled notifications
+ * - Querying and managing notification records
+ * - Emitting lifecycle events for monitoring
+ *
+ * Architecture:
+ * - This service depends only on port interfaces (INotificationSender, INotificationRepository, etc.)
+ * - It knows nothing about specific implementations (Nodemailer, Twilio, MongoDB, etc.)
+ * - This allows swapping implementations without changing this code (dependency inversion)
+ *
+ * Key Responsibilities:
+ * 1. Validation: Ensures recipients have required fields for their channel
+ * 2. Template Processing: Renders templates if templateId is provided
+ * 3. Scheduling: Handles scheduled vs immediate sends
+ * 4. Retry Logic: Attempts retries on failures (respecting maxRetries)
+ * 5. Status Tracking: Updates notification status throughout lifecycle
+ * 6. Event Emission: Fires events for monitoring and integrations
+ */
+
import type { CreateNotificationDto, SendNotificationDto } from "./dtos";
import {
InvalidRecipientError,
@@ -19,11 +47,29 @@ import type {
import { type Notification, type NotificationResult, NotificationStatus } from "./types";
/**
- * Core notification service - contains all business logic
+ * Core notification service - orchestrates all notification business logic
+ *
+ * This service is the main entry point for notification operations and coordinates
+ * between senders, repository, template engine, and other dependencies.
*/
export class NotificationService {
+ // Map of notification channels to their respective senders (e.g., "email" -> NodemailerSender)
+ // This allows O(1) lookup when routing notifications to the correct sender
private readonly senders: Map;
+ /**
+ * Constructor - Initialize the notification service with all dependencies
+ *
+ * @param repository - Repository for persisting notifications (MongoDB, PostgreSQL, etc.)
+ * @param idGenerator - Generator for creating unique notification IDs
+ * @param dateTimeProvider - Provider for working with dates/times (for scheduling)
+ * @param senders - Array of notification senders (email, SMS, push, etc.)
+ * @param templateEngine - Optional template engine for rendering notification content
+ * @param eventEmitter - Optional event emitter for lifecycle events
+ *
+ * The senders array is converted to a Map keyed by channel for fast lookups.
+ * Example: [NodemailerSender, TwilioSmsSender] becomes Map { "email" => NodemailerSender, "sms" => TwilioSmsSender }
+ */
constructor(
private readonly repository: INotificationRepository,
private readonly idGenerator: IIdGenerator,
@@ -32,14 +78,32 @@ export class NotificationService {
private readonly templateEngine?: ITemplateEngine,
private readonly eventEmitter?: INotificationEventEmitter,
) {
+ // Build a map of channel name to sender for efficient routing
+ // This allows us to quickly find the right sender based on notification channel
this.senders = new Map(senders.map((sender) => [sender.channel, sender]));
}
/**
- * Create a new notification without sending it
+ * Create a new notification without sending it immediately
+ *
+ * This method:
+ * 1. Validates the recipient has required fields for the channel
+ * 2. Renders template if templateId is provided
+ * 3. Determines initial status (PENDING for scheduled, QUEUED for immediate)
+ * 4. Persists the notification to repository
+ * 5. Emits a "notification.created" event
+ *
+ * @param dto - Data transfer object containing notification details
+ * @returns Promise - The created notification with generated ID and timestamps
+ * @throws InvalidRecipientError - If recipient is missing required fields for the channel
+ * @throws TemplateError - If template rendering fails
+ *
+ * Use this method when you want to create a notification record but send it later
+ * (e.g., for batch processing, scheduled sends, or queue-based systems)
*/
async create(dto: CreateNotificationDto): Promise {
- // Validate recipient for the channel
+ // Step 1: Validate recipient has required fields for the channel
+ // Example: Email channel requires email address, SMS requires phone number
const sender = this.senders.get(dto.channel);
if (sender && !sender.validateRecipient(dto.recipient)) {
throw new InvalidRecipientError(
@@ -48,14 +112,20 @@ export class NotificationService {
);
}
- // Process template if provided
+ // Step 2: Process template if template ID is provided
+ // This replaces {{variables}} in the template with actual values
let content = dto.content;
if (dto.content.templateId && this.templateEngine) {
content = await this.renderTemplate(dto);
}
+ // Step 3: Determine initial status based on scheduling
+ // If scheduledFor is in the future, status = PENDING (waiting for scheduled time)
+ // Otherwise, status = QUEUED (ready to send immediately)
const isScheduled = dto.scheduledFor && this.dateTimeProvider.isFuture(dto.scheduledFor);
+ // Step 4: Persist the notification to the database
+ // The repository will generate ID, createdAt, and updatedAt timestamps
const notification = await this.repository.create({
channel: dto.channel,
status: isScheduled ? NotificationStatus.PENDING : NotificationStatus.QUEUED,
@@ -71,6 +141,7 @@ export class NotificationService {
metadata: dto.metadata,
});
+ // Step 5: Emit creation event for monitoring/logging
await this.emitEvent({ type: "notification.created", notification });
return notification;
@@ -78,32 +149,75 @@ export class NotificationService {
/**
* Send a notification immediately
+ *
+ * This is a convenience method that combines create() and send():
+ * 1. Creates the notification record
+ * 2. Immediately sends it through the appropriate channel
+ *
+ * @param dto - Notification details (channel, recipient, content, etc.)
+ * @returns Promise - Result with success status and provider details
+ * @throws InvalidRecipientError - If recipient is invalid
+ * @throws SenderNotAvailableError - If no sender for channel or sender not ready
+ * @throws SendFailedError - If send operation fails
+ *
+ * This is the most common method - use it when you want to send right away
*/
async send(dto: SendNotificationDto): Promise {
- // Create the notification
+ // Create the notification record in database
const notification = await this.create(dto);
- // Send it immediately
+ // Send it immediately through the appropriate channel sender
return this.sendNotification(notification);
}
/**
* Send an existing notification by ID
+ *
+ * Useful for:
+ * - Retrying failed notifications
+ * - Sending scheduled notifications when their time arrives
+ * - Processing notifications from a queue
+ *
+ * @param notificationId - The ID of the notification to send
+ * @returns Promise - Result of the send operation
+ * @throws NotificationNotFoundError - If notification doesn't exist
+ * @throws SenderNotAvailableError - If sender not available
*/
async sendById(notificationId: string): Promise {
+ // Retrieve the notification from database
const notification = await this.repository.findById(notificationId);
if (!notification) {
throw new NotificationNotFoundError(notificationId);
}
+ // Send it through the channel sender
return this.sendNotification(notification);
}
/**
- * Internal method to send a notification
+ * Internal method to send a notification through its channel
+ *
+ * This is the core send logic that:
+ * 1. Validates notification can be sent (not already sent/cancelled)
+ * 2. Checks retry limits haven't been exceeded
+ * 3. Finds and validates the appropriate sender
+ * 4. Updates status to SENDING
+ * 5. Calls the sender to perform actual send
+ * 6. Updates status to SENT on success or FAILED on error
+ * 7. Emits lifecycle events throughout the process
+ * 8. Handles retries by incrementing retry count
+ *
+ * @param notification - The notification to send
+ * @returns Promise - Result with success status and details
+ * @throws MaxRetriesExceededError - If retry limit reached
+ * @throws SenderNotAvailableError - If no sender for channel or not ready
+ * @throws SendFailedError - If send fails
+ *
+ * This method is called internally by send(), sendById(), and retry()
*/
private async sendNotification(notification: Notification): Promise {
- // Check if notification can be sent
+ // Guard: Check if notification is already sent (idempotency check)
+ // Prevents duplicate sends if method is called twice
if (notification.status === NotificationStatus.SENT) {
return {
success: true,
@@ -112,6 +226,8 @@ export class NotificationService {
};
}
+ // Guard: Check if notification was cancelled
+ // Cancelled notifications should not be sent
if (notification.status === NotificationStatus.CANCELLED) {
return {
success: false,
@@ -120,46 +236,54 @@ export class NotificationService {
};
}
- // Check retry limit
+ // Guard: Check if retry limit has been exceeded
+ // Prevents infinite retry loops
if (notification.retryCount >= notification.maxRetries) {
throw new MaxRetriesExceededError(notification.id, notification.retryCount);
}
- // Get sender for channel
+ // Get the sender for this notification's channel
+ // Example: For channel="email", get NodemailerSender from the senders map
const sender = this.senders.get(notification.channel);
if (!sender) {
throw new SenderNotAvailableError(notification.channel);
}
- // Check sender is ready
+ // Check if sender is properly configured and ready to send
+ // This verifies credentials, connections, etc.
const isReady = await sender.isReady();
if (!isReady) {
throw new SenderNotAvailableError(notification.channel);
}
try {
- // Update status to sending
+ // Step 1: Update status to SENDING in database
+ // This marks the notification as currently being processed
await this.repository.update(notification.id, {
status: NotificationStatus.SENDING,
updatedAt: this.dateTimeProvider.now(),
});
+ // Emit event for monitoring/logging
await this.emitEvent({
type: "notification.sending",
notification: { ...notification, status: NotificationStatus.SENDING },
});
- // Send the notification
+ // Step 2: Perform the actual send operation through the sender
+ // This calls the external provider (Twilio, Nodemailer, Firebase, etc.)
const result = await sender.send(notification.recipient, notification.content);
if (result.success) {
- // Update status to sent
+ // SUCCESS PATH: Send was successful
+ // Update notification record with sent status and timestamp
const updatedNotification = await this.repository.update(notification.id, {
status: NotificationStatus.SENT,
sentAt: this.dateTimeProvider.now(),
updatedAt: this.dateTimeProvider.now(),
});
+ // Emit success event for monitoring
await this.emitEvent({
type: "notification.sent",
notification: updatedNotification,
@@ -168,7 +292,9 @@ export class NotificationService {
return result;
} else {
- // Sending failed, update retry count
+ // FAILURE PATH: Send failed (provider returned failure)
+ // Update notification with failed status and error message
+ // Increment retry count for potential future retry attempts
const updatedNotification = await this.repository.update(notification.id, {
status: NotificationStatus.FAILED,
error: result.error,
@@ -176,21 +302,25 @@ export class NotificationService {
updatedAt: this.dateTimeProvider.now(),
});
+ // Emit failure event for monitoring/alerts
await this.emitEvent({
type: "notification.failed",
notification: updatedNotification,
error: result.error || "Unknown error",
});
+ // Throw error to propagate failure to caller
throw new SendFailedError(result.error || "Send failed", {
notificationId: notification.id,
result,
});
}
} catch (error) {
- // Handle unexpected errors
+ // EXCEPTION PATH: Unexpected error occurred during send
+ // This catches errors from the sender or other unexpected issues
const errorMessage = error instanceof Error ? error.message : "Unknown error";
+ // Update notification status to FAILED and record error
await this.repository.update(notification.id, {
status: NotificationStatus.FAILED,
error: errorMessage,
@@ -198,12 +328,22 @@ export class NotificationService {
updatedAt: this.dateTimeProvider.now(),
});
+ // Re-throw error for caller to handle
throw error;
}
}
/**
- * Get a notification by ID
+ * Get a notification by its unique ID
+ *
+ * @param notificationId - The notification ID to retrieve
+ * @returns Promise - The notification entity
+ * @throws NotificationNotFoundError - If notification doesn't exist
+ *
+ * Use this to:
+ * - Check notification status
+ * - Display notification details to users
+ * - Verify notification was created
*/
async getById(notificationId: string): Promise {
const notification = await this.repository.findById(notificationId);
@@ -214,39 +354,71 @@ export class NotificationService {
}
/**
- * Query notifications
+ * Query/search for notifications matching criteria
+ *
+ * @param criteria - Query filters (recipientId, channel, status, dates, pagination)
+ * @returns Promise - Array of matching notifications
+ *
+ * Examples:
+ * - Get all notifications for a user: { recipientId: "user-123" }
+ * - Get failed emails: { channel: "email", status: "failed" }
+ * - Get recent urgent notifications: { priority: "urgent", fromDate: "2026-03-01T00:00:00Z", limit: 10 }
*/
async query(criteria: NotificationQueryCriteria): Promise {
return this.repository.find(criteria);
}
/**
- * Count notifications
+ * Count notifications matching criteria
+ *
+ * @param criteria - Query filters (same as query method)
+ * @returns Promise - Count of matching notifications
+ *
+ * Useful for:
+ * - Dashboard statistics (total sent, failed, etc.)
+ * - Pagination (showing "Page 1 of 10")
+ * - Monitoring alerts (e.g., "100 failed notifications in last hour")
*/
async count(criteria: NotificationQueryCriteria): Promise {
return this.repository.count(criteria);
}
/**
- * Cancel a notification
+ * Cancel a pending or queued notification
+ *
+ * @param notificationId - ID of notification to cancel
+ * @returns Promise - The cancelled notification
+ * @throws NotificationNotFoundError - If notification doesn't exist
+ * @throws SendFailedError - If notification already sent (can't cancel sent notifications)
+ *
+ * Use cases:
+ * - User opts out before scheduled notification sends
+ * - Business logic changes and notification is no longer relevant
+ * - Testing/debugging
+ *
+ * Note: You cannot cancel a notification that's already been sent
*/
async cancel(notificationId: string): Promise {
+ // Retrieve the notification
const notification = await this.repository.findById(notificationId);
if (!notification) {
throw new NotificationNotFoundError(notificationId);
}
+ // Check if already sent (can't cancel sent notifications)
if (notification.status === NotificationStatus.SENT) {
throw new SendFailedError("Cannot cancel a notification that has already been sent", {
notificationId,
});
}
+ // Update status to cancelled
const updated = await this.repository.update(notificationId, {
status: NotificationStatus.CANCELLED,
updatedAt: this.dateTimeProvider.now(),
});
+ // Emit cancellation event
await this.emitEvent({ type: "notification.cancelled", notification: updated });
return updated;
@@ -254,6 +426,22 @@ export class NotificationService {
/**
* Retry a failed notification
+ *
+ * @param notificationId - ID of the failed notification to retry
+ * @returns Promise - Result of the retry attempt
+ * @throws NotificationNotFoundError - If notification doesn't exist
+ * @throws SendFailedError - If notification isn't in FAILED status
+ * @throws MaxRetriesExceededError - If retry limit already reached
+ *
+ * This method:
+ * 1. Retrieves the notification
+ * 2. Validates it's in FAILED status
+ * 3. Attempts to send it again (retry count will be incremented)
+ *
+ * Use for:
+ * - Manual retry buttons in admin UI
+ * - Automated retry jobs (e.g., retry all failed from last hour)
+ * - Scheduled retry attempts with exponential backoff
*/
async retry(notificationId: string): Promise {
const notification = await this.repository.findById(notificationId);
@@ -261,26 +449,53 @@ export class NotificationService {
throw new NotificationNotFoundError(notificationId);
}
+ // Only failed notifications can be retried
if (notification.status !== NotificationStatus.FAILED) {
throw new SendFailedError("Only failed notifications can be retried", { notificationId });
}
+ // Attempt to send again (retry count will be checked and incremented)
return this.sendNotification(notification);
}
/**
* Process scheduled notifications that are ready to send
+ *
+ * This method is designed to be called by a scheduled job (cron, queue worker, etc.)
+ * It:
+ * 1. Queries for notifications ready to send (QUEUED or PENDING with past scheduledFor)
+ * 2. Sends each notification
+ * 3. Continues processing even if individual sends fail
+ * 4. Returns results for all processed notifications
+ *
+ * @param limit - Maximum number of notifications to process (default: 100)
+ * @returns Promise - Results for all processed notifications
+ *
+ * Example cron job setup:
+ * ```
+ * // Every minute, process up to 100 scheduled notifications
+ * schedule.scheduleJob('* * * * *', async () => {
+ * const results = await notificationService.processScheduled(100);
+ * console.log(`Processed ${results.length} notifications`);
+ * });
+ * ```
*/
async processScheduled(limit = 100): Promise {
+ // Get notifications that are ready to send
+ // This includes QUEUED and PENDING notifications where scheduledFor <= now
const notifications = await this.repository.findReadyToSend(limit);
const results: NotificationResult[] = [];
+ // Process each notification
+ // Note: We don't stop on errors - we want to process as many as possible
for (const notification of notifications) {
try {
+ // Try to send the notification
const result = await this.sendNotification(notification);
results.push(result);
} catch (error) {
- // Log error but continue processing other notifications
+ // If send fails, record the error but continue processing others
+ // This ensures one bad notification doesn't block the entire batch
results.push({
success: false,
notificationId: notification.id,
@@ -294,6 +509,32 @@ export class NotificationService {
/**
* Mark a notification as delivered (called by webhook/callback)
+ *
+ * Many notification providers (Twilio, SendGrid, etc.) send webhook callbacks
+ * when messages are delivered. This method handles those callbacks.
+ *
+ * @param notificationId - ID of the notification that was delivered
+ * @param metadata - Optional additional metadata from the provider
+ * @returns Promise - The updated notification
+ * @throws NotificationNotFoundError - If notification doesn't exist
+ *
+ * Status flow:
+ * SENT -> DELIVERED (confirmed by provider that recipient received it)
+ *
+ * This is useful for:
+ * - Tracking delivery rates
+ * - Confirming message receipt
+ * - Debugging delivery issues
+ *
+ * Example webhook handler:
+ * ```
+ * @Post('webhooks/twilio')
+ * async twilioWebhook(@Body() body) {
+ * if (body.MessageStatus === 'delivered') {
+ * await this.notificationService.markAsDelivered(body.MessageSid, body);
+ * }
+ * }
+ * ```
*/
async markAsDelivered(
notificationId: string,
@@ -304,39 +545,72 @@ export class NotificationService {
throw new NotificationNotFoundError(notificationId);
}
+ // Update the notification status to DELIVERED
+ // Merge any webhook metadata with existing metadata
const updated = await this.repository.update(notificationId, {
status: NotificationStatus.DELIVERED,
deliveredAt: this.dateTimeProvider.now(),
updatedAt: this.dateTimeProvider.now(),
- metadata: { ...notification.metadata, ...metadata },
+ metadata: { ...notification.metadata, ...metadata }, // Preserve existing + add new
});
+ // Emit delivery event for analytics/monitoring
await this.emitEvent({ type: "notification.delivered", notification: updated });
return updated;
}
/**
- * Render a template with variables
+ * Render a template with variables (private helper)
+ *
+ * This method handles template rendering if a template engine is configured.
+ * It takes a template ID and variables, renders the template, and returns
+ * the rendered content (title, body, html).
+ *
+ * @param dto - The create notification DTO with template info
+ * @returns Promise - Content with rendered template
+ * @throws TemplateError - If template rendering fails
+ *
+ * Template rendering flow:
+ * 1. Check if template engine is available and templateId is provided
+ * 2. If no template, return original content as-is
+ * 3. Call template engine with templateId and variables
+ * 4. Merge rendered content (title, body, html) back into content object
+ *
+ * Example template:
+ * ```
+ * Template ID: "welcome-email"
+ * Template: "Hello {{name}}, welcome to {{appName}}!"
+ * Variables: { name: "John", appName: "MyApp" }
+ * Result: "Hello John, welcome to MyApp!"
+ * ```
*/
private async renderTemplate(dto: CreateNotificationDto) {
+ // Guard: If no template engine or no templateId, return content as-is
+ // This allows non-template notifications to work without a template engine
if (!this.templateEngine || !dto.content.templateId) {
return dto.content;
}
try {
+ // Render the template using the template engine
+ // Pass templateId (e.g., "welcome-email") and variables (e.g., { name: "John" })
const rendered = await this.templateEngine.render(
dto.content.templateId,
- dto.content.templateVars || {},
+ dto.content.templateVars || {}, // Use empty object if no variables provided
);
+ // Merge rendered content back into the content object
+ // This preserves any other content properties while replacing title/body/html
return {
...dto.content,
- title: rendered.title,
- body: rendered.body,
- html: rendered.html,
+ title: rendered.title, // Rendered template title
+ body: rendered.body, // Rendered template body (plain text)
+ html: rendered.html, // Rendered template HTML (if applicable)
};
} catch (error) {
+ // If template rendering fails, throw a descriptive error
+ // This could happen if template doesn't exist or has syntax errors
throw new TemplateError(`Failed to render template: ${dto.content.templateId}`, {
templateId: dto.content.templateId,
error: error instanceof Error ? error.message : "Unknown error",
@@ -345,7 +619,29 @@ export class NotificationService {
}
/**
- * Emit a notification event
+ * Emit a notification event (private helper)
+ *
+ * This method publishes lifecycle events to the event emitter (if configured).
+ * Events are used for:
+ * - Monitoring and logging (track all notification state changes)
+ * - Analytics (count sent, failed, delivered notifications)
+ * - Integrations (trigger workflows, webhooks, alerts)
+ * - Auditing (compliance, tracking)
+ *
+ * @param event - The event to emit (type + data)
+ *
+ * Event types:
+ * - "notification.created" - When notification is created
+ * - "notification.sending" - When send starts
+ * - "notification.sent" - When send succeeds
+ * - "notification.failed" - When send fails
+ * - "notification.delivered" - When provider confirms delivery
+ * - "notification.cancelled" - When notification is cancelled
+ *
+ * Implementation note:
+ * - If no eventEmitter is configured, this is a no-op (silently skip)
+ * - Event emission is async but we await it to ensure proper sequencing
+ * - Events should not throw errors (emitter should handle failures gracefully)
*/
private async emitEvent(event: Parameters[0]): Promise {
if (this.eventEmitter) {
diff --git a/src/core/ports.ts b/src/core/ports.ts
index cad6f1a..9337dcf 100644
--- a/src/core/ports.ts
+++ b/src/core/ports.ts
@@ -1,3 +1,29 @@
+/**
+ * @file Port Interfaces (Hexagonal Architecture)
+ *
+ * This file defines all the "port" interfaces used in the NotificationKit system.
+ * Following hexagonal/clean architecture principles, these are the contracts
+ * that the infrastructure layer must implement.
+ *
+ * What are Ports?
+ * - Ports are interfaces that define HOW the core business logic * (domain layer) communicates with external systems.
+ * - They allow the core to remain independent of specific implementations.
+ * - The infrastructure layer provides "adapters" that implement these ports.
+ *
+ * Port Interfaces Defined:
+ * - INotificationSender: Contract for sending notifications through various channels
+ * - INotificationRepository: Contract for persisting and retrieving notifications
+ * - ITemplateEngine: Contract for rendering notification templates
+ * - IIdGenerator: Contract for generating unique notification IDs
+ * - IDateTimeProvider: Contract for working with dates/times
+ * - INotificationEventEmitter: Contract for emitting notification lifecycle events
+ *
+ * Benefits:
+ * - Testability: Easy to mock these interfaces in tests
+ * - Flexibility: Swap implementations without changing core logic
+ * - Framework independence: Core layer doesn't depend on specific libraries
+ */
+
import type {
Notification,
NotificationChannel,
@@ -8,16 +34,34 @@ import type {
/**
* Port: Notification sender abstraction
- * Infrastructure layer will implement this for each channel (email, SMS, etc.)
+ *
+ * This interface defines the contract that all notification senders must implement.
+ * The infrastructure layer will provide concrete implementations for each channel:
+ * - Email: NodemailerSender, AwsSesSender, etc.
+ * - SMS: TwilioSmsSender, AwsSnsSender, VonageSmsSender, etc.
+ * - Push: FirebasePushSender, OneSignalPushSender, etc.
+ *
+ * Each sender is responsible for:
+ * 1. Sending messages through their specific provider/channel
+ * 2. Validating recipient information matches channel requirements
+ * 3. Checking readiness/configuration before attempting to send
*/
export interface INotificationSender {
/**
- * The channel this sender handles
+ * The channel this sender handles (e.g., "email", "sms", "push")
+ * Used by the core service to route notifications to the correct sender
*/
readonly channel: NotificationChannel;
/**
* Send a notification to a recipient
+ *
+ * @param _recipient - The recipient information (email, phone, device token, etc.)
+ * @param _content - The notification content (title, body, html, etc.)
+ * @returns Promise - Result indicating success/failure and provider details
+ *
+ * This method performs the actual sending operation through the provider's API.
+ * It should handle provider-specific errors and return a consistent result format.
*/
send(
_recipient: NotificationRecipient,
@@ -26,169 +70,522 @@ export interface INotificationSender {
/**
* Check if the sender is properly configured and ready
+ *
+ * @returns Promise - true if ready to send, false otherwise
+ *
+ * This method verifies:
+ * - Required configuration/credentials are present
+ * - Connection to provider can be established
+ * - Any required resources are available
+ *
+ * Called before attempting to send to avoid unnecessary failures
*/
isReady(): Promise;
/**
* Validate recipient has required fields for this channel
+ *
+ * @param _recipient - The recipient to validate
+ * @returns boolean - true if recipient is valid for this channel
+ *
+ * Validation rules by channel:
+ * - Email: Must have valid email address
+ * - SMS: Must have valid phone number
+ * - Push: Must have device token
+ * - In-App: Must have user ID
+ * - Webhook: Must have URL in metadata
*/
validateRecipient(_recipient: NotificationRecipient): boolean;
}
/**
* Port: Notification repository abstraction
- * Infrastructure layer will implement this for persistence
+ *
+ * This interface defines the contract for persisting and retrieving notifications.
+ * The infrastructure layer will provide implementations for different databases:
+ * - MongoDB: MongooseRepository
+ * - PostgreSQL: PostgresRepository (custom)
+ * - In-Memory: InMemoryRepository (for testing)
+ *
+ * The repository handles all CRUD operations and queries for notifications,
+ * allowing the core service to remain database-agnostic.
*/
export interface INotificationRepository {
/**
- * Create a new notification record
+ * Create a new notification record in the database
+ *
+ * @param _notification - Notification data (without id, createdAt, updatedAt)
+ * @returns Promise - The created notification with generated ID and timestamps
+ *
+ * The repository implementation should:
+ * - Generate a unique ID
+ * - Set createdAt and updatedAt timestamps
+ * - Persist to database
+ * - Return the complete notification object
*/
create(
_notification: Omit,
): Promise;
/**
- * Find a notification by ID
+ * Find a notification by its unique ID
+ *
+ * @param _id - The notification ID to search for
+ * @returns Promise - The notification if found, null otherwise
+ *
+ * Used for:
+ * - Retrieving notification details
+ * - Checking notification status
+ * - Retry operations
*/
findById(_id: string): Promise;
/**
- * Find notifications matching criteria
+ * Find notifications matching specified criteria
+ *
+ * @param _criteria - Query filters (recipientId, channel, status, dates, pagination)
+ * @returns Promise - Array of matching notifications
+ *
+ * Supports filtering by:
+ * - Recipient ID (get all notifications for a user)
+ * - Channel (get all email/SMS/push notifications)
+ * - Status (find pending/failed/sent notifications)
+ * - Priority (get urgent notifications)
+ * - Date range (notifications between dates)
+ * - Pagination (limit and offset)
*/
find(_criteria: NotificationQueryCriteria): Promise;
/**
- * Update a notification
+ * Update an existing notification
+ *
+ * @param _id - The notification ID to update
+ * @param _updates - Partial notification data to update
+ * @returns Promise - The updated notification
+ *
+ * Used for:
+ * - Updating status during send process
+ * - Recording send/delivery timestamps
+ * - Incrementing retry count
+ * - Storing error messages
*/
update(_id: string, _updates: Partial): Promise;
/**
- * Delete a notification
+ * Delete a notification by ID
+ *
+ * @param _id - The notification ID to delete
+ * @returns Promise - true if deleted, false if not found
+ *
+ * Note: Be cautious with deletion - consider soft deletes for audit purposes
*/
delete(_id: string): Promise;
/**
* Count notifications matching criteria
+ *
+ * @param _criteria - Query filters (same as find)
+ * @returns Promise - Count of matching notifications
+ *
+ * Useful for:
+ * - Dashboard statistics
+ * - Pagination (total count)
+ * - Monitoring failed notification counts
*/
count(_criteria: NotificationQueryCriteria): Promise;
/**
* Find notifications that are ready to be sent
+ *
+ * @param _limit - Maximum number of notifications to return
+ * @returns Promise - Notifications ready for sending
+ *
+ * Returns notifications that are:
+ * - Status = QUEUED (ready to send immediately), OR
+ * - Status = PENDING with scheduledFor <= now (scheduled time has arrived)
+ *
+ * Ordered by:
+ * 1. Priority (urgent first)
+ * 2. Created date (oldest first)
+ *
+ * Used by scheduled jobs or queue processors to batch-send notifications
*/
findReadyToSend(_limit: number): Promise;
}
/**
* Query criteria for finding notifications
+ *
+ * This interface defines all possible filters for querying notifications.
+ * All fields are optional - you can combine any filters you need.
+ *
+ * Example queries:
+ * ```typescript
+ * // Find all failed emails for a user
+ * { recipientId: "user-123", channel: "email", status: "failed" }
+ *
+ * // Find urgent notifications from last 24 hours
+ * { priority: "urgent", fromDate: "2026-03-30T00:00:00Z", limit: 50 }
+ *
+ * // Paginate through SMS notifications (page 3, 20 per page)
+ * { channel: "sms", limit: 20, offset: 40 }
+ * ```
*/
export interface NotificationQueryCriteria {
- recipientId?: string;
- channel?: NotificationChannel;
- status?: string;
- priority?: string;
- fromDate?: string;
- toDate?: string;
- limit?: number;
- offset?: number;
+ recipientId?: string; // Filter by recipient ID
+ channel?: NotificationChannel; // Filter by channel (email, sms, push, etc.)
+ status?: string; // Filter by status (pending, sent, failed, etc.)
+ priority?: string; // Filter by priority (low, normal, high, urgent)
+ fromDate?: string; // Start date (ISO 8601)
+ toDate?: string; // End date (ISO 8601)
+ limit?: number; // Max results to return (pagination)
+ offset?: number; // Number of results to skip (pagination)
}
/**
- * Port: Template engine abstraction
- * Infrastructure layer will implement this for template rendering
+ * Port: Template Engine - Abstraction for rendering notification templates
+ *
+ * The template engine is responsible for:
+ * - Loading templates by ID (e.g., "welcome-email", "password-reset-sms")
+ * - Rendering templates with variables (e.g., {{ userName }} ā "John Doe")
+ * - Returning rendered content (title, body, HTML)
+ *
+ * Why abstract this?
+ * - Allows different template engines (Handlebars, Mustache, EJS, etc.)
+ * - Supports loading from different sources (filesystem, database, API)
+ * - Testable (mock template engine for tests)
+ * - Swappable implementation without changing core logic
+ *
+ * Infrastructure implementations:
+ * - HandlebarsTemplateEngine (uses Handlebars syntax)
+ * - FileSystemTemplateEngine (loads from .hbs files)
+ * - DatabaseTemplateEngine (loads from database)
+ * - NoOpTemplateEngine (for apps not using templates)
+ *
+ * Template structure example:
+ * ```handlebars
+ * Title: Welcome to {{appName}}!
+ * Body: Hi {{userName}}, thanks for joining {{appName}}.
+ * Your account is now active.
+ * HTML: Welcome to {{appName}}!
+ * Hi {{userName}}, thanks for joining.
+ * ```
*/
export interface ITemplateEngine {
/**
* Render a template with variables
+ *
+ * @param _templateId - Unique template identifier (e.g., "welcome-email")
+ * @param _variables - Key-value pairs for variable substitution
+ * @returns Promise - Rendered title, body, and HTML
+ * @throws TemplateError - If template doesn't exist or rendering fails
+ *
+ * Example:
+ * ```typescript
+ * const result = await templateEngine.render("welcome-email", {
+ * userName: "John Doe",
+ * appName: "MyApp"
+ * });
+ * // Returns: { title: "Welcome to MyApp!", body: "Hi John Doe, ...", html: "..." }
+ * ```
*/
render(_templateId: string, _variables: Record): Promise;
/**
* Check if a template exists
+ *
+ * @param _templateId - Template ID to check
+ * @returns Promise - true if template exists, false otherwise
+ *
+ * Useful for validation before attempting to render
*/
hasTemplate(_templateId: string): Promise;
/**
- * Validate template variables
+ * Validate that all required template variables are provided
+ *
+ * @param _templateId - Template ID
+ * @param _variables - Variables to validate
+ * @returns Promise - true if all required variables present
+ *
+ * Example:
+ * If template requires {{ userName }} and {{ code }}, this checks both are provided
*/
validateVariables(_templateId: string, _variables: Record): Promise;
}
/**
* Result of template rendering
+ *
+ * Contains the rendered content ready to be sent.
+ * - title: Used as email subject, push notification title, etc.
+ * - body: Plain text content
+ * - html: HTML content (for email, optional)
*/
export interface TemplateResult {
- title: string;
- body: string;
- html?: string;
+ title: string; // Rendered title/subject
+ body: string; // Rendered plain text body
+ html?: string; // Rendered HTML body (optional, mainly for email)
}
/**
- * Port: ID generator abstraction
+ * Port: ID Generator - Abstraction for generating unique IDs
+ *
+ * This port allows pluggable ID generation strategies.
+ *
+ * Why abstract this?
+ * - Different ID formats (UUID, nanoid, ULID, Snowflake, etc.)
+ * - Testable (predictable IDs in tests)
+ * - Consistent ID format across the system
+ *
+ * Infrastructure implementations:
+ * - UUIDGenerator (uses uuid v4)
+ * - NanoidGenerator (uses nanoid)
+ * - ULIDGenerator (uses ULID - sortable by time)
+ * - IncrementalIdGenerator (for testing)
*/
export interface IIdGenerator {
/**
- * Generate a unique ID
+ * Generate a unique ID for a notification
+ *
+ * @returns string - A unique identifier
+ *
+ * Requirements:
+ * - Must be globally unique (no collisions)
+ * - Should be URL-safe
+ * - Recommended: Sortable by creation time (ULID)
+ *
+ * Example implementations:
+ * - UUID v4: "550e8400-e29b-41d4-a716-446655440000"
+ * - Nanoid: "V1StGXR8_Z5jdHi6B-myT"
+ * - ULID: "01ARZ3NDEKTSV4RRFFQ69G5FAV"
*/
generate(): string;
}
/**
- * Port: Date/time provider abstraction
+ * Port: Date/Time Provider - Abstraction for date/time operations
+ *
+ * Why abstract date/time?
+ * - Testability: Mock current time in tests
+ * - Consistency: All timestamps in same format (ISO 8601)
+ * - Timezone handling: Normalize to UTC
+ *
+ * Infrastructure implementations:
+ * - SystemDateTimeProvider (uses system time)
+ * - FixedDateTimeProvider (for testing - returns fixed time)
*/
export interface IDateTimeProvider {
/**
- * Get current ISO 8601 timestamp
+ * Get current timestamp in ISO 8601 format
+ *
+ * @returns string - Current UTC time as ISO 8601
+ *
+ * Example: "2026-03-31T14:30:00.000Z"
+ *
+ * Used for:
+ * - Setting createdAt, updatedAt timestamps
+ * - Recording sentAt, deliveredAt times
+ * - Comparing with scheduledFor dates
*/
now(): string;
/**
* Check if a datetime is in the past
+ *
+ * @param _datetime - ISO 8601 datetime string
+ * @returns boolean - true if datetime < now
+ *
+ * Used for:
+ * - Validating scheduled dates
+ * - Finding expired items
*/
isPast(_datetime: string): boolean;
/**
* Check if a datetime is in the future
+ *
+ * @param _datetime - ISO 8601 datetime string
+ * @returns boolean - true if datetime > now
+ *
+ * Used for:
+ * - Checking if notification is scheduled for future
+ * - Validating input dates
*/
isFuture(_datetime: string): boolean;
}
/**
- * Port: Notification queue abstraction (for async processing)
+ * Port: Notification Queue - Abstraction for async notification queue
+ *
+ * The queue is used for asynchronous notification processing:
+ * - Decouple notification creation from sending
+ * - Handle high-volume notification bursts
+ * - Prioritize urgent notifications
+ * - Retry failed notifications
+ *
+ * Why use a queue?
+ * - Performance: Don't block API responses waiting for sends
+ * - Reliability: Persist notifications if sender is temporarily down
+ * - Scalability: Multiple workers can process queue in parallel
+ * - Rate limiting: Control send rate to avoid provider limits
+ *
+ * Infrastructure implementations:
+ * - RedisQueue (Redis-based queue with prioritization)
+ * - BullMQQueue (Bull queue with advanced features)
+ * - SQSQueue (AWS SQS)
+ * - InMemoryQueue (for testing/development)
+ *
+ * Typical flow:
+ * 1. API request creates notification ā notification.status = QUEUED
+ * 2. Notification ID is added to queue
+ * 3. Worker dequeues notification ID
+ * 4. Worker sends notification and updates status
*/
export interface INotificationQueue {
/**
- * Add a notification to the queue
+ * Add a notification to the queue for async processing
+ *
+ * @param _notificationId - ID of notification to queue
+ * @param _priority - Optional priority (urgent notifications processed first)
+ * @returns Promise
+ *
+ * Example:
+ * ```typescript
+ * // Create notification
+ * const notification = await service.create(dto);
+ *
+ * // Queue for async processing
+ * await queue.enqueue(notification.id, notification.priority);
+ * ```
*/
enqueue(_notificationId: string, _priority?: string): Promise;
/**
- * Remove a notification from the queue
+ * Remove and return the next notification ID from the queue
+ *
+ * @returns Promise - Next notification ID, or null if queue is empty
+ *
+ * Worker loop example:
+ * ```typescript
+ * while (true) {
+ * const notificationId = await queue.dequeue();
+ * if (notificationId) {
+ * await notificationService.sendById(notificationId);
+ * } else {
+ * await sleep(1000); // Wait if queue is empty
+ * }
+ * }
+ * ```
*/
dequeue(): Promise;
/**
- * Get queue size
+ * Get the current size of the queue
+ *
+ * @returns Promise - Number of notifications waiting in queue
+ *
+ * Useful for monitoring and alerting (e.g., alert if queue size > 10000)
*/
size(): Promise;
/**
- * Clear the queue
+ * Clear all notifications from the queue
+ *
+ * @returns Promise
+ *
+ * Use with caution - typically only for testing or emergency queue purges
*/
clear(): Promise;
}
/**
- * Port: Event emitter abstraction (for notification events)
+ * Port: Event Emitter - Abstraction for publishing notification lifecycle events
+ *
+ * The event emitter publishes events for monitoring, logging, analytics, and
+ * integrations throughout the notification lifecycle.
+ *
+ * Why emit events?
+ * - Monitoring: Track send success/failure rates
+ * - Analytics: Measure notification engagement
+ * - Logging: Audit trail of all notifications
+ * - Integrations: Trigger webhooks, update CRM, send to data warehouse
+ * - Real-time updates: WebSocket updates to admin dashboard
+ *
+ * Infrastructure implementations:
+ * - EventEmitter2EventEmitter (Node.js EventEmitter2)
+ * - KafkaEventEmitter (publish to Kafka topics)
+ * - SQSEventEmitter (publish to AWS SQS)
+ * - WebhookEventEmitter (HTTP webhooks)
+ * - CompositeEventEmitter (emit to multiple destinations)
+ *
+ * Event flow example:
+ * 1. notification.created ā Log to console, send to analytics
+ * 2. notification.sending ā Update dashboard with "sending" status
+ * 3. notification.sent ā Increment Prometheus counter, log success
+ * 4. notification.delivered ā Update CRM with delivery confirmation
*/
export interface INotificationEventEmitter {
/**
- * Emit an event
+ * Emit a notification lifecycle event
+ *
+ * @param _event - The event to emit (see NotificationEvent type)
+ * @returns Promise
+ *
+ * Events should be fire-and-forget. Event emission failures should not
+ * prevent notification processing (log errors but don't throw).
+ *
+ * Example implementation:
+ * ```typescript
+ * async emit(event: NotificationEvent) {
+ * try {
+ * console.log(`[EVENT] ${event.type}`, event);
+ * await kafka.send({ topic: 'notifications', messages: [{ value: JSON.stringify(event) }] });
+ * } catch (error) {
+ * console.error('Failed to emit event:', error);
+ * // Don't throw - event emission is non-critical
+ * }
+ * }
+ * ```
*/
emit(_event: NotificationEvent): Promise;
}
/**
- * Notification events
+ * Notification lifecycle events
+ *
+ * These are all possible events emitted during a notification's lifecycle.
+ * Each event type has a specific structure with relevant data.
+ *
+ * Event types:
+ *
+ * 1. "notification.created" - Notification was created
+ * - Contains: notification entity
+ * - When: After create() succeeds
+ *
+ * 2. "notification.queued" - Notification was added to async queue
+ * - Contains: notification entity
+ * - When: After enqueue() succeeds
+ *
+ * 3. "notification.sending" - Send operation started
+ * - Contains: notification entity (status = SENDING)
+ * - When: Before calling sender.send()
+ *
+ * 4. "notification.sent" - Send succeeded
+ * - Contains: notification entity (status = SENT), send result
+ * - When: After sender.send() returns success
+ *
+ * 5. "notification.delivered" - Provider confirmed delivery
+ * - Contains: notification entity (status = DELIVERED)
+ * - When: Webhook callback from provider
+ *
+ * 6. "notification.failed" - Send failed
+ * - Contains: notification entity (status = FAILED), error message
+ * - When: After sender.send() returns failure or throws error
+ *
+ * 7. "notification.cancelled" - Notification was cancelled
+ * - Contains: notification entity (status = CANCELLED)
+ * - When: After cancel() succeeds
*/
export type NotificationEvent =
| { type: "notification.created"; notification: Notification }
diff --git a/src/core/types.ts b/src/core/types.ts
index e71d684..ed1a584 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -1,35 +1,57 @@
+/**
+ * @file Core Domain Types and Interfaces
+ *
+ * This file defines all the core domain types, enums, and interfaces for the NotificationKit system.
+ * It contains the fundamental building blocks that represent notifications in the system:
+ *
+ * - NotificationChannel: Enum defining available delivery channels (Email, SMS, Push, etc.)
+ * - NotificationStatus: Enum tracking the lifecycle state of a notification
+ * - NotificationPriority: Enum for categorizing notification urgency
+ * - NotificationRecipient: Interface describing who receives the notification
+ * - NotificationContent: Interface describing what the notification contains
+ * - Notification: Main domain entity representing a complete notification
+ * - NotificationResult: Interface for send operation results
+ *
+ * These types are used throughout the entire system and form the core vocabulary
+ * of the notification domain.
+ */
+
/**
* Notification channel types
+ * Defines the different delivery mechanisms available for sending notifications
*/
export enum NotificationChannel {
- EMAIL = "email",
- SMS = "sms",
- PUSH = "push",
- IN_APP = "in_app",
- WEBHOOK = "webhook",
+ EMAIL = "email", // Email delivery via SMTP or email service providers
+ SMS = "sms", // SMS text messages via telecom providers
+ PUSH = "push", // Mobile push notifications via FCM, APNs, etc.
+ IN_APP = "in_app", // In-application notifications (stored for retrieval)
+ WEBHOOK = "webhook", // HTTP webhook callbacks to external systems
+ WHATSAPP = "whatsapp", // WhatsApp messages via Twilio or Meta Business API
}
/**
* Notification status lifecycle
+ * Tracks the current state of a notification through its delivery process
*/
export enum NotificationStatus {
- PENDING = "pending",
- QUEUED = "queued",
- SENDING = "sending",
- SENT = "sent",
- DELIVERED = "delivered",
- FAILED = "failed",
- CANCELLED = "cancelled",
+ PENDING = "pending", // Created but not yet ready to send (e.g., scheduled for future)
+ QUEUED = "queued", // Ready to be sent, waiting in queue
+ SENDING = "sending", // Currently being sent to provider
+ SENT = "sent", // Successfully sent to provider (but not yet confirmed delivered)
+ DELIVERED = "delivered", // Confirmed delivered to recipient
+ FAILED = "failed", // Send attempt failed (may retry based on configuration)
+ CANCELLED = "cancelled", // Manually cancelled before sending
}
/**
* Notification priority levels
+ * Used for queue ordering and handling urgency
*/
export enum NotificationPriority {
- LOW = "low",
- NORMAL = "normal",
- HIGH = "high",
- URGENT = "urgent",
+ LOW = "low", // Low priority, can be delayed (e.g., newsletters, digests)
+ NORMAL = "normal", // Standard priority for most notifications
+ HIGH = "high", // Important, should be sent soon (e.g., alerts)
+ URGENT = "urgent", // Critical, send immediately (e.g., OTP codes, security alerts)
}
/**
diff --git a/src/index.ts b/src/index.ts
index f39fa91..dc0063d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,22 @@
-// Core domain layer
+/**
+ * @file Main Entry Point for NotificationKit
+ *
+ * This file serves as the primary export point for the entire NotificationKit package.
+ * It exports all modules from three main layers following clean architecture principles:
+ *
+ * 1. Core Layer (./core): Framework-agnostic business logic, types, and domain entities
+ * 2. Infrastructure Layer (./infra): Concrete implementations of senders, repositories, and providers
+ * 3. NestJS Integration Layer (./nest): NestJS-specific module configuration and decorators
+ *
+ * Usage: Import from this file to access any NotificationKit functionality
+ * Example: import { NotificationKitModule, NodemailerSender, NotificationChannel } from '@ciscode/notification-kit'
+ */
+
+// Core domain layer - Contains pure business logic and domain models
export * from "./core";
-// Infrastructure layer
+// Infrastructure layer - Contains provider implementations and adapters
export * from "./infra";
-// NestJS integration layer
+// NestJS integration layer - Contains NestJS module, controllers, and decorators
export * from "./nest";
diff --git a/src/infra/senders/email/nodemailer.sender.ts b/src/infra/senders/email/nodemailer.sender.ts
index 6855b63..0231a79 100644
--- a/src/infra/senders/email/nodemailer.sender.ts
+++ b/src/infra/senders/email/nodemailer.sender.ts
@@ -1,3 +1,48 @@
+/**
+ * Nodemailer Email Sender - SMTP Email Delivery
+ *
+ * This is the email sender implementation using Nodemailer, which supports
+ * any SMTP provider (Gmail, SendGrid, AWS SES, Mailgun, etc.).
+ *
+ * Features:
+ * - SMTP support: Works with any SMTP server
+ * - HTML emails: Supports both plain text and HTML content
+ * - Lazy loading: Nodemailer is loaded only when needed (peer dependency)
+ * - Connection verification: isReady() checks SMTP connection
+ * - Email validation: Validates email format before sending
+ *
+ * Supported providers (any SMTP server):
+ * - Gmail (smtp.gmail.com:587)
+ * - SendGrid (smtp.sendgrid.net:587)
+ * - AWS SES (email-smtp.us-east-1.amazonaws.com:587)
+ * - Mailgun (smtp.mailgun.org:587)
+ * - Office 365 (smtp.office365.com:587)
+ * - Custom SMTP servers
+ *
+ * Configuration example:
+ * ```typescript
+ * const emailSender = new NodemailerSender({
+ * host: 'smtp.gmail.com',
+ * port: 587,
+ * secure: false,
+ * auth: {
+ * user: 'your-email@gmail.com',
+ * pass: 'your-app-password'
+ * },
+ * from: 'noreply@yourapp.com',
+ * fromName: 'Your App Name'
+ * });
+ * ```
+ *
+ * Usage with NotificationKit:
+ * ```typescript
+ * NotificationKitModule.forRoot({
+ * senders: [emailSender],
+ * // ... other config
+ * });
+ * ```
+ */
+
import type {
INotificationSender,
NotificationChannel,
@@ -6,57 +51,96 @@ import type {
NotificationResult,
} from "../../../core";
+/**
+ * Configuration for Nodemailer SMTP transport
+ *
+ * This matches the Nodemailer transport configuration structure.
+ * See: https://nodemailer.com/smtp/
+ */
export interface NodemailerConfig {
- host: string;
- port: number;
- secure?: boolean | undefined;
- auth?:
+ host: string; // SMTP server hostname (e.g., "smtp.gmail.com")
+ port: number; // SMTP port (587 for TLS, 465 for SSL, 25 for unencrypted)
+ secure?: boolean | undefined; // true for port 465 (SSL), false for other ports (TLS)
+ auth?: // SMTP authentication credentials
| {
- user: string;
- pass: string;
+ user: string; // SMTP username (usually your email)
+ pass: string; // SMTP password (use app-specific password for Gmail)
}
| undefined;
- from: string;
- fromName?: string | undefined;
+ from: string; // Default "from" email address
+ fromName?: string | undefined; // Optional "from" display name
}
/**
* Email sender implementation using Nodemailer
- * Supports any SMTP provider (Gmail, SendGrid, AWS SES, etc.)
+ *
+ * Implements the INotificationSender port for email notifications.
+ * Uses Nodemailer (https://nodemailer.com/) for SMTP email delivery.
*/
export class NodemailerSender implements INotificationSender {
readonly channel: NotificationChannel = "email" as NotificationChannel;
+
+ // Transporter is created lazily and cached for reuse
+ // This avoids creating multiple SMTP connections
private transporter: any = null;
constructor(private readonly config: NodemailerConfig) {}
/**
- * Initialize the nodemailer transporter lazily
+ * Initialize the nodemailer transporter (lazy initialization)
+ *
+ * Why lazy initialization?
+ * - Nodemailer is a peer dependency (may not be installed)
+ * - Connection is only needed when actually sending emails
+ * - Avoids startup errors if SMTP is misconfigured
+ * - Transporter is reused for all sends (connection pooling)
+ *
+ * @returns Promise - Nodemailer transporter instance
+ * @private
*/
private async getTransporter(): Promise {
+ // Return cached transporter if already created
if (this.transporter) {
return this.transporter;
}
- // Dynamic import to avoid requiring nodemailer at build time
+ // Dynamic import: Load nodemailer only when needed
+ // This allows NotificationKit to work without nodemailer installed
+ // if you're only using SMS/push notifications
// @ts-expect-error - nodemailer is an optional peer dependency
const nodemailer = await import("nodemailer");
+ // Create SMTP transporter with configured settings
this.transporter = nodemailer.createTransport({
host: this.config.host,
port: this.config.port,
- secure: this.config.secure ?? false,
+ secure: this.config.secure ?? false, // Default to false (TLS on port 587)
auth: this.config.auth,
});
return this.transporter;
}
+ /**
+ * Send an email notification
+ *
+ * This method:
+ * 1. Validates recipient has an email address
+ * 2. Gets or creates the SMTP transporter
+ * 3. Constructs the email (from, to, subject, text, html)
+ * 4. Sends via SMTP
+ * 5. Returns result with success status and provider message ID
+ *
+ * @param _recipient - Notification recipient (must have email field)
+ * @param _content - Notification content (title, body, html)
+ * @returns Promise - Send result
+ */
async send(
_recipient: NotificationRecipient,
_content: NotificationContent,
): Promise {
try {
+ // Validate recipient has email address
if (!_recipient.email) {
return {
success: false,
@@ -65,31 +149,37 @@ export class NodemailerSender implements INotificationSender {
};
}
+ // Get transporter (creates if not exists)
const transporter = await this.getTransporter();
+ // Construct email options
const mailOptions = {
+ // From address: Use "Display Name " format if fromName provided
from: this.config.fromName
? `"${this.config.fromName}" <${this.config.from}>`
: this.config.from,
- to: _recipient.email,
- subject: _content.title,
- text: _content.body,
- html: _content.html,
+ to: _recipient.email, // Recipient email
+ subject: _content.title, // Email subject
+ text: _content.body, // Plain text body
+ html: _content.html, // HTML body (optional, falls back to text)
};
+ // Send the email via SMTP
const info = await transporter.sendMail(mailOptions);
+ // Return success with provider message ID (for tracking)
return {
success: true,
notificationId: _recipient.id,
- providerMessageId: info.messageId,
+ providerMessageId: info.messageId, // Nodemailer message ID
metadata: {
- accepted: info.accepted,
- rejected: info.rejected,
- response: info.response,
+ accepted: info.accepted, // Accepted recipients
+ rejected: info.rejected, // Rejected recipients
+ response: info.response, // SMTP server response
},
};
} catch (error) {
+ // Return failure with error message
return {
success: false,
notificationId: _recipient.id,
@@ -98,20 +188,54 @@ export class NodemailerSender implements INotificationSender {
}
}
+ /**
+ * Check if the email sender is ready to send
+ *
+ * This method verifies the SMTP connection by attempting to connect
+ * to the SMTP server. If connection fails, sending won't work.
+ *
+ * @returns Promise - true if SMTP connection works, false otherwise
+ *
+ * Called by NotificationService before sending to ensure sender is ready.
+ * Prevents attempting sends when SMTP is misconfigured or unreachable.
+ */
async isReady(): Promise {
try {
const transporter = await this.getTransporter();
- await transporter.verify();
+ await transporter.verify(); // Verifies SMTP connection
return true;
} catch {
- return false;
+ return false; // SMTP connection failed (wrong credentials, server down, etc.)
}
}
+ /**
+ * Validate recipient has valid email address
+ *
+ * Checks that:
+ * 1. Recipient has an email field
+ * 2. Email format is valid (basic regex check)
+ *
+ * @param _recipient - Recipient to validate
+ * @returns boolean - true if valid, false otherwise
+ *
+ * Note: This is a basic format check, not a deliverability check.
+ * The email could still bounce if it doesn't exist.
+ */
validateRecipient(_recipient: NotificationRecipient): boolean {
return !!_recipient.email && this.isValidEmail(_recipient.email);
}
+ /**
+ * Validate email format using regex
+ *
+ * Basic email validation: checks for user@domain.tld format.
+ * This is not RFC-compliant but catches most invalid formats.
+ *
+ * @param email - Email address to validate
+ * @returns boolean - true if format looks valid
+ * @private
+ */
private isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
diff --git a/src/infra/senders/index.ts b/src/infra/senders/index.ts
index 77ceae4..ba0fede 100644
--- a/src/infra/senders/index.ts
+++ b/src/infra/senders/index.ts
@@ -6,6 +6,9 @@ export * from "./sms/twilio.sender";
export * from "./sms/aws-sns.sender";
export * from "./sms/vonage.sender";
+// WhatsApp senders
+export * from "./whatsapp";
+
// Push notification senders
export * from "./push/firebase.sender";
export * from "./push/onesignal.sender";
diff --git a/src/infra/senders/whatsapp/index.ts b/src/infra/senders/whatsapp/index.ts
new file mode 100644
index 0000000..83cdbd5
--- /dev/null
+++ b/src/infra/senders/whatsapp/index.ts
@@ -0,0 +1,10 @@
+/**
+ * WhatsApp Senders - Export WhatsApp sender implementations
+ *
+ * This module exports all WhatsApp sender implementations:
+ * - TwilioWhatsAppSender: Real WhatsApp sender using Twilio API
+ * - MockWhatsAppSender: Mock sender for testing without credentials
+ */
+
+export * from "./twilio-whatsapp.sender";
+export * from "./mock-whatsapp.sender";
diff --git a/src/infra/senders/whatsapp/mock-whatsapp.sender.ts b/src/infra/senders/whatsapp/mock-whatsapp.sender.ts
new file mode 100644
index 0000000..5a169f6
--- /dev/null
+++ b/src/infra/senders/whatsapp/mock-whatsapp.sender.ts
@@ -0,0 +1,181 @@
+/**
+ * Mock WhatsApp Sender - Testing WhatsApp Without Real API
+ *
+ * This is a mock implementation of the WhatsApp sender for testing and development
+ * purposes. It simulates sending WhatsApp messages without requiring actual Twilio
+ * credentials or making real API calls.
+ *
+ * Features:
+ * - No credentials required: Works immediately without setup
+ * - Always succeeds: Simulates successful message delivery
+ * - Console logging: Outputs what would be sent (for debugging)
+ * - Media support: Logs media URLs that would be sent
+ * - Template support: Logs template usage
+ * - Fast: No network calls, instant responses
+ *
+ * Use cases:
+ * - Local development without Twilio account
+ * - Testing notification flows
+ * - Demo applications
+ * - CI/CD pipelines without credentials
+ *
+ * Configuration example:
+ * ```typescript
+ * const mockSender = new MockWhatsAppSender({
+ * logMessages: true // Optional: log to console (default: true)
+ * });
+ * ```
+ *
+ * Usage with NotificationKit:
+ * ```typescript
+ * NotificationKitModule.forRoot({
+ * senders: [mockSender],
+ * // ... other config
+ * });
+ * ```
+ */
+
+import type {
+ INotificationSender,
+ NotificationChannel,
+ NotificationContent,
+ NotificationRecipient,
+ NotificationResult,
+} from "../../../core";
+
+/**
+ * Configuration for Mock WhatsApp sender
+ */
+export interface MockWhatsAppConfig {
+ /**
+ * Whether to log messages to console (default: true)
+ * Useful for debugging and seeing what would be sent
+ */
+ logMessages?: boolean;
+}
+
+/**
+ * Mock WhatsApp sender for testing
+ *
+ * Implements the INotificationSender port but doesn't actually send messages.
+ * Perfect for development, testing, and demos.
+ */
+export class MockWhatsAppSender implements INotificationSender {
+ readonly channel: NotificationChannel = "whatsapp" as NotificationChannel;
+
+ constructor(private readonly config: MockWhatsAppConfig = { logMessages: true }) {}
+
+ /**
+ * Simulate sending a WhatsApp message
+ *
+ * This method:
+ * 1. Validates recipient has phone number
+ * 2. Logs what would be sent (if logging enabled)
+ * 3. Returns mock success response
+ * 4. Never actually sends anything
+ *
+ * @param _recipient - Notification recipient
+ * @param _content - Notification content
+ * @returns Promise - Mock success result
+ */
+ async send(
+ _recipient: NotificationRecipient,
+ _content: NotificationContent,
+ ): Promise {
+ // Validate recipient has phone
+ if (!_recipient.phone) {
+ return {
+ success: false,
+ notificationId: _recipient.id,
+ error: "Recipient phone number is required for WhatsApp",
+ };
+ }
+
+ // Validate phone format
+ if (!this.isValidPhoneNumber(_recipient.phone)) {
+ return {
+ success: false,
+ notificationId: _recipient.id,
+ error: `Invalid phone number format. Must be E.164 format (e.g., +1234567890). Got: ${_recipient.phone}`,
+ };
+ }
+
+ // Log what would be sent (if enabled)
+ if (this.config.logMessages) {
+ console.log("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
+ console.log("š± [MockWhatsApp] Simulating WhatsApp send");
+ console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
+ console.log(`To: ${_recipient.phone}`);
+ console.log(`Recipient ID: ${_recipient.id}`);
+
+ if (_content.templateId) {
+ console.log(`\nš Template: ${_content.templateId}`);
+ if (_content.templateVars) {
+ console.log(`Variables: ${JSON.stringify(_content.templateVars, null, 2)}`);
+ }
+ } else {
+ console.log(`\nš¬ Message: ${_content.body}`);
+ }
+
+ const mediaUrl = _content.data?.mediaUrl as string | undefined;
+ if (mediaUrl) {
+ console.log(`š Media: ${mediaUrl}`);
+ }
+
+ console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
+ }
+
+ // Return mock success
+ return {
+ success: true,
+ notificationId: _recipient.id,
+ providerMessageId: `mock-whatsapp-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
+ metadata: {
+ status: "sent",
+ mock: true,
+ timestamp: new Date().toISOString(),
+ recipient: _recipient.phone,
+ messageType: _content.templateId ? "template" : "text",
+ hasMedia: !!_content.data?.mediaUrl,
+ },
+ };
+ }
+
+ /**
+ * Mock always ready
+ *
+ * Since this is a mock sender that doesn't require credentials,
+ * it's always ready to "send" (simulate sending).
+ *
+ * @returns Promise - Always true
+ */
+ async isReady(): Promise {
+ return true;
+ }
+
+ /**
+ * Validate recipient has phone number in E.164 format
+ *
+ * @param _recipient - Recipient to validate
+ * @returns boolean - true if phone exists and is valid
+ */
+ validateRecipient(_recipient: NotificationRecipient): boolean {
+ return !!_recipient.phone && this.isValidPhoneNumber(_recipient.phone);
+ }
+
+ /**
+ * Validate phone number is in E.164 format
+ *
+ * E.164 format: +[country code][number]
+ * Examples: +14155551234, +447911123456, +212612345678
+ *
+ * @param phone - Phone number to validate
+ * @returns boolean - true if valid E.164 format
+ * @private
+ */
+ private isValidPhoneNumber(phone: string): boolean {
+ // E.164 format: + followed by 1-15 digits
+ const phoneRegex = /^\+[1-9]\d{1,14}$/;
+ return phoneRegex.test(phone);
+ }
+}
diff --git a/src/infra/senders/whatsapp/twilio-whatsapp.sender.ts b/src/infra/senders/whatsapp/twilio-whatsapp.sender.ts
new file mode 100644
index 0000000..a37e9b1
--- /dev/null
+++ b/src/infra/senders/whatsapp/twilio-whatsapp.sender.ts
@@ -0,0 +1,323 @@
+/**
+ * Twilio WhatsApp Sender - WhatsApp Message Delivery via Twilio
+ *
+ * This is the WhatsApp sender implementation using Twilio's WhatsApp API,
+ * which provides an easy way to send WhatsApp messages without requiring
+ * direct Meta Business API approval.
+ *
+ * Features:
+ * - WhatsApp messaging: Send text messages via WhatsApp
+ * - Media support: Send images, videos, PDFs, and other documents
+ * - Template support: Use pre-approved WhatsApp message templates (configurable)
+ * - Lazy loading: Twilio SDK is loaded only when needed (peer dependency)
+ * - Connection verification: isReady() checks Twilio credentials
+ * - Phone validation: Validates E.164 format before sending
+ *
+ * Requirements:
+ * - Twilio account with WhatsApp enabled
+ * - WhatsApp Sandbox (for testing) or approved WhatsApp Business Profile
+ * - Recipients must opt-in to receive messages (Sandbox requirement)
+ * - Messages must use approved templates for certain use cases
+ *
+ * Configuration example:
+ * ```typescript
+ * const whatsappSender = new TwilioWhatsAppSender({
+ * accountSid: 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
+ * authToken: 'your-auth-token',
+ * fromNumber: '+14155238886', // Your Twilio WhatsApp number
+ * templates: {
+ * orderShipped: 'order_shipped_v1',
+ * welcomeMessage: 'welcome_v2'
+ * }
+ * });
+ * ```
+ *
+ * Usage with NotificationKit:
+ * ```typescript
+ * NotificationKitModule.forRoot({
+ * senders: [whatsappSender],
+ * // ... other config
+ * });
+ * ```
+ *
+ * Media support example:
+ * ```typescript
+ * await notificationService.send({
+ * channel: NotificationChannel.WHATSAPP,
+ * recipient: { id: 'user-123', phone: '+1234567890' },
+ * content: {
+ * title: 'Invoice',
+ * body: 'Here is your invoice',
+ * data: {
+ * mediaUrl: 'https://example.com/invoice.pdf'
+ * }
+ * }
+ * });
+ * ```
+ */
+
+import type {
+ INotificationSender,
+ NotificationChannel,
+ NotificationContent,
+ NotificationRecipient,
+ NotificationResult,
+} from "../../../core";
+
+/**
+ * Configuration for Twilio WhatsApp sender
+ *
+ * This configuration matches Twilio's WhatsApp API requirements.
+ * See: https://www.twilio.com/docs/whatsapp/api
+ */
+export interface TwilioWhatsAppConfig {
+ accountSid: string; // Twilio Account SID (starts with AC...)
+ authToken: string; // Twilio Auth Token (from console)
+ fromNumber: string; // Your Twilio WhatsApp-enabled phone number (E.164 format: +14155238886)
+
+ /**
+ * Optional: WhatsApp message templates (configurable)
+ *
+ * Templates are required for certain types of messages (promotional, etc.)
+ * in the WhatsApp Business API. Define your approved templates here.
+ *
+ * Usage in notification:
+ * ```typescript
+ * content: {
+ * templateId: 'orderShipped', // maps to 'order_shipped_v1'
+ * templateVars: { orderId: '12345' }
+ * }
+ * ```
+ */
+ templates?: Record;
+}
+
+/**
+ * WhatsApp sender implementation using Twilio API
+ *
+ * Implements the INotificationSender port for WhatsApp notifications.
+ * Uses Twilio's WhatsApp API (https://www.twilio.com/docs/whatsapp) for message delivery.
+ */
+export class TwilioWhatsAppSender implements INotificationSender {
+ readonly channel: NotificationChannel = "whatsapp" as NotificationChannel;
+
+ // Twilio client is created lazily and cached for reuse
+ // This avoids creating multiple connections to Twilio
+ private client: any = null;
+
+ constructor(private readonly config: TwilioWhatsAppConfig) {}
+
+ /**
+ * Initialize the Twilio client (lazy initialization)
+ *
+ * Why lazy initialization?
+ * - Twilio is a peer dependency (may not be installed)
+ * - Connection is only needed when actually sending messages
+ * - Avoids startup errors if Twilio is misconfigured
+ * - Client is reused for all sends (connection pooling)
+ *
+ * @returns Promise - Twilio client instance
+ * @private
+ */
+ private async getClient(): Promise {
+ // Return cached client if already created
+ if (this.client) {
+ return this.client;
+ }
+
+ // Dynamic import: Load Twilio SDK only when needed
+ // This allows NotificationKit to work without Twilio installed
+ // if you're only using email/push notifications
+ // @ts-expect-error - twilio is an optional peer dependency
+ const twilio = await import("twilio");
+
+ // Create Twilio client with credentials
+ this.client = twilio.default(this.config.accountSid, this.config.authToken);
+
+ return this.client;
+ }
+
+ /**
+ * Send a WhatsApp message
+ *
+ * This method:
+ * 1. Validates recipient has a phone number
+ * 2. Validates phone is in E.164 format
+ * 3. Gets or creates the Twilio client
+ * 4. Formats phone numbers for WhatsApp (prefixes with "whatsapp:")
+ * 5. Checks if using a template or plain message
+ * 6. Sends message via Twilio WhatsApp API
+ * 7. Returns result with success status and provider message SID
+ *
+ * @param _recipient - Notification recipient (must have phone field)
+ * @param _content - Notification content (body, optional mediaUrl, optional templateId)
+ * @returns Promise - Send result
+ */
+ async send(
+ _recipient: NotificationRecipient,
+ _content: NotificationContent,
+ ): Promise {
+ try {
+ // Validate recipient has phone number
+ if (!_recipient.phone) {
+ return {
+ success: false,
+ notificationId: _recipient.id,
+ error: "Recipient phone number is required for WhatsApp",
+ };
+ }
+
+ // Validate phone number format (E.164)
+ if (!this.isValidPhoneNumber(_recipient.phone)) {
+ return {
+ success: false,
+ notificationId: _recipient.id,
+ error: `Invalid phone number format. Must be E.164 format (e.g., +1234567890). Got: ${_recipient.phone}`,
+ };
+ }
+
+ // Get Twilio client (creates if not exists)
+ const client = await this.getClient();
+
+ // Format phone numbers for WhatsApp (Twilio requires "whatsapp:" prefix)
+ const fromWhatsApp = `whatsapp:${this.config.fromNumber}`;
+ const toWhatsApp = `whatsapp:${_recipient.phone}`;
+
+ // Prepare message options
+ const messageOptions: any = {
+ from: fromWhatsApp,
+ to: toWhatsApp,
+ };
+
+ // Check if using a template
+ if (_content.templateId && this.config.templates?.[_content.templateId]) {
+ // Template message (for WhatsApp Business API requirements)
+ const templateName = this.config.templates[_content.templateId];
+
+ messageOptions.contentSid = templateName;
+
+ // Add template variables if provided
+ if (_content.templateVars) {
+ messageOptions.contentVariables = JSON.stringify(_content.templateVars);
+ }
+ } else {
+ // Plain text message
+ messageOptions.body = _content.body;
+ }
+
+ // Add media URL if provided (images, videos, PDFs, etc.)
+ // WhatsApp supports: image/*, video/*, audio/*, application/pdf, and more
+ const mediaUrl = _content.data?.mediaUrl as string | undefined;
+ if (mediaUrl) {
+ messageOptions.mediaUrl = [mediaUrl];
+ }
+
+ // Send the message via Twilio WhatsApp API
+ const message = await client.messages.create(messageOptions);
+
+ // Return success with provider message SID (for tracking)
+ return {
+ success: true,
+ notificationId: _recipient.id,
+ providerMessageId: message.sid, // Twilio message SID (e.g., SM...)
+ metadata: {
+ status: message.status, // Message status (queued, sent, delivered, read, failed)
+ dateCreated: message.dateCreated, // When message was created
+ dateSent: message.dateSent, // When message was sent (if available)
+ price: message.price, // Cost of message (if available)
+ priceUnit: message.priceUnit, // Currency of price
+ errorCode: message.errorCode, // Error code if failed
+ errorMessage: message.errorMessage, // Error message if failed
+ },
+ };
+ } catch (error: any) {
+ // Handle Twilio-specific errors
+ const errorMessage = error?.message || "Failed to send WhatsApp message via Twilio";
+ const errorCode = error?.code || undefined;
+
+ return {
+ success: false,
+ notificationId: _recipient.id,
+ error: errorCode ? `[${errorCode}] ${errorMessage}` : errorMessage,
+ metadata: {
+ errorCode,
+ rawError: error,
+ },
+ };
+ }
+ }
+
+ /**
+ * Check if the WhatsApp sender is ready to send
+ *
+ * This method verifies the Twilio credentials by attempting to fetch
+ * the account information. If credentials are invalid, sending won't work.
+ *
+ * @returns Promise - true if Twilio credentials work, false otherwise
+ *
+ * Called by NotificationService before sending to ensure sender is ready.
+ * Prevents attempting sends when Twilio is misconfigured or unreachable.
+ */
+ async isReady(): Promise {
+ try {
+ const client = await this.getClient();
+
+ // Verify credentials by fetching account info
+ await client.api.accounts(this.config.accountSid).fetch();
+
+ return true;
+ } catch (error) {
+ // Credentials invalid or Twilio unreachable
+ console.error(
+ "[TwilioWhatsAppSender] Not ready:",
+ error instanceof Error ? error.message : error,
+ );
+ return false;
+ }
+ }
+
+ /**
+ * Validate recipient has required fields for WhatsApp
+ *
+ * WhatsApp requires:
+ * - phone: Must exist
+ * - phone: Must be in E.164 format (+[country code][number])
+ *
+ * @param _recipient - The recipient to validate
+ * @returns boolean - true if recipient is valid for WhatsApp
+ *
+ * Called by NotificationService before attempting to send.
+ */
+ validateRecipient(_recipient: NotificationRecipient): boolean {
+ return !!_recipient.phone && this.isValidPhoneNumber(_recipient.phone);
+ }
+
+ /**
+ * Validate phone number is in E.164 format
+ *
+ * E.164 format: +[country code][number]
+ * - Starts with +
+ * - Followed by 1-3 digit country code
+ * - Followed by up to 15 total digits
+ *
+ * Valid examples:
+ * - +14155551234 (USA)
+ * - +447911123456 (UK)
+ * - +212612345678 (Morocco)
+ * - +33612345678 (France)
+ *
+ * Invalid examples:
+ * - 4155551234 (missing +)
+ * - +1-415-555-1234 (contains dashes)
+ * - +1 (415) 555-1234 (contains spaces and parentheses)
+ *
+ * @param phone - Phone number to validate
+ * @returns boolean - true if valid E.164 format
+ * @private
+ */
+ private isValidPhoneNumber(phone: string): boolean {
+ // E.164 format regex: + followed by 1-15 digits, no spaces or special chars
+ const phoneRegex = /^\+[1-9]\d{1,14}$/;
+ return phoneRegex.test(phone);
+ }
+}
diff --git a/src/nest/index.ts b/src/nest/index.ts
index ffd8c09..6c630f2 100644
--- a/src/nest/index.ts
+++ b/src/nest/index.ts
@@ -1,18 +1,74 @@
-// Module
+/**
+ * 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.
+ *
+ * What's exported:
+ * - NotificationKitModule: Main module to import in your app
+ * - Interfaces: TypeScript interfaces for configuration
+ * - Constants: Injection tokens for DI
+ * - Decorators: Custom decorators (if any)
+ * - Controllers: REST API and webhook controllers
+ * - Providers: Factory functions for creating providers
+ *
+ * Quick start:
+ * ```typescript
+ * import { NotificationKitModule } from '@ciscode/notification-kit/nest';
+ * import { NodemailerSender } from '@ciscode/notification-kit';
+ *
+ * @Module({
+ * imports: [
+ * NotificationKitModule.register({
+ * senders: [
+ * new NodemailerSender({
+ * host: 'smtp.gmail.com',
+ * port: 587,
+ * auth: { user: 'your@email.com', pass: 'app-password' },
+ * from: 'noreply@yourapp.com',
+ * }),
+ * ],
+ * repository: new InMemoryRepository(),
+ * }),
+ * ],
+ * })
+ * export class AppModule {}
+ * ```
+ *
+ * To inject NotificationService:
+ * ```typescript
+ * import { NotificationService } from '@ciscode/notification-kit';
+ *
+ * @Injectable()
+ * export class MyService {
+ * constructor(private readonly notificationService: NotificationService) {}
+ *
+ * async sendWelcomeEmail(user: User) {
+ * await this.notificationService.send({
+ * channel: 'email',
+ * recipient: { id: user.id, email: user.email },
+ * content: { title: 'Welcome!', body: 'Thanks for signing up' },
+ * });
+ * }
+ * }
+ * ```
+ */
+
+// Module - Main NestJS module
export * from "./module";
-// Interfaces
+// Interfaces - TypeScript types for configuration
export * from "./interfaces";
-// Constants
+// Constants - Injection tokens for dependency injection
export * from "./constants";
-// Decorators
+// Decorators - Custom decorators for controllers/services
export * from "./decorators";
-// Controllers
+// Controllers - REST API and webhook endpoints
export * from "./controllers/notification.controller";
export * from "./controllers/webhook.controller";
-// Providers
+// Providers - Factory functions for creating NestJS providers
export * from "./providers";
diff --git a/src/nest/module.ts b/src/nest/module.ts
index 1775f11..34aa398 100644
--- a/src/nest/module.ts
+++ b/src/nest/module.ts
@@ -1,3 +1,69 @@
+/**
+ * NotificationKit NestJS Module
+ *
+ * This is the main module for integrating NotificationKit with NestJS applications.
+ * It provides dynamic module configuration with both synchronous and asynchronous
+ * registration methods.
+ *
+ * Features:
+ * - Dynamic module: Configure at runtime with different options
+ * - Global module: Services available across entire application
+ * - Async support: Load configuration from ConfigService, database, etc.
+ * - Optional REST API: Built-in endpoints for sending/querying notifications
+ * - Optional webhooks: Endpoints for provider callbacks (Twilio, SendGrid, etc.)
+ *
+ * Usage - Synchronous (direct configuration):
+ * ```typescript
+ * @Module({
+ * imports: [
+ * NotificationKitModule.register({
+ * senders: [emailSender, smsSender],
+ * repository: mongoRepository,
+ * enableRestApi: true,
+ * enableWebhooks: true,
+ * idGenerator: new ULIDGenerator(),
+ * }),
+ * ],
+ * })
+ * export class AppModule {}
+ * ```
+ *
+ * Usage - Asynchronous (with ConfigService):
+ * ```typescript
+ * @Module({
+ * imports: [
+ * NotificationKitModule.registerAsync({
+ * imports: [ConfigModule],
+ * useFactory: (config: ConfigService) => ({
+ * senders: [
+ * new NodemailerSender({
+ * host: config.get('SMTP_HOST'),
+ * port: config.get('SMTP_PORT'),
+ * auth: {
+ * user: config.get('SMTP_USER'),
+ * pass: config.get('SMTP_PASS'),
+ * },
+ * from: config.get('FROM_EMAIL'),
+ * }),
+ * ],
+ * repository: new MongoNotificationRepository(connection),
+ * enableRestApi: true,
+ * }),
+ * inject: [ConfigService],
+ * }),
+ * ],
+ * })
+ * export class AppModule {}
+ * ```
+ *
+ * What gets registered:
+ * - NotificationService: Main business logic service (injectable)
+ * - Senders: Email, SMS, push notification providers
+ * - Repository: Database persistence layer
+ * - Providers: ID generator, date/time, template engine, event emitter
+ * - Controllers (optional): REST API and webhook endpoints
+ */
+
import { Module, type DynamicModule, type Provider, type Type } from "@nestjs/common";
import { NOTIFICATION_KIT_OPTIONS } from "./constants";
@@ -14,69 +80,173 @@ import { createNotificationKitProviders } from "./providers";
export class NotificationKitModule {
/**
* Register module synchronously with direct configuration
+ *
+ * Use this when:
+ * - Configuration is hardcoded or imported directly
+ * - No async dependencies (no ConfigService, database lookups, etc.)
+ * - Simple setup for development/testing
+ *
+ * @param options - NotificationKit configuration object
+ * @returns DynamicModule - Configured NestJS module
+ *
+ * What this does:
+ * 1. Creates providers (NotificationService, senders, repository, etc.)
+ * 2. Creates controllers (REST API, webhooks) if enabled
+ * 3. Exports providers for use throughout the application
+ * 4. Marks module as global (no need to import in every module)
+ *
+ * Example:
+ * ```typescript
+ * NotificationKitModule.register({
+ * senders: [new NodemailerSender({ ... })],
+ * repository: new InMemoryRepository(),
+ * enableRestApi: true,
+ * enableWebhooks: false,
+ * })
+ * ```
*/
static register(options: NotificationKitModuleOptions): DynamicModule {
+ // Create all providers (NotificationService + dependencies)
const providers = this.createProviders(options);
+
+ // Create controllers if enabled (REST API + webhooks)
const controllers = this.createControllers(options);
+
+ // Export providers so they can be injected in other modules
const exports = providers.map((p) => (typeof p === "object" && "provide" in p ? p.provide : p));
return {
- global: true,
- module: NotificationKitModule,
- controllers,
- providers,
- exports,
+ global: true, // Module is global (providers available everywhere)
+ module: NotificationKitModule, // This module class
+ controllers, // REST API + webhook controllers (if enabled)
+ providers, // All services and dependencies
+ exports, // Make providers available for injection
};
}
/**
* Register module asynchronously with factory pattern
+ *
+ * Use this when:
+ * - Configuration comes from ConfigService, environment variables, etc.
+ * - Need to load settings from database or external API
+ * - Want to inject dependencies into configuration factory
+ *
+ * @param options - Async configuration options (useFactory, useClass, useExisting)
+ * @returns DynamicModule - Configured NestJS module
+ *
+ * Three async patterns supported:
+ *
+ * 1. useFactory: Factory function that returns configuration
+ * ```typescript
+ * registerAsync({
+ * useFactory: (config: ConfigService) => ({
+ * senders: [new NodemailerSender({ host: config.get('SMTP_HOST') })]
+ * }),
+ * inject: [ConfigService],
+ * })
+ * ```
+ *
+ * 2. useClass: Class that implements NotificationKitOptionsFactory
+ * ```typescript
+ * registerAsync({
+ * useClass: NotificationKitConfigService,
+ * })
+ * ```
+ *
+ * 3. useExisting: Reference to existing provider
+ * ```typescript
+ * registerAsync({
+ * useExisting: ConfigService,
+ * })
+ * ```
+ *
+ * Note: Controllers are disabled in async mode for simplicity.
+ * You can add them manually in a separate module if needed.
*/
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
};
}
/**
- * Create providers including options and service providers
+ * Create providers including options and service providers (private helper)
+ *
+ * This creates:
+ * - OPTIONS provider: Configuration object
+ * - NotificationService: Main business logic
+ * - Senders: Email, SMS, push providers
+ * - Repository: Database persistence
+ * - ID generator, date/time provider, template engine, event emitter
+ *
+ * @param options - Module configuration
+ * @returns Provider[] - Array of NestJS providers
+ * @private
*/
private static createProviders(options: NotificationKitModuleOptions): Provider[] {
return [
+ // Provide options object (injectable as NOTIFICATION_KIT_OPTIONS)
{
provide: NOTIFICATION_KIT_OPTIONS,
useValue: options,
},
+ // Create all NotificationKit providers (service + dependencies)
...createNotificationKitProviders(options),
];
}
/**
- * Create controllers based on options
+ * Create controllers based on options (private helper)
+ *
+ * Conditionally includes controllers based on enableRestApi and enableWebhooks flags.
+ *
+ * Controllers:
+ * - NotificationController: REST API endpoints for sending/querying notifications
+ * * POST /notifications - Send a notification
+ * * POST /notifications/bulk - Send to multiple recipients
+ * * GET /notifications - Query notifications
+ * * GET /notifications/:id - Get by ID
+ * * POST /notifications/:id/retry - Retry failed notification
+ * * POST /notifications/:id/cancel - Cancel notification
+ *
+ * - WebhookController: Webhook endpoints for provider callbacks
+ * * POST /webhooks/twilio - Twilio status callbacks
+ * * POST /webhooks/sendgrid - SendGrid event webhooks
+ * * POST /webhooks/firebase - Firebase delivery receipts
+ *
+ * @param options - Module configuration
+ * @returns Type[] - Array of controller classes
+ * @private
*/
private static createControllers(options: NotificationKitModuleOptions): Type[] {
const controllers: Type[] = [];
@@ -95,7 +265,14 @@ export class NotificationKitModule {
}
/**
- * Create async providers for registerAsync
+ * Create async providers for registerAsync (private helper)
+ *
+ * When using useClass, we need to register the class as a provider
+ * so it can be injected into the async options factory.
+ *
+ * @param options - Async configuration options
+ * @returns Provider[] - Array of providers
+ * @private
*/
private static createAsyncProviders(options: NotificationKitModuleAsyncOptions): Provider[] {
if (options.useClass) {
@@ -111,37 +288,53 @@ export class NotificationKitModule {
}
/**
- * Create async options provider
+ * Create async options provider (private helper)
+ *
+ * This creates a provider that resolves the module options asynchronously
+ * using one of three patterns:
+ *
+ * 1. useFactory: Direct factory function
+ * 2. useExisting: Factory method on existing provider
+ * 3. useClass: Factory method on new provider instance
+ *
+ * @param options - Async configuration options
+ * @returns Provider - The options provider
+ * @throws Error - If invalid async options provided
+ * @private
*/
private static createAsyncOptionsProvider(options: NotificationKitModuleAsyncOptions): Provider {
+ // Pattern 1: useFactory - Factory function that returns options
if (options.useFactory) {
return {
provide: NOTIFICATION_KIT_OPTIONS,
useFactory: options.useFactory,
- inject: options.inject || [],
+ inject: options.inject || [], // Dependencies to inject into factory
};
}
+ // Pattern 2: useExisting - Call createNotificationKitOptions() on existing provider
if (options.useExisting) {
return {
provide: NOTIFICATION_KIT_OPTIONS,
useFactory: async (optionsFactory: NotificationKitOptionsFactory) => {
return optionsFactory.createNotificationKitOptions();
},
- inject: [options.useExisting],
+ inject: [options.useExisting], // Inject the existing provider
};
}
+ // Pattern 3: useClass - Instantiate class and call createNotificationKitOptions()
if (options.useClass) {
return {
provide: NOTIFICATION_KIT_OPTIONS,
useFactory: async (optionsFactory: NotificationKitOptionsFactory) => {
return optionsFactory.createNotificationKitOptions();
},
- inject: [options.useClass],
+ inject: [options.useClass], // Inject the new class instance
};
}
+ // No valid async pattern provided
throw new Error("Invalid NotificationKitModuleAsyncOptions");
}
}
diff --git a/src/nest/providers.ts b/src/nest/providers.ts
index 5053a2b..e7d8bce 100644
--- a/src/nest/providers.ts
+++ b/src/nest/providers.ts
@@ -1,3 +1,30 @@
+/**
+ * NotificationKit Provider Factory
+ *
+ * This file contains the factory function for creating NestJS providers for NotificationKit.
+ * It handles dependency injection setup for all NotificationKit services and dependencies.
+ *
+ * What this creates:
+ * 1. NOTIFICATION_SENDERS: Array of notification senders (email, SMS, push)
+ * 2. NOTIFICATION_REPOSITORY: Database persistence layer
+ * 3. NOTIFICATION_ID_GENERATOR: ID generation (defaults to UUID)
+ * 4. NOTIFICATION_DATETIME_PROVIDER: Date/time operations (defaults to system time)
+ * 5. NOTIFICATION_TEMPLATE_ENGINE: Template rendering (optional)
+ * 6. NOTIFICATION_EVENT_EMITTER: Event emission (optional)
+ * 7. NOTIFICATION_SERVICE: Main NotificationService instance
+ *
+ * Dependencies:
+ * - Required: senders, repository
+ * - Optional with defaults: idGenerator (UUID), dateTimeProvider (system time)
+ * - Optional: templateEngine, eventEmitter
+ *
+ * The factory handles:
+ * - Default provider instantiation (UUID generator, system date/time)
+ * - Dynamic imports for optional dependencies
+ * - Dependency injection setup with proper injection tokens
+ * - Optional provider handling (templateEngine, eventEmitter)
+ */
+
import type { Provider } from "@nestjs/common";
import { NotificationService } from "../core/notification.service";
@@ -17,30 +44,55 @@ import type { NotificationKitModuleOptions } from "./interfaces";
/**
* Create providers for NotificationKit module
+ *
+ * This factory function creates all NestJS providers needed for NotificationKit
+ * to work. It's called by NotificationKitModule.register() and registerAsync().
+ *
+ * @param options - NotificationKit module configuration
+ * @returns Provider[] - Array of NestJS provider definitions
+ *
+ * Provider creation logic:
+ * 1. Senders: Always required, provided as-is
+ * 2. Repository: Always required, provided as-is
+ * 3. ID Generator: Use provided, or default to UuidGenerator
+ * 4. DateTime Provider: Use provided, or default to DateTimeProvider (system time)
+ * 5. Template Engine: Use provided, or undefined (optional)
+ * 6. Event Emitter: Use provided, or undefined (optional)
+ * 7. NotificationService: Created with all dependencies injected
+ *
+ * All providers are registered with injection tokens from constants.ts,
+ * allowing them to be injected throughout the application.
*/
export function createNotificationKitProviders(options: NotificationKitModuleOptions): Provider[] {
const providers: Provider[] = [];
- // Senders provider
+ // 1. Senders provider (REQUIRED)
+ // Array of notification senders (email, SMS, push, etc.)
+ // Example: [new NodemailerSender(...), new TwilioSender(...)]
providers.push({
provide: NOTIFICATION_SENDERS,
useValue: options.senders,
});
- // Repository provider
+ // 2. Repository provider (REQUIRED)
+ // Database persistence layer for notifications
+ // Example: new MongoNotificationRepository(connection)
providers.push({
provide: NOTIFICATION_REPOSITORY,
useValue: options.repository,
});
- // ID Generator provider
+ // 3. ID Generator provider (optional, defaults to UUID)
+ // Generates unique IDs for notifications
if (options.idGenerator) {
+ // User provided a custom ID generator
providers.push({
provide: NOTIFICATION_ID_GENERATOR,
useValue: options.idGenerator,
});
} else {
- // Default to UuidGenerator
+ // Default to UuidGenerator (generates UUID v4)
+ // Uses async factory to allow dynamic import
providers.push({
provide: NOTIFICATION_ID_GENERATOR,
useFactory: async () => {
@@ -50,14 +102,17 @@ export function createNotificationKitProviders(options: NotificationKitModuleOpt
});
}
- // DateTime Provider
+ // 4. DateTime Provider (optional, defaults to system time)
+ // Provides date/time operations (now(), isPast(), isFuture())
if (options.dateTimeProvider) {
+ // User provided a custom dateTime provider (e.g., for testing with fixed time)
providers.push({
provide: NOTIFICATION_DATETIME_PROVIDER,
useValue: options.dateTimeProvider,
});
} else {
- // Default to DateTimeProvider
+ // Default to DateTimeProvider (uses system time)
+ // Uses async factory to allow dynamic import
providers.push({
provide: NOTIFICATION_DATETIME_PROVIDER,
useFactory: async () => {
@@ -67,7 +122,9 @@ export function createNotificationKitProviders(options: NotificationKitModuleOpt
});
}
- // Template Engine provider (optional)
+ // 5. Template Engine provider (OPTIONAL)
+ // Renders notification templates with variables
+ // If not provided, notifications must specify content directly
if (options.templateEngine) {
providers.push({
provide: NOTIFICATION_TEMPLATE_ENGINE,
@@ -75,7 +132,9 @@ export function createNotificationKitProviders(options: NotificationKitModuleOpt
});
}
- // Event Emitter provider (optional)
+ // 6. Event Emitter provider (OPTIONAL)
+ // Emits notification lifecycle events for monitoring/logging
+ // If not provided, events will not be emitted
if (options.eventEmitter) {
providers.push({
provide: NOTIFICATION_EVENT_EMITTER,
@@ -83,17 +142,20 @@ export function createNotificationKitProviders(options: NotificationKitModuleOpt
});
}
- // NotificationService provider
+ // 7. NotificationService provider (MAIN SERVICE)
+ // Creates the main NotificationService with all dependencies injected
+ // This is the service you'll inject into your controllers/services
providers.push({
provide: NOTIFICATION_SERVICE,
useFactory: (
- repository: any,
- idGenerator: any,
- dateTimeProvider: any,
- senders: any[],
- templateEngine?: any,
- eventEmitter?: any,
+ repository: any, // INotificationRepository implementation
+ idGenerator: any, // IIdGenerator implementation
+ dateTimeProvider: any, // IDateTimeProvider implementation
+ senders: any[], // Array of INotificationSender implementations
+ templateEngine?: any, // ITemplateEngine implementation (optional)
+ eventEmitter?: any, // INotificationEventEmitter implementation (optional)
) => {
+ // Instantiate NotificationService with all dependencies
return new NotificationService(
repository,
idGenerator,
@@ -103,13 +165,14 @@ export function createNotificationKitProviders(options: NotificationKitModuleOpt
eventEmitter,
);
},
+ // Specify which tokens to inject into the factory function
inject: [
- NOTIFICATION_REPOSITORY,
- NOTIFICATION_ID_GENERATOR,
- NOTIFICATION_DATETIME_PROVIDER,
- NOTIFICATION_SENDERS,
- { token: NOTIFICATION_TEMPLATE_ENGINE, optional: true },
- { token: NOTIFICATION_EVENT_EMITTER, optional: true },
+ NOTIFICATION_REPOSITORY, // Required
+ NOTIFICATION_ID_GENERATOR, // Required (has default)
+ NOTIFICATION_DATETIME_PROVIDER, // Required (has default)
+ NOTIFICATION_SENDERS, // Required
+ { token: NOTIFICATION_TEMPLATE_ENGINE, optional: true }, // Optional
+ { token: NOTIFICATION_EVENT_EMITTER, optional: true }, // Optional
],
});