refactor(api): describe bank operation create body as oneOf by kind (WealthLedger/backend#172)
Backend verify / commits (pull_request) Successful in 1m32s
Backend verify / security (pull_request) Successful in 1m8s
Backend verify / verify (pull_request) Successful in 6m24s

This commit is contained in:
2026-11-04 14:09:22 +03:00
parent fa0a6e5603
commit 55054295fd
2 changed files with 58 additions and 0 deletions
@@ -1,6 +1,7 @@
import { import {
ApiProperty, ApiProperty,
ApiPropertyOptional, ApiPropertyOptional,
getSchemaPath,
IntersectionType, IntersectionType,
OmitType, OmitType,
PartialType, PartialType,
@@ -153,6 +154,49 @@ export class CreateBankOperationDto {
@MaxLength(MAX_OPERATION_DESCRIPTION_LENGTH) @MaxLength(MAX_OPERATION_DESCRIPTION_LENGTH)
description?: string; description?: string;
} }
// OpenAPI-only variants. Runtime validation stays on CreateBankOperationDto so
// invalid combinations keep the existing BANK_INVALID_INPUT response.
class BankOperationCreateBaseDto extends OmitType(CreateBankOperationDto, [
'kind', 'targetCardId', 'categoryId', 'originalExpenseId',
] as const) {}
export class CreateExpenseOperationDto extends BankOperationCreateBaseDto {
@ApiProperty({ enum: [BankOperationKind.Expense] }) declare kind: 'expense';
@ApiPropertyOptional({ format: 'uuid' }) declare categoryId?: string;
}
export class CreateTopupOperationDto extends BankOperationCreateBaseDto {
@ApiProperty({ enum: [BankOperationKind.Topup] }) declare kind: 'topup';
}
export class CreateTransferOperationDto extends BankOperationCreateBaseDto {
@ApiProperty({ enum: [BankOperationKind.Transfer] }) declare kind: 'transfer';
@ApiProperty({ format: 'uuid' }) declare targetCardId: string;
}
export class CreateRefundOperationDto extends BankOperationCreateBaseDto {
@ApiProperty({ enum: [BankOperationKind.Refund] }) declare kind: 'refund';
@ApiProperty({ format: 'uuid' }) declare originalExpenseId: string;
}
export const createBankOperationSchema = {
oneOf: [
CreateExpenseOperationDto,
CreateTopupOperationDto,
CreateTransferOperationDto,
CreateRefundOperationDto,
].map((model) => ({ $ref: getSchemaPath(model) })),
discriminator: {
propertyName: 'kind',
mapping: {
expense: getSchemaPath(CreateExpenseOperationDto),
topup: getSchemaPath(CreateTopupOperationDto),
transfer: getSchemaPath(CreateTransferOperationDto),
refund: getSchemaPath(CreateRefundOperationDto),
},
},
};
export class UpdateBankOperationDto extends PartialType( export class UpdateBankOperationDto extends PartialType(
OmitType(CreateBankOperationDto, ['id'] as const), OmitType(CreateBankOperationDto, ['id'] as const),
{ skipNullProperties: false }, { skipNullProperties: false },
@@ -16,7 +16,9 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { import {
ApiBearerAuth, ApiBearerAuth,
ApiBody,
ApiCreatedResponse, ApiCreatedResponse,
ApiExtraModels,
ApiNoContentResponse, ApiNoContentResponse,
ApiOkResponse, ApiOkResponse,
ApiResponse, ApiResponse,
@@ -46,6 +48,11 @@ import {
CategoryNameDto, CategoryNameDto,
CreateBankCardDto, CreateBankCardDto,
CreateBankOperationDto, CreateBankOperationDto,
createBankOperationSchema,
CreateExpenseOperationDto,
CreateRefundOperationDto,
CreateTopupOperationDto,
CreateTransferOperationDto,
ExpenseCategoryDto, ExpenseCategoryDto,
UpdateBankCardDto, UpdateBankCardDto,
UpdateBankOperationDto, UpdateBankOperationDto,
@@ -73,6 +80,12 @@ type AuthRequest = Request & { user: AccessPayload };
}, },
}) })
@ApiTags('bank-cards') @ApiTags('bank-cards')
@ApiExtraModels(
CreateExpenseOperationDto,
CreateTopupOperationDto,
CreateTransferOperationDto,
CreateRefundOperationDto,
)
@ApiBearerAuth() @ApiBearerAuth()
@ApiProblemResponses() @ApiProblemResponses()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@@ -131,6 +144,7 @@ export class BankCardsController {
} }
@Post('operations') @Post('operations')
@ApiBody({ schema: createBankOperationSchema })
@ApiCreatedResponse({ type: BankOperationDto }) @ApiCreatedResponse({ type: BankOperationDto })
createOperation( createOperation(
@Req() req: AuthRequest, @Req() req: AuthRequest,