From aeb3974cf9ffb61542ea1423316c4f252b2dccb6 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:23:29 +0200 Subject: [PATCH 1/3] feat: add MROS reporting module (#3606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add MROS reporting module Adds MROS (Meldestelle für Geldwäscherei) module for compliance dashboard. Tracks suspicious activity reports linked to UserData with status enum (Draft, Submitted, Confirmed, Closed), submission date, authority reference and case manager. Exposes admin-only CRUD endpoints. * chore: prettier format mros service constructor * refactor: open mros endpoints to compliance role Switches MrosController guards from ADMIN to COMPLIANCE so compliance operators can use the reporting dashboard. ADMIN and SUPER_ADMIN keep access via the additional roles chain. --- migration/1776932301279-AddMros.js | 28 +++++++++++ .../supporting/mros/dto/create-mros.dto.ts | 24 ++++++++++ .../supporting/mros/dto/update-mros.dto.ts | 21 ++++++++ .../supporting/mros/mros-status.enum.ts | 6 +++ .../supporting/mros/mros.controller.ts | 48 +++++++++++++++++++ src/subdomains/supporting/mros/mros.entity.ts | 22 +++++++++ src/subdomains/supporting/mros/mros.module.ts | 16 +++++++ .../supporting/mros/mros.repository.ts | 11 +++++ .../supporting/mros/mros.service.ts | 41 ++++++++++++++++ .../supporting/supporting.module.ts | 2 + 10 files changed, 219 insertions(+) create mode 100644 migration/1776932301279-AddMros.js create mode 100644 src/subdomains/supporting/mros/dto/create-mros.dto.ts create mode 100644 src/subdomains/supporting/mros/dto/update-mros.dto.ts create mode 100644 src/subdomains/supporting/mros/mros-status.enum.ts create mode 100644 src/subdomains/supporting/mros/mros.controller.ts create mode 100644 src/subdomains/supporting/mros/mros.entity.ts create mode 100644 src/subdomains/supporting/mros/mros.module.ts create mode 100644 src/subdomains/supporting/mros/mros.repository.ts create mode 100644 src/subdomains/supporting/mros/mros.service.ts diff --git a/migration/1776932301279-AddMros.js b/migration/1776932301279-AddMros.js new file mode 100644 index 0000000000..d380a2b6df --- /dev/null +++ b/migration/1776932301279-AddMros.js @@ -0,0 +1,28 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class AddMros1776932301279 { + name = 'AddMros1776932301279' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "mros" ("id" int NOT NULL IDENTITY(1,1), "updated" datetime2 NOT NULL CONSTRAINT "DF_mros_updated" DEFAULT getdate(), "created" datetime2 NOT NULL CONSTRAINT "DF_mros_created" DEFAULT getdate(), "status" nvarchar(256) NOT NULL, "submissionDate" datetime2, "authorityReference" nvarchar(256), "caseManager" nvarchar(256) NOT NULL, "userDataId" int NOT NULL, CONSTRAINT "PK_mros" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "mros" ADD CONSTRAINT "FK_mros_userData" FOREIGN KEY ("userDataId") REFERENCES "user_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "mros" DROP CONSTRAINT "FK_mros_userData"`); + await queryRunner.query(`DROP TABLE "mros"`); + } +} diff --git a/src/subdomains/supporting/mros/dto/create-mros.dto.ts b/src/subdomains/supporting/mros/dto/create-mros.dto.ts new file mode 100644 index 0000000000..8c20009d51 --- /dev/null +++ b/src/subdomains/supporting/mros/dto/create-mros.dto.ts @@ -0,0 +1,24 @@ +import { IsDateString, IsEnum, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { MrosStatus } from '../mros-status.enum'; + +export class CreateMrosDto { + @IsNotEmpty() + @IsInt() + userDataId: number; + + @IsNotEmpty() + @IsEnum(MrosStatus) + status: MrosStatus; + + @IsOptional() + @IsDateString() + submissionDate?: Date; + + @IsOptional() + @IsString() + authorityReference?: string; + + @IsNotEmpty() + @IsString() + caseManager: string; +} diff --git a/src/subdomains/supporting/mros/dto/update-mros.dto.ts b/src/subdomains/supporting/mros/dto/update-mros.dto.ts new file mode 100644 index 0000000000..94faa0c17f --- /dev/null +++ b/src/subdomains/supporting/mros/dto/update-mros.dto.ts @@ -0,0 +1,21 @@ +import { IsDateString, IsEnum, IsString } from 'class-validator'; +import { IsOptionalButNotNull } from 'src/shared/validators/is-not-null.validator'; +import { MrosStatus } from '../mros-status.enum'; + +export class UpdateMrosDto { + @IsOptionalButNotNull() + @IsEnum(MrosStatus) + status?: MrosStatus; + + @IsOptionalButNotNull() + @IsDateString() + submissionDate?: Date; + + @IsOptionalButNotNull() + @IsString() + authorityReference?: string; + + @IsOptionalButNotNull() + @IsString() + caseManager?: string; +} diff --git a/src/subdomains/supporting/mros/mros-status.enum.ts b/src/subdomains/supporting/mros/mros-status.enum.ts new file mode 100644 index 0000000000..fa5dc906f9 --- /dev/null +++ b/src/subdomains/supporting/mros/mros-status.enum.ts @@ -0,0 +1,6 @@ +export enum MrosStatus { + DRAFT = 'Draft', + SUBMITTED = 'Submitted', + CONFIRMED = 'Confirmed', + CLOSED = 'Closed', +} diff --git a/src/subdomains/supporting/mros/mros.controller.ts b/src/subdomains/supporting/mros/mros.controller.ts new file mode 100644 index 0000000000..78991b976c --- /dev/null +++ b/src/subdomains/supporting/mros/mros.controller.ts @@ -0,0 +1,48 @@ +import { Body, Controller, Get, Param, Post, Put, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiBearerAuth, ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger'; +import { RoleGuard } from 'src/shared/auth/role.guard'; +import { UserActiveGuard } from 'src/shared/auth/user-active.guard'; +import { UserRole } from 'src/shared/auth/user-role.enum'; +import { CreateMrosDto } from './dto/create-mros.dto'; +import { UpdateMrosDto } from './dto/update-mros.dto'; +import { Mros } from './mros.entity'; +import { MrosService } from './mros.service'; + +@ApiTags('Mros') +@Controller('mros') +export class MrosController { + constructor(private readonly mrosService: MrosService) {} + + @Post() + @ApiBearerAuth() + @ApiExcludeEndpoint() + @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) + async createMros(@Body() dto: CreateMrosDto): Promise { + await this.mrosService.create(dto); + } + + @Put(':id') + @ApiBearerAuth() + @ApiExcludeEndpoint() + @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) + async updateMros(@Param('id') id: string, @Body() dto: UpdateMrosDto): Promise { + await this.mrosService.update(+id, dto); + } + + @Get() + @ApiBearerAuth() + @ApiExcludeEndpoint() + @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) + async getAll(): Promise { + return this.mrosService.getAll(); + } + + @Get(':id') + @ApiBearerAuth() + @ApiExcludeEndpoint() + @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) + async getById(@Param('id') id: string): Promise { + return this.mrosService.getById(+id); + } +} diff --git a/src/subdomains/supporting/mros/mros.entity.ts b/src/subdomains/supporting/mros/mros.entity.ts new file mode 100644 index 0000000000..cae91bcf39 --- /dev/null +++ b/src/subdomains/supporting/mros/mros.entity.ts @@ -0,0 +1,22 @@ +import { IEntity } from 'src/shared/models/entity'; +import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity'; +import { Column, Entity, ManyToOne } from 'typeorm'; +import { MrosStatus } from './mros-status.enum'; + +@Entity() +export class Mros extends IEntity { + @ManyToOne(() => UserData, { nullable: false }) + userData: UserData; + + @Column({ length: 256 }) + status: MrosStatus; + + @Column({ type: 'datetime2', nullable: true }) + submissionDate?: Date; + + @Column({ length: 256, nullable: true }) + authorityReference?: string; + + @Column({ length: 256 }) + caseManager: string; +} diff --git a/src/subdomains/supporting/mros/mros.module.ts b/src/subdomains/supporting/mros/mros.module.ts new file mode 100644 index 0000000000..3593564319 --- /dev/null +++ b/src/subdomains/supporting/mros/mros.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SharedModule } from 'src/shared/shared.module'; +import { UserModule } from 'src/subdomains/generic/user/user.module'; +import { MrosController } from './mros.controller'; +import { Mros } from './mros.entity'; +import { MrosRepository } from './mros.repository'; +import { MrosService } from './mros.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Mros]), SharedModule, UserModule], + controllers: [MrosController], + providers: [MrosRepository, MrosService], + exports: [], +}) +export class MrosModule {} diff --git a/src/subdomains/supporting/mros/mros.repository.ts b/src/subdomains/supporting/mros/mros.repository.ts new file mode 100644 index 0000000000..cfbf8592e7 --- /dev/null +++ b/src/subdomains/supporting/mros/mros.repository.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { BaseRepository } from 'src/shared/repositories/base.repository'; +import { EntityManager } from 'typeorm'; +import { Mros } from './mros.entity'; + +@Injectable() +export class MrosRepository extends BaseRepository { + constructor(manager: EntityManager) { + super(Mros, manager); + } +} diff --git a/src/subdomains/supporting/mros/mros.service.ts b/src/subdomains/supporting/mros/mros.service.ts new file mode 100644 index 0000000000..cdce8768f9 --- /dev/null +++ b/src/subdomains/supporting/mros/mros.service.ts @@ -0,0 +1,41 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { UserDataService } from 'src/subdomains/generic/user/models/user-data/user-data.service'; +import { CreateMrosDto } from './dto/create-mros.dto'; +import { UpdateMrosDto } from './dto/update-mros.dto'; +import { Mros } from './mros.entity'; +import { MrosRepository } from './mros.repository'; + +@Injectable() +export class MrosService { + constructor( + private readonly repo: MrosRepository, + private readonly userDataService: UserDataService, + ) {} + + async create(dto: CreateMrosDto): Promise { + const entity = this.repo.create(dto); + + entity.userData = await this.userDataService.getUserData(dto.userDataId); + if (!entity.userData) throw new NotFoundException('UserData not found'); + + return this.repo.save(entity); + } + + async update(id: number, dto: UpdateMrosDto): Promise { + const entity = await this.repo.findOneBy({ id }); + if (!entity) throw new NotFoundException('Mros not found'); + + return this.repo.save({ ...entity, ...dto }); + } + + async getAll(): Promise { + return this.repo.find({ relations: { userData: true } }); + } + + async getById(id: number): Promise { + const entity = await this.repo.findOne({ where: { id }, relations: { userData: true } }); + if (!entity) throw new NotFoundException('Mros not found'); + + return entity; + } +} diff --git a/src/subdomains/supporting/supporting.module.ts b/src/subdomains/supporting/supporting.module.ts index 5f2650bebb..6ead699738 100644 --- a/src/subdomains/supporting/supporting.module.ts +++ b/src/subdomains/supporting/supporting.module.ts @@ -8,6 +8,7 @@ import { DexModule } from './dex/dex.module'; import { FiatOutputModule } from './fiat-output/fiat-output.module'; import { FiatPayInModule } from './fiat-payin/fiat-payin.module'; import { LogModule } from './log/log.module'; +import { MrosModule } from './mros/mros.module'; import { NotificationModule } from './notification/notification.module'; import { PayInModule } from './payin/payin.module'; import { PayoutModule } from './payout/payout.module'; @@ -34,6 +35,7 @@ import { SupportIssueModule } from './support-issue/support-issue.module'; FiatOutputModule, SupportIssueModule, RecallModule, + MrosModule, ], controllers: [], providers: [], From 0adf91f042d309db93d38cc012f1e3ec94649ab3 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:24:53 +0200 Subject: [PATCH 2/3] feat: add recall list endpoints for compliance dashboard (#3610) * feat: add recall list endpoints for compliance dashboard Adds GET /recall and GET /recall/:id returning recalls with eager-loaded bankTx, checkoutTx and user relations. Both routes are admin-only, consistent with existing POST/PUT. * refactor: open recall endpoints to compliance role Switches RecallController guards from ADMIN to COMPLIANCE so compliance operators can use the dashboard end-to-end (list, create, update). ADMIN and SUPER_ADMIN still have access via additional roles. --- .../supporting/recall/recall.controller.ts | 23 ++++++++++++++++--- .../supporting/recall/recall.service.ts | 14 +++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/subdomains/supporting/recall/recall.controller.ts b/src/subdomains/supporting/recall/recall.controller.ts index 0e13e23051..f049ba98b8 100644 --- a/src/subdomains/supporting/recall/recall.controller.ts +++ b/src/subdomains/supporting/recall/recall.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Param, Post, Put, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Put, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger'; import { RoleGuard } from 'src/shared/auth/role.guard'; @@ -6,6 +6,7 @@ import { UserActiveGuard } from 'src/shared/auth/user-active.guard'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { CreateRecallDto } from './dto/create-recall.dto'; import { UpdateRecallDto } from './dto/update-recall.dto'; +import { Recall } from './recall.entity'; import { RecallService } from './recall.service'; @ApiTags('Recall') @@ -16,7 +17,7 @@ export class RecallController { @Post() @ApiBearerAuth() @ApiExcludeEndpoint() - @UseGuards(AuthGuard(), RoleGuard(UserRole.ADMIN), UserActiveGuard()) + @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) async createRecall(@Body() dto: CreateRecallDto): Promise { await this.recallService.create(dto); } @@ -24,8 +25,24 @@ export class RecallController { @Put(':id') @ApiBearerAuth() @ApiExcludeEndpoint() - @UseGuards(AuthGuard(), RoleGuard(UserRole.ADMIN), UserActiveGuard()) + @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) async updateRecall(@Param('id') id: string, @Body() dto: UpdateRecallDto): Promise { await this.recallService.update(+id, dto); } + + @Get() + @ApiBearerAuth() + @ApiExcludeEndpoint() + @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) + async getAll(): Promise { + return this.recallService.getAll(); + } + + @Get(':id') + @ApiBearerAuth() + @ApiExcludeEndpoint() + @UseGuards(AuthGuard(), RoleGuard(UserRole.COMPLIANCE), UserActiveGuard()) + async getById(@Param('id') id: string): Promise { + return this.recallService.getById(+id); + } } diff --git a/src/subdomains/supporting/recall/recall.service.ts b/src/subdomains/supporting/recall/recall.service.ts index ec19931891..f638875d95 100644 --- a/src/subdomains/supporting/recall/recall.service.ts +++ b/src/subdomains/supporting/recall/recall.service.ts @@ -48,4 +48,18 @@ export class RecallService { return this.repo.save({ ...entity, ...dto }); } + + async getAll(): Promise { + return this.repo.find({ relations: { bankTx: true, checkoutTx: true, user: true } }); + } + + async getById(id: number): Promise { + const entity = await this.repo.findOne({ + where: { id }, + relations: { bankTx: true, checkoutTx: true, user: true }, + }); + if (!entity) throw new NotFoundException('Recall not found'); + + return entity; + } } From 80c6d8dcfa1888347c2f4f250098ba44dae2a37b Mon Sep 17 00:00:00 2001 From: David May <85513542+davidleomay@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:39:46 +0200 Subject: [PATCH 3/3] fix: MROS migration (#3613) --- ...76932301279-AddMros.js => 1776937038432-AddMros.js} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename migration/{1776932301279-AddMros.js => 1776937038432-AddMros.js} (52%) diff --git a/migration/1776932301279-AddMros.js b/migration/1776937038432-AddMros.js similarity index 52% rename from migration/1776932301279-AddMros.js rename to migration/1776937038432-AddMros.js index d380a2b6df..50d735c418 100644 --- a/migration/1776932301279-AddMros.js +++ b/migration/1776937038432-AddMros.js @@ -7,22 +7,22 @@ * @class * @implements {MigrationInterface} */ -module.exports = class AddMros1776932301279 { - name = 'AddMros1776932301279' +module.exports = class AddMros1776937038432 { + name = 'AddMros1776937038432' /** * @param {QueryRunner} queryRunner */ async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "mros" ("id" int NOT NULL IDENTITY(1,1), "updated" datetime2 NOT NULL CONSTRAINT "DF_mros_updated" DEFAULT getdate(), "created" datetime2 NOT NULL CONSTRAINT "DF_mros_created" DEFAULT getdate(), "status" nvarchar(256) NOT NULL, "submissionDate" datetime2, "authorityReference" nvarchar(256), "caseManager" nvarchar(256) NOT NULL, "userDataId" int NOT NULL, CONSTRAINT "PK_mros" PRIMARY KEY ("id"))`); - await queryRunner.query(`ALTER TABLE "mros" ADD CONSTRAINT "FK_mros_userData" FOREIGN KEY ("userDataId") REFERENCES "user_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`CREATE TABLE "mros" ("id" int NOT NULL IDENTITY(1,1), "updated" datetime2 NOT NULL CONSTRAINT "DF_f6ade72c09ca260e3ce42ba0781" DEFAULT getdate(), "created" datetime2 NOT NULL CONSTRAINT "DF_d7ed4994a2c27be9ea6c21b1c21" DEFAULT getdate(), "status" nvarchar(256) NOT NULL, "submissionDate" datetime2, "authorityReference" nvarchar(256), "caseManager" nvarchar(256) NOT NULL, "userDataId" int NOT NULL, CONSTRAINT "PK_48a5606a1194ef6f78c24999754" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "mros" ADD CONSTRAINT "FK_021227644566f36c31912257a39" FOREIGN KEY ("userDataId") REFERENCES "user_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); } /** * @param {QueryRunner} queryRunner */ async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "mros" DROP CONSTRAINT "FK_mros_userData"`); + await queryRunner.query(`ALTER TABLE "mros" DROP CONSTRAINT "FK_021227644566f36c31912257a39"`); await queryRunner.query(`DROP TABLE "mros"`); } }