Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .freebuff/project-id
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f0c4cc0d-4346-4931-ae7a-b1670a0d5e69
6873507d-0234-4bdb-b97b-da04098eb1dc
16 changes: 16 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ import { BillingModule } from "./billing/billing.module";
import { PaymentsModule } from "./payments/payments.module";
import { RateLimitingModule } from "./rate-limiting/rate-limiting.module";
import { ReconciliationModule } from "./reconciliation/reconciliation.module";
// Modules – notifications
import { NotificationModule } from "./notifications/notification.module";

// Auth entities
import { User } from "./core/user/entities/user.entity";
Expand Down Expand Up @@ -115,6 +117,13 @@ import { FileScanResult } from "./infrastructure/file-upload/entities/file-scan-
import { ReconciliationAudit } from "./reconciliation/entities/reconciliation-audit.entity";
import { ReconciliationInvoice } from "./reconciliation/entities/reconciliation-invoice.entity";
import { StellarTransaction } from "./reconciliation/entities/stellar-transaction.entity";
// Notification entities
import { Notification } from "./notifications/entities/notification.entity";
import { NotificationTemplate } from "./notifications/entities/notification-template.entity";
import { NotificationPreference } from "./notifications/entities/notification-preference.entity";
import { NotificationAggregation } from "./notifications/entities/notification-aggregation.entity";
import { NotificationDeliveryLog } from "./notifications/entities/notification-delivery-log.entity";
import { NotificationAnalytics } from "./notifications/entities/notification-analytics.entity";
// Modules – webhooks
import { WebhookModule } from "./infrastructure/webhooks/webhook.module";
// Modules – file upload
Expand Down Expand Up @@ -220,6 +229,12 @@ import { TenantModuleState } from "./modules/registry/entities/tenant-module-sta
ReconciliationAudit,
ReconciliationInvoice,
StellarTransaction,
Notification,
NotificationTemplate,
NotificationPreference,
NotificationAggregation,
NotificationDeliveryLog,
NotificationAnalytics,
],
synchronize: true,
logging: true,
Expand Down Expand Up @@ -272,6 +287,7 @@ import { TenantModuleState } from "./modules/registry/entities/tenant-module-sta
BillingModule,
PaymentsModule,
ReconciliationModule,
NotificationModule,
],

controllers: [AppController],
Expand Down
62 changes: 62 additions & 0 deletions src/notifications/dto/notification-analytics.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
IsOptional,
IsString,
IsEnum,
IsDateString,
IsInt,
Min,
Max,
} from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { NotificationChannel } from '../entities/notification.entity';

export class QueryAnalyticsDto {
@ApiPropertyOptional({ description: 'Start of date range (ISO 8601)' })
@IsOptional()
@IsDateString()
from?: string;

@ApiPropertyOptional({ description: 'End of date range (ISO 8601)' })
@IsOptional()
@IsDateString()
to?: string;

@ApiPropertyOptional({ enum: ['hourly', 'daily'], default: 'daily' })
@IsOptional()
@IsString()
granularity?: 'hourly' | 'daily' = 'daily';

@ApiPropertyOptional({ enum: NotificationChannel })
@IsOptional()
@IsEnum(NotificationChannel)
channel?: NotificationChannel;

@ApiPropertyOptional({ description: 'Filter by notification category' })
@IsOptional()
@IsString()
category?: string;

@ApiPropertyOptional({ default: 30, minimum: 1, maximum: 365 })
@IsOptional()
@IsInt()
@Min(1)
@Max(365)
limit?: number = 30;
}

export class EngagementSummaryDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
userId?: string;

@ApiPropertyOptional({ description: 'Start of date range (ISO 8601)' })
@IsOptional()
@IsDateString()
from?: string;

@ApiPropertyOptional({ description: 'End of date range (ISO 8601)' })
@IsOptional()
@IsDateString()
to?: string;
}
116 changes: 116 additions & 0 deletions src/notifications/dto/notification-preference.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
IsString,
IsOptional,
IsEnum,
IsBoolean,
IsInt,
Min,
Max,
IsArray,
ValidateNested,
IsObject,
IsEmail,
Matches,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { NotificationCategory } from '../entities/notification.entity';
import { NotificationChannelPreference } from '../entities/notification-preference.entity';

export class UpdateNotificationPreferenceDto {
@ApiProperty({ enum: NotificationCategory })
@IsEnum(NotificationCategory)
category: NotificationCategory;

@ApiProperty({ description: 'Channel to configure (email, sms, push, webhook, in_app)' })
@IsString()
channel: string;

@ApiProperty({ enum: NotificationChannelPreference })
@IsEnum(NotificationChannelPreference)
preference: NotificationChannelPreference;

@ApiPropertyOptional({ enum: ['hourly', 'daily', 'weekly'], description: 'For digest mode' })
@IsOptional()
@IsString()
digestFrequency?: string;

@ApiPropertyOptional({ description: 'Quiet hours start (0-23)', minimum: 0, maximum: 23 })
@IsOptional()
@IsInt()
@Min(0)
@Max(23)
quietHoursStart?: number;

@ApiPropertyOptional({ description: 'Quiet hours end (0-23)', minimum: 0, maximum: 23 })
@IsOptional()
@IsInt()
@Min(0)
@Max(23)
quietHoursEnd?: number;

@ApiPropertyOptional({ description: 'IANA timezone (e.g., America/New_York)' })
@IsOptional()
@IsString()
timezone?: string;

@ApiPropertyOptional({ description: 'Email address for email channel' })
@IsOptional()
@IsEmail()
emailAddress?: string;

@ApiPropertyOptional({ description: 'Phone number for SMS channel (E.164 format)' })
@IsOptional()
@IsString()
@Matches(/^\+[1-9]\d{1,14}$/)
phoneNumber?: string;

@ApiPropertyOptional({ description: 'Device push token' })
@IsOptional()
@IsString()
pushToken?: string;

@ApiPropertyOptional({ description: 'Webhook callback URL' })
@IsOptional()
@IsString()
webhookUrl?: string;

@ApiPropertyOptional({
enum: ['low', 'normal', 'high', 'critical'],
description: 'Minimum priority to trigger this channel',
})
@IsOptional()
@IsString()
minPriority?: string;

@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
active?: boolean;

@ApiPropertyOptional()
@IsOptional()
@IsObject()
metadata?: Record<string, any>;
}

export class BulkUpdatePreferenceDto {
@ApiProperty({ type: [UpdateNotificationPreferenceDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => UpdateNotificationPreferenceDto)
preferences: UpdateNotificationPreferenceDto[];
}

export class SetAllChannelsDto {
@ApiProperty({ enum: NotificationCategory })
@IsEnum(NotificationCategory)
category: NotificationCategory;

@ApiProperty({
enum: ['all_on', 'all_off', 'in_app_only', 'essential_only'],
description: 'Preset: enable/disable all channels at once',
})
@IsString()
preset: 'all_on' | 'all_off' | 'in_app_only' | 'essential_only';
}
122 changes: 122 additions & 0 deletions src/notifications/dto/notification-template.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
IsString,
IsOptional,
IsBoolean,
IsArray,
IsObject,
MaxLength,
MinLength,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class CreateNotificationTemplateDto {
@ApiProperty({ example: 'portfolio_alert' })
@IsString()
@MinLength(2)
@MaxLength(255)
name: string;

@ApiProperty({ example: 'Portfolio value threshold alert' })
@IsString()
@MinLength(1)
@MaxLength(255)
description: string;

@ApiProperty({ example: 'email', description: 'Target channel' })
@IsString()
channel: string;

@ApiPropertyOptional({ example: 'Your portfolio has reached {{threshold}}' })
@IsOptional()
@IsString()
subject?: string;

@ApiProperty({ example: 'Hello {{name}}, your portfolio is now {{value}}.' })
@IsString()
bodyTemplate: string;

@ApiPropertyOptional({
description: 'HTML template with Handlebars-style variables',
})
@IsOptional()
@IsString()
htmlTemplate?: string;

@ApiPropertyOptional({ description: 'SMS-specific template' })
@IsOptional()
@IsString()
smsTemplate?: string;

@ApiPropertyOptional({
example: ['name', 'value', 'threshold'],
description: 'Template variable names',
})
@IsOptional()
@IsArray()
variables?: string[];

@ApiPropertyOptional({ example: 'portfolio' })
@IsOptional()
@IsString()
category?: string;

@ApiPropertyOptional()
@IsOptional()
@IsObject()
metadata?: Record<string, any>;
}

export class UpdateNotificationTemplateDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(255)
description?: string;

@ApiPropertyOptional()
@IsOptional()
@IsString()
subject?: string;

@ApiPropertyOptional()
@IsOptional()
@IsString()
bodyTemplate?: string;

@ApiPropertyOptional()
@IsOptional()
@IsString()
htmlTemplate?: string;

@ApiPropertyOptional()
@IsOptional()
@IsString()
smsTemplate?: string;

@ApiPropertyOptional()
@IsOptional()
@IsArray()
variables?: string[];

@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
active?: boolean;

@ApiPropertyOptional()
@IsOptional()
@IsObject()
metadata?: Record<string, any>;
}

export class RenderTemplatePreviewDto {
@ApiProperty()
@IsString()
templateName: string;

@ApiProperty({
example: { name: 'Alice', value: '$15,000', threshold: '$10,000' },
})
@IsObject()
variables: Record<string, any>;
}
Loading
Loading