npm package: @skoolite/notify-hub
Description: Multi-channel notification hub for SMS, WhatsApp, and Email
Author: Skoolyte
Latest version: 0.3.1

# @skoolite/notify-hub

> **DISCLAIMER: INTERNAL USE PACKAGE**
>
> This package is built for internal use in Skoolyte projects. It is published publicly for convenience but comes with **NO SUPPORT**, **NO WARRANTY**, and **NO GUARANTEES**.
>
> - **No Issues/Bug Fixes**: Issues and PRs will not be monitored or addressed
> - **Breaking Changes**: Expect frequent breaking changes without notice
> - **No SLA**: This is not production-ready software for general use
> - **Use at Your Own Risk**: If you choose to use this package, you are responsible for any consequences
>
> **If you need a production-ready notification library, consider alternatives like [Novu](https://novu.co), [Knock](https://knock.app), or direct provider SDKs.**

---

Multi-channel notification hub for SMS, WhatsApp, and Email. Send notifications through Twilio, Meta WhatsApp Cloud API, local Pakistan SMS providers, and AWS SES with a unified API.

## Features

- **Multi-channel**: SMS, WhatsApp, and Email support
- **Multiple providers**: Twilio, Meta WhatsApp, local Pakistan SMS (Telenor, Jazz, Zong), AWS SES
- **Smart routing**: Route messages to different providers based on phone number prefix
- **Automatic fallback**: Fall back to secondary provider on failure
- **Retry logic**: Exponential backoff with jitter for transient errors
- **Bulk sending**: Send to many recipients with rate limiting
- **Type-safe**: Full TypeScript support with comprehensive types
- **Provider agnostic**: Switch providers without code changes

## Installation

```bash
npm install @skoolite/notify-hub
# or
pnpm add @skoolite/notify-hub
# or
yarn add @skoolite/notify-hub
```

## Quick Start

```typescript
import { NotifyHub } from '@skoolite/notify-hub';

// Initialize with Twilio
const notifyHub = new NotifyHub({
  sms: {
    primary: {
      provider: 'twilio',
      config: {
        accountSid: process.env.TWILIO_ACCOUNT_SID!,
        authToken: process.env.TWILIO_AUTH_TOKEN!,
        fromNumber: '+1234567890',
      },
    },
  },
});

// Send SMS
const result = await notifyHub.sms('+923001234567', 'Hello from NotifyHub!');

if (result.success) {
  console.log('Message sent:', result.messageId);
} else {
  console.error('Failed:', result.error?.message);
}
```

## Configuration

### Full Configuration Example

```typescript
import { NotifyHub } from '@skoolite/notify-hub';

const notifyHub = new NotifyHub({
  // Default channel for notifications
  defaultChannel: 'sms',

  // SMS Configuration
  sms: {
    // Primary provider
    primary: {
      provider: 'twilio',
      config: {
        accountSid: process.env.TWILIO_ACCOUNT_SID!,
        authToken: process.env.TWILIO_AUTH_TOKEN!,
        fromNumber: '+1234567890',
      },
    },
    // Fallback provider (optional)
    fallback: {
      provider: 'local',
      config: {
        provider: 'telenor',
        apiKey: process.env.TELENOR_API_KEY!,
        senderId: 'SKOOLYTE',
      },
    },
    // Smart routing based on phone prefix
    routing: {
      '+92': 'fallback',  // Pakistan -> local provider
      '*': 'primary',      // All others -> Twilio
    },
  },

  // WhatsApp Configuration
  whatsapp: {
    provider: 'meta',
    config: {
      accessToken: process.env.META_ACCESS_TOKEN!,
      phoneNumberId: process.env.WHATSAPP_PHONE_ID!,
      businessAccountId: process.env.WHATSAPP_BUSINESS_ID,
    },
  },

  // Email Configuration
  email: {
    provider: 'ses',
    config: {
      region: 'us-east-1',
      fromAddress: 'noreply@example.com',
      fromName: 'My App',
    },
  },

  // Retry Configuration
  retry: {
    maxAttempts: 3,
    backoffMs: 1000,
    backoffMultiplier: 2,
    maxBackoffMs: 30000,
    jitter: true,
  },

  // Logger (optional)
  logger: console, // or winston, pino, etc.
});
```

## Sending SMS

### Simple SMS

```typescript
// Send to a single recipient
const result = await notifyHub.sms('+923001234567', 'Your OTP is 123456');
```

### SMS with Options

```typescript
const result = await notifyHub.sms('+923001234567', 'Hello!', {
  from: '+1555000000',  // Override from number
  metadata: { userId: '123' },  // Custom metadata
});
```

### Bulk SMS

```typescript
const messages = [
  { to: '+923001234567', message: 'Hello John!' },
  { to: '+923001234568', message: 'Hello Jane!' },
  { to: '+923001234569', message: 'Hello Bob!' },
];

const result = await notifyHub.bulkSms(messages, {
  rateLimit: { messagesPerSecond: 10 },
  concurrency: 5,
  stopOnError: false,
  onProgress: (sent, total) => {
    console.log(`Progress: ${sent}/${total}`);
  },
});

console.log(`Sent: ${result.summary.sent}, Failed: ${result.summary.failed}`);
```

### Get Delivery Status

```typescript
const status = await notifyHub.getSmsStatus('SM1234567890');
// Returns: 'queued' | 'sent' | 'delivered' | 'failed' | 'undelivered'
```

## Sending WhatsApp Messages

### Template Messages

Template messages can be sent anytime (no 24-hour window restriction):

```typescript
const result = await notifyHub.whatsappTemplate('+923001234567', {
  name: 'appointment_reminder',
  language: 'en',
  variables: {
    '1': 'John',
    '2': 'Dr. Smith',
    '3': 'Tomorrow at 10 AM',
  },
});
```

### Text Messages

Text messages require an open conversation (24-hour window):

```typescript
const result = await notifyHub.whatsapp('+923001234567', 'Thank you for your message!');
```

## Sending Email

```typescript
const result = await notifyHub.email('user@example.com', {
  subject: 'Welcome!',
  body: 'Plain text content',
  html: '<h1>Welcome!</h1><p>HTML content</p>',
});
```

## Generic Send

Use the `send` method for channel-agnostic sending:

```typescript
// SMS
await notifyHub.send({
  channel: 'sms',
  to: '+923001234567',
  message: 'Hello!',
});

// WhatsApp Template
await notifyHub.send({
  channel: 'whatsapp',
  to: '+923001234567',
  template: {
    name: 'welcome',
    language: 'en',
    variables: ['John'],
  },
});

// Email
await notifyHub.send({
  channel: 'email',
  to: 'user@example.com',
  email: {
    subject: 'Hello',
    body: 'Hello!',
  },
});
```

## Smart Routing

Route messages to different providers based on phone number prefix:

```typescript
const notifyHub = new NotifyHub({
  sms: {
    primary: { provider: 'twilio', config: { ... } },
    fallback: { provider: 'local', config: { provider: 'telenor', ... } },
    routing: {
      '+92': 'fallback',   // Pakistan numbers -> local provider (cheaper)
      '+1': 'primary',     // US numbers -> Twilio
      '*': 'primary',      // Default -> Twilio
    },
  },
});

// This uses local provider (Pakistan number)
await notifyHub.sms('+923001234567', 'Hello!');

// This uses Twilio (US number)
await notifyHub.sms('+14155551234', 'Hello!');
```

## Automatic Fallback

If the primary provider fails with a retriable error, NotifyHub automatically tries the fallback:

```typescript
const notifyHub = new NotifyHub({
  sms: {
    primary: { provider: 'local', config: { ... } },
    fallback: { provider: 'twilio', config: { ... } },
  },
});

// If local provider fails, automatically retries with Twilio
await notifyHub.sms('+923001234567', 'Hello!');
```

## Custom Providers

Implement your own provider:

```typescript
import { SmsProvider, SendResult } from '@skoolite/notify-hub';

class MySmsProvider implements SmsProvider {
  readonly name = 'my-provider';

  async send(to: string, message: string): Promise<SendResult> {
    // Your implementation
    return {
      success: true,
      messageId: 'my-msg-id',
      channel: 'sms',
      status: 'sent',
      provider: this.name,
      timestamp: new Date(),
    };
  }

  async getStatus(messageId: string): Promise<MessageStatus> {
    // Your implementation
    return 'delivered';
  }
}

const notifyHub = new NotifyHub({
  sms: {
    primary: {
      provider: 'custom',
      instance: new MySmsProvider(),
    },
  },
});
```

## NestJS Integration

```typescript
// notify-hub.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NotifyHub } from '@skoolite/notify-hub';

@Global()
@Module({
  providers: [
    {
      provide: NotifyHub,
      useFactory: (config: ConfigService) => {
        return new NotifyHub({
          sms: {
            primary: {
              provider: 'twilio',
              config: {
                accountSid: config.getOrThrow('TWILIO_ACCOUNT_SID'),
                authToken: config.getOrThrow('TWILIO_AUTH_TOKEN'),
                fromNumber: config.getOrThrow('TWILIO_FROM_NUMBER'),
              },
            },
          },
        });
      },
      inject: [ConfigService],
    },
  ],
  exports: [NotifyHub],
})
export class NotifyHubModule {}

// appointment.service.ts
import { Injectable } from '@nestjs/common';
import { NotifyHub } from '@skoolite/notify-hub';

@Injectable()
export class AppointmentService {
  constructor(private readonly notifyHub: NotifyHub) {}

  async sendReminder(phone: string, message: string) {
    return this.notifyHub.sms(phone, message);
  }
}
```

## Utility Functions

NotifyHub exports useful utility functions:

```typescript
import {
  validatePhoneNumber,
  toE164,
  isPakistanMobile,
  getPakistanNetwork,
  withRetry,
} from '@skoolite/notify-hub';

// Validate and parse phone number
const result = validatePhoneNumber('03001234567', 'PK');
console.log(result.e164); // '+923001234567'

// Convert to E.164 format
const e164 = toE164('0300-1234567', 'PK');
console.log(e164); // '+923001234567'

// Check if Pakistan mobile
console.log(isPakistanMobile('+923001234567')); // true

// Get Pakistan network
console.log(getPakistanNetwork('+923451234567')); // 'telenor'

// Retry with exponential backoff
const data = await withRetry(
  () => fetchData(),
  { maxAttempts: 3, backoffMs: 1000, backoffMultiplier: 2, jitter: true }
);
```

## API Reference

### NotifyHub

| Method | Description |
|--------|-------------|
| `sms(to, message, options?)` | Send SMS |
| `bulkSms(messages, options?)` | Send bulk SMS |
| `getSmsStatus(messageId)` | Get SMS delivery status |
| `whatsapp(to, message, options?)` | Send WhatsApp text message |
| `whatsappTemplate(to, template, options?)` | Send WhatsApp template |
| `bulkWhatsApp(messages, options?)` | Send bulk WhatsApp |
| `email(to, email, options?)` | Send email |
| `bulkEmail(messages, options?)` | Send bulk email |
| `send(notification)` | Generic send |
| `initialize()` | Initialize providers (auto-called) |
| `destroy()` | Cleanup resources |

### SendResult

```typescript
interface SendResult {
  success: boolean;
  messageId?: string;
  channel: 'sms' | 'whatsapp' | 'email';
  status: 'queued' | 'sent' | 'delivered' | 'failed' | 'undelivered';
  provider: string;
  error?: { code: string; message: string; retriable: boolean };
  cost?: { amount: number; currency: string };
  timestamp: Date;
  metadata?: Record<string, unknown>;
}
```

## License

MIT