feat: implement user-scoped receipt aliases with global fallback; enhance alias management in admin panel
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-04 19:43:13 +02:00
parent d73ea5ef7c
commit 64b06435cf
15 changed files with 751 additions and 36 deletions
@@ -1,4 +1,4 @@
import { IsInt, IsString, MinLength } from 'class-validator';
import { IsBoolean, IsInt, IsOptional, IsString, MinLength } from 'class-validator';
export class CreateReceiptAliasDto {
@IsString()
@@ -7,4 +7,8 @@ export class CreateReceiptAliasDto {
@IsInt()
productId!: number;
@IsOptional()
@IsBoolean()
isGlobal?: boolean;
}
@@ -1,25 +1,30 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post } from '@nestjs/common';
import { ReceiptAliasService } from './receipt-alias.service';
import { CreateReceiptAliasDto } from './dto/create-receipt-alias.dto';
import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
@Roles('admin')
@Controller('receipt-aliases')
export class ReceiptAliasController {
constructor(private readonly receiptAliasService: ReceiptAliasService) {}
@Get()
findAll() {
return this.receiptAliasService.findAll();
findAll(@CurrentUser() user: { userId: number; role: string }) {
return this.receiptAliasService.findAllForUser(user.userId, user.role);
}
@Post()
upsert(@Body() dto: CreateReceiptAliasDto) {
return this.receiptAliasService.upsert(dto);
upsert(
@Body() dto: CreateReceiptAliasDto,
@CurrentUser() user: { userId: number; role: string },
) {
return this.receiptAliasService.upsert(dto, user.userId, user.role);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.receiptAliasService.remove(id);
remove(
@Param('id', ParseIntPipe) id: number,
@CurrentUser() user: { userId: number; role: string },
) {
return this.receiptAliasService.remove(id, user.userId, user.role);
}
}
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { ForbiddenException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateReceiptAliasDto } from './dto/create-receipt-alias.dto';
@@ -6,23 +6,88 @@ import { CreateReceiptAliasDto } from './dto/create-receipt-alias.dto';
export class ReceiptAliasService {
constructor(private readonly prisma: PrismaService) {}
findAll() {
findAllForUser(userId: number, role: string) {
const where = role === 'admin'
? undefined
: {
OR: [
{ ownerId: userId, isGlobal: false },
{ isGlobal: true },
],
};
return this.prisma.receiptAlias.findMany({
where,
include: { product: { select: { id: true, name: true, canonicalName: true } } },
orderBy: { receiptName: 'asc' },
});
}
async upsert(dto: CreateReceiptAliasDto) {
async upsert(dto: CreateReceiptAliasDto, userId: number, role: string) {
const normalized = dto.receiptName.toLowerCase().trim();
return this.prisma.receiptAlias.upsert({
where: { receiptName: normalized },
create: { receiptName: normalized, productId: dto.productId },
update: { productId: dto.productId },
const wantsGlobal = dto.isGlobal === true;
if (wantsGlobal && role !== 'admin') {
throw new ForbiddenException('Endast admin kan skapa globala alias');
}
if (wantsGlobal) {
const existing = await this.prisma.receiptAlias.findFirst({
where: { receiptName: normalized, isGlobal: true },
});
if (existing) {
return this.prisma.receiptAlias.update({
where: { id: existing.id },
data: { productId: dto.productId },
});
}
return this.prisma.receiptAlias.create({
data: {
receiptName: normalized,
productId: dto.productId,
isGlobal: true,
ownerId: null,
},
});
}
const existing = await this.prisma.receiptAlias.findFirst({
where: { receiptName: normalized, ownerId: userId, isGlobal: false },
});
if (existing) {
return this.prisma.receiptAlias.update({
where: { id: existing.id },
data: { productId: dto.productId },
});
}
return this.prisma.receiptAlias.create({
data: {
receiptName: normalized,
productId: dto.productId,
ownerId: userId,
isGlobal: false,
},
});
}
remove(id: number) {
async remove(id: number, userId: number, role: string) {
const alias = await this.prisma.receiptAlias.findUnique({ where: { id } });
if (!alias) {
return this.prisma.receiptAlias.delete({ where: { id } });
}
const canDelete =
role === 'admin' ||
(alias.ownerId === userId && alias.isGlobal === false);
if (!canDelete) {
throw new ForbiddenException('Du har inte behörighet att ta bort aliaset');
}
return this.prisma.receiptAlias.delete({ where: { id } });
}
}