From a8db7209ca2cf916091fbce7cef96230354200d1 Mon Sep 17 00:00:00 2001 From: Manjik Shrestha Date: Tue, 15 Apr 2025 17:03:29 +0545 Subject: [PATCH 1/6] Dev (#45) * added documentation for asterisk worker concept * xref feature * reporting doc * updated reporting * refine problem statement * feat: reporting feature * feat: refactor bruno's variable * prisma updated | code refactor * refactor getReportsByxref function * remove unknown * use broadcast status type * refactor xref logs * updated bruno * removed rumsan/core package * updated bruno --------- Co-authored-by: Raktim Shrestha Co-authored-by: Santosh Shrestha Co-authored-by: myanzik Co-authored-by: Pratiksharai-rumsan --- bruno/broadcast/get-project-reports.bru | 2 +- bruno/broadcast/list-by-xref.bru | 6 ++++-- bruno/broadcast/send/send-ivr.bru | 12 ++++++++++-- bruno/broadcast/send/send-voice.bru | 6 ++++-- package.json | 3 +-- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/bruno/broadcast/get-project-reports.bru b/bruno/broadcast/get-project-reports.bru index d0045a8..aceab72 100644 --- a/bruno/broadcast/get-project-reports.bru +++ b/bruno/broadcast/get-project-reports.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{url}}/broadcasts/{{xref-id}}/reports + url: {{url}}/broadcasts/{{xref}}/reports body: json auth: none } diff --git a/bruno/broadcast/list-by-xref.bru b/bruno/broadcast/list-by-xref.bru index b582be0..86c3b22 100644 --- a/bruno/broadcast/list-by-xref.bru +++ b/bruno/broadcast/list-by-xref.bru @@ -5,13 +5,15 @@ meta { } get { - url: {{url}}/broadcasts?xref=hqbdoaa5eh3o8jdto671yey3 + url: {{url}}/broadcasts?xref={{xref}} + body: json auth: none } params:query { - xref: hqbdoaa5eh3o8jdto671yey3 + xref: {{xref}} + } headers { diff --git a/bruno/broadcast/send/send-ivr.bru b/bruno/broadcast/send/send-ivr.bru index 619e8e8..de73451 100644 --- a/bruno/broadcast/send/send-ivr.bru +++ b/bruno/broadcast/send/send-ivr.bru @@ -15,16 +15,24 @@ headers { } body:json { + // https://rahat-rumsan.s3.us-east-1.amazonaws.com/aa/dev/QmNwTcwhcj5yCENjgiuk69fv35pBYW8aFGbddqStpFmgZp + + // https://rahat-rumsan.s3.us-east-1.amazonaws.com/aa/dev/Qmbfws68prEzRrVzRE8TLdYfpnf1hifJuRonW4aQPwh1rm { "transport": "{{transport-id-voice}}", "message": { - "content": "https://rahat-rumsan.s3.us-east-1.amazonaws.com/aa/dev/Qmbfws68prEzRrVzRE8TLdYfpnf1hifJuRonW4aQPwh1rm", + "content": "https://rahat-rumsan.s3.us-east-1.amazonaws.com/aa/dev/QmQatbr2xMggcbR2uKtvyxHiKHz7s5NUeu9xHo1zhJ6BPE", "meta": { "type": "new-ivr" } }, "addresses": [ - "9841602388" + "9801109703" + // "9705378701", + // "9845160080" + // "9705069368" + // "9808520541", + // "9849214068" ], "maxAttempts": 2, "trigger": "IMMEDIATE", diff --git a/bruno/broadcast/send/send-voice.bru b/bruno/broadcast/send/send-voice.bru index 8a78fa6..6237c31 100644 --- a/bruno/broadcast/send/send-voice.bru +++ b/bruno/broadcast/send/send-voice.bru @@ -22,11 +22,12 @@ body:json { "meta": {} }, "addresses": [ - "{{dev-phone}}" + "9841602388" ], "maxAttempts": 2, - "trigger": "SCHEDULED", + "trigger": "IMMEDIATE", "webhook": "", + "xref":"test-1", "options": { "scheduledTime": "2h", "allowedTimeFrame": "09:00-18:00", @@ -40,5 +41,6 @@ tests { if(data){ bru.setVar("session-id", data.cuid); + bru.setVar("xref",data.xref); } } diff --git a/package.json b/package.json index e193700..0cd8e98 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,7 @@ "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "5.17.0", "@rumsan/app": "0.0.5", - "@rumsan/core": "^3.0.140", - "@rumsan/extensions": "^0.0.28", + "@rumsan/extensions": "0.0.28", "@rumsan/prisma": "1.0.130", "@rumsan/sdk": "^0.0.45", "@types/amqplib": "^0.10.5", From 9c729c1609c632430fe2f455be4708e0f3f71d0b Mon Sep 17 00:00:00 2001 From: manjik-rumsan Date: Thu, 29 Jan 2026 17:34:36 +0545 Subject: [PATCH 2/6] fix chanel hangup on playback finish --- .../src/workers/ivr.service.ts | 65 +++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/apps/asterisk-worker/src/workers/ivr.service.ts b/apps/asterisk-worker/src/workers/ivr.service.ts index b832c5d..a0261b3 100644 --- a/apps/asterisk-worker/src/workers/ivr.service.ts +++ b/apps/asterisk-worker/src/workers/ivr.service.ts @@ -301,15 +301,11 @@ export class IVRService implements OnModuleInit, OnModuleDestroy { } //////////// WORK FLOW CODE /////////// - private async playPrompt(channelId: string, media: string) { - const channelState = this.channelStates.get(channelId); - if (!channelState || !channelState.isActive) { - this.logger.warn( - `Attempted to play prompt on inactive or unknown channel: ${channelId}`, - ); - return; - } - + private async playPrompt( + channelId: string, + media: string, + immediateHangup = false, + ) { try { // Stop any active playback await this.stopActivePlayback(channelId); @@ -317,10 +313,7 @@ export class IVRService implements OnModuleInit, OnModuleDestroy { // Create a new playback const playback = this.client.Playback(); - const playbackId = playback.id; - channelState.activePlayback = playback; - channelState.activePlaybackId = playbackId; - + this.activePlaybacks.set(channelId, playback); await this.client.channels.play({ channelId: channelId, playbackId: playbackId, @@ -330,29 +323,30 @@ export class IVRService implements OnModuleInit, OnModuleDestroy { this.logger.log(`Playback started: ${media} on channel: ${channelId}`); // Handle playback completion - playback.once('PlaybackFinished', () => { - // Only process if this is still the active playback - if (channelState.activePlaybackId !== playbackId) { - this.logger.log( - `PlaybackFinished for stale playback ${playbackId} on channel: ${channelId}, ignoring`, - ); - return; - } - + playback.once('PlaybackFinished', async () => { this.logger.log(`Playback finished on channel: ${channelId}`); - channelState.activePlayback = null; - channelState.activePlaybackId = null; - // Schedule hangup after 10 seconds if channel is still active - if (channelState.isActive) { + this.activePlaybacks.delete(channelId); + if (immediateHangup) { + // Hang up immediately after playback (option has hangup: true) + try { + await this.client.channels.hangup({ channelId }); + this.logger.log( + `Channel ${channelId} hung up immediately after playback (hangup: true)`, + ); + this.cleanupChannel(channelId); + } catch (error) { + this.logger.error( + `Error hanging up channel ${channelId}: ${error.message}`, + ); + this.cleanupChannel(channelId); + } + } else { + // Schedule hangup after 10 seconds this.scheduleHangup(channelId, 10000); // 10000 ms = 10s } }); } catch (error) { - this.logger.error( - `Error playing prompt on channel ${channelId}: ${error.message}`, - ); - channelState.activePlayback = null; - channelState.activePlaybackId = null; + this.logger.error(`Error playing prompt: ${error.message}`); } } @@ -506,10 +500,13 @@ export class IVRService implements OnModuleInit, OnModuleDestroy { const options = channelState.ivrDialPlan.main?.options || []; const option = options.find((opt) => opt.digit === parseInt(digit)); - - // Play the corresponding prompt + // Play the corresponding prompt; if option has hangup: true, hang up immediately after playback if (option && option.prompt) { - await this.playPrompt(channelId, option.prompt.replace('.wav', '')); + await this.playPrompt( + channel.id, + option.prompt.replace('.wav', ''), + option.hangup === true, + ); } else { await this.playPrompt(channelId, 'sound:option-is-invalid'); } From e758a5478e66b0df8601b5f65d017bf7c0df0d7b Mon Sep 17 00:00:00 2001 From: manjik-rumsan Date: Thu, 29 Jan 2026 17:43:47 +0545 Subject: [PATCH 3/6] fixed type issues --- .../src/workers/ivr.service.ts | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/apps/asterisk-worker/src/workers/ivr.service.ts b/apps/asterisk-worker/src/workers/ivr.service.ts index a0261b3..9fa0116 100644 --- a/apps/asterisk-worker/src/workers/ivr.service.ts +++ b/apps/asterisk-worker/src/workers/ivr.service.ts @@ -306,6 +306,14 @@ export class IVRService implements OnModuleInit, OnModuleDestroy { media: string, immediateHangup = false, ) { + const channelState = this.channelStates.get(channelId); + if (!channelState || !channelState.isActive) { + this.logger.warn( + `Attempted to play prompt on inactive or unknown channel: ${channelId}`, + ); + return; + } + try { // Stop any active playback await this.stopActivePlayback(channelId); @@ -313,7 +321,10 @@ export class IVRService implements OnModuleInit, OnModuleDestroy { // Create a new playback const playback = this.client.Playback(); - this.activePlaybacks.set(channelId, playback); + const playbackId = playback.id; + channelState.activePlayback = playback; + channelState.activePlaybackId = playbackId; + await this.client.channels.play({ channelId: channelId, playbackId: playbackId, @@ -324,8 +335,17 @@ export class IVRService implements OnModuleInit, OnModuleDestroy { // Handle playback completion playback.once('PlaybackFinished', async () => { + // Only process if this is still the active playback + if (channelState.activePlaybackId !== playbackId) { + this.logger.log( + `PlaybackFinished for stale playback ${playbackId} on channel: ${channelId}, ignoring`, + ); + return; + } + this.logger.log(`Playback finished on channel: ${channelId}`); - this.activePlaybacks.delete(channelId); + channelState.activePlayback = null; + channelState.activePlaybackId = null; if (immediateHangup) { // Hang up immediately after playback (option has hangup: true) try { @@ -342,11 +362,17 @@ export class IVRService implements OnModuleInit, OnModuleDestroy { } } else { // Schedule hangup after 10 seconds - this.scheduleHangup(channelId, 10000); // 10000 ms = 10s + if (channelState.isActive) { + this.scheduleHangup(channelId, 10000); // 10000 ms = 10s + } } }); } catch (error) { - this.logger.error(`Error playing prompt: ${error.message}`); + this.logger.error( + `Error playing prompt on channel ${channelId}: ${error.message}`, + ); + channelState.activePlayback = null; + channelState.activePlaybackId = null; } } From 495d856a15408d37f9843339e9f0a0c8b3d99550 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 08:56:37 +0000 Subject: [PATCH 4/6] Initial plan From b8a44fb556dc5d4c218d96a0e40509e6c7fb47bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 08:59:00 +0000 Subject: [PATCH 5/6] feat: Add GitHub Copilot instructions for repository Co-authored-by: raghav-rumsan <166870857+raghav-rumsan@users.noreply.github.com> --- .github/copilot-instructions.md | 294 ++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..809afb4 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,294 @@ +# GitHub Copilot Instructions for Rumsan Connect + +## Repository Overview + +Rumsan Connect is a communication hub that registers external services like SMS, Email, Asterisk (Voice), Slack, and WhatsApp. It provides a uniform method to broadcast messages to recipients with built-in queue and scheduling services to ensure all messages are delivered successfully. + +## Technology Stack + +- **Framework**: NestJS (with Fastify adapter) +- **Language**: TypeScript +- **Build Tool**: Nx monorepo +- **Database**: PostgreSQL with Prisma ORM +- **Queue**: Bull (Redis-based job queue) +- **Logging**: Winston +- **Testing**: Jest +- **Code Quality**: ESLint + Prettier + +## Project Structure + +This is an Nx monorepo with the following structure: + +### Apps +- `apps/connect/` - Main NestJS application server +- `apps/asterisk-worker/` - Asterisk voice communication worker +- `apps/connect-e2e/` - End-to-end tests for connect +- `apps/asterisk-worker-e2e/` - End-to-end tests for asterisk-worker + +### Libraries +- `libs/queue/` - Queue management library +- `libs/transports/` - Communication transport implementations (SMTP, Voice, API, SES, Echo) +- `libs/workers/` - Worker implementations +- `libs/sdk/` - SDK library published as `@rumsan/connect` + +### Path Aliases +Use the following TypeScript path aliases: +- `@rsconnect/queue` - Queue library +- `@rsconnect/transports` - Transports library +- `@rsconnect/workers` - Workers library +- `@rumsan/connect` - SDK library + +## Code Style and Conventions + +### Prettier Configuration +- Single quotes +- Trailing commas (all) +- Tab width: 2 spaces +- Semicolons: required +- Print width: 80 characters +- End of line: auto + +### ESLint +- Nx module boundary enforcement is enabled +- TypeScript and JavaScript linting via Nx plugins +- Jest environment for test files (*.spec.ts) + +### Naming Conventions +- Use camelCase for variables and functions +- Use PascalCase for classes, interfaces, and types +- Use kebab-case for file names +- Use descriptive names that reflect purpose + +### File Structure +- DTOs in `dto/` subdirectories +- Entities in `entities/` subdirectories +- Services follow NestJS naming: `*.service.ts` +- Modules follow NestJS naming: `*.module.ts` +- Tests follow naming: `*.spec.ts` + +## Development Commands + +### Starting the Application +```bash +# Start main connect application +npx nx serve connect +# or +npm run start:dev + +# Start asterisk worker +npx nx serve asterisk-worker +# or +npm run start:asterisk-worker +``` + +### Building +```bash +# Build all projects +npm run build:all +# or +npx nx run-many --target=build --all + +# Build specific project +npx nx build connect +``` + +### Testing +```bash +# Run tests for specific project +npx nx test connect + +# Run all tests +npx nx run-many -t test + +# Run e2e tests +npx nx e2e connect-e2e +``` + +### Database +```bash +# Generate Prisma client +npm run prisma:generate + +# Run migrations +npm run prisma:migrate +``` + +### Linting +```bash +# Lint specific project +npx nx lint connect + +# Lint all projects +npx nx run-many -t lint +``` + +## Architecture and Patterns + +### NestJS Architecture +- Follow NestJS modular architecture +- Use dependency injection for services +- Implement proper DTOs with class-validator decorators +- Use NestJS decorators appropriately (@Injectable, @Controller, etc.) + +### API Design +- Global prefix: `/api/v1` +- Use Swagger/OpenAPI decorators for API documentation +- Enable CORS by default +- Payload size limit: 50mb +- Use validation pipes with whitelist and transform options + +### Database (Prisma) +- Database provider: PostgreSQL +- Use Prisma Client for database operations +- Generate client after schema changes +- Follow Prisma naming conventions + +### Queue Management (Bull) +- Use Bull for job queue management +- Implement proper processors for different job types +- Handle job failures and retries appropriately + +### Transport Types +Available transport types (enum TransportType): +- SMTP - Email via SMTP +- VOICE - Voice calls via Asterisk +- API - Generic API transport +- SES - AWS Simple Email Service +- ECHO - Echo/test transport + +### Session and Broadcast Status +- SessionStatus: NEW, PENDING, COMPLETED, FAILED +- BroadcastStatus: SCHEDULED, PENDING, SUCCESS, FAIL +- TriggerType: IMMEDIATE, SCHEDULED, MANUAL + +### Logging +- Use Winston logger for structured logging +- Logger is configured globally via NestModule +- Use appropriate log levels (error, warn, info, debug) + +### Error Handling +- Use RsExceptionFilter for global exception handling +- Implement proper error responses +- Use ResponseTransformInterceptor for response formatting + +## Testing Best Practices + +### Unit Tests +- Write tests in `*.spec.ts` files +- Use Jest as the testing framework +- Follow NestJS testing patterns with TestingModule +- Mock external dependencies +- Test edge cases and error scenarios + +### E2E Tests +- Place e2e tests in separate e2e apps +- Test full API flows +- Use realistic test data + +## Dependencies and Packages + +### Core Dependencies +- @nestjs/* - NestJS framework packages +- @prisma/client - Prisma ORM client +- bull - Job queue +- nodemailer - Email sending +- asterisk-manager, ari-client - Asterisk integration +- amqplib - RabbitMQ integration +- winston, nest-winston - Logging + +### Development Dependencies +- @nx/* - Nx monorepo tools +- @types/* - TypeScript type definitions +- eslint, prettier - Code quality tools +- jest - Testing framework +- prisma - Prisma CLI + +## Best Practices + +1. **Type Safety**: Always use TypeScript types and interfaces. Avoid `any`. + +2. **Validation**: Use class-validator decorators in DTOs for input validation. + +3. **Transformation**: Enable implicit conversion in ValidationPipe for proper DTO transformation. + +4. **Module Organization**: Keep related functionality in dedicated modules following NestJS patterns. + +5. **Configuration**: Use @nestjs/config for environment configuration. + +6. **Error Messages**: Provide clear, actionable error messages. + +7. **Documentation**: Document APIs with Swagger decorators. + +8. **Testing**: Write tests for new functionality and maintain existing tests. + +9. **Code Reuse**: Utilize shared libraries in `libs/` for common functionality. + +10. **Nx Commands**: Use Nx commands for consistent project operations across the monorepo. + +## Common Patterns + +### Creating a New Module +```typescript +// module-name.module.ts +import { Module } from '@nestjs/common'; +import { ModuleNameService } from './module-name.service'; +import { ModuleNameController } from './module-name.controller'; + +@Module({ + controllers: [ModuleNameController], + providers: [ModuleNameService], + exports: [ModuleNameService], +}) +export class ModuleNameModule {} +``` + +### Creating a DTO +```typescript +// dto/create-something.dto.ts +import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateSomethingDto { + @ApiProperty({ description: 'Name of the item' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiPropertyOptional({ description: 'Optional description' }) + @IsString() + @IsOptional() + description?: string; +} +``` + +### Creating a Service +```typescript +// module-name.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '@rumsan/prisma'; + +@Injectable() +export class ModuleNameService { + private readonly logger = new Logger(ModuleNameService.name); + + constructor(private prisma: PrismaService) {} + + async findAll() { + return this.prisma.modelName.findMany(); + } +} +``` + +## Git Workflow + +- Follow conventional commit messages +- Keep commits focused and atomic +- Write descriptive commit messages +- Test before committing + +## Additional Notes + +- The repository uses pnpm as the package manager +- Docker configurations are available for both connect and asterisk-worker +- Bruno collections are available in the `bruno/` directory for API testing +- Documentation is in the `docs/` directory From b3fde543a773ad3be96a179214584c3adc9e7bde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 08:59:35 +0000 Subject: [PATCH 6/6] docs: Add external package aliases to Copilot instructions Co-authored-by: raghav-rumsan <166870857+raghav-rumsan@users.noreply.github.com> --- .github/copilot-instructions.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 809afb4..4089c58 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -37,6 +37,10 @@ Use the following TypeScript path aliases: - `@rsconnect/transports` - Transports library - `@rsconnect/workers` - Workers library - `@rumsan/connect` - SDK library +- `@rumsan/prisma` - Prisma service (external package) +- `@rumsan/extensions` - Extensions library (external package) +- `@rumsan/app` - App utilities (external package) +- `@rumsan/sdk` - Rumsan SDK (external package) ## Code Style and Conventions