diff --git a/packages/server/src/modules/Auth/Auth.controller.ts b/packages/server/src/modules/Auth/Auth.controller.ts index 534e66c01..b8b18554a 100644 --- a/packages/server/src/modules/Auth/Auth.controller.ts +++ b/packages/server/src/modules/Auth/Auth.controller.ts @@ -14,12 +14,19 @@ import { ApiOperation, ApiBody, ApiParam, - ApiExcludeController, + ApiResponse, + ApiExtraModels, + getSchemaPath, } from '@nestjs/swagger'; import { PublicRoute } from './guards/jwt.guard'; import { AuthenticationApplication } from './AuthApplication.sevice'; import { AuthSignupDto } from './dtos/AuthSignup.dto'; import { AuthSigninDto } from './dtos/AuthSignin.dto'; +import { AuthSignupVerifyDto } from './dtos/AuthSignupVerify.dto'; +import { AuthSendResetPasswordDto } from './dtos/AuthSendResetPassword.dto'; +import { AuthResetPasswordDto } from './dtos/AuthResetPassword.dto'; +import { AuthSigninResponseDto } from './dtos/AuthSigninResponse.dto'; +import { AuthMetaResponseDto } from './dtos/AuthMetaResponse.dto'; import { LocalAuthGuard } from './guards/Local.guard'; import { AuthSigninService } from './commands/AuthSignin.service'; import { TenantModel } from '../System/models/TenantModel'; @@ -27,7 +34,7 @@ import { SystemUser } from '../System/models/SystemUser'; @Controller('/auth') @ApiTags('Auth') -@ApiExcludeController() +@ApiExtraModels(AuthSigninResponseDto, AuthMetaResponseDto) @PublicRoute() @Throttle({ auth: {} }) export class AuthController { @@ -43,10 +50,15 @@ export class AuthController { @UseGuards(LocalAuthGuard) @ApiOperation({ summary: 'Sign in a user' }) @ApiBody({ type: AuthSigninDto }) + @ApiResponse({ + status: 200, + description: 'Sign-in successful. Returns access token and tenant/organization IDs.', + schema: { $ref: getSchemaPath(AuthSigninResponseDto) }, + }) async signin( @Request() req: Request & { user: SystemUser }, @Body() signinDto: AuthSigninDto, - ) { + ): Promise { const { user } = req; const tenant = await this.tenantModel.query().findById(user.tenantId); @@ -61,59 +73,47 @@ export class AuthController { @Post('/signup') @ApiOperation({ summary: 'Sign up a new user' }) @ApiBody({ type: AuthSignupDto }) + @ApiResponse({ status: 201, description: 'Sign-up initiated. Check email for confirmation.' }) signup(@Request() req: Request, @Body() signupDto: AuthSignupDto) { return this.authApp.signUp(signupDto); } @Post('/signup/verify') @ApiOperation({ summary: 'Confirm user signup' }) - @ApiBody({ - schema: { - type: 'object', - properties: { - email: { type: 'string', example: 'user@example.com' }, - token: { type: 'string', example: 'confirmation-token' }, - }, - }, - }) - signupConfirm(@Body('email') email: string, @Body('token') token: string) { - return this.authApp.signUpConfirm(email, token); + @ApiBody({ type: AuthSignupVerifyDto }) + @ApiResponse({ status: 200, description: 'Signup confirmed successfully.' }) + signupConfirm(@Body() body: AuthSignupVerifyDto) { + return this.authApp.signUpConfirm(body.email, body.token); } @Post('/send_reset_password') @ApiOperation({ summary: 'Send reset password email' }) - @ApiBody({ - schema: { - type: 'object', - properties: { - email: { type: 'string', example: 'user@example.com' }, - }, - }, - }) - sendResetPassword(@Body('email') email: string) { - return this.authApp.sendResetPassword(email); + @ApiBody({ type: AuthSendResetPasswordDto }) + @ApiResponse({ status: 200, description: 'Reset password email sent if the account exists.' }) + sendResetPassword(@Body() body: AuthSendResetPasswordDto) { + return this.authApp.sendResetPassword(body.email); } @Post('/reset_password/:token') @ApiOperation({ summary: 'Reset password using token' }) - @ApiParam({ name: 'token', description: 'Reset password token' }) - @ApiBody({ - schema: { - type: 'object', - properties: { - password: { type: 'string', example: 'new-password' }, - }, - }, - }) + @ApiParam({ name: 'token', description: 'Reset password token from email link' }) + @ApiBody({ type: AuthResetPasswordDto }) + @ApiResponse({ status: 200, description: 'Password reset successfully.' }) resetPassword( @Param('token') token: string, - @Body('password') password: string, + @Body() body: AuthResetPasswordDto, ) { - return this.authApp.resetPassword(token, password); + return this.authApp.resetPassword(token, body.password); } @Get('/meta') - meta() { + @ApiOperation({ summary: 'Get auth metadata (e.g. signup disabled)' }) + @ApiResponse({ + status: 200, + description: 'Auth metadata for the login/signup page.', + schema: { $ref: getSchemaPath(AuthMetaResponseDto) }, + }) + meta(): Promise { return this.authApp.getAuthMeta(); } } diff --git a/packages/server/src/modules/Auth/dtos/AuthMetaResponse.dto.ts b/packages/server/src/modules/Auth/dtos/AuthMetaResponse.dto.ts new file mode 100644 index 000000000..b41624025 --- /dev/null +++ b/packages/server/src/modules/Auth/dtos/AuthMetaResponse.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AuthMetaResponseDto { + @ApiProperty({ description: 'Whether signup is disabled' }) + signupDisabled: boolean; +} diff --git a/packages/server/src/modules/Auth/dtos/AuthResetPassword.dto.ts b/packages/server/src/modules/Auth/dtos/AuthResetPassword.dto.ts new file mode 100644 index 000000000..273ac7396 --- /dev/null +++ b/packages/server/src/modules/Auth/dtos/AuthResetPassword.dto.ts @@ -0,0 +1,12 @@ +import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class AuthResetPasswordDto { + @ApiProperty({ + example: 'new-password', + description: 'New password', + }) + @IsNotEmpty() + @IsString() + password: string; +} diff --git a/packages/server/src/modules/Auth/dtos/AuthSendResetPassword.dto.ts b/packages/server/src/modules/Auth/dtos/AuthSendResetPassword.dto.ts new file mode 100644 index 000000000..dfad60c1b --- /dev/null +++ b/packages/server/src/modules/Auth/dtos/AuthSendResetPassword.dto.ts @@ -0,0 +1,12 @@ +import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class AuthSendResetPasswordDto { + @ApiProperty({ + example: 'user@example.com', + description: 'User email address to send reset link to', + }) + @IsNotEmpty() + @IsString() + email: string; +} diff --git a/packages/server/src/modules/Auth/dtos/AuthSigninResponse.dto.ts b/packages/server/src/modules/Auth/dtos/AuthSigninResponse.dto.ts new file mode 100644 index 000000000..615dc6eb6 --- /dev/null +++ b/packages/server/src/modules/Auth/dtos/AuthSigninResponse.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AuthSigninResponseDto { + @ApiProperty({ description: 'JWT access token' }) + accessToken: string; + + @ApiProperty({ description: 'Organization ID' }) + organizationId: string; + + @ApiProperty({ description: 'Tenant ID' }) + tenantId: number; + + @ApiProperty({ description: 'User ID' }) + userId: number; +} diff --git a/packages/server/src/modules/Auth/dtos/AuthSignupVerify.dto.ts b/packages/server/src/modules/Auth/dtos/AuthSignupVerify.dto.ts new file mode 100644 index 000000000..07774e0a5 --- /dev/null +++ b/packages/server/src/modules/Auth/dtos/AuthSignupVerify.dto.ts @@ -0,0 +1,20 @@ +import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class AuthSignupVerifyDto { + @ApiProperty({ + example: 'user@example.com', + description: 'User email address', + }) + @IsNotEmpty() + @IsString() + email: string; + + @ApiProperty({ + example: 'confirmation-token', + description: 'Signup confirmation token from email', + }) + @IsNotEmpty() + @IsString() + token: string; +} diff --git a/packages/server/src/modules/BankingCategorize/BankingCategorize.controller.ts b/packages/server/src/modules/BankingCategorize/BankingCategorize.controller.ts index 400208851..b60aa8c43 100644 --- a/packages/server/src/modules/BankingCategorize/BankingCategorize.controller.ts +++ b/packages/server/src/modules/BankingCategorize/BankingCategorize.controller.ts @@ -2,7 +2,12 @@ import { Body, Controller, Delete, Param, Post, Query } from '@nestjs/common'; import { castArray, omit } from 'lodash'; import { BankingCategorizeApplication } from './BankingCategorize.application'; import { CategorizeBankTransactionRouteDto } from './dtos/CategorizeBankTransaction.dto'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; @Controller('banking/categorize') @@ -29,16 +34,27 @@ export class BankingCategorizeController { } @Delete('/bulk') - @ApiOperation({ summary: 'Uncategorize bank transactions.' }) + @ApiOperation({ summary: 'Uncategorize bank transactions in bulk.' }) + @ApiQuery({ + name: 'uncategorizedTransactionIds', + required: true, + type: [Number], + isArray: true, + description: 'Array of uncategorized transaction IDs to uncategorize', + }) @ApiResponse({ status: 200, description: 'The bank transactions have been uncategorized successfully.', }) public uncategorizeTransactionsBulk( - @Query() uncategorizedTransactionIds: number[] | number, + @Query('uncategorizedTransactionIds') + uncategorizedTransactionIds: number[] | number, ) { + const ids = castArray(uncategorizedTransactionIds).map((id) => + Number(id), + ); return this.bankingCategorizeApplication.uncategorizeTransactionsBulk( - castArray(uncategorizedTransactionIds), + ids, ); } diff --git a/packages/server/src/modules/BankingMatching/BankingMatching.controller.ts b/packages/server/src/modules/BankingMatching/BankingMatching.controller.ts index d4d0f34b0..51f674934 100644 --- a/packages/server/src/modules/BankingMatching/BankingMatching.controller.ts +++ b/packages/server/src/modules/BankingMatching/BankingMatching.controller.ts @@ -1,12 +1,21 @@ -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { + ApiExtraModels, + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, + getSchemaPath, +} from '@nestjs/swagger'; import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common'; import { BankingMatchingApplication } from './BankingMatchingApplication'; -import { GetMatchedTransactionsFilter } from './types'; import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto'; +import { GetMatchedTransactionsQueryDto } from './dtos/GetMatchedTransactionsQuery.dto'; +import { GetMatchedTransactionsResponseDto } from './dtos/GetMatchedTransactionsResponse.dto'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; @Controller('banking/matching') @ApiTags('Banking Transactions Matching') +@ApiExtraModels(GetMatchedTransactionsResponseDto) @ApiCommonHeaders() export class BankingMatchingController { constructor( @@ -15,13 +24,25 @@ export class BankingMatchingController { @Get('matched') @ApiOperation({ summary: 'Retrieves the matched transactions.' }) + @ApiQuery({ + name: 'uncategorizedTransactionIds', + required: true, + type: [Number], + isArray: true, + description: 'Uncategorized transaction IDs to match', + }) + @ApiResponse({ + status: 200, + description: 'Matched transactions (perfect and possible matches).', + schema: { $ref: getSchemaPath(GetMatchedTransactionsResponseDto) }, + }) async getMatchedTransactions( @Query('uncategorizedTransactionIds') uncategorizedTransactionIds: number[], - @Query() filter: GetMatchedTransactionsFilter, + @Query() filter: GetMatchedTransactionsQueryDto, ) { return this.bankingMatchingApplication.getMatchedTransactions( - uncategorizedTransactionIds, - filter, + uncategorizedTransactionIds ?? [], + filter as any, ); } diff --git a/packages/server/src/modules/BankingMatching/dtos/GetMatchedTransactionsQuery.dto.ts b/packages/server/src/modules/BankingMatching/dtos/GetMatchedTransactionsQuery.dto.ts new file mode 100644 index 000000000..9c6163807 --- /dev/null +++ b/packages/server/src/modules/BankingMatching/dtos/GetMatchedTransactionsQuery.dto.ts @@ -0,0 +1,37 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, IsNumber, Min, Max } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class GetMatchedTransactionsQueryDto { + @ApiPropertyOptional({ description: 'Filter from date', example: '2024-01-01' }) + @IsOptional() + @IsString() + fromDate?: string; + + @ApiPropertyOptional({ description: 'Filter to date', example: '2024-12-31' }) + @IsOptional() + @IsString() + toDate?: string; + + @ApiPropertyOptional({ description: 'Minimum amount', example: 0 }) + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + minAmount?: number; + + @ApiPropertyOptional({ description: 'Maximum amount', example: 10000 }) + @IsOptional() + @Type(() => Number) + @IsNumber() + @Max(Number.MAX_SAFE_INTEGER) + maxAmount?: number; + + @ApiPropertyOptional({ + description: 'Transaction type filter', + example: 'SaleInvoice', + }) + @IsOptional() + @IsString() + transactionType?: string; +} diff --git a/packages/server/src/modules/BankingMatching/dtos/GetMatchedTransactionsResponse.dto.ts b/packages/server/src/modules/BankingMatching/dtos/GetMatchedTransactionsResponse.dto.ts new file mode 100644 index 000000000..861e05658 --- /dev/null +++ b/packages/server/src/modules/BankingMatching/dtos/GetMatchedTransactionsResponse.dto.ts @@ -0,0 +1,44 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class MatchedTransactionItemDto { + @ApiProperty({ description: 'Transaction amount', example: 100.5 }) + amount: number; + + @ApiProperty({ description: 'Formatted amount', example: '$100.50' }) + amountFormatted: string; + + @ApiProperty({ description: 'Transaction date', example: '2024-01-15' }) + date: string; + + @ApiProperty({ description: 'Formatted date', example: 'Jan 15, 2024' }) + dateFormatted: string; + + @ApiProperty({ description: 'Reference number', example: 'REF-001' }) + referenceNo: string; + + @ApiProperty({ description: 'Transaction number', example: 'TXN-001' }) + transactionNo: string; + + @ApiProperty({ description: 'Transaction ID', example: 1 }) + transactionId: number; + + @ApiProperty({ description: 'Transaction type', example: 'SaleInvoice' }) + transactionType: string; +} + +export class GetMatchedTransactionsResponseDto { + @ApiProperty({ + description: 'Perfect matches (amount and date match)', + type: [MatchedTransactionItemDto], + }) + perfectMatches: MatchedTransactionItemDto[]; + + @ApiProperty({ + description: 'Possible matches (candidates)', + type: [MatchedTransactionItemDto], + }) + possibleMatches: MatchedTransactionItemDto[]; + + @ApiProperty({ description: 'Total pending amount', example: 500 }) + totalPending: number; +} diff --git a/packages/server/src/modules/BankingMatching/dtos/MatchBankTransaction.dto.ts b/packages/server/src/modules/BankingMatching/dtos/MatchBankTransaction.dto.ts index 0a9e973cf..306073d14 100644 --- a/packages/server/src/modules/BankingMatching/dtos/MatchBankTransaction.dto.ts +++ b/packages/server/src/modules/BankingMatching/dtos/MatchBankTransaction.dto.ts @@ -30,7 +30,12 @@ export class MatchTransactionEntryDto { export class MatchBankTransactionDto { @IsArray() @ArrayMinSize(1) - uncategorizedTransactions: Array + @ApiProperty({ + description: 'Uncategorized transaction IDs to match', + type: [Number], + example: [1, 2], + }) + uncategorizedTransactions: Array; @IsArray() @ValidateNested({ each: true }) diff --git a/packages/server/src/modules/BankingTransactions/controllers/BankingUncategorizedTransactions.controller.ts b/packages/server/src/modules/BankingTransactions/controllers/BankingUncategorizedTransactions.controller.ts index 2d50bf66b..3a6e4b727 100644 --- a/packages/server/src/modules/BankingTransactions/controllers/BankingUncategorizedTransactions.controller.ts +++ b/packages/server/src/modules/BankingTransactions/controllers/BankingUncategorizedTransactions.controller.ts @@ -5,13 +5,17 @@ import { ApiResponse, ApiParam, ApiQuery, + ApiExtraModels, + getSchemaPath, } from '@nestjs/swagger'; import { GetUncategorizedTransactionsQueryDto } from '../dtos/GetUncategorizedTransactionsQuery.dto'; +import { GetAutofillCategorizeTransactionResponseDto } from '../dtos/GetAutofillCategorizeTransactionResponse.dto'; import { BankingTransactionsApplication } from '../BankingTransactionsApplication.service'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; @Controller('banking/uncategorized') @ApiTags('Banking Uncategorized Transactions') +@ApiExtraModels(GetAutofillCategorizeTransactionResponseDto) @ApiCommonHeaders() export class BankingUncategorizedTransactionsController { constructor( @@ -20,29 +24,29 @@ export class BankingUncategorizedTransactionsController { @Get('autofill') @ApiOperation({ summary: 'Get autofill values for categorize transactions' }) + @ApiQuery({ + name: 'uncategorizedTransactionIds', + required: true, + type: [Number], + isArray: true, + description: 'Uncategorized transaction IDs to get autofill for', + }) @ApiResponse({ status: 200, description: 'Returns autofill values for categorize transactions', - }) - @ApiParam({ - name: 'accountId', - required: true, - type: Number, - description: 'Bank account ID', - }) - @ApiQuery({ - name: 'uncategorizeTransactionsId', - required: true, - type: Number, - description: 'Uncategorize transactions ID', + schema: { $ref: getSchemaPath(GetAutofillCategorizeTransactionResponseDto) }, }) async getAutofillCategorizeTransaction( @Query('uncategorizedTransactionIds') uncategorizedTransactionIds: Array | number, ) { - console.log(uncategorizedTransactionIds); + const ids = Array.isArray(uncategorizedTransactionIds) + ? uncategorizedTransactionIds + : uncategorizedTransactionIds != null + ? [uncategorizedTransactionIds] + : []; return this.bankingTransactionsApplication.getAutofillCategorizeTransaction( - uncategorizedTransactionIds, + ids, ); } diff --git a/packages/server/src/modules/BankingTransactions/dtos/GetAutofillCategorizeTransactionResponse.dto.ts b/packages/server/src/modules/BankingTransactions/dtos/GetAutofillCategorizeTransactionResponse.dto.ts new file mode 100644 index 000000000..031f1017e --- /dev/null +++ b/packages/server/src/modules/BankingTransactions/dtos/GetAutofillCategorizeTransactionResponse.dto.ts @@ -0,0 +1,54 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class GetAutofillCategorizeTransactionResponseDto { + @ApiPropertyOptional({ + description: 'Assigned credit/debit account ID from recognition', + example: 10, + }) + creditAccountId?: number | null; + + @ApiPropertyOptional({ + description: 'Bank account ID (debit)', + example: 5, + }) + debitAccountId?: number | null; + + @ApiProperty({ description: 'Total amount of uncategorized transactions', example: -150.5 }) + amount: number; + + @ApiProperty({ description: 'Formatted amount', example: '$150.50' }) + formattedAmount: string; + + @ApiProperty({ description: 'Transaction date', example: '2024-01-15' }) + date: string; + + @ApiProperty({ description: 'Formatted date', example: 'Jan 15, 2024' }) + formattedDate: string; + + @ApiProperty({ description: 'Whether the transaction is recognized by a rule', example: true }) + isRecognized: boolean; + + @ApiPropertyOptional({ description: 'Bank rule ID that recognized the transaction', example: 1 }) + recognizedByRuleId?: number | null; + + @ApiPropertyOptional({ description: 'Bank rule name that recognized the transaction', example: 'Salary Rule' }) + recognizedByRuleName?: string | null; + + @ApiPropertyOptional({ description: 'Reference number', example: 'REF-001' }) + referenceNo?: string | null; + + @ApiProperty({ description: 'Transaction type (category)', example: 'other_expense' }) + transactionType: string; + + @ApiProperty({ description: 'Whether this is a deposit transaction', example: false }) + isDepositTransaction: boolean; + + @ApiProperty({ description: 'Whether this is a withdrawal transaction', example: true }) + isWithdrawalTransaction: boolean; + + @ApiPropertyOptional({ description: 'Assigned payee from recognition' }) + payee?: string | null; + + @ApiPropertyOptional({ description: 'Assigned memo from recognition' }) + memo?: string | null; +} diff --git a/packages/server/src/modules/BankingTransactionsExclude/BankingTransactionsExclude.controller.ts b/packages/server/src/modules/BankingTransactionsExclude/BankingTransactionsExclude.controller.ts index f0a040995..5cae8fe6e 100644 --- a/packages/server/src/modules/BankingTransactionsExclude/BankingTransactionsExclude.controller.ts +++ b/packages/server/src/modules/BankingTransactionsExclude/BankingTransactionsExclude.controller.ts @@ -4,12 +4,10 @@ import { Delete, Get, Param, - Post, Put, Query, } from '@nestjs/common'; import { ExcludeBankTransactionsApplication } from './ExcludeBankTransactionsApplication'; -import { ExcludedBankTransactionsQuery } from './types/BankTransactionsExclude.types'; import { ApiExtraModels, ApiOperation, @@ -18,11 +16,13 @@ import { getSchemaPath, } from '@nestjs/swagger'; import { GetExcludedBankTransactionResponseDto } from './dtos/GetExcludedBankTransactionResponse.dto'; +import { ExcludeBankTransactionsBulkDto } from './dtos/ExcludeBankTransactionsBulk.dto'; +import { GetExcludedBankTransactionsQueryDto } from './dtos/GetExcludedBankTransactionsQuery.dto'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; @Controller('banking/exclude') @ApiTags('Banking Transactions') -@ApiExtraModels(GetExcludedBankTransactionResponseDto) +@ApiExtraModels(GetExcludedBankTransactionResponseDto, ExcludeBankTransactionsBulkDto) @ApiCommonHeaders() export class BankingTransactionsExcludeController { constructor( @@ -31,15 +31,19 @@ export class BankingTransactionsExcludeController { @Put('bulk') @ApiOperation({ summary: 'Exclude the given bank transactions.' }) - public excludeBankTransactions(@Body('ids') ids: number[]) { - return this.excludeBankTransactionsApplication.excludeBankTransactions(ids); + @ApiResponse({ status: 200, description: 'Bank transactions excluded successfully.' }) + public excludeBankTransactions(@Body() body: ExcludeBankTransactionsBulkDto) { + return this.excludeBankTransactionsApplication.excludeBankTransactions( + body.ids, + ); } @Delete('bulk') @ApiOperation({ summary: 'Unexclude the given bank transactions.' }) - public unexcludeBankTransactions(@Body('ids') ids: number[]) { + @ApiResponse({ status: 200, description: 'Bank transactions unexcluded successfully.' }) + public unexcludeBankTransactions(@Body() body: ExcludeBankTransactionsBulkDto) { return this.excludeBankTransactionsApplication.unexcludeBankTransactions( - ids, + body.ids, ); } @@ -57,10 +61,10 @@ export class BankingTransactionsExcludeController { }, }) public getExcludedBankTransactions( - @Query() query: ExcludedBankTransactionsQuery, + @Query() query: GetExcludedBankTransactionsQueryDto, ) { return this.excludeBankTransactionsApplication.getExcludedBankTransactions( - query, + query as any, ); } diff --git a/packages/server/src/modules/BankingTransactionsExclude/dtos/ExcludeBankTransactionsBulk.dto.ts b/packages/server/src/modules/BankingTransactionsExclude/dtos/ExcludeBankTransactionsBulk.dto.ts new file mode 100644 index 000000000..85671632f --- /dev/null +++ b/packages/server/src/modules/BankingTransactionsExclude/dtos/ExcludeBankTransactionsBulk.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsNumber, ArrayMinSize } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class ExcludeBankTransactionsBulkDto { + @ApiProperty({ + description: 'IDs of uncategorized bank transactions to exclude or unexclude', + type: [Number], + example: [1, 2, 3], + }) + @IsArray() + @ArrayMinSize(1) + @IsNumber({}, { each: true }) + @Type(() => Number) + ids: number[]; +} diff --git a/packages/server/src/modules/BankingTransactionsExclude/dtos/GetExcludedBankTransactionsQuery.dto.ts b/packages/server/src/modules/BankingTransactionsExclude/dtos/GetExcludedBankTransactionsQuery.dto.ts new file mode 100644 index 000000000..a6abd165b --- /dev/null +++ b/packages/server/src/modules/BankingTransactionsExclude/dtos/GetExcludedBankTransactionsQuery.dto.ts @@ -0,0 +1,45 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsNumber, IsDateString } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class GetExcludedBankTransactionsQueryDto { + @ApiPropertyOptional({ description: 'Page number', example: 1 }) + @IsOptional() + @Type(() => Number) + @IsNumber() + page?: number; + + @ApiPropertyOptional({ description: 'Page size', example: 25 }) + @IsOptional() + @Type(() => Number) + @IsNumber() + pageSize?: number; + + @ApiPropertyOptional({ description: 'Filter by bank account ID', example: 1 }) + @IsOptional() + @Type(() => Number) + @IsNumber() + accountId?: number; + + @ApiPropertyOptional({ description: 'Minimum date (ISO)', example: '2024-01-01' }) + @IsOptional() + @IsDateString() + minDate?: string; + + @ApiPropertyOptional({ description: 'Maximum date (ISO)', example: '2024-12-31' }) + @IsOptional() + @IsDateString() + maxDate?: string; + + @ApiPropertyOptional({ description: 'Minimum amount', example: 0 }) + @IsOptional() + @Type(() => Number) + @IsNumber() + minAmount?: number; + + @ApiPropertyOptional({ description: 'Maximum amount', example: 10000 }) + @IsOptional() + @Type(() => Number) + @IsNumber() + maxAmount?: number; +} diff --git a/packages/server/src/modules/Bills/Bills.controller.ts b/packages/server/src/modules/Bills/Bills.controller.ts index 26253be7e..bc61359da 100644 --- a/packages/server/src/modules/Bills/Bills.controller.ts +++ b/packages/server/src/modules/Bills/Bills.controller.ts @@ -2,6 +2,7 @@ import { ApiExtraModels, ApiOperation, ApiParam, + ApiQuery, ApiResponse, ApiTags, getSchemaPath, @@ -155,6 +156,10 @@ export class BillsController { type: Number, description: 'The bill id', }) + @ApiResponse({ + status: 200, + description: 'List of payment transactions for the bill.', + }) getBillPaymentTransactions(@Param('id') billId: number) { return this.billsApplication.getBillPaymentTransactions(billId); } @@ -195,7 +200,17 @@ export class BillsController { @Get('due') @RequirePermission(BillAction.View, AbilitySubject.Bill) @ApiOperation({ summary: 'Retrieves the due bills.' }) - getDueBills(@Body('vendorId') vendorId?: number) { + @ApiQuery({ + name: 'vendor_id', + required: false, + type: Number, + description: 'Filter due bills by vendor ID.', + }) + @ApiResponse({ + status: 200, + description: 'List of due bills (optionally filtered by vendor).', + }) + getDueBills(@Query('vendor_id') vendorId?: number) { return this.billsApplication.getDueBills(vendorId); } } diff --git a/packages/server/src/modules/Contacts/Contacts.controller.ts b/packages/server/src/modules/Contacts/Contacts.controller.ts index 6d035cacd..6078a25bf 100644 --- a/packages/server/src/modules/Contacts/Contacts.controller.ts +++ b/packages/server/src/modules/Contacts/Contacts.controller.ts @@ -6,9 +6,10 @@ import { Patch, ParseIntPipe, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiParam } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { GetContactsAutoCompleteQuery } from './dtos/GetContactsAutoCompleteQuery.dto'; import { GetAutoCompleteContactsService } from './queries/GetAutoCompleteContacts.service'; +import { GetContactService } from './queries/GetContact.service'; import { ActivateContactService } from './commands/ActivateContact.service'; import { InactivateContactService } from './commands/InactivateContact.service'; @@ -17,6 +18,7 @@ import { InactivateContactService } from './commands/InactivateContact.service'; export class ContactsController { constructor( private readonly getAutoCompleteService: GetAutoCompleteContactsService, + private readonly getContactService: GetContactService, private readonly activateContactService: ActivateContactService, private readonly inactivateContactService: InactivateContactService, ) {} @@ -27,6 +29,14 @@ export class ContactsController { return this.getAutoCompleteService.autocompleteContacts(query); } + @Get(':id') + @ApiOperation({ summary: 'Get contact by ID (customer or vendor)' }) + @ApiParam({ name: 'id', type: Number, description: 'Contact ID' }) + @ApiResponse({ status: 200, description: 'Contact details (under "customer" key for form/duplicate use)' }) + getContact(@Param('id', ParseIntPipe) contactId: number) { + return this.getContactService.getContact(contactId); + } + @Patch(':id/activate') @ApiOperation({ summary: 'Activate a contact' }) @ApiParam({ name: 'id', type: 'number', description: 'Contact ID' }) diff --git a/packages/server/src/modules/Contacts/Contacts.module.ts b/packages/server/src/modules/Contacts/Contacts.module.ts index ce7690319..34d5c2043 100644 --- a/packages/server/src/modules/Contacts/Contacts.module.ts +++ b/packages/server/src/modules/Contacts/Contacts.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { GetAutoCompleteContactsService } from './queries/GetAutoCompleteContacts.service'; +import { GetContactService } from './queries/GetContact.service'; import { ContactsController } from './Contacts.controller'; import { ActivateContactService } from './commands/ActivateContact.service'; import { InactivateContactService } from './commands/InactivateContact.service'; @@ -7,6 +8,7 @@ import { InactivateContactService } from './commands/InactivateContact.service'; @Module({ providers: [ GetAutoCompleteContactsService, + GetContactService, ActivateContactService, InactivateContactService, ], diff --git a/packages/server/src/modules/Contacts/queries/GetContact.service.ts b/packages/server/src/modules/Contacts/queries/GetContact.service.ts new file mode 100644 index 000000000..d82aa26df --- /dev/null +++ b/packages/server/src/modules/Contacts/queries/GetContact.service.ts @@ -0,0 +1,27 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Contact } from '../models/Contact'; +import { ContactTransfromer } from '../Contact.transformer'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; + +@Injectable() +export class GetContactService { + constructor( + private readonly transformer: TransformerInjectable, + @Inject(Contact.name) + private readonly contactModel: TenantModelProxy, + ) {} + + /** + * Retrieve contact by id (customer or vendor). + * Returns transformed contact for duplicate/form use. + */ + async getContact(contactId: number): Promise> { + const contact = await this.contactModel() + .query() + .findById(contactId) + .throwIfNotFound(); + + return this.transformer.transform(contact, new ContactTransfromer()); + } +} diff --git a/packages/webapp/package.json b/packages/webapp/package.json index 8172fc1a9..cbf123d42 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -5,6 +5,7 @@ "dependencies": { "@bigcapital/email-components": "workspace:*", "@bigcapital/pdf-templates": "workspace:*", + "@bigcapital/sdk-ts": "workspace:*", "@bigcapital/utils": "workspace:*", "@blueprintjs-formik/core": "^0.3.7", "@blueprintjs-formik/datetime": "^0.4.0", @@ -93,8 +94,8 @@ "react-intl-universal": "^2.4.7", "react-loadable": "^5.5.0", "react-plaid-link": "^3.2.1", - "react-query": "^3.6.0", - "react-query-devtools": "^2.1.1", + "@tanstack/react-query": "^5.62.0", + "@tanstack/react-query-devtools": "^5.62.0", "react-redux": "^7.2.9", "react-router": "5.3.4", "react-router-breadcrumbs-hoc": "^3.2.10", diff --git a/shared/sdk-ts/openapi.json b/shared/sdk-ts/openapi.json index f1b132381..6020d0f51 100644 --- a/shared/sdk-ts/openapi.json +++ b/shared/sdk-ts/openapi.json @@ -35933,6 +35933,31 @@ ] } }, + "/api/contacts/{id}": { + "get": { + "operationId": "ContactsController_getContact", + "summary": "Get contact by ID (customer or vendor)", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Contact ID", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Contact details (under \"customer\" key for form/duplicate use)" + } + }, + "tags": [ + "Contacts" + ] + } + }, "/api/contacts/auto-complete": { "get": { "operationId": "ContactsController_getAutoComplete", diff --git a/shared/sdk-ts/src/bank-rules.ts b/shared/sdk-ts/src/bank-rules.ts index a728a93f9..181d41132 100644 --- a/shared/sdk-ts/src/bank-rules.ts +++ b/shared/sdk-ts/src/bank-rules.ts @@ -1,6 +1,6 @@ import type { ApiFetcher } from './fetch-utils'; import { paths } from './schema'; -import { OpForPath, OpRequestBody, OpResponseBody } from './utils'; +import { OpForPath, OpQueryParams, OpRequestBody, OpResponseBody } from './utils'; export const BANK_RULES_ROUTES = { RULES: '/api/banking/rules', @@ -19,6 +19,7 @@ export const BANK_RULES_ROUTES = { RECOGNIZED_LIST: '/api/banking/recognized', PENDING: '/api/banking/pending', UNCATEGORIZED_AUTOFILL: '/api/banking/uncategorized/autofill', + CATEGORIZE_BULK: '/api/banking/categorize/bulk', } as const satisfies Record; export type BankRulesListResponse = OpResponseBody>; @@ -27,9 +28,36 @@ export type CreateBankRuleBody = OpRequestBody>; export type CreateBankRuleResponse = OpResponseBody>; -/** Path params for pause/resume bank account (id = bankAccountId). */ +/** Path params for pause/resume/disconnect/refresh bank account (id = bankAccountId). */ export type PauseBankAccountParams = OpForPath extends { parameters: { path: infer P } } ? P : never; export type ResumeBankAccountParams = OpForPath extends { parameters: { path: infer P } } ? P : never; +export type DisconnectBankAccountParams = OpForPath extends { parameters: { path: infer P } } ? P : never; +export type RefreshBankAccountParams = OpForPath extends { parameters: { path: infer P } } ? P : never; +export type UnmatchMatchedTransactionParams = OpForPath extends { parameters: { path: infer P } } ? P : never; + +/** Response for GET /api/banking/matching/matched (from server GetMatchedTransactionsResponseDto). */ +export type MatchedTransactionsResponse = OpResponseBody>; + +/** Query params for GET /api/banking/matching/matched. */ +export type GetMatchedTransactionsQuery = OpQueryParams>; + +/** Body for POST /api/banking/matching/match (use referenceType, referenceId - camelCase). */ +export type MatchTransactionBody = OpRequestBody>; + +/** Body for PUT/DELETE /api/banking/exclude/bulk (from server ExcludeBankTransactionsBulkDto). */ +export type ExcludeBankTransactionsBulkBody = OpRequestBody>; + +/** Query params for GET /api/banking/exclude. */ +export type GetExcludedBankTransactionsQuery = OpQueryParams>; + +/** Query params for GET /api/banking/pending. */ +export type GetPendingTransactionsQuery = OpQueryParams>; + +/** Response for GET /api/banking/uncategorized/autofill (from server GetAutofillCategorizeTransactionResponseDto). */ +export type AutofillCategorizeTransactionResponse = OpResponseBody>; + +/** Response for GET /api/banking/recognized (single). */ +export type RecognizedTransactionResponse = OpResponseBody>; export async function fetchBankRules(fetcher: ApiFetcher): Promise { const get = fetcher.path(BANK_RULES_ROUTES.RULES).method('get').create(); @@ -115,28 +143,24 @@ export async function resumeBankAccount( export async function fetchMatchedTransactions( fetcher: ApiFetcher, - uncategorizedTransactionIds: number[] -): Promise { + uncategorizedTransactionIds: number[], + query?: GetMatchedTransactionsQuery +): Promise { const get = fetcher .path(BANK_RULES_ROUTES.MATCHING_MATCHED) .method('get') .create(); const ids = uncategorizedTransactionIds.map(String); - const { data } = await get({ uncategorizedTransactionIds: ids }); + const { data } = await get({ uncategorizedTransactionIds: ids, ...query }); return data; } -export type MatchTransactionBody = { - uncategorizedTransactions: number[]; - matchedTransactions: Array<{ reference_type: string; reference_id: number }>; -}; - export async function matchTransaction( fetcher: ApiFetcher, body: MatchTransactionBody ): Promise { const post = fetcher.path(BANK_RULES_ROUTES.MATCHING_MATCH).method('post').create(); - await (post as (body: unknown) => Promise)(body); + await post(body); } export async function unmatchMatchedTransaction( @@ -168,24 +192,24 @@ export async function unexcludeBankTransaction( export async function excludeBankTransactionsBulk( fetcher: ApiFetcher, - ids: Array + body: ExcludeBankTransactionsBulkBody ): Promise { const put = fetcher.path(BANK_RULES_ROUTES.EXCLUDE_BULK).method('put').create(); - await (put as (body?: { ids?: unknown[] }) => Promise)({ ids }); + await put(body); } export async function unexcludeBankTransactionsBulk( fetcher: ApiFetcher, - ids: Array + body: ExcludeBankTransactionsBulkBody ): Promise { const del = fetcher.path(BANK_RULES_ROUTES.EXCLUDE_BULK).method('delete').create(); - await (del as (body?: { ids?: unknown[] }) => Promise)({ ids }); + await del(body); } export async function fetchRecognizedTransaction( fetcher: ApiFetcher, recognizedTransactionId: number -): Promise { +): Promise { const get = fetcher.path(BANK_RULES_ROUTES.RECOGNIZED).method('get').create(); const { data } = await get({ recognizedTransactionId }); return data; @@ -196,44 +220,57 @@ export async function fetchRecognizedTransactions( params?: Record ): Promise { const get = fetcher.path(BANK_RULES_ROUTES.RECOGNIZED_LIST).method('get').create(); - const { data } = await (get as (q?: Record) => Promise<{ data: unknown }>)( - params ?? {} - ); + const { data } = await get(params ?? {}); return data; } export async function fetchExcludedBankTransactions( fetcher: ApiFetcher, - params?: Record + params?: GetExcludedBankTransactionsQuery ): Promise { const get = fetcher.path(BANK_RULES_ROUTES.EXCLUDED_LIST).method('get').create(); - const { data } = await (get as (q?: Record) => Promise<{ data: unknown }>)( - params ?? {} - ); + const { data } = await get(params ?? {}); return data; } export async function fetchPendingTransactions( fetcher: ApiFetcher, - params?: Record + params?: GetPendingTransactionsQuery ): Promise { const get = fetcher.path(BANK_RULES_ROUTES.PENDING).method('get').create(); - const { data } = await (get as (q?: Record) => Promise<{ data: unknown }>)( - params ?? {} - ); + const { data } = await get(params ?? {}); return data; } export async function fetchAutofillCategorizeTransaction( fetcher: ApiFetcher, uncategorizedTransactionIds: number[] -): Promise { +): Promise { const get = fetcher .path(BANK_RULES_ROUTES.UNCATEGORIZED_AUTOFILL) .method('get') .create(); - const { data } = await (get as (q: unknown) => Promise<{ data: unknown }>)({ + // Server expects uncategorizedTransactionIds (array). Schema types update after openapi regen. + const { data } = await get({ uncategorizedTransactionIds, - }); - return data; + } as never); + return data as AutofillCategorizeTransactionResponse; +} + +/** + * Uncategorize bank transactions in bulk (DELETE /api/banking/categorize/bulk with query uncategorizedTransactionIds). + */ +export async function uncategorizeTransactionsBulk( + fetcher: ApiFetcher, + uncategorizedTransactionIds: number[], +): Promise { + const del = fetcher + .path(BANK_RULES_ROUTES.CATEGORIZE_BULK) + .method('delete') + .create(); + await (del as (params: { + query?: { uncategorizedTransactionIds: number[] }; + }) => Promise)({ + query: { uncategorizedTransactionIds }, + }); } diff --git a/shared/sdk-ts/src/bills.ts b/shared/sdk-ts/src/bills.ts index 14cc92f70..8496c182f 100644 --- a/shared/sdk-ts/src/bills.ts +++ b/shared/sdk-ts/src/bills.ts @@ -17,6 +17,11 @@ export type Bill = OpResponseBody>; export type CreateBillBody = OpRequestBody>; export type EditBillBody = OpRequestBody>; export type GetBillsQuery = OpQueryParams>; +export type BulkDeleteBillsBody = OpRequestBody>; +export type ValidateBulkDeleteBillsResponse = OpResponseBody< + OpForPath +>; +export type GetDueBillsQuery = OpQueryParams>; export async function fetchBills( fetcher: ApiFetcher, @@ -56,3 +61,51 @@ export async function deleteBill(fetcher: ApiFetcher, id: number): Promise const del = fetcher.path(BILLS_ROUTES.BY_ID).method('delete').create(); await del({ id }); } + +export async function openBill(fetcher: ApiFetcher, id: number): Promise { + const patch = fetcher.path(BILLS_ROUTES.OPEN).method('patch').create(); + await patch({ id }); +} + +export async function bulkDeleteBills( + fetcher: ApiFetcher, + body: BulkDeleteBillsBody +): Promise { + const post = fetcher.path(BILLS_ROUTES.BULK_DELETE).method('post').create(); + await post(body); +} + +export async function validateBulkDeleteBills( + fetcher: ApiFetcher, + body: BulkDeleteBillsBody +): Promise { + const post = fetcher + .path(BILLS_ROUTES.VALIDATE_BULK_DELETE) + .method('post') + .create(); + const { data } = await post(body); + return data; +} + +export async function fetchDueBills( + fetcher: ApiFetcher, + query?: GetDueBillsQuery +): Promise { + const get = fetcher.path(BILLS_ROUTES.DUE).method('get').create(); + const { data } = await (get as (params?: GetDueBillsQuery) => Promise<{ data: unknown }>)( + (query ?? {}) as GetDueBillsQuery + ); + return Array.isArray(data) ? data : []; +} + +export async function fetchBillPaymentTransactions( + fetcher: ApiFetcher, + id: number +): Promise { + const get = fetcher + .path(BILLS_ROUTES.PAYMENT_TRANSACTIONS) + .method('get') + .create(); + const { data } = await get({ id }); + return (data as unknown[]) ?? []; +} diff --git a/shared/sdk-ts/src/cashflow-accounts.ts b/shared/sdk-ts/src/cashflow-accounts.ts index 12d53e205..601ec7ce8 100644 --- a/shared/sdk-ts/src/cashflow-accounts.ts +++ b/shared/sdk-ts/src/cashflow-accounts.ts @@ -9,6 +9,13 @@ export const BANKING_ACCOUNTS_ROUTES = { export type BankingAccountsListResponse = OpResponseBody>; +/** Bank account summary response (schema does not define response body). */ +export interface BankingAccountSummaryResponse { + name: string; + totalUncategorizedTransactions: number; + totalRecognizedTransactions: number; +} + export async function fetchBankingAccounts(fetcher: ApiFetcher): Promise { const get = fetcher.path(BANKING_ACCOUNTS_ROUTES.LIST).method('get').create(); const { data } = await get({}); @@ -18,8 +25,8 @@ export async function fetchBankingAccounts(fetcher: ApiFetcher): Promise { +): Promise { const get = fetcher.path(BANKING_ACCOUNTS_ROUTES.SUMMARY).method('get').create(); const { data } = await get({ bankAccountId }); - return data; + return data as BankingAccountSummaryResponse; } diff --git a/shared/sdk-ts/src/contacts.ts b/shared/sdk-ts/src/contacts.ts index 9d1c19d15..f833d245f 100644 --- a/shared/sdk-ts/src/contacts.ts +++ b/shared/sdk-ts/src/contacts.ts @@ -2,14 +2,24 @@ import type { ApiFetcher } from './fetch-utils'; import { paths } from './schema'; import { OpForPath, OpResponseBody } from './utils'; -export const CONTACTS_ROUTES = { +const CONTACTS_ROUTES = { + BY_ID: '/api/contacts/{id}', AUTO_COMPLETE: '/api/contacts/auto-complete', ACTIVATE: '/api/contacts/{id}/activate', INACTIVATE: '/api/contacts/{id}/inactivate', -} as const satisfies Record; +} as const; +export { CONTACTS_ROUTES }; + +export type ContactResponse = OpResponseBody>; export type ContactsAutoCompleteResponse = OpResponseBody>; +export async function fetchContact(fetcher: ApiFetcher, id: number): Promise { + const get = fetcher.path(CONTACTS_ROUTES.BY_ID as keyof paths).method('get').create(); + const { data } = await get({ id }); + return data as ContactResponse; +} + export async function fetchContactsAutoComplete(fetcher: ApiFetcher): Promise { const get = fetcher.path(CONTACTS_ROUTES.AUTO_COMPLETE).method('get').create(); const { data } = await get({}); diff --git a/shared/sdk-ts/src/expenses.ts b/shared/sdk-ts/src/expenses.ts index e0d397d9a..5bf525598 100644 --- a/shared/sdk-ts/src/expenses.ts +++ b/shared/sdk-ts/src/expenses.ts @@ -15,6 +15,12 @@ export type Expense = OpResponseBody>; export type EditExpenseBody = OpRequestBody>; export type GetExpensesQuery = OpQueryParams>; +export type BulkDeleteExpensesBody = OpRequestBody< + OpForPath +>; +export type ValidateBulkDeleteExpensesResponse = OpResponseBody< + OpForPath +>; export async function fetchExpenses( fetcher: ApiFetcher, @@ -59,3 +65,23 @@ export async function publishExpense(fetcher: ApiFetcher, id: number): Promise { + const post = fetcher.path(EXPENSES_ROUTES.BULK_DELETE).method('post').create(); + await post(body); +} + +export async function validateBulkDeleteExpenses( + fetcher: ApiFetcher, + body: BulkDeleteExpensesBody +): Promise { + const post = fetcher + .path(EXPENSES_ROUTES.VALIDATE_BULK_DELETE) + .method('post') + .create(); + const { data } = await post(body); + return data; +} diff --git a/shared/sdk-ts/src/schema.ts b/shared/sdk-ts/src/schema.ts index 339b76c00..3d4d61c05 100644 --- a/shared/sdk-ts/src/schema.ts +++ b/shared/sdk-ts/src/schema.ts @@ -4517,6 +4517,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/contacts/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get contact by ID (customer or vendor) */ + get: operations["ContactsController_getContact"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/contacts/auto-complete": { parameters: { query?: never; @@ -40037,6 +40054,27 @@ export interface operations { }; }; }; + ContactsController_getContact: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Contact ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Contact details (under "customer" key for form/duplicate use) */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; ContactsController_getAutoComplete: { parameters: { query?: never;