1
0

feat(sdk-ts): add authentication fetch utils

This commit is contained in:
Ahmed Bouhuolia
2026-03-05 19:50:38 +02:00
parent 99ae7d7099
commit 4c059d610e
28 changed files with 670 additions and 108 deletions
@@ -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<AuthSigninResponseDto> {
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<AuthMetaResponseDto> {
return this.authApp.getAuthMeta();
}
}
@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class AuthMetaResponseDto {
@ApiProperty({ description: 'Whether signup is disabled' })
signupDisabled: boolean;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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,
);
}
@@ -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,
);
}
@@ -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;
}
@@ -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;
}
@@ -30,7 +30,12 @@ export class MatchTransactionEntryDto {
export class MatchBankTransactionDto {
@IsArray()
@ArrayMinSize(1)
uncategorizedTransactions: Array<number>
@ApiProperty({
description: 'Uncategorized transaction IDs to match',
type: [Number],
example: [1, 2],
})
uncategorizedTransactions: Array<number>;
@IsArray()
@ValidateNested({ each: true })
@@ -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> | number,
) {
console.log(uncategorizedTransactionIds);
const ids = Array.isArray(uncategorizedTransactionIds)
? uncategorizedTransactionIds
: uncategorizedTransactionIds != null
? [uncategorizedTransactionIds]
: [];
return this.bankingTransactionsApplication.getAutofillCategorizeTransaction(
uncategorizedTransactionIds,
ids,
);
}
@@ -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;
}
@@ -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,
);
}
@@ -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[];
}
@@ -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;
}
@@ -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);
}
}
@@ -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' })
@@ -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,
],
@@ -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<typeof Contact>,
) {}
/**
* Retrieve contact by id (customer or vendor).
* Returns transformed contact for duplicate/form use.
*/
async getContact(contactId: number): Promise<Record<string, unknown>> {
const contact = await this.contactModel()
.query()
.findById(contactId)
.throwIfNotFound();
return this.transformer.transform(contact, new ContactTransfromer());
}
}
+3 -2
View File
@@ -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",
+25
View File
@@ -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",
+68 -31
View File
@@ -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<string, keyof paths>;
export type BankRulesListResponse = OpResponseBody<OpForPath<typeof BANK_RULES_ROUTES.RULES, 'get'>>;
@@ -27,9 +28,36 @@ export type CreateBankRuleBody = OpRequestBody<OpForPath<typeof BANK_RULES_ROUTE
export type EditBankRuleBody = OpRequestBody<OpForPath<typeof BANK_RULES_ROUTES.RULE_BY_ID, 'put'>>;
export type CreateBankRuleResponse = OpResponseBody<OpForPath<typeof BANK_RULES_ROUTES.RULES, 'post'>>;
/** Path params for pause/resume bank account (id = bankAccountId). */
/** Path params for pause/resume/disconnect/refresh bank account (id = bankAccountId). */
export type PauseBankAccountParams = OpForPath<typeof BANK_RULES_ROUTES.ACCOUNTS_PAUSE, 'post'> extends { parameters: { path: infer P } } ? P : never;
export type ResumeBankAccountParams = OpForPath<typeof BANK_RULES_ROUTES.ACCOUNTS_RESUME, 'post'> extends { parameters: { path: infer P } } ? P : never;
export type DisconnectBankAccountParams = OpForPath<typeof BANK_RULES_ROUTES.ACCOUNTS_DISCONNECT, 'post'> extends { parameters: { path: infer P } } ? P : never;
export type RefreshBankAccountParams = OpForPath<typeof BANK_RULES_ROUTES.ACCOUNTS_REFRESH, 'post'> extends { parameters: { path: infer P } } ? P : never;
export type UnmatchMatchedTransactionParams = OpForPath<typeof BANK_RULES_ROUTES.MATCHING_UNMATCH, 'patch'> extends { parameters: { path: infer P } } ? P : never;
/** Response for GET /api/banking/matching/matched (from server GetMatchedTransactionsResponseDto). */
export type MatchedTransactionsResponse = OpResponseBody<OpForPath<typeof BANK_RULES_ROUTES.MATCHING_MATCHED, 'get'>>;
/** Query params for GET /api/banking/matching/matched. */
export type GetMatchedTransactionsQuery = OpQueryParams<OpForPath<typeof BANK_RULES_ROUTES.MATCHING_MATCHED, 'get'>>;
/** Body for POST /api/banking/matching/match (use referenceType, referenceId - camelCase). */
export type MatchTransactionBody = OpRequestBody<OpForPath<typeof BANK_RULES_ROUTES.MATCHING_MATCH, 'post'>>;
/** Body for PUT/DELETE /api/banking/exclude/bulk (from server ExcludeBankTransactionsBulkDto). */
export type ExcludeBankTransactionsBulkBody = OpRequestBody<OpForPath<typeof BANK_RULES_ROUTES.EXCLUDE_BULK, 'put'>>;
/** Query params for GET /api/banking/exclude. */
export type GetExcludedBankTransactionsQuery = OpQueryParams<OpForPath<typeof BANK_RULES_ROUTES.EXCLUDED_LIST, 'get'>>;
/** Query params for GET /api/banking/pending. */
export type GetPendingTransactionsQuery = OpQueryParams<OpForPath<typeof BANK_RULES_ROUTES.PENDING, 'get'>>;
/** Response for GET /api/banking/uncategorized/autofill (from server GetAutofillCategorizeTransactionResponseDto). */
export type AutofillCategorizeTransactionResponse = OpResponseBody<OpForPath<typeof BANK_RULES_ROUTES.UNCATEGORIZED_AUTOFILL, 'get'>>;
/** Response for GET /api/banking/recognized (single). */
export type RecognizedTransactionResponse = OpResponseBody<OpForPath<typeof BANK_RULES_ROUTES.RECOGNIZED, 'get'>>;
export async function fetchBankRules(fetcher: ApiFetcher): Promise<BankRulesListResponse> {
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<unknown> {
uncategorizedTransactionIds: number[],
query?: GetMatchedTransactionsQuery
): Promise<MatchedTransactionsResponse> {
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<void> {
const post = fetcher.path(BANK_RULES_ROUTES.MATCHING_MATCH).method('post').create();
await (post as (body: unknown) => Promise<unknown>)(body);
await post(body);
}
export async function unmatchMatchedTransaction(
@@ -168,24 +192,24 @@ export async function unexcludeBankTransaction(
export async function excludeBankTransactionsBulk(
fetcher: ApiFetcher,
ids: Array<number | string>
body: ExcludeBankTransactionsBulkBody
): Promise<void> {
const put = fetcher.path(BANK_RULES_ROUTES.EXCLUDE_BULK).method('put').create();
await (put as (body?: { ids?: unknown[] }) => Promise<unknown>)({ ids });
await put(body);
}
export async function unexcludeBankTransactionsBulk(
fetcher: ApiFetcher,
ids: Array<number | string>
body: ExcludeBankTransactionsBulkBody
): Promise<void> {
const del = fetcher.path(BANK_RULES_ROUTES.EXCLUDE_BULK).method('delete').create();
await (del as (body?: { ids?: unknown[] }) => Promise<unknown>)({ ids });
await del(body);
}
export async function fetchRecognizedTransaction(
fetcher: ApiFetcher,
recognizedTransactionId: number
): Promise<unknown> {
): Promise<RecognizedTransactionResponse> {
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<string, unknown>
): Promise<unknown> {
const get = fetcher.path(BANK_RULES_ROUTES.RECOGNIZED_LIST).method('get').create();
const { data } = await (get as (q?: Record<string, unknown>) => Promise<{ data: unknown }>)(
params ?? {}
);
const { data } = await get(params ?? {});
return data;
}
export async function fetchExcludedBankTransactions(
fetcher: ApiFetcher,
params?: Record<string, unknown>
params?: GetExcludedBankTransactionsQuery
): Promise<unknown> {
const get = fetcher.path(BANK_RULES_ROUTES.EXCLUDED_LIST).method('get').create();
const { data } = await (get as (q?: Record<string, unknown>) => Promise<{ data: unknown }>)(
params ?? {}
);
const { data } = await get(params ?? {});
return data;
}
export async function fetchPendingTransactions(
fetcher: ApiFetcher,
params?: Record<string, unknown>
params?: GetPendingTransactionsQuery
): Promise<unknown> {
const get = fetcher.path(BANK_RULES_ROUTES.PENDING).method('get').create();
const { data } = await (get as (q?: Record<string, unknown>) => Promise<{ data: unknown }>)(
params ?? {}
);
const { data } = await get(params ?? {});
return data;
}
export async function fetchAutofillCategorizeTransaction(
fetcher: ApiFetcher,
uncategorizedTransactionIds: number[]
): Promise<unknown> {
): Promise<AutofillCategorizeTransactionResponse> {
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<void> {
const del = fetcher
.path(BANK_RULES_ROUTES.CATEGORIZE_BULK)
.method('delete')
.create();
await (del as (params: {
query?: { uncategorizedTransactionIds: number[] };
}) => Promise<unknown>)({
query: { uncategorizedTransactionIds },
});
}
+53
View File
@@ -17,6 +17,11 @@ export type Bill = OpResponseBody<OpForPath<typeof BILLS_ROUTES.BY_ID, 'get'>>;
export type CreateBillBody = OpRequestBody<OpForPath<typeof BILLS_ROUTES.LIST, 'post'>>;
export type EditBillBody = OpRequestBody<OpForPath<typeof BILLS_ROUTES.BY_ID, 'put'>>;
export type GetBillsQuery = OpQueryParams<OpForPath<typeof BILLS_ROUTES.LIST, 'get'>>;
export type BulkDeleteBillsBody = OpRequestBody<OpForPath<typeof BILLS_ROUTES.BULK_DELETE, 'post'>>;
export type ValidateBulkDeleteBillsResponse = OpResponseBody<
OpForPath<typeof BILLS_ROUTES.VALIDATE_BULK_DELETE, 'post'>
>;
export type GetDueBillsQuery = OpQueryParams<OpForPath<typeof BILLS_ROUTES.DUE, 'get'>>;
export async function fetchBills(
fetcher: ApiFetcher,
@@ -56,3 +61,51 @@ export async function deleteBill(fetcher: ApiFetcher, id: number): Promise<void>
const del = fetcher.path(BILLS_ROUTES.BY_ID).method('delete').create();
await del({ id });
}
export async function openBill(fetcher: ApiFetcher, id: number): Promise<void> {
const patch = fetcher.path(BILLS_ROUTES.OPEN).method('patch').create();
await patch({ id });
}
export async function bulkDeleteBills(
fetcher: ApiFetcher,
body: BulkDeleteBillsBody
): Promise<void> {
const post = fetcher.path(BILLS_ROUTES.BULK_DELETE).method('post').create();
await post(body);
}
export async function validateBulkDeleteBills(
fetcher: ApiFetcher,
body: BulkDeleteBillsBody
): Promise<ValidateBulkDeleteBillsResponse> {
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<unknown[]> {
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<unknown[]> {
const get = fetcher
.path(BILLS_ROUTES.PAYMENT_TRANSACTIONS)
.method('get')
.create();
const { data } = await get({ id });
return (data as unknown[]) ?? [];
}
+9 -2
View File
@@ -9,6 +9,13 @@ export const BANKING_ACCOUNTS_ROUTES = {
export type BankingAccountsListResponse = OpResponseBody<OpForPath<typeof BANKING_ACCOUNTS_ROUTES.LIST, 'get'>>;
/** 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<BankingAccountsListResponse> {
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<Banking
export async function fetchBankingAccountSummary(
fetcher: ApiFetcher,
bankAccountId: number
): Promise<unknown> {
): Promise<BankingAccountSummaryResponse> {
const get = fetcher.path(BANKING_ACCOUNTS_ROUTES.SUMMARY).method('get').create();
const { data } = await get({ bankAccountId });
return data;
return data as BankingAccountSummaryResponse;
}
+12 -2
View File
@@ -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<string, keyof paths>;
} as const;
export { CONTACTS_ROUTES };
export type ContactResponse = OpResponseBody<OpForPath<typeof CONTACTS_ROUTES.BY_ID, 'get'>>;
export type ContactsAutoCompleteResponse = OpResponseBody<OpForPath<typeof CONTACTS_ROUTES.AUTO_COMPLETE, 'get'>>;
export async function fetchContact(fetcher: ApiFetcher, id: number): Promise<ContactResponse> {
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<ContactsAutoCompleteResponse> {
const get = fetcher.path(CONTACTS_ROUTES.AUTO_COMPLETE).method('get').create();
const { data } = await get({});
+26
View File
@@ -15,6 +15,12 @@ export type Expense = OpResponseBody<OpForPath<typeof EXPENSES_ROUTES.BY_ID, 'ge
export type CreateExpenseBody = OpRequestBody<OpForPath<typeof EXPENSES_ROUTES.LIST, 'post'>>;
export type EditExpenseBody = OpRequestBody<OpForPath<typeof EXPENSES_ROUTES.BY_ID, 'put'>>;
export type GetExpensesQuery = OpQueryParams<OpForPath<typeof EXPENSES_ROUTES.LIST, 'get'>>;
export type BulkDeleteExpensesBody = OpRequestBody<
OpForPath<typeof EXPENSES_ROUTES.BULK_DELETE, 'post'>
>;
export type ValidateBulkDeleteExpensesResponse = OpResponseBody<
OpForPath<typeof EXPENSES_ROUTES.VALIDATE_BULK_DELETE, 'post'>
>;
export async function fetchExpenses(
fetcher: ApiFetcher,
@@ -59,3 +65,23 @@ export async function publishExpense(fetcher: ApiFetcher, id: number): Promise<v
const post = fetcher.path(EXPENSES_ROUTES.PUBLISH).method('post').create();
await post({ id });
}
export async function bulkDeleteExpenses(
fetcher: ApiFetcher,
body: BulkDeleteExpensesBody
): Promise<void> {
const post = fetcher.path(EXPENSES_ROUTES.BULK_DELETE).method('post').create();
await post(body);
}
export async function validateBulkDeleteExpenses(
fetcher: ApiFetcher,
body: BulkDeleteExpensesBody
): Promise<ValidateBulkDeleteExpensesResponse> {
const post = fetcher
.path(EXPENSES_ROUTES.VALIDATE_BULK_DELETE)
.method('post')
.create();
const { data } = await post(body);
return data;
}
+38
View File
@@ -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;