Compare commits

..

18 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
a96ced10db Merge pull request #958 from bigcapitalhq/premissions-guard
fix(server): permissions guard for read and write endpoints
2026-02-15 22:58:01 +02:00
Ahmed Bouhuolia
2d39e38578 fix(server): premissions guard for read and write endpoints 2026-02-15 22:55:10 +02:00
Ahmed Bouhuolia
af80afcf59 Merge pull request #955 from bigcapitalhq/fix/user-invite-email
fix: user invite email not sending and null variables
2026-02-14 00:34:08 +02:00
Ahmed Bouhuolia
66cb0521e5 fix: user invite email not sending and null variables
- Add missing BullModule queue registration and BullBoardModule to UsersModule
- Add invitingUser to event payloads to track who sent the invite
- Fix incorrect global variable in SendInviteUsersMailMessage (__views_dir -> __images_dirname)
- Use invitingUser as fromUser instead of invited user in email
- Update processors to use BullMQ pattern

Fixes issues:
1. Email not sending due to missing queue/processor registration
2. Null variables in email (firstName/lastName) because fromUser was the invited user
3. Image attachment failing due to wrong path
2026-02-14 00:31:28 +02:00
Ahmed Bouhuolia
9204b76346 Merge pull request #950 from bigcapitalhq/fix-tax-rates
fix(server): use `DrawerActionsBar` instead of `DashboardActionsBar` in drawer components
2026-02-12 23:21:28 +02:00
Ahmed Bouhuolia
36cbb1eef5 fix: use DrawerActionsBar instead of DashboardActionsBar in drawer components
Replace DashboardActionsBar with DrawerActionsBar in all drawer action bars
for consistency with the design system:
- ContactDetailActionsBar
- CustomerDetailsActionsBar
- VendorCreditDetailActionsBar
- TaxRateDetailsContentActionsBar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:19:16 +02:00
Ahmed Bouhuolia
441e27581b Merge pull request #949 from bigcapitalhq/fix-tax-rates
fix: tax rates API and UI improvements
2026-02-12 20:08:49 +02:00
Ahmed Bouhuolia
e0d9a56a29 fix: tax rates API and UI improvements
- Add @ToNumber() decorator to rate field for proper validation
- Fix getTaxRates to return { data: taxRates } response
- Fix useTaxRate URL typo and response handling
- Fix activate/inactivate endpoint methods and paths
- Apply TEXT_MUTED class to description and compound tax
- Add dark mode support for rate number display
2026-02-12 20:06:49 +02:00
Ahmed Bouhuolia
5a017104ce Merge pull request #948 from bigcapitalhq/fix/abouolia/rerecognize-transactions-on-rule-edit
fix: paper template scrollable area
2026-02-12 15:02:12 +02:00
Ahmed Bouhuolia
25ca620836 fix: add consistent Box wrapper to paper template forms in customize components 2026-02-12 14:59:55 +02:00
Ahmed Bouhuolia
5a3655e093 Merge pull request #944 from bigcapitalhq/fix/abouolia/rerecognize-transactions-on-rule-edit
fix(server): re-recognize transactions when bank rule is edited
2026-02-11 23:18:18 +02:00
Ahmed Bouhuolia
49c2777587 fix: re-recognize transactions when bank rule is edited (closes #809) 2026-02-11 23:15:20 +02:00
Ahmed Bouhuolia
a5680c08c2 Merge pull request #943 from bigcapitalhq/fix/abouolia/bank-rule-payee-field-validation
fix(server): add missing S3_FORCE_PATH_STYLE environment variable
2026-02-11 19:36:52 +02:00
Ahmed Bouhuolia
d909dad1bf fix: add missing S3_FORCE_PATH_STYLE environment variable
The S3 module was referencing config.forcePathStyle but the value
was never being read from the environment. This adds the missing
forcePathStyle configuration to the S3 config.

Closes #940

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 19:35:21 +02:00
Ahmed Bouhuolia
f32cc752ef Merge pull request #942 from bigcapitalhq/fix/abouolia/bank-rule-payee-field-validation
fix(server): allow 'payee' field in bank rule conditions validation
2026-02-11 19:13:35 +02:00
Ahmed Bouhuolia
a7f98201cc fix: allow 'payee' field in bank rule conditions validation
The BankRuleConditionDto validation only allowed 'description' and 'amount'
fields, but the frontend also sends 'payee' as a valid condition field.
This caused a 400 Bad Request error when creating rules with payee conditions.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 19:11:58 +02:00
Ahmed Bouhuolia
a1d0fc3f0a Merge pull request #941 from bigcapitalhq/fix/ahmedbouhuolia/phone-validation-formatted-numbers
fix(webapp): allow formatted phone numbers in customer and vendor forms
2026-02-11 18:39:52 +02:00
Ahmed Bouhuolia
11575cfb96 fix(webapp): allow formatted phone numbers in customer and vendor forms 2026-02-11 18:37:39 +02:00
69 changed files with 614 additions and 149 deletions

View File

@@ -6,4 +6,5 @@ export default registerAs('s3', () => ({
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
endpoint: process.env.S3_ENDPOINT, endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET, bucket: process.env.S3_BUCKET,
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
})); }));

View File

@@ -9,6 +9,7 @@ import {
ParseIntPipe, ParseIntPipe,
Put, Put,
HttpCode, HttpCode,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AccountsApplication } from './AccountsApplication.service'; import { AccountsApplication } from './AccountsApplication.service';
import { CreateAccountDTO } from './CreateAccount.dto'; import { CreateAccountDTO } from './CreateAccount.dto';
@@ -32,6 +33,11 @@ import {
BulkDeleteDto, BulkDeleteDto,
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { AccountAction } from './Accounts.types';
@Controller('accounts') @Controller('accounts')
@ApiTags('Accounts') @ApiTags('Accounts')
@@ -40,11 +46,13 @@ import {
@ApiExtraModels(GetAccountTransactionResponseDto) @ApiExtraModels(GetAccountTransactionResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class AccountsController { export class AccountsController {
constructor(private readonly accountsApplication: AccountsApplication) { } constructor(private readonly accountsApplication: AccountsApplication) { }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@HttpCode(200) @HttpCode(200)
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.', 'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.',
@@ -67,6 +75,7 @@ export class AccountsController {
@Post('bulk-delete') @Post('bulk-delete')
@HttpCode(200) @HttpCode(200)
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({ summary: 'Deletes multiple accounts in bulk.' }) @ApiOperation({ summary: 'Deletes multiple accounts in bulk.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -81,6 +90,7 @@ export class AccountsController {
} }
@Post() @Post()
@RequirePermission(AccountAction.CREATE, AbilitySubject.Account)
@ApiOperation({ summary: 'Create an account' }) @ApiOperation({ summary: 'Create an account' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -91,6 +101,7 @@ export class AccountsController {
} }
@Put(':id') @Put(':id')
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Edit the given account.' }) @ApiOperation({ summary: 'Edit the given account.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -111,6 +122,7 @@ export class AccountsController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({ summary: 'Delete the given account.' }) @ApiOperation({ summary: 'Delete the given account.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -129,6 +141,7 @@ export class AccountsController {
@Post(':id/activate') @Post(':id/activate')
@HttpCode(200) @HttpCode(200)
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Activate the given account.' }) @ApiOperation({ summary: 'Activate the given account.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -147,6 +160,7 @@ export class AccountsController {
@Post(':id/inactivate') @Post(':id/inactivate')
@HttpCode(200) @HttpCode(200)
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Inactivate the given account.' }) @ApiOperation({ summary: 'Inactivate the given account.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -164,6 +178,7 @@ export class AccountsController {
} }
@Get('types') @Get('types')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account types.' }) @ApiOperation({ summary: 'Retrieves the account types.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -180,6 +195,7 @@ export class AccountsController {
} }
@Get('transactions') @Get('transactions')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account transactions.' }) @ApiOperation({ summary: 'Retrieves the account transactions.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -198,6 +214,7 @@ export class AccountsController {
} }
@Get(':id') @Get(':id')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account details.' }) @ApiOperation({ summary: 'Retrieves the account details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -216,6 +233,7 @@ export class AccountsController {
} }
@Get() @Get()
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the accounts.' }) @ApiOperation({ summary: 'Retrieves the accounts.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -16,7 +16,7 @@ import { ToNumber } from '@/common/decorators/Validators';
class BankRuleConditionDto { class BankRuleConditionDto {
@IsNotEmpty() @IsNotEmpty()
@IsIn(['description', 'amount']) @IsIn(['description', 'amount', 'payee'])
field: string; field: string;
@IsNotEmpty() @IsNotEmpty()

View File

@@ -15,8 +15,13 @@ export const RecognizeUncategorizedTransactionsJob =
export const RecognizeUncategorizedTransactionsQueue = export const RecognizeUncategorizedTransactionsQueue =
'recognize-uncategorized-transactions-queue'; 'recognize-uncategorized-transactions-queue';
export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload { export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload {
ruleId: number, ruleId: number,
transactionsCriteria: any; transactionsCriteria?: RecognizeTransactionsCriteria;
/**
* When true, first reverts recognized transactions before recognizing again.
* Used when a bank rule is edited to ensure transactions previously recognized
* by lower-priority rules are re-evaluated against the updated rule.
*/
shouldRevert?: boolean;
} }

View File

@@ -93,6 +93,10 @@ export class RecognizeTranasctionsService {
q.whereIn('id', rulesIds); q.whereIn('id', rulesIds);
} }
q.withGraphFetched('conditions'); q.withGraphFetched('conditions');
// Order by the 'order' field to ensure higher priority rules (lower order values)
// are matched first.
q.orderBy('order', 'asc');
}); });
const bankRulesByAccountId = transformToMapBy( const bankRulesByAccountId = transformToMapBy(

View File

@@ -69,10 +69,13 @@ export class TriggerRecognizedTransactionsSubscriber {
const tenantPayload = await this.tenancyContect.getTenantJobPayload(); const tenantPayload = await this.tenancyContect.getTenantJobPayload();
const payload = { const payload = {
ruleId: bankRule.id, ruleId: bankRule.id,
shouldRevert: true,
...tenantPayload, ...tenantPayload,
} as RecognizeUncategorizedTransactionsJobPayload; } as RecognizeUncategorizedTransactionsJobPayload;
// Re-recognize the transactions based on the new rules. // Re-recognize the transactions based on the new rules.
// Setting shouldRevert to true ensures that transactions previously recognized
// by this or lower-priority rules are re-evaluated against the updated rule.
await this.recognizeTransactionsQueue.add( await this.recognizeTransactionsQueue.add(
RecognizeUncategorizedTransactionsJob, RecognizeUncategorizedTransactionsJob,
payload, payload,

View File

@@ -3,6 +3,7 @@ import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common'; import { Scope } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls'; import { ClsService, UseCls } from 'nestjs-cls';
import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service'; import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
import { RevertRecognizedTransactionsService } from '../commands/RevertRecognizedTransactions.service';
import { import {
RecognizeUncategorizedTransactionsJobPayload, RecognizeUncategorizedTransactionsJobPayload,
RecognizeUncategorizedTransactionsQueue, RecognizeUncategorizedTransactionsQueue,
@@ -15,10 +16,12 @@ import {
export class RegonizeTransactionsPrcessor extends WorkerHost { export class RegonizeTransactionsPrcessor extends WorkerHost {
/** /**
* @param {RecognizeTranasctionsService} recognizeTranasctionsService - * @param {RecognizeTranasctionsService} recognizeTranasctionsService -
* @param {RevertRecognizedTransactionsService} revertRecognizedTransactionsService -
* @param {ClsService} clsService - * @param {ClsService} clsService -
*/ */
constructor( constructor(
private readonly recognizeTranasctionsService: RecognizeTranasctionsService, private readonly recognizeTranasctionsService: RecognizeTranasctionsService,
private readonly revertRecognizedTransactionsService: RevertRecognizedTransactionsService,
private readonly clsService: ClsService, private readonly clsService: ClsService,
) { ) {
super(); super();
@@ -29,12 +32,21 @@ export class RegonizeTransactionsPrcessor extends WorkerHost {
*/ */
@UseCls() @UseCls()
async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) { async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) {
const { ruleId, transactionsCriteria } = job.data; const { ruleId, transactionsCriteria, shouldRevert } = job.data;
this.clsService.set('organizationId', job.data.organizationId); this.clsService.set('organizationId', job.data.organizationId);
this.clsService.set('userId', job.data.userId); this.clsService.set('userId', job.data.userId);
try { try {
// If shouldRevert is true, first revert recognized transactions before re-recognizing.
// This is used when a bank rule is edited to ensure transactions previously recognized
// by lower-priority rules are re-evaluated against the updated rule.
if (shouldRevert) {
await this.revertRecognizedTransactionsService.revertRecognizedTransactions(
ruleId,
transactionsCriteria,
);
}
await this.recognizeTranasctionsService.recognizeTransactions( await this.recognizeTranasctionsService.recognizeTransactions(
ruleId, ruleId,
transactionsCriteria, transactionsCriteria,

View File

@@ -7,6 +7,7 @@ import {
Post, Post,
Put, Put,
Query, Query,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { BillPaymentsApplication } from './BillPaymentsApplication.service'; import { BillPaymentsApplication } from './BillPaymentsApplication.service';
import { import {
@@ -26,12 +27,18 @@ import { BillPaymentsPages } from './commands/BillPaymentsPages.service';
import { BillPaymentResponseDto } from './dtos/BillPaymentResponse.dto'; import { BillPaymentResponseDto } from './dtos/BillPaymentResponse.dto';
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto'; import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { IPaymentMadeAction } from './types/BillPayments.types';
@Controller('bill-payments') @Controller('bill-payments')
@ApiTags('Bill Payments') @ApiTags('Bill Payments')
@ApiExtraModels(BillPaymentResponseDto) @ApiExtraModels(BillPaymentResponseDto)
@ApiExtraModels(PaginatedResponseDto) @ApiExtraModels(PaginatedResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class BillPaymentsController { export class BillPaymentsController {
constructor( constructor(
private billPaymentsApplication: BillPaymentsApplication, private billPaymentsApplication: BillPaymentsApplication,
@@ -39,12 +46,14 @@ export class BillPaymentsController {
) {} ) {}
@Post() @Post()
@RequirePermission(IPaymentMadeAction.Create, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Create a new bill payment.' }) @ApiOperation({ summary: 'Create a new bill payment.' })
public createBillPayment(@Body() billPaymentDTO: CreateBillPaymentDto) { public createBillPayment(@Body() billPaymentDTO: CreateBillPaymentDto) {
return this.billPaymentsApplication.createBillPayment(billPaymentDTO); return this.billPaymentsApplication.createBillPayment(billPaymentDTO);
} }
@Delete(':billPaymentId') @Delete(':billPaymentId')
@RequirePermission(IPaymentMadeAction.Delete, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Delete the given bill payment.' }) @ApiOperation({ summary: 'Delete the given bill payment.' })
@ApiParam({ @ApiParam({
name: 'billPaymentId', name: 'billPaymentId',
@@ -59,6 +68,7 @@ export class BillPaymentsController {
} }
@Put(':billPaymentId') @Put(':billPaymentId')
@RequirePermission(IPaymentMadeAction.Edit, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Edit the given bill payment.' }) @ApiOperation({ summary: 'Edit the given bill payment.' })
@ApiParam({ @ApiParam({
name: 'billPaymentId', name: 'billPaymentId',
@@ -77,6 +87,7 @@ export class BillPaymentsController {
} }
@Get('/new-page/entries') @Get('/new-page/entries')
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ @ApiOperation({
summary: summary:
'Retrieves the payable entries of the new page once vendor be selected.', 'Retrieves the payable entries of the new page once vendor be selected.',
@@ -95,6 +106,7 @@ export class BillPaymentsController {
} }
@Get(':billPaymentId/bills') @Get(':billPaymentId/bills')
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Retrieves the bills of the given bill payment.' }) @ApiOperation({ summary: 'Retrieves the bills of the given bill payment.' })
@ApiParam({ @ApiParam({
name: 'billPaymentId', name: 'billPaymentId',
@@ -107,6 +119,7 @@ export class BillPaymentsController {
} }
@Get('/:billPaymentId/edit-page') @Get('/:billPaymentId/edit-page')
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ @ApiOperation({
summary: 'Retrieves the edit page of the given bill payment.', summary: 'Retrieves the edit page of the given bill payment.',
}) })
@@ -126,6 +139,7 @@ export class BillPaymentsController {
} }
@Get(':billPaymentId') @Get(':billPaymentId')
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Retrieves the bill payment details.' }) @ApiOperation({ summary: 'Retrieves the bill payment details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -145,6 +159,7 @@ export class BillPaymentsController {
} }
@Get() @Get()
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Retrieves the bill payments list.' }) @ApiOperation({ summary: 'Retrieves the bill payments list.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -17,6 +17,7 @@ import {
Get, Get,
Query, Query,
HttpCode, HttpCode,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { BillsApplication } from './Bills.application'; import { BillsApplication } from './Bills.application';
import { IBillsFilter } from './Bills.types'; import { IBillsFilter } from './Bills.types';
@@ -28,6 +29,11 @@ import {
BulkDeleteDto, BulkDeleteDto,
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { BillAction } from './Bills.types';
@Controller('bills') @Controller('bills')
@ApiTags('Bills') @ApiTags('Bills')
@@ -35,10 +41,12 @@ import {
@ApiExtraModels(PaginatedResponseDto) @ApiExtraModels(PaginatedResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiExtraModels(ValidateBulkDeleteResponseDto)
@UseGuards(AuthorizationGuard, PermissionGuard)
export class BillsController { export class BillsController {
constructor(private billsApplication: BillsApplication) { } constructor(private billsApplication: BillsApplication) { }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
@ApiOperation({ @ApiOperation({
summary: 'Validate which bills can be deleted and return the results.', summary: 'Validate which bills can be deleted and return the results.',
}) })
@@ -58,6 +66,7 @@ export class BillsController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
@ApiOperation({ summary: 'Deletes multiple bills.' }) @ApiOperation({ summary: 'Deletes multiple bills.' })
@HttpCode(200) @HttpCode(200)
@ApiResponse({ @ApiResponse({
@@ -73,12 +82,14 @@ export class BillsController {
} }
@Post() @Post()
@RequirePermission(BillAction.Create, AbilitySubject.Bill)
@ApiOperation({ summary: 'Create a new bill.' }) @ApiOperation({ summary: 'Create a new bill.' })
createBill(@Body() billDTO: CreateBillDto) { createBill(@Body() billDTO: CreateBillDto) {
return this.billsApplication.createBill(billDTO); return this.billsApplication.createBill(billDTO);
} }
@Put(':id') @Put(':id')
@RequirePermission(BillAction.Edit, AbilitySubject.Bill)
@ApiOperation({ summary: 'Edit the given bill.' }) @ApiOperation({ summary: 'Edit the given bill.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@@ -91,6 +102,7 @@ export class BillsController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
@ApiOperation({ summary: 'Delete the given bill.' }) @ApiOperation({ summary: 'Delete the given bill.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@@ -103,6 +115,7 @@ export class BillsController {
} }
@Get() @Get()
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ summary: 'Retrieves the bills.' }) @ApiOperation({ summary: 'Retrieves the bills.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -132,6 +145,7 @@ export class BillsController {
} }
@Get(':id/payment-transactions') @Get(':id/payment-transactions')
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ @ApiOperation({
summary: 'Retrieve the specific bill associated payment transactions.', summary: 'Retrieve the specific bill associated payment transactions.',
}) })
@@ -146,6 +160,7 @@ export class BillsController {
} }
@Get(':id') @Get(':id')
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ summary: 'Retrieves the bill details.' }) @ApiOperation({ summary: 'Retrieves the bill details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -165,6 +180,7 @@ export class BillsController {
} }
@Patch(':id/open') @Patch(':id/open')
@RequirePermission(BillAction.Edit, AbilitySubject.Bill)
@ApiOperation({ summary: 'Open the given bill.' }) @ApiOperation({ summary: 'Open the given bill.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@@ -177,6 +193,7 @@ export class BillsController {
} }
@Get('due') @Get('due')
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ summary: 'Retrieves the due bills.' }) @ApiOperation({ summary: 'Retrieves the due bills.' })
getDueBills(@Body('vendorId') vendorId?: number) { getDueBills(@Body('vendorId') vendorId?: number) {
return this.billsApplication.getDueBills(vendorId); return this.billsApplication.getDueBills(vendorId);

View File

@@ -18,6 +18,7 @@ import {
Put, Put,
Query, Query,
Res, Res,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { CreditNoteApplication } from './CreditNoteApplication.service'; import { CreditNoteApplication } from './CreditNoteApplication.service';
import { ICreditNotesQueryDTO } from './types/CreditNotes.types'; import { ICreditNotesQueryDTO } from './types/CreditNotes.types';
@@ -30,6 +31,11 @@ import {
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CreditNoteAction } from './types/CreditNotes.types';
@Controller('credit-notes') @Controller('credit-notes')
@ApiTags('Credit Notes') @ApiTags('Credit Notes')
@@ -37,6 +43,7 @@ import { AcceptType } from '@/constants/accept-type';
@ApiExtraModels(PaginatedResponseDto) @ApiExtraModels(PaginatedResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CreditNotesController { export class CreditNotesController {
/** /**
* @param {CreditNoteApplication} creditNoteApplication - The credit note application service. * @param {CreditNoteApplication} creditNoteApplication - The credit note application service.
@@ -44,6 +51,7 @@ export class CreditNotesController {
constructor(private creditNoteApplication: CreditNoteApplication) { } constructor(private creditNoteApplication: CreditNoteApplication) { }
@Post() @Post()
@RequirePermission(CreditNoteAction.Create, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Create a new credit note' }) @ApiOperation({ summary: 'Create a new credit note' })
@ApiResponse({ status: 201, description: 'Credit note successfully created' }) @ApiResponse({ status: 201, description: 'Credit note successfully created' })
@ApiResponse({ status: 400, description: 'Invalid input data' }) @ApiResponse({ status: 400, description: 'Invalid input data' })
@@ -52,6 +60,7 @@ export class CreditNotesController {
} }
@Get('state') @Get('state')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get credit note state' }) @ApiOperation({ summary: 'Get credit note state' })
@ApiResponse({ status: 200, description: 'Returns the credit note state' }) @ApiResponse({ status: 200, description: 'Returns the credit note state' })
getCreditNoteState() { getCreditNoteState() {
@@ -59,6 +68,7 @@ export class CreditNotesController {
} }
@Get(':id') @Get(':id')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get a specific credit note by ID' }) @ApiOperation({ summary: 'Get a specific credit note by ID' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' }) @ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ @ApiResponse({
@@ -92,6 +102,7 @@ export class CreditNotesController {
} }
@Get() @Get()
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get all credit notes' }) @ApiOperation({ summary: 'Get all credit notes' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -115,6 +126,7 @@ export class CreditNotesController {
} }
@Put(':id') @Put(':id')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Update a credit note' }) @ApiOperation({ summary: 'Update a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' }) @ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Credit note successfully updated' }) @ApiResponse({ status: 200, description: 'Credit note successfully updated' })
@@ -131,6 +143,7 @@ export class CreditNotesController {
} }
@Put(':id/open') @Put(':id/open')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Open a credit note' }) @ApiOperation({ summary: 'Open a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' }) @ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Credit note successfully opened' }) @ApiResponse({ status: 200, description: 'Credit note successfully opened' })
@@ -140,6 +153,7 @@ export class CreditNotesController {
} }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(CreditNoteAction.Delete, AbilitySubject.CreditNote)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validates which credit notes can be deleted and returns the results.', 'Validates which credit notes can be deleted and returns the results.',
@@ -161,6 +175,7 @@ export class CreditNotesController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(CreditNoteAction.Delete, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Deletes multiple credit notes.' }) @ApiOperation({ summary: 'Deletes multiple credit notes.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -173,6 +188,7 @@ export class CreditNotesController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(CreditNoteAction.Delete, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Delete a credit note' }) @ApiOperation({ summary: 'Delete a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' }) @ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Credit note successfully deleted' }) @ApiResponse({ status: 200, description: 'Credit note successfully deleted' })

View File

@@ -7,6 +7,7 @@ import {
Post, Post,
Put, Put,
Query, Query,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { CustomersApplication } from './CustomersApplication.service'; import { CustomersApplication } from './CustomersApplication.service';
import { CustomerOpeningBalanceEditDto } from './dtos/CustomerOpeningBalanceEdit.dto'; import { CustomerOpeningBalanceEditDto } from './dtos/CustomerOpeningBalanceEdit.dto';
@@ -26,15 +27,22 @@ import {
ValidateBulkDeleteCustomersResponseDto, ValidateBulkDeleteCustomersResponseDto,
} from './dtos/BulkDeleteCustomers.dto'; } from './dtos/BulkDeleteCustomers.dto';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CustomerAction } from './types/Customers.types';
@Controller('customers') @Controller('customers')
@ApiTags('Customers') @ApiTags('Customers')
@ApiExtraModels(CustomerResponseDto) @ApiExtraModels(CustomerResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CustomersController { export class CustomersController {
constructor(private customersApplication: CustomersApplication) { } constructor(private customersApplication: CustomersApplication) { }
@Get(':id') @Get(':id')
@RequirePermission(CustomerAction.View, AbilitySubject.Customer)
@ApiOperation({ summary: 'Retrieves the customer details.' }) @ApiOperation({ summary: 'Retrieves the customer details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -46,6 +54,7 @@ export class CustomersController {
} }
@Get() @Get()
@RequirePermission(CustomerAction.View, AbilitySubject.Customer)
@ApiOperation({ summary: 'Retrieves the customers paginated list.' }) @ApiOperation({ summary: 'Retrieves the customers paginated list.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -60,6 +69,7 @@ export class CustomersController {
} }
@Post() @Post()
@RequirePermission(CustomerAction.Create, AbilitySubject.Customer)
@ApiOperation({ summary: 'Create a new customer.' }) @ApiOperation({ summary: 'Create a new customer.' })
@ApiResponse({ @ApiResponse({
status: 201, status: 201,
@@ -71,6 +81,7 @@ export class CustomersController {
} }
@Put(':id') @Put(':id')
@RequirePermission(CustomerAction.Edit, AbilitySubject.Customer)
@ApiOperation({ summary: 'Edit the given customer.' }) @ApiOperation({ summary: 'Edit the given customer.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -85,6 +96,7 @@ export class CustomersController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
@ApiOperation({ summary: 'Delete the given customer.' }) @ApiOperation({ summary: 'Delete the given customer.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -95,6 +107,7 @@ export class CustomersController {
} }
@Put(':id/opening-balance') @Put(':id/opening-balance')
@RequirePermission(CustomerAction.Edit, AbilitySubject.Customer)
@ApiOperation({ summary: 'Edit the opening balance of the given customer.' }) @ApiOperation({ summary: 'Edit the opening balance of the given customer.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -112,6 +125,7 @@ export class CustomersController {
} }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validates which customers can be deleted and returns counts of deletable and non-deletable customers.', 'Validates which customers can be deleted and returns counts of deletable and non-deletable customers.',
@@ -131,6 +145,7 @@ export class CustomersController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
@ApiOperation({ summary: 'Deletes multiple customers in bulk.' }) @ApiOperation({ summary: 'Deletes multiple customers in bulk.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -7,6 +7,7 @@ import {
Post, Post,
Put, Put,
Query, Query,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ExpensesApplication } from './ExpensesApplication.service'; import { ExpensesApplication } from './ExpensesApplication.service';
import { IExpensesFilter } from './Expenses.types'; import { IExpensesFilter } from './Expenses.types';
@@ -25,6 +26,11 @@ import {
BulkDeleteDto, BulkDeleteDto,
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ExpenseAction } from './Expenses.types';
@Controller('expenses') @Controller('expenses')
@ApiTags('Expenses') @ApiTags('Expenses')
@@ -34,10 +40,12 @@ import {
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
) )
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ExpensesController { export class ExpensesController {
constructor(private readonly expensesApplication: ExpensesApplication) { } constructor(private readonly expensesApplication: ExpensesApplication) { }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(ExpenseAction.Delete, AbilitySubject.Expense)
@ApiOperation({ @ApiOperation({
summary: 'Validate which expenses can be deleted and return the results.', summary: 'Validate which expenses can be deleted and return the results.',
}) })
@@ -58,6 +66,7 @@ export class ExpensesController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(ExpenseAction.Delete, AbilitySubject.Expense)
@ApiOperation({ summary: 'Deletes multiple expenses.' }) @ApiOperation({ summary: 'Deletes multiple expenses.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -76,6 +85,7 @@ export class ExpensesController {
* @param {IExpenseCreateDTO} expenseDTO * @param {IExpenseCreateDTO} expenseDTO
*/ */
@Post() @Post()
@RequirePermission(ExpenseAction.Create, AbilitySubject.Expense)
@ApiOperation({ summary: 'Create a new expense transaction.' }) @ApiOperation({ summary: 'Create a new expense transaction.' })
public createExpense(@Body() expenseDTO: CreateExpenseDto) { public createExpense(@Body() expenseDTO: CreateExpenseDto) {
return this.expensesApplication.createExpense(expenseDTO); return this.expensesApplication.createExpense(expenseDTO);
@@ -87,6 +97,7 @@ export class ExpensesController {
* @param {IExpenseEditDTO} expenseDTO * @param {IExpenseEditDTO} expenseDTO
*/ */
@Put(':id') @Put(':id')
@RequirePermission(ExpenseAction.Edit, AbilitySubject.Expense)
@ApiOperation({ summary: 'Edit the given expense transaction.' }) @ApiOperation({ summary: 'Edit the given expense transaction.' })
public editExpense( public editExpense(
@Param('id') expenseId: number, @Param('id') expenseId: number,
@@ -100,6 +111,7 @@ export class ExpensesController {
* @param {number} expenseId * @param {number} expenseId
*/ */
@Delete(':id') @Delete(':id')
@RequirePermission(ExpenseAction.Delete, AbilitySubject.Expense)
@ApiOperation({ summary: 'Delete the given expense transaction.' }) @ApiOperation({ summary: 'Delete the given expense transaction.' })
public deleteExpense(@Param('id') expenseId: number) { public deleteExpense(@Param('id') expenseId: number) {
return this.expensesApplication.deleteExpense(expenseId); return this.expensesApplication.deleteExpense(expenseId);
@@ -110,6 +122,7 @@ export class ExpensesController {
* @param {number} expenseId * @param {number} expenseId
*/ */
@Post(':id/publish') @Post(':id/publish')
@RequirePermission(ExpenseAction.Edit, AbilitySubject.Expense)
@ApiOperation({ summary: 'Publish the given expense transaction.' }) @ApiOperation({ summary: 'Publish the given expense transaction.' })
public publishExpense(@Param('id') expenseId: number) { public publishExpense(@Param('id') expenseId: number) {
return this.expensesApplication.publishExpense(expenseId); return this.expensesApplication.publishExpense(expenseId);
@@ -119,6 +132,7 @@ export class ExpensesController {
* Get the expense transaction details. * Get the expense transaction details.
*/ */
@Get() @Get()
@RequirePermission(ExpenseAction.View, AbilitySubject.Expense)
@ApiOperation({ summary: 'Get the expense transactions.' }) @ApiOperation({ summary: 'Get the expense transactions.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -146,6 +160,7 @@ export class ExpensesController {
* @param {number} expenseId * @param {number} expenseId
*/ */
@Get(':id') @Get(':id')
@RequirePermission(ExpenseAction.View, AbilitySubject.Expense)
@ApiOperation({ summary: 'Get the expense transaction details.' }) @ApiOperation({ summary: 'Get the expense transaction details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -1,5 +1,5 @@
import { Response } from 'express'; import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common'; import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { APAgingSummaryApplication } from './APAgingSummaryApplication'; import { APAgingSummaryApplication } from './APAgingSummaryApplication';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { import {
@@ -11,14 +11,21 @@ import {
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto'; import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
import { APAgingSummaryResponseExample } from './APAgingSummary.swagger'; import { APAgingSummaryResponseExample } from './APAgingSummary.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('reports/payable-aging-summary') @Controller('reports/payable-aging-summary')
@ApiTags('Reports') @ApiTags('Reports')
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class APAgingSummaryController { export class APAgingSummaryController {
constructor(private readonly APAgingSummaryApp: APAgingSummaryApplication) { } constructor(private readonly APAgingSummaryApp: APAgingSummaryApplication) { }
@Get() @Get()
@RequirePermission(ReportsAction.READ_AP_AGING_SUMMARY, AbilitySubject.Report)
@ApiOperation({ summary: 'Get payable aging summary' }) @ApiOperation({ summary: 'Get payable aging summary' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -1,5 +1,4 @@
import { Controller, Get, Headers } from '@nestjs/common'; import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { Query, Res } from '@nestjs/common';
import { ARAgingSummaryApplication } from './ARAgingSummaryApplication'; import { ARAgingSummaryApplication } from './ARAgingSummaryApplication';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express'; import { Response } from 'express';
@@ -12,14 +11,21 @@ import {
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto'; import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
import { ARAgingSummaryResponseExample } from './ARAgingSummary.swagger'; import { ARAgingSummaryResponseExample } from './ARAgingSummary.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('reports/receivable-aging-summary') @Controller('reports/receivable-aging-summary')
@ApiTags('Reports') @ApiTags('Reports')
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ARAgingSummaryController { export class ARAgingSummaryController {
constructor(private readonly ARAgingSummaryApp: ARAgingSummaryApplication) {} constructor(private readonly ARAgingSummaryApp: ARAgingSummaryApplication) {}
@Get() @Get()
@RequirePermission(ReportsAction.READ_AR_AGING_SUMMARY, AbilitySubject.Report)
@ApiOperation({ summary: 'Get receivable aging summary' }) @ApiOperation({ summary: 'Get receivable aging summary' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -1,5 +1,5 @@
import { Response } from 'express'; import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common'; import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { BalanceSheetApplication } from './BalanceSheetApplication'; import { BalanceSheetApplication } from './BalanceSheetApplication';
import { import {
@@ -11,10 +11,16 @@ import {
import { BalanceSheetQueryDto } from './BalanceSheet.dto'; import { BalanceSheetQueryDto } from './BalanceSheet.dto';
import { BalanceSheetResponseExample } from './BalanceSheet.swagger'; import { BalanceSheetResponseExample } from './BalanceSheet.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('/reports/balance-sheet') @Controller('/reports/balance-sheet')
@ApiTags('Reports') @ApiTags('Reports')
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class BalanceSheetStatementController { export class BalanceSheetStatementController {
constructor(private readonly balanceSheetApp: BalanceSheetApplication) {} constructor(private readonly balanceSheetApp: BalanceSheetApplication) {}
@@ -25,6 +31,7 @@ export class BalanceSheetStatementController {
* @param {string} acceptHeader - Accept header. * @param {string} acceptHeader - Accept header.
*/ */
@Get('') @Get('')
@RequirePermission(ReportsAction.READ_BALANCE_SHEET, AbilitySubject.Report)
@ApiOperation({ summary: 'Get balance sheet statement' }) @ApiOperation({ summary: 'Get balance sheet statement' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -1,5 +1,5 @@
import { Response } from 'express'; import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common'; import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { CashflowSheetApplication } from './CashflowSheetApplication'; import { CashflowSheetApplication } from './CashflowSheetApplication';
import { import {
@@ -11,14 +11,21 @@ import {
import { CashFlowStatementQueryDto } from './CashFlowStatementQuery.dto'; import { CashFlowStatementQueryDto } from './CashFlowStatementQuery.dto';
import { CashflowStatementResponseExample } from './CashflowStatement.swagger'; import { CashflowStatementResponseExample } from './CashflowStatement.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('reports/cashflow-statement') @Controller('reports/cashflow-statement')
@ApiTags('Reports') @ApiTags('Reports')
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CashflowController { export class CashflowController {
constructor(private readonly cashflowSheetApp: CashflowSheetApplication) { } constructor(private readonly cashflowSheetApp: CashflowSheetApplication) { }
@Get() @Get()
@RequirePermission(ReportsAction.READ_CASHFLOW, AbilitySubject.Report)
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Cashflow statement report', description: 'Cashflow statement report',

View File

@@ -5,22 +5,29 @@ import {
ApiTags, ApiTags,
} from '@nestjs/swagger'; } from '@nestjs/swagger';
import { Response } from 'express'; import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common'; import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { GeneralLedgerApplication } from './GeneralLedgerApplication'; import { GeneralLedgerApplication } from './GeneralLedgerApplication';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { GeneralLedgerQueryDto } from './GeneralLedgerQuery.dto'; import { GeneralLedgerQueryDto } from './GeneralLedgerQuery.dto';
import { GeneralLedgerResponseExample } from './GeneralLedger.swagger'; import { GeneralLedgerResponseExample } from './GeneralLedger.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('/reports/general-ledger') @Controller('/reports/general-ledger')
@ApiTags('Reports') @ApiTags('Reports')
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class GeneralLedgerController { export class GeneralLedgerController {
constructor( constructor(
private readonly generalLedgerApplication: GeneralLedgerApplication, private readonly generalLedgerApplication: GeneralLedgerApplication,
) {} ) {}
@Get() @Get()
@RequirePermission(ReportsAction.READ_GENERAL_LEDGET, AbilitySubject.Report)
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'General ledger report', description: 'General ledger report',

View File

@@ -1,4 +1,4 @@
import { Controller, Get, Headers, Query, Res } from '@nestjs/common'; import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { Response } from 'express'; import { Response } from 'express';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { JournalSheetApplication } from './JournalSheetApplication'; import { JournalSheetApplication } from './JournalSheetApplication';
@@ -11,14 +11,21 @@ import {
import { JournalSheetQueryDto } from './JournalSheetQuery.dto'; import { JournalSheetQueryDto } from './JournalSheetQuery.dto';
import { JournalSheetResponseExample } from './JournalSheet.swagger'; import { JournalSheetResponseExample } from './JournalSheet.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('/reports/journal') @Controller('/reports/journal')
@ApiTags('Reports') @ApiTags('Reports')
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class JournalSheetController { export class JournalSheetController {
constructor(private readonly journalSheetApp: JournalSheetApplication) {} constructor(private readonly journalSheetApp: JournalSheetApplication) {}
@Get() @Get()
@RequirePermission(ReportsAction.READ_JOURNAL, AbilitySubject.Report)
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Journal report', description: 'Journal report',

View File

@@ -1,5 +1,5 @@
import { Response } from 'express'; import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common'; import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { ProfitLossSheetApplication } from './ProfitLossSheetApplication'; import { ProfitLossSheetApplication } from './ProfitLossSheetApplication';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { import {
@@ -11,10 +11,16 @@ import {
import { ProfitLossSheetQueryDto } from './ProfitLossSheetQuery.dto'; import { ProfitLossSheetQueryDto } from './ProfitLossSheetQuery.dto';
import { ProfitLossSheetResponseExample } from './ProfitLossSheet.swagger'; import { ProfitLossSheetResponseExample } from './ProfitLossSheet.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('/reports/profit-loss-sheet') @Controller('/reports/profit-loss-sheet')
@ApiTags('Reports') @ApiTags('Reports')
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ProfitLossSheetController { export class ProfitLossSheetController {
constructor( constructor(
private readonly profitLossSheetApp: ProfitLossSheetApplication, private readonly profitLossSheetApp: ProfitLossSheetApplication,
@@ -27,6 +33,7 @@ export class ProfitLossSheetController {
* @param {string} acceptHeader * @param {string} acceptHeader
*/ */
@Get('/') @Get('/')
@RequirePermission(ReportsAction.READ_PROFIT_LOSS, AbilitySubject.Report)
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Profit & loss statement', description: 'Profit & loss statement',

View File

@@ -14,6 +14,7 @@ import {
Post, Post,
Put, Put,
Query, Query,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { InventoryAdjustmentsApplicationService } from './InventoryAdjustmentsApplication.service'; import { InventoryAdjustmentsApplicationService } from './InventoryAdjustmentsApplication.service';
import { IInventoryAdjustmentsFilter } from './types/InventoryAdjustments.types'; import { IInventoryAdjustmentsFilter } from './types/InventoryAdjustments.types';
@@ -21,17 +22,24 @@ import { InventoryAdjustment } from './models/InventoryAdjustment';
import { CreateQuickInventoryAdjustmentDto } from './dtos/CreateQuickInventoryAdjustment.dto'; import { CreateQuickInventoryAdjustmentDto } from './dtos/CreateQuickInventoryAdjustment.dto';
import { InventoryAdjustmentResponseDto } from './dtos/InventoryAdjustmentResponse.dto'; import { InventoryAdjustmentResponseDto } from './dtos/InventoryAdjustmentResponse.dto';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { InventoryAdjustmentAction } from './types/InventoryAdjustments.types';
@Controller('inventory-adjustments') @Controller('inventory-adjustments')
@ApiTags('Inventory Adjustments') @ApiTags('Inventory Adjustments')
@ApiExtraModels(InventoryAdjustmentResponseDto) @ApiExtraModels(InventoryAdjustmentResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class InventoryAdjustmentsController { export class InventoryAdjustmentsController {
constructor( constructor(
private readonly inventoryAdjustmentsApplicationService: InventoryAdjustmentsApplicationService, private readonly inventoryAdjustmentsApplicationService: InventoryAdjustmentsApplicationService,
) {} ) {}
@Post('quick') @Post('quick')
@RequirePermission(InventoryAdjustmentAction.CREATE, AbilitySubject.InventoryAdjustment)
@ApiOperation({ summary: 'Create a quick inventory adjustment.' }) @ApiOperation({ summary: 'Create a quick inventory adjustment.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -46,6 +54,7 @@ export class InventoryAdjustmentsController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(InventoryAdjustmentAction.DELETE, AbilitySubject.InventoryAdjustment)
@ApiOperation({ summary: 'Delete the given inventory adjustment.' }) @ApiOperation({ summary: 'Delete the given inventory adjustment.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -60,6 +69,7 @@ export class InventoryAdjustmentsController {
} }
@Get() @Get()
@RequirePermission(InventoryAdjustmentAction.VIEW, AbilitySubject.InventoryAdjustment)
@ApiOperation({ summary: 'Retrieves the inventory adjustments.' }) @ApiOperation({ summary: 'Retrieves the inventory adjustments.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -78,6 +88,7 @@ export class InventoryAdjustmentsController {
} }
@Get(':id') @Get(':id')
@RequirePermission(InventoryAdjustmentAction.VIEW, AbilitySubject.InventoryAdjustment)
@ApiOperation({ summary: 'Retrieves the inventory adjustment details.' }) @ApiOperation({ summary: 'Retrieves the inventory adjustment details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -94,6 +105,7 @@ export class InventoryAdjustmentsController {
} }
@Put(':id/publish') @Put(':id/publish')
@RequirePermission(InventoryAdjustmentAction.EDIT, AbilitySubject.InventoryAdjustment)
@ApiOperation({ summary: 'Publish the given inventory adjustment.' }) @ApiOperation({ summary: 'Publish the given inventory adjustment.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -9,6 +9,7 @@ import {
Put, Put,
Query, Query,
HttpCode, HttpCode,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { TenantController } from '../Tenancy/Tenant.controller'; import { TenantController } from '../Tenancy/Tenant.controller';
import { ItemsApplicationService } from './ItemsApplication.service'; import { ItemsApplicationService } from './ItemsApplication.service';
@@ -35,6 +36,11 @@ import {
ValidateBulkDeleteItemsResponseDto, ValidateBulkDeleteItemsResponseDto,
} from './dtos/BulkDeleteItems.dto'; } from './dtos/BulkDeleteItems.dto';
import { ItemApiErrorResponseDto } from './dtos/ItemErrorResponse.dto'; import { ItemApiErrorResponseDto } from './dtos/ItemErrorResponse.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ItemAction } from '@/interfaces/Item';
@Controller('/items') @Controller('/items')
@ApiTags('Items') @ApiTags('Items')
@@ -48,12 +54,14 @@ import { ItemApiErrorResponseDto } from './dtos/ItemErrorResponse.dto';
@ApiExtraModels(ValidateBulkDeleteItemsResponseDto) @ApiExtraModels(ValidateBulkDeleteItemsResponseDto)
@ApiExtraModels(ItemApiErrorResponseDto) @ApiExtraModels(ItemApiErrorResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ItemsController extends TenantController { export class ItemsController extends TenantController {
constructor(private readonly itemsApplication: ItemsApplicationService) { constructor(private readonly itemsApplication: ItemsApplicationService) {
super(); super();
} }
@Get() @Get()
@RequirePermission(ItemAction.VIEW, AbilitySubject.Item)
@ApiOperation({ summary: 'Retrieves the item list.' }) @ApiOperation({ summary: 'Retrieves the item list.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -144,6 +152,7 @@ export class ItemsController extends TenantController {
* @returns The updated item id. * @returns The updated item id.
*/ */
@Put(':id') @Put(':id')
@RequirePermission(ItemAction.EDIT, AbilitySubject.Item)
@ApiOperation({ summary: 'Edit the given item (product or service).' }) @ApiOperation({ summary: 'Edit the given item (product or service).' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -174,6 +183,7 @@ export class ItemsController extends TenantController {
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@HttpCode(200) @HttpCode(200)
@RequirePermission(ItemAction.DELETE, AbilitySubject.Item)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validates which items can be deleted and returns counts of deletable and non-deletable items.', 'Validates which items can be deleted and returns counts of deletable and non-deletable items.',
@@ -194,6 +204,7 @@ export class ItemsController extends TenantController {
@Post('bulk-delete') @Post('bulk-delete')
@HttpCode(200) @HttpCode(200)
@RequirePermission(ItemAction.DELETE, AbilitySubject.Item)
@ApiOperation({ summary: 'Deletes multiple items in bulk.' }) @ApiOperation({ summary: 'Deletes multiple items in bulk.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -208,6 +219,7 @@ export class ItemsController extends TenantController {
} }
@Post() @Post()
@RequirePermission(ItemAction.CREATE, AbilitySubject.Item)
@ApiOperation({ summary: 'Create a new item (product or service).' }) @ApiOperation({ summary: 'Create a new item (product or service).' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -230,6 +242,7 @@ export class ItemsController extends TenantController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(ItemAction.DELETE, AbilitySubject.Item)
@ApiOperation({ summary: 'Delete the given item (product or service).' }) @ApiOperation({ summary: 'Delete the given item (product or service).' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -255,6 +268,7 @@ export class ItemsController extends TenantController {
} }
@Patch(':id/inactivate') @Patch(':id/inactivate')
@RequirePermission(ItemAction.EDIT, AbilitySubject.Item)
@ApiOperation({ summary: 'Inactivate the given item (product or service).' }) @ApiOperation({ summary: 'Inactivate the given item (product or service).' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -273,6 +287,7 @@ export class ItemsController extends TenantController {
} }
@Patch(':id/activate') @Patch(':id/activate')
@RequirePermission(ItemAction.EDIT, AbilitySubject.Item)
@ApiOperation({ summary: 'Activate the given item (product or service).' }) @ApiOperation({ summary: 'Activate the given item (product or service).' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -291,6 +306,7 @@ export class ItemsController extends TenantController {
} }
@Get(':id') @Get(':id')
@RequirePermission(ItemAction.VIEW, AbilitySubject.Item)
@ApiOperation({ summary: 'Get the given item (product or service).' }) @ApiOperation({ summary: 'Get the given item (product or service).' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -312,6 +328,7 @@ export class ItemsController extends TenantController {
} }
@Get(':id/invoices') @Get(':id/invoices')
@RequirePermission(ItemAction.VIEW, AbilitySubject.Item)
@ApiOperation({ @ApiOperation({
summary: 'Retrieves the item associated invoices transactions.', summary: 'Retrieves the item associated invoices transactions.',
}) })
@@ -337,6 +354,7 @@ export class ItemsController extends TenantController {
} }
@Get(':id/bills') @Get(':id/bills')
@RequirePermission(ItemAction.VIEW, AbilitySubject.Item)
@ApiOperation({ @ApiOperation({
summary: 'Retrieves the item associated bills transactions.', summary: 'Retrieves the item associated bills transactions.',
}) })
@@ -362,6 +380,7 @@ export class ItemsController extends TenantController {
} }
@Get(':id/estimates') @Get(':id/estimates')
@RequirePermission(ItemAction.VIEW, AbilitySubject.Item)
@ApiOperation({ @ApiOperation({
summary: 'Retrieves the item associated estimates transactions.', summary: 'Retrieves the item associated estimates transactions.',
}) })
@@ -387,6 +406,7 @@ export class ItemsController extends TenantController {
} }
@Get(':id/receipts') @Get(':id/receipts')
@RequirePermission(ItemAction.VIEW, AbilitySubject.Item)
@ApiOperation({ @ApiOperation({
summary: 'Retrieves the item associated receipts transactions.', summary: 'Retrieves the item associated receipts transactions.',
}) })

View File

@@ -8,6 +8,7 @@ import {
Post, Post,
Put, Put,
Query, Query,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ManualJournalsApplication } from './ManualJournalsApplication.service'; import { ManualJournalsApplication } from './ManualJournalsApplication.service';
import { import {
@@ -29,16 +30,23 @@ import {
BulkDeleteDto, BulkDeleteDto,
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ManualJournalAction } from './types/ManualJournals.types';
@Controller('manual-journals') @Controller('manual-journals')
@ApiTags('Manual Journals') @ApiTags('Manual Journals')
@ApiExtraModels(ManualJournalResponseDto) @ApiExtraModels(ManualJournalResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ManualJournalsController { export class ManualJournalsController {
constructor(private manualJournalsApplication: ManualJournalsApplication) { } constructor(private manualJournalsApplication: ManualJournalsApplication) { }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(ManualJournalAction.Delete, AbilitySubject.ManualJournal)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validate which manual journals can be deleted and return the results.', 'Validate which manual journals can be deleted and return the results.',
@@ -60,6 +68,7 @@ export class ManualJournalsController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(ManualJournalAction.Delete, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Deletes multiple manual journals.' }) @ApiOperation({ summary: 'Deletes multiple manual journals.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -75,6 +84,7 @@ export class ManualJournalsController {
} }
@Post() @Post()
@RequirePermission(ManualJournalAction.Create, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Create a new manual journal.' }) @ApiOperation({ summary: 'Create a new manual journal.' })
@ApiResponse({ @ApiResponse({
status: 201, status: 201,
@@ -86,6 +96,7 @@ export class ManualJournalsController {
} }
@Put(':id') @Put(':id')
@RequirePermission(ManualJournalAction.Edit, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Edit the given manual journal.' }) @ApiOperation({ summary: 'Edit the given manual journal.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -110,6 +121,7 @@ export class ManualJournalsController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(ManualJournalAction.Delete, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Delete the given manual journal.' }) @ApiOperation({ summary: 'Delete the given manual journal.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -127,6 +139,7 @@ export class ManualJournalsController {
} }
@Patch(':id/publish') @Patch(':id/publish')
@RequirePermission(ManualJournalAction.Edit, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Publish the given manual journal.' }) @ApiOperation({ summary: 'Publish the given manual journal.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -147,6 +160,7 @@ export class ManualJournalsController {
} }
@Get(':id') @Get(':id')
@RequirePermission(ManualJournalAction.View, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Retrieves the manual journal details.' }) @ApiOperation({ summary: 'Retrieves the manual journal details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -167,6 +181,7 @@ export class ManualJournalsController {
} }
@Get() @Get()
@RequirePermission(ManualJournalAction.View, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Retrieves the manual journals paginated list.' }) @ApiOperation({ summary: 'Retrieves the manual journals paginated list.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -19,6 +19,7 @@ import {
Put, Put,
Query, Query,
Res, Res,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { PaymentReceivesApplication } from './PaymentReceived.application'; import { PaymentReceivesApplication } from './PaymentReceived.application';
import { import {
@@ -38,6 +39,11 @@ import {
BulkDeleteDto, BulkDeleteDto,
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { PaymentReceiveAction } from './types/PaymentReceived.types';
@Controller('payments-received') @Controller('payments-received')
@ApiTags('Payments Received') @ApiTags('Payments Received')
@@ -46,6 +52,7 @@ import {
@ApiExtraModels(PaymentReceivedStateResponseDto) @ApiExtraModels(PaymentReceivedStateResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class PaymentReceivesController { export class PaymentReceivesController {
constructor(private paymentReceivesApplication: PaymentReceivesApplication) { } constructor(private paymentReceivesApplication: PaymentReceivesApplication) { }
@@ -94,6 +101,7 @@ export class PaymentReceivesController {
} }
@Post() @Post()
@RequirePermission(PaymentReceiveAction.Create, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Create a new payment received.' }) @ApiOperation({ summary: 'Create a new payment received.' })
public createPaymentReceived( public createPaymentReceived(
@Body() paymentReceiveDTO: CreatePaymentReceivedDto, @Body() paymentReceiveDTO: CreatePaymentReceivedDto,
@@ -104,6 +112,7 @@ export class PaymentReceivesController {
} }
@Put(':id') @Put(':id')
@RequirePermission(PaymentReceiveAction.Edit, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Edit the given payment received.' }) @ApiOperation({ summary: 'Edit the given payment received.' })
public editPaymentReceive( public editPaymentReceive(
@Param('id', ParseIntPipe) paymentReceiveId: number, @Param('id', ParseIntPipe) paymentReceiveId: number,
@@ -116,6 +125,7 @@ export class PaymentReceivesController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(PaymentReceiveAction.Delete, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Delete the given payment received.' }) @ApiOperation({ summary: 'Delete the given payment received.' })
public deletePaymentReceive( public deletePaymentReceive(
@Param('id', ParseIntPipe) paymentReceiveId: number, @Param('id', ParseIntPipe) paymentReceiveId: number,
@@ -126,6 +136,7 @@ export class PaymentReceivesController {
} }
@Get() @Get()
@RequirePermission(PaymentReceiveAction.View, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Retrieves the payment received list.' }) @ApiOperation({ summary: 'Retrieves the payment received list.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -151,6 +162,7 @@ export class PaymentReceivesController {
} }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(PaymentReceiveAction.Delete, AbilitySubject.PaymentReceive)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validates which payments received can be deleted and returns the results.', 'Validates which payments received can be deleted and returns the results.',
@@ -172,6 +184,7 @@ export class PaymentReceivesController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(PaymentReceiveAction.Delete, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Deletes multiple payments received.' }) @ApiOperation({ summary: 'Deletes multiple payments received.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -187,6 +200,7 @@ export class PaymentReceivesController {
} }
@Get('state') @Get('state')
@RequirePermission(PaymentReceiveAction.View, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Retrieves the payment received state.' }) @ApiOperation({ summary: 'Retrieves the payment received state.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -200,6 +214,7 @@ export class PaymentReceivesController {
} }
@Get(':id/invoices') @Get(':id/invoices')
@RequirePermission(PaymentReceiveAction.View, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Retrieves the payment received invoices.' }) @ApiOperation({ summary: 'Retrieves the payment received invoices.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -215,6 +230,7 @@ export class PaymentReceivesController {
} }
@Get(':id') @Get(':id')
@RequirePermission(PaymentReceiveAction.View, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Retrieves the payment received details.' }) @ApiOperation({ summary: 'Retrieves the payment received details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { BullBoardModule } from '@bull-board/nestjs'; import { BullBoardModule } from '@bull-board/nestjs';
import { BullAdapter } from '@bull-board/api/bullAdapter'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bullmq';
import { PaymentReceivesController } from './PaymentsReceived.controller'; import { PaymentReceivesController } from './PaymentsReceived.controller';
import { PaymentReceivesApplication } from './PaymentReceived.application'; import { PaymentReceivesApplication } from './PaymentReceived.application';
import { CreatePaymentReceivedService } from './commands/CreatePaymentReceived.serivce'; import { CreatePaymentReceivedService } from './commands/CreatePaymentReceived.serivce';
@@ -99,7 +99,7 @@ import { ValidateBulkDeletePaymentReceivedService } from './ValidateBulkDeletePa
BullModule.registerQueue({ name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE }), BullModule.registerQueue({ name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE }),
BullBoardModule.forFeature({ BullBoardModule.forFeature({
name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE, name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE,
adapter: BullAdapter, adapter: BullMQAdapter,
}), }),
], ],
}) })

View File

@@ -1,6 +1,6 @@
import { JOB_REF, Process, Processor } from '@nestjs/bull'; import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bull'; import { Job } from 'bullmq';
import { Inject, Scope } from '@nestjs/common'; import { Scope } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls'; import { ClsService, UseCls } from 'nestjs-cls';
import { import {
SEND_PAYMENT_RECEIVED_MAIL_JOB, SEND_PAYMENT_RECEIVED_MAIL_JOB,
@@ -13,20 +13,18 @@ import { SendPaymentReceivedMailPayload } from '../types/PaymentReceived.types';
name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE, name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE,
scope: Scope.REQUEST, scope: Scope.REQUEST,
}) })
export class SendPaymentReceivedMailProcessor { export class SendPaymentReceivedMailProcessor extends WorkerHost {
constructor( constructor(
private readonly sendPaymentReceivedMail: SendPaymentReceiveMailNotification, private readonly sendPaymentReceivedMail: SendPaymentReceiveMailNotification,
private readonly clsService: ClsService, private readonly clsService: ClsService,
) {
super();
}
@Inject(JOB_REF)
private readonly jobRef: Job<SendPaymentReceivedMailPayload>,
) { }
@Process(SEND_PAYMENT_RECEIVED_MAIL_JOB)
@UseCls() @UseCls()
async handleSendMail() { async process(job: Job<SendPaymentReceivedMailPayload>) {
const { messageOptions, paymentReceivedId, organizationId, userId } = const { messageOptions, paymentReceivedId, organizationId, userId } =
this.jobRef.data; job.data;
this.clsService.set('organizationId', organizationId); this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId); this.clsService.set('userId', userId);

View File

@@ -0,0 +1,67 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { REQUIRED_PERMISSION_KEY, RequiredPermission } from './RequirePermission.decorator';
import { AbilitySubject } from './Roles.types';
/**
* Guard that checks if the user has the required permission to access a route.
* Uses CASL ability instance attached to the request by AuthorizationGuard.
*/
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
/**
* Checks if the user has the required permission to access the route.
* @param context - The execution context
* @returns A boolean indicating if the user can access the route
* @throws ForbiddenException if the user doesn't have the required permission
*/
canActivate(context: ExecutionContext): boolean {
const requiredPermission = this.reflector.getAllAndOverride<RequiredPermission>(
REQUIRED_PERMISSION_KEY,
[context.getHandler(), context.getClass()],
);
// If no permission is required, allow access
if (!requiredPermission) {
return true;
}
const request = context.switchToHttp().getRequest<Request>();
const ability = (request as any).ability;
// If no ability instance is attached to the request, deny access
if (!ability) {
throw new ForbiddenException('Ability instance not found. Ensure AuthorizationGuard is applied.');
}
const { ability: action, subject } = requiredPermission;
// Check if the user has the required permission using CASL ability
const hasPermission = ability.can(action, subject);
if (!hasPermission) {
throw new ForbiddenException(
`You do not have permission to ${action} ${subject}`,
);
}
return true;
}
/**
* Helper method to check if a subject value is a valid AbilitySubject.
* @param subject - The subject value to check
* @returns True if the subject is a valid AbilitySubject enum value
*/
private isValidSubject(subject: string): subject is AbilitySubject {
return Object.values(AbilitySubject).includes(subject as AbilitySubject);
}
}

View File

@@ -0,0 +1,29 @@
import { SetMetadata } from '@nestjs/common';
import { AbilitySubject } from './Roles.types';
export const REQUIRED_PERMISSION_KEY = 'requiredPermission';
export interface RequiredPermission {
ability: string;
subject: AbilitySubject | string;
}
/**
* Decorator to specify required ability and subject for a route handler or controller.
* @param ability - The ability/action required (e.g., 'Create', 'View', 'Edit', 'Delete')
* @param subject - The subject/entity the ability applies to (e.g., AbilitySubject.Item, AbilitySubject.SaleInvoice)
* @example
* ```typescript
* @RequirePermission('Create', AbilitySubject.Item)
* @Post()
* async createItem(@Body() dto: CreateItemDto) { ... }
*
* @RequirePermission('View', AbilitySubject.SaleInvoice)
* @Get(':id')
* async getInvoice(@Param('id') id: number) { ... }
* ```
*/
export const RequirePermission = (
ability: string,
subject: AbilitySubject | string,
) => SetMetadata(REQUIRED_PERMISSION_KEY, { ability, subject });

View File

@@ -10,6 +10,8 @@ import { RolePermission } from './models/RolePermission.model';
import { RolesController } from './Roles.controller'; import { RolesController } from './Roles.controller';
import { RolesApplication } from './Roles.application'; import { RolesApplication } from './Roles.application';
import { RolePermissionsSchema } from './queries/RolePermissionsSchema'; import { RolePermissionsSchema } from './queries/RolePermissionsSchema';
import { AuthorizationGuard } from './Authorization.guard';
import { PermissionGuard } from './Permission.guard';
const models = [ const models = [
RegisterTenancyModel(Role), RegisterTenancyModel(Role),
@@ -25,9 +27,11 @@ const models = [
GetRoleService, GetRoleService,
GetRolesService, GetRolesService,
RolesApplication, RolesApplication,
RolePermissionsSchema RolePermissionsSchema,
AuthorizationGuard,
PermissionGuard,
], ],
controllers: [RolesController], controllers: [RolesController],
exports: [...models], exports: [...models, AuthorizationGuard, PermissionGuard],
}) })
export class RolesModule {} export class RolesModule {}

View File

@@ -1,3 +1,4 @@
import { IsOptional } from '@/common/decorators/Validators';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { import {
@@ -41,7 +42,7 @@ export class CommandRolePermissionDto {
export class CreateRolePermissionDto extends CommandRolePermissionDto { } export class CreateRolePermissionDto extends CommandRolePermissionDto { }
export class EditRolePermissionDto extends CommandRolePermissionDto { export class EditRolePermissionDto extends CommandRolePermissionDto {
@IsNumber() @IsNumber()
@IsNotEmpty() @IsOptional()
@ApiProperty({ @ApiProperty({
example: 1, example: 1,
description: 'The permission ID', description: 'The permission ID',
@@ -59,7 +60,6 @@ class CommandRoleDto {
roleName: string; roleName: string;
@IsString() @IsString()
@IsNotEmpty()
@ApiProperty({ @ApiProperty({
example: 'Administrator', example: 'Administrator',
description: 'The description of the role', description: 'The description of the role',
@@ -71,9 +71,9 @@ export class CreateRoleDto extends CommandRoleDto {
@IsArray() @IsArray()
@ArrayMinSize(1) @ArrayMinSize(1)
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => CommandRolePermissionDto) @Type(() => CreateRolePermissionDto)
@ApiProperty({ @ApiProperty({
type: [CommandRolePermissionDto], type: [CreateRolePermissionDto],
description: 'The permissions of the role', description: 'The permissions of the role',
}) })
permissions: Array<CreateRolePermissionDto>; permissions: Array<CreateRolePermissionDto>;

View File

@@ -19,6 +19,7 @@ import {
Put, Put,
Query, Query,
Res, Res,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { SaleEstimatesApplication } from './SaleEstimates.application'; import { SaleEstimatesApplication } from './SaleEstimates.application';
import { import {
@@ -40,6 +41,11 @@ import {
BulkDeleteDto, BulkDeleteDto,
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { SaleEstimateAction } from './types/SaleEstimates.types';
@Controller('sale-estimates') @Controller('sale-estimates')
@ApiTags('Sale Estimates') @ApiTags('Sale Estimates')
@@ -48,8 +54,10 @@ import {
@ApiExtraModels(SaleEstiamteStateResponseDto) @ApiExtraModels(SaleEstiamteStateResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiExtraModels(ValidateBulkDeleteResponseDto)
@UseGuards(AuthorizationGuard, PermissionGuard)
export class SaleEstimatesController { export class SaleEstimatesController {
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(SaleEstimateAction.Delete, AbilitySubject.SaleEstimate)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validates which sale estimates can be deleted and returns the results.', 'Validates which sale estimates can be deleted and returns the results.',
@@ -71,6 +79,7 @@ export class SaleEstimatesController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(SaleEstimateAction.Delete, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Deletes multiple sale estimates.' }) @ApiOperation({ summary: 'Deletes multiple sale estimates.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -93,6 +102,7 @@ export class SaleEstimatesController {
) { } ) { }
@Post() @Post()
@RequirePermission(SaleEstimateAction.Create, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Create a new sale estimate.' }) @ApiOperation({ summary: 'Create a new sale estimate.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -105,6 +115,7 @@ export class SaleEstimatesController {
} }
@Put(':id') @Put(':id')
@RequirePermission(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Edit the given sale estimate.' }) @ApiOperation({ summary: 'Edit the given sale estimate.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -131,6 +142,7 @@ export class SaleEstimatesController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(SaleEstimateAction.Delete, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Delete the given sale estimate.' }) @ApiOperation({ summary: 'Delete the given sale estimate.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -153,6 +165,7 @@ export class SaleEstimatesController {
} }
@Get('state') @Get('state')
@RequirePermission(SaleEstimateAction.View, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Retrieves the sale estimate state.' }) @ApiOperation({ summary: 'Retrieves the sale estimate state.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -166,6 +179,7 @@ export class SaleEstimatesController {
} }
@Get() @Get()
@RequirePermission(SaleEstimateAction.View, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Retrieves the sale estimates.' }) @ApiOperation({ summary: 'Retrieves the sale estimates.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -189,6 +203,7 @@ export class SaleEstimatesController {
} }
@Post(':id/deliver') @Post(':id/deliver')
@RequirePermission(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Deliver the given sale estimate.' }) @ApiOperation({ summary: 'Deliver the given sale estimate.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -207,6 +222,7 @@ export class SaleEstimatesController {
} }
@Put(':id/approve') @Put(':id/approve')
@RequirePermission(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Approve the given sale estimate.' }) @ApiOperation({ summary: 'Approve the given sale estimate.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@@ -221,6 +237,7 @@ export class SaleEstimatesController {
} }
@Put(':id/reject') @Put(':id/reject')
@RequirePermission(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Reject the given sale estimate.' }) @ApiOperation({ summary: 'Reject the given sale estimate.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@@ -235,6 +252,7 @@ export class SaleEstimatesController {
} }
@Post(':id/notify-sms') @Post(':id/notify-sms')
@RequirePermission(SaleEstimateAction.NotifyBySms, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Notify the given sale estimate by SMS.' }) @ApiOperation({ summary: 'Notify the given sale estimate by SMS.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@@ -251,6 +269,7 @@ export class SaleEstimatesController {
} }
@Get(':id/sms-details') @Get(':id/sms-details')
@RequirePermission(SaleEstimateAction.View, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Retrieves the sale estimate SMS details.' }) @ApiOperation({ summary: 'Retrieves the sale estimate SMS details.' })
public getSaleEstimateSmsDetails( public getSaleEstimateSmsDetails(
@Param('id', ParseIntPipe) saleEstimateId: number, @Param('id', ParseIntPipe) saleEstimateId: number,
@@ -262,6 +281,7 @@ export class SaleEstimatesController {
@Post(':id/mail') @Post(':id/mail')
@HttpCode(200) @HttpCode(200)
@RequirePermission(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Send the given sale estimate by mail.' }) @ApiOperation({ summary: 'Send the given sale estimate by mail.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@@ -280,6 +300,7 @@ export class SaleEstimatesController {
} }
@Get(':id/mail') @Get(':id/mail')
@RequirePermission(SaleEstimateAction.View, AbilitySubject.SaleEstimate)
@ApiOperation({ summary: 'Retrieves the sale estimate mail state.' }) @ApiOperation({ summary: 'Retrieves the sale estimate mail state.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@@ -296,6 +317,7 @@ export class SaleEstimatesController {
} }
@Get(':id') @Get(':id')
@RequirePermission(SaleEstimateAction.View, AbilitySubject.SaleEstimate)
@ApiOperation({ @ApiOperation({
summary: 'Retrieves the sale estimate details.', summary: 'Retrieves the sale estimate details.',
}) })

View File

@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { BullBoardModule } from '@bull-board/nestjs'; import { BullBoardModule } from '@bull-board/nestjs';
import { BullAdapter } from '@bull-board/api/bullAdapter'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bullmq';
import { TenancyContext } from '../Tenancy/TenancyContext.service'; import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module'; import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service'; import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
@@ -58,7 +58,7 @@ import { SendSaleEstimateMailProcess } from './processes/SendSaleEstimateMail.pr
BullModule.registerQueue({ name: SendSaleEstimateMailQueue }), BullModule.registerQueue({ name: SendSaleEstimateMailQueue }),
BullBoardModule.forFeature({ BullBoardModule.forFeature({
name: SendSaleEstimateMailQueue, name: SendSaleEstimateMailQueue,
adapter: BullAdapter, adapter: BullMQAdapter,
}), }),
], ],
controllers: [SaleEstimatesController], controllers: [SaleEstimatesController],

View File

@@ -1,5 +1,5 @@
import { InjectQueue } from '@nestjs/bullmq'; import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bull'; import { Queue } from 'bullmq';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { ContactMailNotification } from '@/modules/MailNotification/ContactMailNotification'; import { ContactMailNotification } from '@/modules/MailNotification/ContactMailNotification';

View File

@@ -1,7 +1,6 @@
import { Process, Processor } from '@nestjs/bull'; import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bull'; import { Job } from 'bullmq';
import { Inject, Scope } from '@nestjs/common'; import { Scope } from '@nestjs/common';
import { JOB_REF } from '@nestjs/bull';
import { import {
SendSaleEstimateMailJob, SendSaleEstimateMailJob,
SendSaleEstimateMailQueue, SendSaleEstimateMailQueue,
@@ -13,18 +12,17 @@ import { ClsService, UseCls } from 'nestjs-cls';
name: SendSaleEstimateMailQueue, name: SendSaleEstimateMailQueue,
scope: Scope.REQUEST, scope: Scope.REQUEST,
}) })
export class SendSaleEstimateMailProcess { export class SendSaleEstimateMailProcess extends WorkerHost {
constructor( constructor(
private readonly sendEstimateMailService: SendSaleEstimateMail, private readonly sendEstimateMailService: SendSaleEstimateMail,
private readonly clsService: ClsService, private readonly clsService: ClsService,
@Inject(JOB_REF) ) {
private readonly jobRef: Job, super();
) { } }
@Process(SendSaleEstimateMailJob)
@UseCls() @UseCls()
async handleSendMail() { async process(job: Job) {
const { saleEstimateId, messageOptions, organizationId, userId } = this.jobRef.data; const { saleEstimateId, messageOptions, organizationId, userId } = job.data;
this.clsService.set('organizationId', organizationId); this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId); this.clsService.set('userId', userId);

View File

@@ -12,6 +12,7 @@ import {
Put, Put,
Query, Query,
Res, Res,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { import {
ISaleInvoiceWriteoffDTO, ISaleInvoiceWriteoffDTO,
@@ -43,6 +44,11 @@ import {
BulkDeleteDto, BulkDeleteDto,
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { SaleInvoiceAction } from './SaleInvoice.types';
@Controller('sale-invoices') @Controller('sale-invoices')
@ApiTags('Sale Invoices') @ApiTags('Sale Invoices')
@@ -52,10 +58,12 @@ import {
@ApiExtraModels(GenerateSaleInvoiceSharableLinkResponseDto) @ApiExtraModels(GenerateSaleInvoiceSharableLinkResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiExtraModels(ValidateBulkDeleteResponseDto)
@UseGuards(AuthorizationGuard, PermissionGuard)
export class SaleInvoicesController { export class SaleInvoicesController {
constructor(private saleInvoiceApplication: SaleInvoiceApplication) { } constructor(private saleInvoiceApplication: SaleInvoiceApplication) { }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(SaleInvoiceAction.Delete, AbilitySubject.SaleInvoice)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validates which sale invoices can be deleted and returns the results.', 'Validates which sale invoices can be deleted and returns the results.',
@@ -77,6 +85,7 @@ export class SaleInvoicesController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(SaleInvoiceAction.Delete, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Deletes multiple sale invoices.' }) @ApiOperation({ summary: 'Deletes multiple sale invoices.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -90,6 +99,7 @@ export class SaleInvoicesController {
} }
@Post() @Post()
@RequirePermission(SaleInvoiceAction.Create, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Create a new sale invoice.' }) @ApiOperation({ summary: 'Create a new sale invoice.' })
@ApiResponse({ @ApiResponse({
status: 201, status: 201,
@@ -121,6 +131,7 @@ export class SaleInvoicesController {
} }
@Put(':id') @Put(':id')
@RequirePermission(SaleInvoiceAction.Edit, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Edit the given sale invoice.' }) @ApiOperation({ summary: 'Edit the given sale invoice.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -141,6 +152,7 @@ export class SaleInvoicesController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(SaleInvoiceAction.Delete, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Delete the given sale invoice.' }) @ApiOperation({ summary: 'Delete the given sale invoice.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -158,6 +170,7 @@ export class SaleInvoicesController {
} }
@Get('receivable') @Get('receivable')
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the receivable sale invoices.' }) @ApiOperation({ summary: 'Retrieves the receivable sale invoices.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -176,6 +189,7 @@ export class SaleInvoicesController {
} }
@Get('state') @Get('state')
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the sale invoice state.' }) @ApiOperation({ summary: 'Retrieves the sale invoice state.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -190,6 +204,7 @@ export class SaleInvoicesController {
} }
@Get(':id') @Get(':id')
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the sale invoice details.' }) @ApiOperation({ summary: 'Retrieves the sale invoice details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -228,6 +243,7 @@ export class SaleInvoicesController {
} }
@Get() @Get()
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the sale invoices.' }) @ApiOperation({ summary: 'Retrieves the sale invoices.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -251,6 +267,7 @@ export class SaleInvoicesController {
} }
@Put(':id/deliver') @Put(':id/deliver')
@RequirePermission(SaleInvoiceAction.Edit, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Deliver the given sale invoice.' }) @ApiOperation({ summary: 'Deliver the given sale invoice.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -269,6 +286,7 @@ export class SaleInvoicesController {
} }
@Post(':id/writeoff') @Post(':id/writeoff')
@RequirePermission(SaleInvoiceAction.Writeoff, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Write off the given sale invoice.' }) @ApiOperation({ summary: 'Write off the given sale invoice.' })
@HttpCode(200) @HttpCode(200)
@ApiResponse({ @ApiResponse({
@@ -290,6 +308,7 @@ export class SaleInvoicesController {
} }
@Post(':id/cancel-writeoff') @Post(':id/cancel-writeoff')
@RequirePermission(SaleInvoiceAction.Writeoff, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Cancel the written off sale invoice.' }) @ApiOperation({ summary: 'Cancel the written off sale invoice.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -309,6 +328,7 @@ export class SaleInvoicesController {
} }
@Get(':id/payments') @Get(':id/payments')
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the sale invoice payments.' }) @ApiOperation({ summary: 'Retrieves the sale invoice payments.' })
@ApiResponse({ status: 404, description: 'The sale invoice not found.' }) @ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({ @ApiParam({
@@ -322,6 +342,7 @@ export class SaleInvoicesController {
} }
@Get(':id/html') @Get(':id/html')
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the sale invoice HTML.' }) @ApiOperation({ summary: 'Retrieves the sale invoice HTML.' })
@ApiResponse({ status: 404, description: 'The sale invoice not found.' }) @ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({ @ApiParam({
@@ -335,6 +356,7 @@ export class SaleInvoicesController {
} }
@Get(':id/mail') @Get(':id/mail')
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the sale invoice mail state.' }) @ApiOperation({ summary: 'Retrieves the sale invoice mail state.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -354,6 +376,7 @@ export class SaleInvoicesController {
} }
@Post(':id/generate-link') @Post(':id/generate-link')
@RequirePermission(SaleInvoiceAction.Edit, AbilitySubject.SaleInvoice)
@ApiOperation({ @ApiOperation({
summary: 'Generate sharable sale invoice link (private or public)', summary: 'Generate sharable sale invoice link (private or public)',
}) })

View File

@@ -46,8 +46,8 @@ import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { MailNotificationModule } from '../MailNotification/MailNotification.module'; import { MailNotificationModule } from '../MailNotification/MailNotification.module';
import { SendSaleInvoiceMailProcessor } from './processors/SendSaleInvoiceMail.processor'; import { SendSaleInvoiceMailProcessor } from './processors/SendSaleInvoiceMail.processor';
import { BullBoardModule } from '@bull-board/nestjs'; import { BullBoardModule } from '@bull-board/nestjs';
import { BullAdapter } from '@bull-board/api/bullAdapter'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bullmq';
import { SendSaleInvoiceQueue } from './constants'; import { SendSaleInvoiceQueue } from './constants';
import { InvoicePaymentIntegrationSubscriber } from './subscribers/InvoicePaymentIntegrationSubscriber'; import { InvoicePaymentIntegrationSubscriber } from './subscribers/InvoicePaymentIntegrationSubscriber';
import { InvoiceChangeStatusOnMailSentSubscriber } from './subscribers/InvoiceChangeStatusOnMailSentSubscriber'; import { InvoiceChangeStatusOnMailSentSubscriber } from './subscribers/InvoiceChangeStatusOnMailSentSubscriber';
@@ -85,7 +85,7 @@ import { ValidateBulkDeleteSaleInvoicesService } from './ValidateBulkDeleteSaleI
BullModule.registerQueue({ name: SendSaleInvoiceQueue }), BullModule.registerQueue({ name: SendSaleInvoiceQueue }),
BullBoardModule.forFeature({ BullBoardModule.forFeature({
name: SendSaleInvoiceQueue, name: SendSaleInvoiceQueue,
adapter: BullAdapter, adapter: BullMQAdapter,
}), }),
], ],
controllers: [SaleInvoicesController], controllers: [SaleInvoicesController],

View File

@@ -1,9 +1,8 @@
import { JOB_REF, Process, Processor } from '@nestjs/bull'; import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bull'; import { Job } from 'bullmq';
import { SendSaleInvoiceMailJob, SendSaleInvoiceQueue } from '../constants'; import { SendSaleInvoiceMailJob, SendSaleInvoiceQueue } from '../constants';
import { SendSaleInvoiceMail } from '../commands/SendSaleInvoiceMail'; import { SendSaleInvoiceMail } from '../commands/SendSaleInvoiceMail';
import { Inject, Scope } from '@nestjs/common'; import { Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { ClsService, UseCls } from 'nestjs-cls'; import { ClsService, UseCls } from 'nestjs-cls';
import { SendSaleInvoiceMailJobPayload } from '../SaleInvoice.types'; import { SendSaleInvoiceMailJobPayload } from '../SaleInvoice.types';
@@ -11,20 +10,18 @@ import { SendSaleInvoiceMailJobPayload } from '../SaleInvoice.types';
name: SendSaleInvoiceQueue, name: SendSaleInvoiceQueue,
scope: Scope.REQUEST, scope: Scope.REQUEST,
}) })
export class SendSaleInvoiceMailProcessor { export class SendSaleInvoiceMailProcessor extends WorkerHost {
constructor( constructor(
private readonly sendSaleInvoiceMail: SendSaleInvoiceMail, private readonly sendSaleInvoiceMail: SendSaleInvoiceMail,
@Inject(REQUEST) private readonly request: Request,
@Inject(JOB_REF)
private readonly jobRef: Job<SendSaleInvoiceMailJobPayload>,
private readonly clsService: ClsService, private readonly clsService: ClsService,
) { } ) {
super();
}
@Process(SendSaleInvoiceMailJob)
@UseCls() @UseCls()
async handleSendInvoice() { async process(job: Job<SendSaleInvoiceMailJobPayload>) {
const { messageOptions, saleInvoiceId, organizationId, userId } = const { messageOptions, saleInvoiceId, organizationId, userId } =
this.jobRef.data; job.data;
this.clsService.set('organizationId', organizationId); this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId); this.clsService.set('userId', userId);

View File

@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { BullBoardModule } from '@bull-board/nestjs'; import { BullBoardModule } from '@bull-board/nestjs';
import { BullAdapter } from '@bull-board/api/bullAdapter'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bullmq';
import { SaleReceiptApplication } from './SaleReceiptApplication.service'; import { SaleReceiptApplication } from './SaleReceiptApplication.service';
import { CreateSaleReceipt } from './commands/CreateSaleReceipt.service'; import { CreateSaleReceipt } from './commands/CreateSaleReceipt.service';
import { EditSaleReceipt } from './commands/EditSaleReceipt.service'; import { EditSaleReceipt } from './commands/EditSaleReceipt.service';
@@ -66,7 +66,7 @@ import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleR
BullModule.registerQueue({ name: SendSaleReceiptMailQueue }), BullModule.registerQueue({ name: SendSaleReceiptMailQueue }),
BullBoardModule.forFeature({ BullBoardModule.forFeature({
name: SendSaleReceiptMailQueue, name: SendSaleReceiptMailQueue,
adapter: BullAdapter, adapter: BullMQAdapter,
}), }),
], ],
providers: [ providers: [

View File

@@ -1,4 +1,4 @@
import { InjectQueue } from '@nestjs/bull'; import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq'; import { Queue } from 'bullmq';
import { import {
DEFAULT_RECEIPT_MAIL_CONTENT, DEFAULT_RECEIPT_MAIL_CONTENT,

View File

@@ -1,30 +1,26 @@
import { Process, Processor } from '@nestjs/bull'; import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bull'; import { Job } from 'bullmq';
import { Inject, Scope } from '@nestjs/common'; import { Scope } from '@nestjs/common';
import { JOB_REF } from '@nestjs/bull';
import { SendSaleReceiptMailQueue, SendSaleReceiptMailJob } from '../constants'; import { SendSaleReceiptMailQueue, SendSaleReceiptMailJob } from '../constants';
import { SaleReceiptMailNotification } from '../commands/SaleReceiptMailNotification'; import { SaleReceiptMailNotification } from '../commands/SaleReceiptMailNotification';
import { SaleReceiptSendMailPayload } from '../types/SaleReceipts.types';
import { ClsService, UseCls } from 'nestjs-cls'; import { ClsService, UseCls } from 'nestjs-cls';
@Processor({ @Processor({
name: SendSaleReceiptMailQueue, name: SendSaleReceiptMailQueue,
scope: Scope.REQUEST, scope: Scope.REQUEST,
}) })
export class SendSaleReceiptMailProcess { export class SendSaleReceiptMailProcess extends WorkerHost {
constructor( constructor(
private readonly saleReceiptMailNotification: SaleReceiptMailNotification, private readonly saleReceiptMailNotification: SaleReceiptMailNotification,
private readonly clsService: ClsService, private readonly clsService: ClsService,
) {
super();
}
@Inject(JOB_REF)
private readonly jobRef: Job<SaleReceiptSendMailPayload>,
) { }
@Process(SendSaleReceiptMailJob)
@UseCls() @UseCls()
async handleSendMailJob() { async process(job: Job) {
const { messageOpts, saleReceiptId, organizationId, userId } = const { messageOpts, saleReceiptId, organizationId, userId } =
this.jobRef.data; job.data;
this.clsService.set('organizationId', organizationId); this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId); this.clsService.set('userId', userId);

View File

@@ -1,16 +1,22 @@
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Body, Controller, Get, Put } from '@nestjs/common'; import { Body, Controller, Get, Put, UseGuards } from '@nestjs/common';
import { SettingsApplicationService } from './SettingsApplication.service'; import { SettingsApplicationService } from './SettingsApplication.service';
import { ISettingsDTO } from './Settings.types'; import { ISettingsDTO, PreferencesAction } from './Settings.types';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
@Controller('settings') @Controller('settings')
@ApiTags('Settings') @ApiTags('Settings')
@UseGuards(AuthorizationGuard, PermissionGuard)
export class SettingsController { export class SettingsController {
constructor( constructor(
private readonly settingsApplicationService: SettingsApplicationService, private readonly settingsApplicationService: SettingsApplicationService,
) {} ) {}
@Put() @Put()
@RequirePermission(PreferencesAction.Mutate, AbilitySubject.Preferences)
@ApiOperation({ summary: 'Save the given settings.' }) @ApiOperation({ summary: 'Save the given settings.' })
async saveSettings(@Body() settingsDTO: ISettingsDTO) { async saveSettings(@Body() settingsDTO: ISettingsDTO) {
return this.settingsApplicationService.saveSettings(settingsDTO); return this.settingsApplicationService.saveSettings(settingsDTO);

View File

@@ -62,10 +62,11 @@ export class TaxRatesApplication {
/** /**
* Retrieves the tax rates list. * Retrieves the tax rates list.
* @returns {Promise<ITaxRate[]>} * @returns {Promise<{ data: ITaxRate[] }>}
*/ */
public getTaxRates() { public async getTaxRates() {
return this.getTaxRatesService.getTaxRates(); const taxRates = await this.getTaxRatesService.getTaxRates();
return { data: taxRates };
} }
/** /**

View File

@@ -6,6 +6,7 @@ import {
Param, Param,
Post, Post,
Put, Put,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { TaxRatesApplication } from './TaxRate.application'; import { TaxRatesApplication } from './TaxRate.application';
import { import {
@@ -18,15 +19,22 @@ import {
import { CreateTaxRateDto, EditTaxRateDto } from './dtos/TaxRate.dto'; import { CreateTaxRateDto, EditTaxRateDto } from './dtos/TaxRate.dto';
import { TaxRateResponseDto } from './dtos/TaxRateResponse.dto'; import { TaxRateResponseDto } from './dtos/TaxRateResponse.dto';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { TaxRateAction } from './TaxRates.types';
@Controller('tax-rates') @Controller('tax-rates')
@ApiTags('Tax Rates') @ApiTags('Tax Rates')
@ApiExtraModels(TaxRateResponseDto) @ApiExtraModels(TaxRateResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class TaxRatesController { export class TaxRatesController {
constructor(private readonly taxRatesApplication: TaxRatesApplication) { } constructor(private readonly taxRatesApplication: TaxRatesApplication) { }
@Post() @Post()
@RequirePermission(TaxRateAction.CREATE, AbilitySubject.TaxRate)
@ApiOperation({ summary: 'Create a new tax rate.' }) @ApiOperation({ summary: 'Create a new tax rate.' })
@ApiResponse({ @ApiResponse({
status: 201, status: 201,
@@ -38,6 +46,7 @@ export class TaxRatesController {
} }
@Put(':id') @Put(':id')
@RequirePermission(TaxRateAction.EDIT, AbilitySubject.TaxRate)
@ApiOperation({ summary: 'Edit the given tax rate.' }) @ApiOperation({ summary: 'Edit the given tax rate.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -54,6 +63,7 @@ export class TaxRatesController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(TaxRateAction.DELETE, AbilitySubject.TaxRate)
@ApiOperation({ summary: 'Delete the given tax rate.' }) @ApiOperation({ summary: 'Delete the given tax rate.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -67,6 +77,7 @@ export class TaxRatesController {
} }
@Get(':id') @Get(':id')
@RequirePermission(TaxRateAction.VIEW, AbilitySubject.TaxRate)
@ApiOperation({ summary: 'Retrieves the tax rate details.' }) @ApiOperation({ summary: 'Retrieves the tax rate details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -80,22 +91,29 @@ export class TaxRatesController {
} }
@Get() @Get()
@RequirePermission(TaxRateAction.VIEW, AbilitySubject.TaxRate)
@ApiOperation({ summary: 'Retrieves the tax rates.' }) @ApiOperation({ summary: 'Retrieves the tax rates.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'The tax rates have been successfully retrieved.', description: 'The tax rates have been successfully retrieved.',
schema: { schema: {
type: 'object',
properties: {
data: {
type: 'array', type: 'array',
items: { items: {
$ref: getSchemaPath(TaxRateResponseDto), $ref: getSchemaPath(TaxRateResponseDto),
}, },
}, },
},
},
}) })
public getTaxRates() { public getTaxRates() {
return this.taxRatesApplication.getTaxRates(); return this.taxRatesApplication.getTaxRates();
} }
@Put(':id/activate') @Put(':id/activate')
@RequirePermission(TaxRateAction.EDIT, AbilitySubject.TaxRate)
@ApiOperation({ summary: 'Activate the given tax rate.' }) @ApiOperation({ summary: 'Activate the given tax rate.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -109,6 +127,7 @@ export class TaxRatesController {
} }
@Put(':id/inactivate') @Put(':id/inactivate')
@RequirePermission(TaxRateAction.EDIT, AbilitySubject.TaxRate)
@ApiOperation({ summary: 'Inactivate the given tax rate.' }) @ApiOperation({ summary: 'Inactivate the given tax rate.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -1,3 +1,4 @@
import { ToNumber } from '@/common/decorators/Validators';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { import {
@@ -30,6 +31,7 @@ export class CommandTaxRateDto {
*/ */
@IsNumber() @IsNumber()
@IsNotEmpty() @IsNotEmpty()
@ToNumber()
@ApiProperty({ @ApiProperty({
description: 'The rate of the tax rate.', description: 'The rate of the tax rate.',
example: 10, example: 10,

View File

@@ -22,7 +22,7 @@ export class UsersController {
/** /**
* Edit details of the given user. * Edit details of the given user.
*/ */
@Post(':id') @Put(':id')
@ApiOperation({ summary: 'Edit details of the given user.' }) @ApiOperation({ summary: 'Edit details of the given user.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -1,4 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bullmq';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ActivateUserService } from './commands/ActivateUser.service'; import { ActivateUserService } from './commands/ActivateUser.service';
import { DeleteUserService } from './commands/DeleteUser.service'; import { DeleteUserService } from './commands/DeleteUser.service';
import { EditUserService } from './commands/EditUser.service'; import { EditUserService } from './commands/EditUser.service';
@@ -18,11 +21,24 @@ import { AcceptInviteUserService } from './commands/AcceptInviteUser.service';
import { InviteTenantUserService } from './commands/InviteUser.service'; import { InviteTenantUserService } from './commands/InviteUser.service';
import { UsersInviteController } from './UsersInvite.controller'; import { UsersInviteController } from './UsersInvite.controller';
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module'; import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
import { SendInviteUserMailQueue } from './Users.constants';
import InviteSendMainNotificationSubscribe from './subscribers/InviteSendMailNotification.subscriber';
import { SendInviteUserMailProcessor } from './processors/SendInviteUserMail.processor';
import { SendInviteUsersMailMessage } from './commands/SendInviteUsersMailMessage.service';
import { MailModule } from '../Mail/Mail.module';
const models = [InjectSystemModel(UserInvite)]; const models = [InjectSystemModel(UserInvite)];
@Module({ @Module({
imports: [TenancyModule], imports: [
TenancyModule,
MailModule,
BullModule.registerQueue({ name: SendInviteUserMailQueue }),
BullBoardModule.forFeature({
name: SendInviteUserMailQueue,
adapter: BullMQAdapter,
}),
],
exports: [...models], exports: [...models],
providers: [ providers: [
...models, ...models,
@@ -39,6 +55,9 @@ const models = [InjectSystemModel(UserInvite)];
SyncTenantUserMutateSubscriber, SyncTenantUserMutateSubscriber,
SyncSystemSendInviteSubscriber, SyncSystemSendInviteSubscriber,
SyncTenantAcceptInviteSubscriber, SyncTenantAcceptInviteSubscriber,
InviteSendMainNotificationSubscribe,
SendInviteUserMailProcessor,
SendInviteUsersMailMessage,
UsersApplication UsersApplication
], ],
controllers: [UsersController, UsersInviteController], controllers: [UsersController, UsersInviteController],

View File

@@ -32,10 +32,12 @@ export interface ITenantUserDeletedPayload {
export interface IUserInvitedEventPayload { export interface IUserInvitedEventPayload {
inviteToken: string; inviteToken: string;
user: ModelObject<TenantUser>; user: ModelObject<TenantUser>;
invitingUser: ModelObject<TenantUser>;
} }
export interface IUserInviteTenantSyncedEventPayload { export interface IUserInviteTenantSyncedEventPayload {
invite: ModelObject<UserInvite>; invite: ModelObject<UserInvite>;
user: ModelObject<TenantUser>; user: ModelObject<TenantUser>;
invitingUser: ModelObject<TenantUser>;
} }
export interface IUserInviteResendEventPayload { export interface IUserInviteResendEventPayload {

View File

@@ -43,11 +43,11 @@ export class InactivateUserService {
// Throw serivce error if the user is already inactivated. // Throw serivce error if the user is already inactivated.
this.throwErrorIfUserInactive(tenantUser); this.throwErrorIfUserInactive(tenantUser);
// Marks the tenant user as active. // Marks the tenant user as inactive.
await this.tenantUserModel() await this.tenantUserModel()
.query() .query()
.findById(userId) .findById(userId)
.update({ active: true }); .update({ active: false });
// Triggers `onTenantUserActivated` event. // Triggers `onTenantUserActivated` event.
await this.eventEmitter.emitAsync(events.tenantUser.onInactivated, { await this.eventEmitter.emitAsync(events.tenantUser.onInactivated, {

View File

@@ -15,11 +15,13 @@ import { events } from '@/common/events/events';
import { Role } from '@/modules/Roles/models/Role.model'; import { Role } from '@/modules/Roles/models/Role.model';
import { ModelObject } from 'objection'; import { ModelObject } from 'objection';
import { SendInviteUserDto } from '../dtos/InviteUser.dto'; import { SendInviteUserDto } from '../dtos/InviteUser.dto';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable() @Injectable()
export class InviteTenantUserService { export class InviteTenantUserService {
constructor( constructor(
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly tenancyContext: TenancyContext,
@Inject(TenantUser.name) @Inject(TenantUser.name)
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>, private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
@@ -53,10 +55,18 @@ export class InviteTenantUserService {
active: true, active: true,
invitedAt: new Date(), invitedAt: new Date(),
}); });
// Retrieves the authorized user (inviting user).
const authorizedUser = await this.tenancyContext.getSystemUser();
const invitingUser = await this.tenantUserModel()
.query()
.findOne({ systemUserId: authorizedUser.id });
// Triggers `onUserSendInvite` event. // Triggers `onUserSendInvite` event.
await this.eventEmitter.emitAsync(events.inviteUser.sendInvite, { await this.eventEmitter.emitAsync(events.inviteUser.sendInvite, {
inviteToken, inviteToken,
user, user,
invitingUser,
} as IUserInvitedEventPayload); } as IUserInvitedEventPayload);
return { invitedUser: user }; return { invitedUser: user };

View File

@@ -27,7 +27,7 @@ export class SendInviteUsersMailMessage {
invite: ModelObject<UserInvite>, invite: ModelObject<UserInvite>,
) { ) {
const tenant = await this.tenancyContext.getTenant(true); const tenant = await this.tenancyContext.getTenant(true);
const root = path.join(global.__views_dir, '/images/bigcapital.png'); const root = path.join(global.__images_dirname, '/bigcapital.png');
const baseURL = this.configService.get('baseURL'); const baseURL = this.configService.get('baseURL');
const mail = new Mail() const mail = new Mail()

View File

@@ -1,7 +1,6 @@
import { JOB_REF, Process, Processor } from '@nestjs/bull'; import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bull'; import { Job } from 'bullmq';
import { Inject, Scope } from '@nestjs/common'; import { Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { ClsService, UseCls } from 'nestjs-cls'; import { ClsService, UseCls } from 'nestjs-cls';
import { import {
SendInviteUserMailJob, SendInviteUserMailJob,
@@ -14,19 +13,17 @@ import { SendInviteUsersMailMessage } from '../commands/SendInviteUsersMailMessa
name: SendInviteUserMailQueue, name: SendInviteUserMailQueue,
scope: Scope.REQUEST, scope: Scope.REQUEST,
}) })
export class SendInviteUserMailProcessor { export class SendInviteUserMailProcessor extends WorkerHost {
constructor( constructor(
private readonly sendInviteUsersMailService: SendInviteUsersMailMessage, private readonly sendInviteUsersMailService: SendInviteUsersMailMessage,
@Inject(REQUEST) private readonly request: Request,
@Inject(JOB_REF)
private readonly jobRef: Job<SendInviteUserMailJobPayload>,
private readonly clsService: ClsService, private readonly clsService: ClsService,
) { } ) {
super();
}
@Process(SendInviteUserMailJob)
@UseCls() @UseCls()
async handleSendInviteMail() { async process(job: Job<SendInviteUserMailJobPayload>) {
const { fromUser, invite, organizationId, userId } = this.jobRef.data; const { fromUser, invite, organizationId, userId } = job.data;
this.clsService.set('organizationId', organizationId); this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId); this.clsService.set('userId', userId);

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bull'; import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bull'; import { Queue } from 'bullmq';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import { import {
@@ -29,6 +29,7 @@ export default class InviteSendMainNotificationSubscribe {
async sendMailNotification({ async sendMailNotification({
invite, invite,
user, user,
invitingUser,
}: IUserInviteTenantSyncedEventPayload) { }: IUserInviteTenantSyncedEventPayload) {
const tenant = await this.tenancyContext.getTenant(); const tenant = await this.tenancyContext.getTenant();
const authedUser = await this.tenancyContext.getSystemUser(); const authedUser = await this.tenancyContext.getSystemUser();
@@ -37,7 +38,7 @@ export default class InviteSendMainNotificationSubscribe {
const userId = authedUser.id; const userId = authedUser.id;
this.sendInviteMailQueue.add(SendInviteUserMailJob, { this.sendInviteMailQueue.add(SendInviteUserMailJob, {
fromUser: user, fromUser: invitingUser,
invite, invite,
userId, userId,
organizationId, organizationId,

View File

@@ -33,7 +33,7 @@ export class SyncSystemSendInviteSubscriber {
* @param {IUserInvitedEventPayload} payload - * @param {IUserInvitedEventPayload} payload -
*/ */
@OnEvent(events.inviteUser.sendInvite) @OnEvent(events.inviteUser.sendInvite)
async syncSendInviteSystem({ inviteToken, user }: IUserInvitedEventPayload) { async syncSendInviteSystem({ inviteToken, user, invitingUser }: IUserInvitedEventPayload) {
const authorizedUser = await this.tenancyContext.getSystemUser(); const authorizedUser = await this.tenancyContext.getSystemUser();
const tenantId = authorizedUser.tenantId; const tenantId = authorizedUser.tenantId;
@@ -63,6 +63,7 @@ export class SyncSystemSendInviteSubscriber {
{ {
invite, invite,
user, user,
invitingUser,
} as IUserInviteTenantSyncedEventPayload, } as IUserInviteTenantSyncedEventPayload,
); );
} }

View File

@@ -7,6 +7,7 @@ import {
Post, Post,
Put, Put,
Query, Query,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { VendorCreditsApplicationService } from './VendorCreditsApplication.service'; import { VendorCreditsApplicationService } from './VendorCreditsApplication.service';
import { IVendorCreditsQueryDTO } from './types/VendorCredit.types'; import { IVendorCreditsQueryDTO } from './types/VendorCredit.types';
@@ -26,17 +27,24 @@ import {
BulkDeleteDto, BulkDeleteDto,
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { VendorCreditAction } from './types/VendorCredit.types';
@Controller('vendor-credits') @Controller('vendor-credits')
@ApiTags('Vendor Credits') @ApiTags('Vendor Credits')
@ApiCommonHeaders() @ApiCommonHeaders()
@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiExtraModels(ValidateBulkDeleteResponseDto)
@UseGuards(AuthorizationGuard, PermissionGuard)
export class VendorCreditsController { export class VendorCreditsController {
constructor( constructor(
private readonly vendorCreditsApplication: VendorCreditsApplicationService, private readonly vendorCreditsApplication: VendorCreditsApplicationService,
) { } ) { }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(VendorCreditAction.Delete, AbilitySubject.VendorCredit)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validates which vendor credits can be deleted and returns the results.', 'Validates which vendor credits can be deleted and returns the results.',
@@ -58,6 +66,7 @@ export class VendorCreditsController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(VendorCreditAction.Delete, AbilitySubject.VendorCredit)
@ApiOperation({ summary: 'Deletes multiple vendor credits.' }) @ApiOperation({ summary: 'Deletes multiple vendor credits.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -73,24 +82,28 @@ export class VendorCreditsController {
} }
@Post() @Post()
@RequirePermission(VendorCreditAction.Create, AbilitySubject.VendorCredit)
@ApiOperation({ summary: 'Create a new vendor credit.' }) @ApiOperation({ summary: 'Create a new vendor credit.' })
async createVendorCredit(@Body() dto: CreateVendorCreditDto) { async createVendorCredit(@Body() dto: CreateVendorCreditDto) {
return this.vendorCreditsApplication.createVendorCredit(dto); return this.vendorCreditsApplication.createVendorCredit(dto);
} }
@Put(':id/open') @Put(':id/open')
@RequirePermission(VendorCreditAction.Edit, AbilitySubject.VendorCredit)
@ApiOperation({ summary: 'Open the given vendor credit.' }) @ApiOperation({ summary: 'Open the given vendor credit.' })
async openVendorCredit(@Param('id') vendorCreditId: number) { async openVendorCredit(@Param('id') vendorCreditId: number) {
return this.vendorCreditsApplication.openVendorCredit(vendorCreditId); return this.vendorCreditsApplication.openVendorCredit(vendorCreditId);
} }
@Get() @Get()
@RequirePermission(VendorCreditAction.View, AbilitySubject.VendorCredit)
@ApiOperation({ summary: 'Retrieves the vendor credits.' }) @ApiOperation({ summary: 'Retrieves the vendor credits.' })
async getVendorCredits(@Query() filterDTO: IVendorCreditsQueryDTO) { async getVendorCredits(@Query() filterDTO: IVendorCreditsQueryDTO) {
return this.vendorCreditsApplication.getVendorCredits(filterDTO); return this.vendorCreditsApplication.getVendorCredits(filterDTO);
} }
@Put(':id') @Put(':id')
@RequirePermission(VendorCreditAction.Edit, AbilitySubject.VendorCredit)
@ApiOperation({ summary: 'Edit the given vendor credit.' }) @ApiOperation({ summary: 'Edit the given vendor credit.' })
async editVendorCredit( async editVendorCredit(
@Param('id') vendorCreditId: number, @Param('id') vendorCreditId: number,
@@ -100,12 +113,14 @@ export class VendorCreditsController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(VendorCreditAction.Delete, AbilitySubject.VendorCredit)
@ApiOperation({ summary: 'Delete the given vendor credit.' }) @ApiOperation({ summary: 'Delete the given vendor credit.' })
async deleteVendorCredit(@Param('id') vendorCreditId: number) { async deleteVendorCredit(@Param('id') vendorCreditId: number) {
return this.vendorCreditsApplication.deleteVendorCredit(vendorCreditId); return this.vendorCreditsApplication.deleteVendorCredit(vendorCreditId);
} }
@Get(':id') @Get(':id')
@RequirePermission(VendorCreditAction.View, AbilitySubject.VendorCredit)
@ApiOperation({ summary: 'Retrieves the vendor credit details.' }) @ApiOperation({ summary: 'Retrieves the vendor credit details.' })
async getVendorCredit(@Param('id') vendorCreditId: number) { async getVendorCredit(@Param('id') vendorCreditId: number) {
return this.vendorCreditsApplication.getVendorCredit(vendorCreditId); return this.vendorCreditsApplication.getVendorCredit(vendorCreditId);

View File

@@ -7,6 +7,7 @@ import {
Post, Post,
Put, Put,
Query, Query,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { VendorsApplication } from './VendorsApplication.service'; import { VendorsApplication } from './VendorsApplication.service';
import { VendorOpeningBalanceEditDto } from './dtos/VendorOpeningBalanceEdit.dto'; import { VendorOpeningBalanceEditDto } from './dtos/VendorOpeningBalanceEdit.dto';
@@ -24,44 +25,56 @@ import {
BulkDeleteVendorsDto, BulkDeleteVendorsDto,
ValidateBulkDeleteVendorsResponseDto, ValidateBulkDeleteVendorsResponseDto,
} from './dtos/BulkDeleteVendors.dto'; } from './dtos/BulkDeleteVendors.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { VendorAction } from '../Customers/types/Customers.types';
@Controller('vendors') @Controller('vendors')
@ApiTags('Vendors') @ApiTags('Vendors')
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class VendorsController { export class VendorsController {
constructor(private vendorsApplication: VendorsApplication) {} constructor(private vendorsApplication: VendorsApplication) {}
@Get() @Get()
@RequirePermission(VendorAction.View, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Retrieves the vendors.' }) @ApiOperation({ summary: 'Retrieves the vendors.' })
getVendors(@Query() filterDTO: GetVendorsQueryDto) { getVendors(@Query() filterDTO: GetVendorsQueryDto) {
return this.vendorsApplication.getVendors(filterDTO); return this.vendorsApplication.getVendors(filterDTO);
} }
@Get(':id') @Get(':id')
@RequirePermission(VendorAction.View, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Retrieves the vendor details.' }) @ApiOperation({ summary: 'Retrieves the vendor details.' })
getVendor(@Param('id') vendorId: number) { getVendor(@Param('id') vendorId: number) {
return this.vendorsApplication.getVendor(vendorId); return this.vendorsApplication.getVendor(vendorId);
} }
@Post() @Post()
@RequirePermission(VendorAction.Create, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Create a new vendor.' }) @ApiOperation({ summary: 'Create a new vendor.' })
createVendor(@Body() vendorDTO: CreateVendorDto) { createVendor(@Body() vendorDTO: CreateVendorDto) {
return this.vendorsApplication.createVendor(vendorDTO); return this.vendorsApplication.createVendor(vendorDTO);
} }
@Put(':id') @Put(':id')
@RequirePermission(VendorAction.Edit, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Edit the given vendor.' }) @ApiOperation({ summary: 'Edit the given vendor.' })
editVendor(@Param('id') vendorId: number, @Body() vendorDTO: EditVendorDto) { editVendor(@Param('id') vendorId: number, @Body() vendorDTO: EditVendorDto) {
return this.vendorsApplication.editVendor(vendorId, vendorDTO); return this.vendorsApplication.editVendor(vendorId, vendorDTO);
} }
@Delete(':id') @Delete(':id')
@RequirePermission(VendorAction.Delete, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Delete the given vendor.' }) @ApiOperation({ summary: 'Delete the given vendor.' })
deleteVendor(@Param('id') vendorId: number) { deleteVendor(@Param('id') vendorId: number) {
return this.vendorsApplication.deleteVendor(vendorId); return this.vendorsApplication.deleteVendor(vendorId);
} }
@Put(':id/opening-balance') @Put(':id/opening-balance')
@RequirePermission(VendorAction.Edit, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Edit the given vendor opening balance.' }) @ApiOperation({ summary: 'Edit the given vendor opening balance.' })
editOpeningBalance( editOpeningBalance(
@Param('id') vendorId: number, @Param('id') vendorId: number,
@@ -74,6 +87,7 @@ export class VendorsController {
} }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(VendorAction.Delete, AbilitySubject.Vendor)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validates which vendors can be deleted and returns counts of deletable and non-deletable vendors.', 'Validates which vendors can be deleted and returns counts of deletable and non-deletable vendors.',
@@ -93,6 +107,7 @@ export class VendorsController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(VendorAction.Delete, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Deletes multiple vendors in bulk.' }) @ApiOperation({ summary: 'Deletes multiple vendors in bulk.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -17,8 +17,8 @@ const Schema = Yup.object().shape({
.label(intl.get('display_name_')), .label(intl.get('display_name_')),
email: Yup.string().email().nullable(), email: Yup.string().email().nullable(),
work_phone: Yup.number(), work_phone: Yup.string().nullable(),
personal_phone: Yup.number(), personal_phone: Yup.string().nullable(),
website: Yup.string().url().nullable(), website: Yup.string().url().nullable(),
active: Yup.boolean(), active: Yup.boolean(),
@@ -30,7 +30,7 @@ const Schema = Yup.object().shape({
billing_address_city: Yup.string().trim(), billing_address_city: Yup.string().trim(),
billing_address_state: Yup.string().trim(), billing_address_state: Yup.string().trim(),
billing_address_postcode: Yup.string().nullable(), billing_address_postcode: Yup.string().nullable(),
billing_address_phone: Yup.number(), billing_address_phone: Yup.string().nullable(),
shipping_address_country: Yup.string().trim(), shipping_address_country: Yup.string().trim(),
shipping_address_1: Yup.string().trim(), shipping_address_1: Yup.string().trim(),
@@ -38,7 +38,7 @@ const Schema = Yup.object().shape({
shipping_address_city: Yup.string().trim(), shipping_address_city: Yup.string().trim(),
shipping_address_state: Yup.string().trim(), shipping_address_state: Yup.string().trim(),
shipping_address_postcode: Yup.string().nullable(), shipping_address_postcode: Yup.string().nullable(),
shipping_address_phone: Yup.number(), shipping_address_phone: Yup.string().nullable(),
opening_balance: Yup.number().nullable(), opening_balance: Yup.number().nullable(),
currency_code: Yup.string(), currency_code: Yup.string(),

View File

@@ -15,7 +15,7 @@ import { useContactDetailDrawerContext } from './ContactDetailDrawerProvider';
import { withAlertActions } from '@/containers/Alert/withAlertActions'; import { withAlertActions } from '@/containers/Alert/withAlertActions';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
import { DashboardActionsBar, Icon, FormattedMessage as T } from '@/components'; import { DrawerActionsBar, Icon, FormattedMessage as T } from '@/components';
import { safeCallback, compose } from '@/utils'; import { safeCallback, compose } from '@/utils';
@@ -46,7 +46,7 @@ function ContactDetailActionsBar({
}; };
return ( return (
<DashboardActionsBar> <DrawerActionsBar>
<NavbarGroup> <NavbarGroup>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
@@ -63,7 +63,7 @@ function ContactDetailActionsBar({
onClick={safeCallback(onDeleteContact)} onClick={safeCallback(onDeleteContact)}
/> />
</NavbarGroup> </NavbarGroup>
</DashboardActionsBar> </DrawerActionsBar>
); );
} }

View File

@@ -23,7 +23,6 @@ import { withDialogActions } from '@/containers/Dialog/withDialogActions';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
import { import {
DashboardActionsBar,
Can, Can,
Icon, Icon,
FormattedMessage as T, FormattedMessage as T,

View File

@@ -20,7 +20,7 @@ import {
If, If,
Icon, Icon,
FormattedMessage as T, FormattedMessage as T,
DashboardActionsBar, DrawerActionsBar,
Can, Can,
} from '@/components'; } from '@/components';
@@ -63,7 +63,7 @@ function VendorCreditDetailActionsBar({
}; };
return ( return (
<DashboardActionsBar> <DrawerActionsBar>
<NavbarGroup> <NavbarGroup>
<Can I={VendorCreditAction.Edit} a={AbilitySubject.VendorCredit}> <Can I={VendorCreditAction.Edit} a={AbilitySubject.VendorCredit}>
<Button <Button
@@ -105,7 +105,7 @@ function VendorCreditDetailActionsBar({
</If> </If>
</Can> </Can>
</NavbarGroup> </NavbarGroup>
</DashboardActionsBar> </DrawerActionsBar>
); );
} }

View File

@@ -57,7 +57,7 @@ function GlobalErrors({
if (globalErrors.access_denied) { if (globalErrors.access_denied) {
toastKeySomethingWrong = AppToaster.show( toastKeySomethingWrong = AppToaster.show(
{ {
message: intl.get('global_error.you_dont_have_permissions'), message: globalErrors.access_denied.message || intl.get('global_error.you_dont_have_permissions'),
intent: Intent.DANGER, intent: Intent.DANGER,
onDismiss: () => { onDismiss: () => {
globalErrorsSet({ access_denied: false }); globalErrorsSet({ access_denied: false });

View File

@@ -16,6 +16,7 @@ import { useDrawerActions } from '@/hooks/state';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider'; import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils'; import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
import { Box } from '@/components';
export function CreditNoteCustomizeContent() { export function CreditNoteCustomizeContent() {
const { payload, name } = useDrawerContext(); const { payload, name } = useDrawerContext();
@@ -45,7 +46,9 @@ function CreditNoteCustomizeFormContent() {
return ( return (
<ElementCustomizeContent> <ElementCustomizeContent>
<ElementCustomize.PaperTemplate> <ElementCustomize.PaperTemplate>
<Box overflow="auto" flex="1 1" px={4} py={6}>
<CreditNotePaperTemplateFormConnected /> <CreditNotePaperTemplateFormConnected />
</Box>
</ElementCustomize.PaperTemplate> </ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}> <ElementCustomize.FieldsTab id={'general'} label={'General'}>

View File

@@ -16,6 +16,7 @@ import { useDrawerActions } from '@/hooks/state';
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm'; import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider'; import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils'; import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
import { Box } from '@/components';
export function EstimateCustomizeContent() { export function EstimateCustomizeContent() {
const { payload, name } = useDrawerContext(); const { payload, name } = useDrawerContext();
@@ -44,7 +45,9 @@ function EstimateCustomizeFormContent() {
return ( return (
<ElementCustomizeContent> <ElementCustomizeContent>
<ElementCustomize.PaperTemplate> <ElementCustomize.PaperTemplate>
<Box overflow="auto" flex="1 1" px={4} py={6}>
<EstimatePaperTemplateFormConnected /> <EstimatePaperTemplateFormConnected />
</Box>
</ElementCustomize.PaperTemplate> </ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}> <ElementCustomize.FieldsTab id={'general'} label={'General'}>

View File

@@ -19,6 +19,7 @@ import { useDrawerActions } from '@/hooks/state';
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm'; import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider'; import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils'; import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
import { Box } from '@/components';
export function PaymentReceivedCustomizeContent() { export function PaymentReceivedCustomizeContent() {
const { payload, name } = useDrawerContext(); const { payload, name } = useDrawerContext();
@@ -51,7 +52,9 @@ function PaymentReceivedCustomizeFormContent() {
return ( return (
<ElementCustomizeContent> <ElementCustomizeContent>
<ElementCustomize.PaperTemplate> <ElementCustomize.PaperTemplate>
<Box overflow="auto" flex="1 1" px={4} py={6}>
<PaymentReceivedPaperTemplateFormConnected /> <PaymentReceivedPaperTemplateFormConnected />
</Box>
</ElementCustomize.PaperTemplate> </ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}> <ElementCustomize.FieldsTab id={'general'} label={'General'}>

View File

@@ -16,6 +16,7 @@ import { useDrawerActions } from '@/hooks/state';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider'; import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider'; import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils'; import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
import { Box } from '@/components';
export function ReceiptCustomizeContent() { export function ReceiptCustomizeContent() {
const { payload, name } = useDrawerContext(); const { payload, name } = useDrawerContext();
@@ -44,7 +45,9 @@ function ReceiptCustomizeFormContent() {
return ( return (
<ElementCustomizeContent> <ElementCustomizeContent>
<ElementCustomize.PaperTemplate> <ElementCustomize.PaperTemplate>
<Box overflow="auto" flex="1 1" px={4} py={6}>
<ReceiptPaperTemplateFormConnected /> <ReceiptPaperTemplateFormConnected />
</Box>
</ElementCustomize.PaperTemplate> </ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}> <ElementCustomize.FieldsTab id={'general'} label={'General'}>

View File

@@ -1,8 +1,8 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { Intent, Tag } from '@blueprintjs/core'; import { Intent, Tag, Classes } from '@blueprintjs/core';
import { Align } from '@/constants'; import { Align } from '@/constants';
import styled from 'styled-components'; import clsx from 'classnames';
const codeAccessor = (taxRate) => { const codeAccessor = (taxRate) => {
return ( return (
@@ -28,13 +28,17 @@ const nameAccessor = (taxRate) => {
return ( return (
<> <>
<span>{taxRate.name}</span> <span>{taxRate.name}</span>
{!!taxRate.is_compound && <CompoundText>(Compound tax)</CompoundText>} {!!taxRate.is_compound && (
<span className={clsx(Classes.TEXT_MUTED)}>(Compound tax)</span>
)}
</> </>
); );
}; };
const DescriptionAccessor = (taxRate) => { const DescriptionAccessor = (taxRate) => {
return <DescriptionText>{taxRate.description}</DescriptionText>; return (
<span className={clsx(Classes.TEXT_MUTED)}>{taxRate.description}</span>
);
}; };
/** /**
@@ -72,11 +76,3 @@ export const useTaxRatesTableColumns = () => {
]; ];
}; };
const CompoundText = styled('span')`
color: #738091;
margin-left: 5px;
`;
const DescriptionText = styled('span')`
color: #5f6b7c;
`;

View File

@@ -13,7 +13,7 @@ import {
Position, Position,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import * as R from 'ramda'; import * as R from 'ramda';
import { AppToaster, Can, DashboardActionsBar, Icon } from '@/components'; import { AppToaster, Can, DrawerActionsBar, Icon } from '@/components';
import { AbilitySubject, TaxRateAction } from '@/constants/abilityOption'; import { AbilitySubject, TaxRateAction } from '@/constants/abilityOption';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
import { withAlertActions } from '@/containers/Alert/withAlertActions'; import { withAlertActions } from '@/containers/Alert/withAlertActions';
@@ -83,7 +83,7 @@ function TaxRateDetailsContentActionsBar({
}; };
return ( return (
<DashboardActionsBar> <DrawerActionsBar>
<NavbarGroup> <NavbarGroup>
<Can I={TaxRateAction.Edit} a={AbilitySubject.TaxRate}> <Can I={TaxRateAction.Edit} a={AbilitySubject.TaxRate}>
<Button <Button
@@ -137,7 +137,7 @@ function TaxRateDetailsContentActionsBar({
</Popover> </Popover>
</Can> </Can>
</NavbarGroup> </NavbarGroup>
</DashboardActionsBar> </DrawerActionsBar>
); );
} }

View File

@@ -74,9 +74,13 @@ const TaxRateHeader = styled(`div`)`
const TaxRateAmount = styled('div')` const TaxRateAmount = styled('div')`
line-height: 1; line-height: 1;
font-size: 30px; font-size: 30px;
color: #565b71;
font-weight: 600; font-weight: 600;
display: inline-block; display: inline-block;
color: var(--x-color-amount-text, #565b71);
.bp4-dark & {
color: rgba(255, 255, 255, 0.9);
}
`; `;
const TaxRateActiveTag = styled(Tag)` const TaxRateActiveTag = styled(Tag)`

View File

@@ -10,8 +10,8 @@ const Schema = Yup.object().shape({
display_name: Yup.string().trim().required().label(intl.get('display_name_')), display_name: Yup.string().trim().required().label(intl.get('display_name_')),
email: Yup.string().email().nullable(), email: Yup.string().email().nullable(),
work_phone: Yup.number(), work_phone: Yup.string().nullable(),
personal_phone: Yup.number(), personal_phone: Yup.string().nullable(),
website: Yup.string().url().nullable(), website: Yup.string().url().nullable(),
active: Yup.boolean(), active: Yup.boolean(),
@@ -23,7 +23,7 @@ const Schema = Yup.object().shape({
billing_address_city: Yup.string().trim(), billing_address_city: Yup.string().trim(),
billing_address_state: Yup.string().trim(), billing_address_state: Yup.string().trim(),
billing_address_postcode: Yup.string().nullable(), billing_address_postcode: Yup.string().nullable(),
billing_address_phone: Yup.number(), billing_address_phone: Yup.string().nullable(),
shipping_address_country: Yup.string().trim(), shipping_address_country: Yup.string().trim(),
shipping_address_1: Yup.string().trim(), shipping_address_1: Yup.string().trim(),
@@ -31,7 +31,7 @@ const Schema = Yup.object().shape({
shipping_address_city: Yup.string().trim(), shipping_address_city: Yup.string().trim(),
shipping_address_state: Yup.string().trim(), shipping_address_state: Yup.string().trim(),
shipping_address_postcode: Yup.string().nullable(), shipping_address_postcode: Yup.string().nullable(),
shipping_address_phone: Yup.number(), shipping_address_phone: Yup.string().nullable(),
opening_balance: Yup.number().nullable(), opening_balance: Yup.number().nullable(),
currency_code: Yup.string(), currency_code: Yup.string(),

View File

@@ -37,10 +37,10 @@ export function useTaxRate(taxRateId: string, props) {
[QUERY_TYPES.TAX_RATES, taxRateId], [QUERY_TYPES.TAX_RATES, taxRateId],
{ {
method: 'get', method: 'get',
url: `tax-rates/${taxRateId}}`, url: `tax-rates/${taxRateId}`,
}, },
{ {
select: (res) => res.data.data, select: (res) => res.data,
...props, ...props,
}, },
); );
@@ -106,7 +106,7 @@ export function useActivateTaxRate(props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation((id) => apiRequest.post(`tax-rates/${id}/active`), { return useMutation((id) => apiRequest.put(`tax-rates/${id}/activate`), {
onSuccess: (res, id) => { onSuccess: (res, id) => {
commonInvalidateQueries(queryClient); commonInvalidateQueries(queryClient);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]); queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]);
@@ -122,7 +122,7 @@ export function useInactivateTaxRate(props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation((id) => apiRequest.post(`tax-rates/${id}/inactive`), { return useMutation((id) => apiRequest.put(`tax-rates/${id}/inactivate`), {
onSuccess: (res, id) => { onSuccess: (res, id) => {
commonInvalidateQueries(queryClient); commonInvalidateQueries(queryClient);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]); queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]);

View File

@@ -58,7 +58,7 @@ export default function useApiRequest() {
setLogout(); setLogout();
} }
if (status === 403) { if (status === 403) {
setGlobalErrors({ access_denied: true }); setGlobalErrors({ access_denied: { message: data.message } });
} }
if (status === 429) { if (status === 429) {
setGlobalErrors({ too_many_requests: true }); setGlobalErrors({ too_many_requests: true });