Skip to content

Latest commit

 

History

History
230 lines (225 loc) · 7.05 KB

File metadata and controls

230 lines (225 loc) · 7.05 KB

Example Diff: Implementation of Issue #47

diff --git a/src/models/notification-preference.model.ts b/src/models/notification-preference.model.ts
new file mode 100644
index 0000000..a3f8c12
--- /dev/null
+++ b/src/models/notification-preference.model.ts
@@ -0,0 +1,34 @@
+import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from './user.model';
+
+export enum NotificationChannel {
+  EMAIL = 'email',
+  IN_APP = 'in_app',
+  SMS = 'sms',
+}
+
+export enum NotificationEventType {
+  ORDER_STATUS = 'order_status',
+  MARKETING = 'marketing',
+  SECURITY = 'security',
+}
+
+@Entity('notification_preferences')
+export class NotificationPreference {
+  @PrimaryGeneratedColumn('uuid')
+  id: string;
+
+  @ManyToOne(() => User, { onDelete: 'CASCADE' })
+  @JoinColumn({ name: 'user_id' })
+  user: User;
+
+  @Column({ type: 'enum', enum: NotificationChannel })
+  channel: NotificationChannel;
+
+  @Column({ type: 'enum', enum: NotificationEventType })
+  eventType: NotificationEventType;
+
+  @Column({ type: 'boolean', default: true })
+  enabled: boolean;
+}

diff --git a/src/services/notification-preference.service.ts b/src/services/notification-preference.service.ts
new file mode 100644
index 0000000..b7e2d91
--- /dev/null
+++ b/src/services/notification-preference.service.ts
@@ -0,0 +1,58 @@
+import { Injectable, BadRequestException } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import { NotificationPreference, NotificationChannel, NotificationEventType } from '../models/notification-preference.model';
+
+@Injectable()
+export class NotificationPreferenceService {
+  constructor(
+    @InjectRepository(NotificationPreference)
+    private readonly prefRepo: Repository<NotificationPreference>,
+  ) {}
+
+  async getByUserId(userId: string): Promise<NotificationPreference[]> {
+    return this.prefRepo.find({
+      where: { user: { id: userId } },
+      order: { channel: 'ASC', eventType: 'ASC' },
+    });
+  }
+
+  async updatePreference(
+    userId: string,
+    channel: NotificationChannel,
+    eventType: NotificationEventType,
+    enabled: boolean,
+  ): Promise<NotificationPreference> {
+    if (eventType === NotificationEventType.SECURITY && !enabled) {
+      throw new BadRequestException('Security notifications cannot be disabled');
+    }
+
+    let pref = await this.prefRepo.findOne({
+      where: {
+        user: { id: userId },
+        channel,
+        eventType,
+      },
+    });
+
+    if (!pref) {
+      pref = this.prefRepo.create({
+        user: { id: userId },
+        channel,
+        eventType,
+        enabled,
+      });
+    } else {
+      pref.enabled = enabled;
+    }
+
+    return this.prefRepo.save(pref);
+  }
+
+  async initializeDefaults(userId: string): Promise<void> {
+    const channels = Object.values(NotificationChannel);
+    const events = Object.values(NotificationEventType);
+
+    const prefs = channels.flatMap(channel =>
+      events.map(eventType =>
+        this.prefRepo.create({ user: { id: userId }, channel, eventType, enabled: true }),
+      ),
+    );
+
+    await this.prefRepo.save(prefs);
+  }
+}

diff --git a/src/controllers/notification-preference.controller.ts b/src/controllers/notification-preference.controller.ts
new file mode 100644
index 0000000..c4d9e33
--- /dev/null
+++ b/src/controllers/notification-preference.controller.ts
@@ -0,0 +1,42 @@
+import { Controller, Get, Put, Param, Body, UseGuards } from '@nestjs/common';
+import { AuthGuard } from '../guards/auth.guard';
+import { OwnerGuard } from '../guards/owner.guard';
+import { NotificationPreferenceService } from '../services/notification-preference.service';
+import { UpdatePreferenceDto } from '../dto/update-preference.dto';
+
+@Controller('api/users/:userId/notification-preferences')
+@UseGuards(AuthGuard, OwnerGuard)
+export class NotificationPreferenceController {
+  constructor(
+    private readonly prefService: NotificationPreferenceService,
+  ) {}
+
+  @Get()
+  async getPreferences(@Param('userId') userId: string) {
+    return this.prefService.getByUserId(userId);
+  }
+
+  @Put()
+  async updatePreference(
+    @Param('userId') userId: string,
+    @Body() dto: UpdatePreferenceDto,
+  ) {
+    return this.prefService.updatePreference(
+      userId,
+      dto.channel,
+      dto.eventType,
+      dto.enabled,
+    );
+  }
+}

diff --git a/src/dto/update-preference.dto.ts b/src/dto/update-preference.dto.ts
new file mode 100644
index 0000000..e1a2b3c
--- /dev/null
+++ b/src/dto/update-preference.dto.ts
@@ -0,0 +1,18 @@
+import { IsEnum, IsBoolean } from 'class-validator';
+import { NotificationChannel, NotificationEventType } from '../models/notification-preference.model';
+
+export class UpdatePreferenceDto {
+  @IsEnum(NotificationChannel)
+  channel: NotificationChannel;
+
+  @IsEnum(NotificationEventType)
+  eventType: NotificationEventType;
+
+  @IsBoolean()
+  enabled: boolean;
+}

diff --git a/src/tests/notification-preference.service.spec.ts b/src/tests/notification-preference.service.spec.ts
new file mode 100644
index 0000000..f5c8a91
--- /dev/null
+++ b/src/tests/notification-preference.service.spec.ts
@@ -0,0 +1,47 @@
+import { Test } from '@nestjs/testing';
+import { getRepositoryToken } from '@nestjs/typeorm';
+import { BadRequestException } from '@nestjs/common';
+import { NotificationPreferenceService } from '../services/notification-preference.service';
+import { NotificationPreference, NotificationChannel, NotificationEventType } from '../models/notification-preference.model';
+
+describe('NotificationPreferenceService', () => {
+  let service: NotificationPreferenceService;
+  let mockRepo: any;
+
+  beforeEach(async () => {
+    mockRepo = {
+      find: jest.fn(),
+      findOne: jest.fn(),
+      create: jest.fn(dto => dto),
+      save: jest.fn(entity => Promise.resolve(entity)),
+    };
+
+    const module = await Test.createTestingModule({
+      providers: [
+        NotificationPreferenceService,
+        { provide: getRepositoryToken(NotificationPreference), useValue: mockRepo },
+      ],
+    }).compile();
+
+    service = module.get(NotificationPreferenceService);
+  });
+
+  it('should reject disabling security notifications', async () => {
+    await expect(
+      service.updatePreference('user-1', NotificationChannel.EMAIL, NotificationEventType.SECURITY, false),
+    ).rejects.toThrow(BadRequestException);
+  });
+
+  it('should create preference if not exists', async () => {
+    mockRepo.findOne.mockResolvedValue(null);
+    const result = await service.updatePreference(
+      'user-1', NotificationChannel.SMS, NotificationEventType.MARKETING, false,
+    );
+    expect(mockRepo.create).toHaveBeenCalled();
+    expect(result.enabled).toBe(false);
+  });
+
+  it('should initialize defaults for all channel-event combinations', async () => {
+    await service.initializeDefaults('user-1');
+    // 3 channels × 3 event types = 9 preferences
+    expect(mockRepo.save).toHaveBeenCalledWith(
+      expect.arrayContaining([expect.objectContaining({ enabled: true })]),
+    );
+  });
+});