Compare commits

...

27 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
17deeb18e3 Merge pull request #965 from bigcapitalhq/fix/ahmedbouhuolia/cashflow-transaction-type-consistency
fix: correct cash flow transaction type naming inconsistencies
2026-02-16 23:05:22 +02:00
Ahmed Bouhuolia
8416b45f4e fix: correct cash flow transaction type naming inconsistencies
- Fix typo ONWERS_DRAWING -> OWNERS_DRAWING in server constants
- Change OwnerDrawing -> owner_drawing for consistency in webapp
- Fix typo TRANSACRIONS_TYPE -> TRANSACTIONS_TYPE
- Fix typo OnwersDrawing -> OwnerDrawing
- Add missing Icon and FDateInput imports
- Add dark mode styling for BranchRowDivider

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-02-16 23:02:38 +02:00
Ahmed Bouhuolia
3cc5aab80e Merge pull request #963 from bigcapitalhq/fix/ahmedbouhuolia/mail-queue-cleanup
fix: correct queue name, add missing await, and clean up constants
2026-02-16 22:27:08 +02:00
Ahmed Bouhuolia
93711a7bf4 fix: correct queue name, add missing await, and clean up constants 2026-02-16 22:23:41 +02:00
Ahmed Bouhuolia
43066c4f1f Merge pull request #962 from bigcapitalhq/feature/ahmedbouhuolia/add-permission-guards-to-credit-controllers
fix(server): add permission guards to credit note and vendor credit controllers
2026-02-16 20:07:36 +02:00
Ahmed Bouhuolia
d5402b6a9b feat: add permission guards to credit note and vendor credit controllers
Add AuthorizationGuard and PermissionGuard to the following controllers:
- CreditNoteRefundsController
- CreditNotesApplyInvoiceController
- VendorCreditApplyBillsController
- VendorCreditsRefundController

Add @RequirePermission decorators with appropriate actions:
- View action for GET endpoints
- Edit action for POST/DELETE endpoints
- Refund action for refund-related operations

Also fixes AuthorizationGuard to use userId from clsService instead of
user.id from request for consistency with the abilities cache.
2026-02-16 20:04:48 +02:00
Ahmed Bouhuolia
174aec78ca fix(webapp): send mail preview style 2026-02-16 17:45:04 +02:00
Ahmed Bouhuolia
7162e948dd Merge pull request #961 from bigcapitalhq/fix/trial-balance-sheet-filtering
fix: correct trial balance sheet filtering logic
2026-02-16 15:34:33 +02:00
Ahmed Bouhuolia
8ad1be1d52 fix: correct trial balance sheet filtering logic
- Include child account transactions when filtering parent accounts
- Fix order of operations in accounts section mapping
- Ensure accounts with child transactions are not incorrectly filtered out

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:27:33 +02:00
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
91 changed files with 726 additions and 180 deletions

View File

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

View File

@@ -9,6 +9,7 @@ import {
ParseIntPipe,
Put,
HttpCode,
UseGuards,
} from '@nestjs/common';
import { AccountsApplication } from './AccountsApplication.service';
import { CreateAccountDTO } from './CreateAccount.dto';
@@ -32,6 +33,11 @@ import {
BulkDeleteDto,
ValidateBulkDeleteResponseDto,
} 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')
@ApiTags('Accounts')
@@ -40,11 +46,13 @@ import {
@ApiExtraModels(GetAccountTransactionResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class AccountsController {
constructor(private readonly accountsApplication: AccountsApplication) { }
@Post('validate-bulk-delete')
@HttpCode(200)
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({
summary:
'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')
@HttpCode(200)
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({ summary: 'Deletes multiple accounts in bulk.' })
@ApiResponse({
status: 200,
@@ -81,6 +90,7 @@ export class AccountsController {
}
@Post()
@RequirePermission(AccountAction.CREATE, AbilitySubject.Account)
@ApiOperation({ summary: 'Create an account' })
@ApiResponse({
status: 200,
@@ -91,6 +101,7 @@ export class AccountsController {
}
@Put(':id')
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Edit the given account.' })
@ApiResponse({
status: 200,
@@ -111,6 +122,7 @@ export class AccountsController {
}
@Delete(':id')
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({ summary: 'Delete the given account.' })
@ApiResponse({
status: 200,
@@ -129,6 +141,7 @@ export class AccountsController {
@Post(':id/activate')
@HttpCode(200)
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Activate the given account.' })
@ApiResponse({
status: 200,
@@ -147,6 +160,7 @@ export class AccountsController {
@Post(':id/inactivate')
@HttpCode(200)
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Inactivate the given account.' })
@ApiResponse({
status: 200,
@@ -164,6 +178,7 @@ export class AccountsController {
}
@Get('types')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account types.' })
@ApiResponse({
status: 200,
@@ -180,6 +195,7 @@ export class AccountsController {
}
@Get('transactions')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account transactions.' })
@ApiResponse({
status: 200,
@@ -198,6 +214,7 @@ export class AccountsController {
}
@Get(':id')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account details.' })
@ApiResponse({
status: 200,
@@ -216,6 +233,7 @@ export class AccountsController {
}
@Get()
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the accounts.' })
@ApiResponse({
status: 200,

View File

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

View File

@@ -15,8 +15,13 @@ export const RecognizeUncategorizedTransactionsJob =
export const RecognizeUncategorizedTransactionsQueue =
'recognize-uncategorized-transactions-queue';
export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload {
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.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(

View File

@@ -69,10 +69,13 @@ export class TriggerRecognizedTransactionsSubscriber {
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
const payload = {
ruleId: bankRule.id,
shouldRevert: true,
...tenantPayload,
} as RecognizeUncategorizedTransactionsJobPayload;
// 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(
RecognizeUncategorizedTransactionsJob,
payload,

View File

@@ -3,6 +3,7 @@ import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls';
import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
import { RevertRecognizedTransactionsService } from '../commands/RevertRecognizedTransactions.service';
import {
RecognizeUncategorizedTransactionsJobPayload,
RecognizeUncategorizedTransactionsQueue,
@@ -15,10 +16,12 @@ import {
export class RegonizeTransactionsPrcessor extends WorkerHost {
/**
* @param {RecognizeTranasctionsService} recognizeTranasctionsService -
* @param {RevertRecognizedTransactionsService} revertRecognizedTransactionsService -
* @param {ClsService} clsService -
*/
constructor(
private readonly recognizeTranasctionsService: RecognizeTranasctionsService,
private readonly revertRecognizedTransactionsService: RevertRecognizedTransactionsService,
private readonly clsService: ClsService,
) {
super();
@@ -29,12 +32,21 @@ export class RegonizeTransactionsPrcessor extends WorkerHost {
*/
@UseCls()
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('userId', job.data.userId);
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(
ruleId,
transactionsCriteria,

View File

@@ -27,7 +27,7 @@ export enum CASHFLOW_DIRECTION {
}
export enum CASHFLOW_TRANSACTION_TYPE {
ONWERS_DRAWING = 'OwnerDrawing',
OWNERS_DRAWING = 'OwnerDrawing',
OWNER_CONTRIBUTION = 'OwnerContribution',
OTHER_INCOME = 'OtherIncome',
TRANSFER_FROM_ACCOUNT = 'TransferFromAccount',
@@ -36,7 +36,7 @@ export enum CASHFLOW_TRANSACTION_TYPE {
}
export const CASHFLOW_TRANSACTION_TYPE_META = {
[`${CASHFLOW_TRANSACTION_TYPE.ONWERS_DRAWING}`]: {
[`${CASHFLOW_TRANSACTION_TYPE.OWNERS_DRAWING}`]: {
type: 'OwnerDrawing',
direction: CASHFLOW_DIRECTION.OUT,
creditType: [ACCOUNT_TYPE.EQUITY],

View File

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

View File

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

View File

@@ -1,20 +1,35 @@
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { ICreditNoteRefundDTO } from '../CreditNotes/types/CreditNotes.types';
import { CreditNotesRefundsApplication } from './CreditNotesRefundsApplication.service';
import { RefundCreditNote } from './models/RefundCreditNote';
import { CreditNoteRefundDto } from './dto/CreditNoteRefund.dto';
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 { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
@Controller('credit-notes')
@ApiTags('Credit Note Refunds')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CreditNoteRefundsController {
constructor(
private readonly creditNotesRefundsApplication: CreditNotesRefundsApplication,
) {}
@Get(':creditNoteId/refunds')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Retrieve the credit note graph.' })
getCreditNoteRefunds(@Param('creditNoteId') creditNoteId: number) {
return this.creditNotesRefundsApplication.getCreditNoteRefunds(
@@ -29,6 +44,7 @@ export class CreditNoteRefundsController {
* @returns {Promise<RefundCreditNote>}
*/
@Post(':creditNoteId/refunds')
@RequirePermission(CreditNoteAction.Refund, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Create a refund for the given credit note.' })
createRefundCreditNote(
@Param('creditNoteId') creditNoteId: number,
@@ -46,6 +62,7 @@ export class CreditNoteRefundsController {
* @returns {Promise<void>}
*/
@Delete('refunds/:refundCreditId')
@RequirePermission(CreditNoteAction.Refund, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Delete a refund for the given credit note.' })
deleteRefundCreditNote(
@Param('refundCreditId') refundCreditId: number,

View File

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

View File

@@ -1,15 +1,31 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import {
Body,
Controller,
Get,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
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 { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
@Controller('credit-notes')
@ApiTags('Credit Notes Apply Invoice')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CreditNotesApplyInvoiceController {
constructor(
private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices,
) {}
@Get(':creditNoteId/applied-invoices')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Applied credit note to invoices' })
@ApiResponse({
status: 200,
@@ -24,6 +40,7 @@ export class CreditNotesApplyInvoiceController {
}
@Post(':creditNoteId/apply-invoices')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Apply credit note to invoices' })
@ApiResponse({
status: 200,

View File

@@ -7,6 +7,7 @@ import {
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { CustomersApplication } from './CustomersApplication.service';
import { CustomerOpeningBalanceEditDto } from './dtos/CustomerOpeningBalanceEdit.dto';
@@ -26,15 +27,22 @@ import {
ValidateBulkDeleteCustomersResponseDto,
} from './dtos/BulkDeleteCustomers.dto';
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')
@ApiTags('Customers')
@ApiExtraModels(CustomerResponseDto)
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CustomersController {
constructor(private customersApplication: CustomersApplication) { }
@Get(':id')
@RequirePermission(CustomerAction.View, AbilitySubject.Customer)
@ApiOperation({ summary: 'Retrieves the customer details.' })
@ApiResponse({
status: 200,
@@ -46,6 +54,7 @@ export class CustomersController {
}
@Get()
@RequirePermission(CustomerAction.View, AbilitySubject.Customer)
@ApiOperation({ summary: 'Retrieves the customers paginated list.' })
@ApiResponse({
status: 200,
@@ -60,6 +69,7 @@ export class CustomersController {
}
@Post()
@RequirePermission(CustomerAction.Create, AbilitySubject.Customer)
@ApiOperation({ summary: 'Create a new customer.' })
@ApiResponse({
status: 201,
@@ -71,6 +81,7 @@ export class CustomersController {
}
@Put(':id')
@RequirePermission(CustomerAction.Edit, AbilitySubject.Customer)
@ApiOperation({ summary: 'Edit the given customer.' })
@ApiResponse({
status: 200,
@@ -85,6 +96,7 @@ export class CustomersController {
}
@Delete(':id')
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
@ApiOperation({ summary: 'Delete the given customer.' })
@ApiResponse({
status: 200,
@@ -95,6 +107,7 @@ export class CustomersController {
}
@Put(':id/opening-balance')
@RequirePermission(CustomerAction.Edit, AbilitySubject.Customer)
@ApiOperation({ summary: 'Edit the opening balance of the given customer.' })
@ApiResponse({
status: 200,
@@ -112,6 +125,7 @@ export class CustomersController {
}
@Post('validate-bulk-delete')
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
@ApiOperation({
summary:
'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')
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
@ApiOperation({ summary: 'Deletes multiple customers in bulk.' })
@ApiResponse({
status: 200,

View File

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

View File

@@ -1,5 +1,5 @@
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 { AcceptType } from '@/constants/accept-type';
import {
@@ -11,14 +11,21 @@ import {
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
import { APAgingSummaryResponseExample } from './APAgingSummary.swagger';
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')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class APAgingSummaryController {
constructor(private readonly APAgingSummaryApp: APAgingSummaryApplication) { }
@Get()
@RequirePermission(ReportsAction.READ_AP_AGING_SUMMARY, AbilitySubject.Report)
@ApiOperation({ summary: 'Get payable aging summary' })
@ApiResponse({
status: 200,

View File

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

View File

@@ -1,5 +1,5 @@
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 { BalanceSheetApplication } from './BalanceSheetApplication';
import {
@@ -11,10 +11,16 @@ import {
import { BalanceSheetQueryDto } from './BalanceSheet.dto';
import { BalanceSheetResponseExample } from './BalanceSheet.swagger';
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')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class BalanceSheetStatementController {
constructor(private readonly balanceSheetApp: BalanceSheetApplication) {}
@@ -25,6 +31,7 @@ export class BalanceSheetStatementController {
* @param {string} acceptHeader - Accept header.
*/
@Get('')
@RequirePermission(ReportsAction.READ_BALANCE_SHEET, AbilitySubject.Report)
@ApiOperation({ summary: 'Get balance sheet statement' })
@ApiResponse({
status: 200,

View File

@@ -1,5 +1,5 @@
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 { CashflowSheetApplication } from './CashflowSheetApplication';
import {
@@ -11,14 +11,21 @@ import {
import { CashFlowStatementQueryDto } from './CashFlowStatementQuery.dto';
import { CashflowStatementResponseExample } from './CashflowStatement.swagger';
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')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CashflowController {
constructor(private readonly cashflowSheetApp: CashflowSheetApplication) { }
@Get()
@RequirePermission(ReportsAction.READ_CASHFLOW, AbilitySubject.Report)
@ApiResponse({
status: 200,
description: 'Cashflow statement report',

View File

@@ -5,22 +5,29 @@ import {
ApiTags,
} from '@nestjs/swagger';
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 { AcceptType } from '@/constants/accept-type';
import { GeneralLedgerQueryDto } from './GeneralLedgerQuery.dto';
import { GeneralLedgerResponseExample } from './GeneralLedger.swagger';
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')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class GeneralLedgerController {
constructor(
private readonly generalLedgerApplication: GeneralLedgerApplication,
) {}
@Get()
@RequirePermission(ReportsAction.READ_GENERAL_LEDGET, AbilitySubject.Report)
@ApiResponse({
status: 200,
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 { AcceptType } from '@/constants/accept-type';
import { JournalSheetApplication } from './JournalSheetApplication';
@@ -11,14 +11,21 @@ import {
import { JournalSheetQueryDto } from './JournalSheetQuery.dto';
import { JournalSheetResponseExample } from './JournalSheet.swagger';
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')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class JournalSheetController {
constructor(private readonly journalSheetApp: JournalSheetApplication) {}
@Get()
@RequirePermission(ReportsAction.READ_JOURNAL, AbilitySubject.Report)
@ApiResponse({
status: 200,
description: 'Journal report',

View File

@@ -1,5 +1,5 @@
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 { AcceptType } from '@/constants/accept-type';
import {
@@ -11,10 +11,16 @@ import {
import { ProfitLossSheetQueryDto } from './ProfitLossSheetQuery.dto';
import { ProfitLossSheetResponseExample } from './ProfitLossSheet.swagger';
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')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ProfitLossSheetController {
constructor(
private readonly profitLossSheetApp: ProfitLossSheetApplication,
@@ -27,6 +33,7 @@ export class ProfitLossSheetController {
* @param {string} acceptHeader
*/
@Get('/')
@RequirePermission(ReportsAction.READ_PROFIT_LOSS, AbilitySubject.Report)
@ApiResponse({
status: 200,
description: 'Profit & loss statement',

View File

@@ -172,8 +172,11 @@ export class TrialBalanceSheet extends FinancialSheet {
private filterNoneTransactions = (
accountNode: ITrialBalanceAccount
): boolean => {
const accountLedger = this.repository.totalAccountsLedger.whereAccountId(
accountNode.id,
const depsAccountsIds =
this.repository.accountsDepGraph.dependenciesOf(accountNode.id);
const accountLedger = this.repository.totalAccountsLedger.whereAccountsIds(
[accountNode.id, ...depsAccountsIds]
);
return !accountLedger.isEmpty();
};
@@ -241,8 +244,8 @@ export class TrialBalanceSheet extends FinancialSheet {
*/
private accountsSection(accounts: ModelObject<Account>[]) {
return R.compose(
this.nestedAccountsNode,
this.accountsFilter,
this.nestedAccountsNode,
this.accountsMapper
)(accounts);
}
@@ -250,7 +253,6 @@ export class TrialBalanceSheet extends FinancialSheet {
/**
* Retrieve trial balance sheet statement data.
* Note: Retruns null in case there is no transactions between the given date periods.
*
* @return {ITrialBalanceSheetData}
*/
public reportData(): ITrialBalanceSheetData {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullAdapter } from '@bull-board/api/bullAdapter';
import { BullModule } from '@nestjs/bull';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bullmq';
import { PaymentReceivesController } from './PaymentsReceived.controller';
import { PaymentReceivesApplication } from './PaymentReceived.application';
import { CreatePaymentReceivedService } from './commands/CreatePaymentReceived.serivce';
@@ -99,7 +99,7 @@ import { ValidateBulkDeletePaymentReceivedService } from './ValidateBulkDeletePa
BullModule.registerQueue({ name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE }),
BullBoardModule.forFeature({
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 { Job } from 'bull';
import { Inject, Scope } from '@nestjs/common';
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';
import { Scope } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls';
import {
SEND_PAYMENT_RECEIVED_MAIL_JOB,
@@ -13,20 +13,18 @@ import { SendPaymentReceivedMailPayload } from '../types/PaymentReceived.types';
name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE,
scope: Scope.REQUEST,
})
export class SendPaymentReceivedMailProcessor {
export class SendPaymentReceivedMailProcessor extends WorkerHost {
constructor(
private readonly sendPaymentReceivedMail: SendPaymentReceiveMailNotification,
private readonly clsService: ClsService,
) {
super();
}
@Inject(JOB_REF)
private readonly jobRef: Job<SendPaymentReceivedMailPayload>,
) { }
@Process(SEND_PAYMENT_RECEIVED_MAIL_JOB)
@UseCls()
async handleSendMail() {
async process(job: Job<SendPaymentReceivedMailPayload>) {
const { messageOptions, paymentReceivedId, organizationId, userId } =
this.jobRef.data;
job.data;
this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId);

View File

@@ -31,9 +31,10 @@ export class AuthorizationGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const { user } = request as any;
const userId = this.clsService.get('userId');
if (ABILITIES_CACHE.has(user.id)) {
(request as any).ability = ABILITIES_CACHE.get(user.id);
if (ABILITIES_CACHE.has(userId)) {
(request as any).ability = ABILITIES_CACHE.get(userId);
} else {
const ability = await this.getAbilityForUser();
(request as any).ability = ability;

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 { RolesApplication } from './Roles.application';
import { RolePermissionsSchema } from './queries/RolePermissionsSchema';
import { AuthorizationGuard } from './Authorization.guard';
import { PermissionGuard } from './Permission.guard';
const models = [
RegisterTenancyModel(Role),
@@ -25,9 +27,11 @@ const models = [
GetRoleService,
GetRolesService,
RolesApplication,
RolePermissionsSchema
RolePermissionsSchema,
AuthorizationGuard,
PermissionGuard,
],
controllers: [RolesController],
exports: [...models],
exports: [...models, AuthorizationGuard, PermissionGuard],
})
export class RolesModule {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ import { CommonMailOptionsDTO } from '@/modules/MailNotification/MailNotificatio
import { CommonMailOptions } from '@/modules/MailNotification/MailNotification.types';
import { EditSaleEstimateDto } from '../dtos/SaleEstimate.dto';
export const SendSaleEstimateMailQueue = 'SendSaleEstimateMailProcessor';
export const SendSaleEstimateMailQueue = 'SendSaleEstimateMailQueue';
export const SendSaleEstimateMailJob = 'SendSaleEstimateMailProcess';
export interface ISaleEstimateDTO {

View File

@@ -12,6 +12,7 @@ import {
Put,
Query,
Res,
UseGuards,
} from '@nestjs/common';
import {
ISaleInvoiceWriteoffDTO,
@@ -43,6 +44,11 @@ import {
BulkDeleteDto,
ValidateBulkDeleteResponseDto,
} 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')
@ApiTags('Sale Invoices')
@@ -52,10 +58,12 @@ import {
@ApiExtraModels(GenerateSaleInvoiceSharableLinkResponseDto)
@ApiCommonHeaders()
@ApiExtraModels(ValidateBulkDeleteResponseDto)
@UseGuards(AuthorizationGuard, PermissionGuard)
export class SaleInvoicesController {
constructor(private saleInvoiceApplication: SaleInvoiceApplication) { }
@Post('validate-bulk-delete')
@RequirePermission(SaleInvoiceAction.Delete, AbilitySubject.SaleInvoice)
@ApiOperation({
summary:
'Validates which sale invoices can be deleted and returns the results.',
@@ -77,6 +85,7 @@ export class SaleInvoicesController {
}
@Post('bulk-delete')
@RequirePermission(SaleInvoiceAction.Delete, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Deletes multiple sale invoices.' })
@ApiResponse({
status: 200,
@@ -90,6 +99,7 @@ export class SaleInvoicesController {
}
@Post()
@RequirePermission(SaleInvoiceAction.Create, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Create a new sale invoice.' })
@ApiResponse({
status: 201,
@@ -121,6 +131,7 @@ export class SaleInvoicesController {
}
@Put(':id')
@RequirePermission(SaleInvoiceAction.Edit, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Edit the given sale invoice.' })
@ApiResponse({
status: 200,
@@ -141,6 +152,7 @@ export class SaleInvoicesController {
}
@Delete(':id')
@RequirePermission(SaleInvoiceAction.Delete, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Delete the given sale invoice.' })
@ApiResponse({
status: 200,
@@ -158,6 +170,7 @@ export class SaleInvoicesController {
}
@Get('receivable')
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the receivable sale invoices.' })
@ApiResponse({
status: 200,
@@ -176,6 +189,7 @@ export class SaleInvoicesController {
}
@Get('state')
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the sale invoice state.' })
@ApiResponse({
status: 200,
@@ -190,6 +204,7 @@ export class SaleInvoicesController {
}
@Get(':id')
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the sale invoice details.' })
@ApiResponse({
status: 200,
@@ -228,6 +243,7 @@ export class SaleInvoicesController {
}
@Get()
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the sale invoices.' })
@ApiResponse({
status: 200,
@@ -251,6 +267,7 @@ export class SaleInvoicesController {
}
@Put(':id/deliver')
@RequirePermission(SaleInvoiceAction.Edit, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Deliver the given sale invoice.' })
@ApiResponse({
status: 200,
@@ -269,6 +286,7 @@ export class SaleInvoicesController {
}
@Post(':id/writeoff')
@RequirePermission(SaleInvoiceAction.Writeoff, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Write off the given sale invoice.' })
@HttpCode(200)
@ApiResponse({
@@ -290,6 +308,7 @@ export class SaleInvoicesController {
}
@Post(':id/cancel-writeoff')
@RequirePermission(SaleInvoiceAction.Writeoff, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Cancel the written off sale invoice.' })
@ApiResponse({
status: 200,
@@ -309,6 +328,7 @@ export class SaleInvoicesController {
}
@Get(':id/payments')
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the sale invoice payments.' })
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
@@ -322,6 +342,7 @@ export class SaleInvoicesController {
}
@Get(':id/html')
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the sale invoice HTML.' })
@ApiResponse({ status: 404, description: 'The sale invoice not found.' })
@ApiParam({
@@ -335,6 +356,7 @@ export class SaleInvoicesController {
}
@Get(':id/mail')
@RequirePermission(SaleInvoiceAction.View, AbilitySubject.SaleInvoice)
@ApiOperation({ summary: 'Retrieves the sale invoice mail state.' })
@ApiResponse({
status: 200,
@@ -354,6 +376,7 @@ export class SaleInvoicesController {
}
@Post(':id/generate-link')
@RequirePermission(SaleInvoiceAction.Edit, AbilitySubject.SaleInvoice)
@ApiOperation({
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 { SendSaleInvoiceMailProcessor } from './processors/SendSaleInvoiceMail.processor';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullAdapter } from '@bull-board/api/bullAdapter';
import { BullModule } from '@nestjs/bull';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bullmq';
import { SendSaleInvoiceQueue } from './constants';
import { InvoicePaymentIntegrationSubscriber } from './subscribers/InvoicePaymentIntegrationSubscriber';
import { InvoiceChangeStatusOnMailSentSubscriber } from './subscribers/InvoiceChangeStatusOnMailSentSubscriber';
@@ -85,7 +85,7 @@ import { ValidateBulkDeleteSaleInvoicesService } from './ValidateBulkDeleteSaleI
BullModule.registerQueue({ name: SendSaleInvoiceQueue }),
BullBoardModule.forFeature({
name: SendSaleInvoiceQueue,
adapter: BullAdapter,
adapter: BullMQAdapter,
}),
],
controllers: [SaleInvoicesController],

View File

@@ -1,9 +1,6 @@
// import config from '@/config';
export const SendSaleInvoiceQueue = 'SendSaleInvoiceQueue';
export const SendSaleInvoiceMailJob = 'SendSaleInvoiceMailJob';
export const DEFAULT_INVOICE_MAIL_SUBJECT =
'Invoice {Invoice Number} from {Company Name} for {Customer Name}';
export const DEFAULT_INVOICE_MAIL_CONTENT = `Hi {Customer Name},

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,22 @@
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 { 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')
@ApiTags('Settings')
@UseGuards(AuthorizationGuard, PermissionGuard)
export class SettingsController {
constructor(
private readonly settingsApplicationService: SettingsApplicationService,
) {}
@Put()
@RequirePermission(PreferencesAction.Mutate, AbilitySubject.Preferences)
@ApiOperation({ summary: 'Save the given settings.' })
async saveSettings(@Body() settingsDTO: ISettingsDTO) {
return this.settingsApplicationService.saveSettings(settingsDTO);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,7 @@
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 { DeleteUserService } from './commands/DeleteUser.service';
import { EditUserService } from './commands/EditUser.service';
@@ -18,11 +21,24 @@ import { AcceptInviteUserService } from './commands/AcceptInviteUser.service';
import { InviteTenantUserService } from './commands/InviteUser.service';
import { UsersInviteController } from './UsersInvite.controller';
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)];
@Module({
imports: [TenancyModule],
imports: [
TenancyModule,
MailModule,
BullModule.registerQueue({ name: SendInviteUserMailQueue }),
BullBoardModule.forFeature({
name: SendInviteUserMailQueue,
adapter: BullMQAdapter,
}),
],
exports: [...models],
providers: [
...models,
@@ -39,6 +55,9 @@ const models = [InjectSystemModel(UserInvite)];
SyncTenantUserMutateSubscriber,
SyncSystemSendInviteSubscriber,
SyncTenantAcceptInviteSubscriber,
InviteSendMainNotificationSubscribe,
SendInviteUserMailProcessor,
SendInviteUsersMailMessage,
UsersApplication
],
controllers: [UsersController, UsersInviteController],

View File

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

View File

@@ -43,11 +43,11 @@ export class InactivateUserService {
// Throw serivce error if the user is already inactivated.
this.throwErrorIfUserInactive(tenantUser);
// Marks the tenant user as active.
// Marks the tenant user as inactive.
await this.tenantUserModel()
.query()
.findById(userId)
.update({ active: true });
.update({ active: false });
// Triggers `onTenantUserActivated` event.
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 { ModelObject } from 'objection';
import { SendInviteUserDto } from '../dtos/InviteUser.dto';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class InviteTenantUserService {
constructor(
private readonly eventEmitter: EventEmitter2,
private readonly tenancyContext: TenancyContext,
@Inject(TenantUser.name)
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
@@ -53,10 +55,18 @@ export class InviteTenantUserService {
active: true,
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.
await this.eventEmitter.emitAsync(events.inviteUser.sendInvite, {
inviteToken,
user,
invitingUser,
} as IUserInvitedEventPayload);
return { invitedUser: user };

View File

@@ -27,7 +27,7 @@ export class SendInviteUsersMailMessage {
invite: ModelObject<UserInvite>,
) {
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 mail = new Mail()

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,33 @@
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { VendorCreditApplyBillsApplicationService } from './VendorCreditApplyBillsApplication.service';
import { IVendorCreditApplyToInvoicesDTO } from './types/VendorCreditApplyBills.types';
import { ApiTags } from '@nestjs/swagger';
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 { VendorCreditAction } from '../VendorCredit/types/VendorCredit.types';
@Controller('vendor-credits')
@ApiTags('Vendor Credits Apply Bills')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class VendorCreditApplyBillsController {
constructor(
private readonly vendorCreditApplyBillsApplication: VendorCreditApplyBillsApplicationService,
) {}
@Get(':vendorCreditId/bills-to-apply')
@RequirePermission(VendorCreditAction.View, AbilitySubject.VendorCredit)
async getVendorCreditToApplyBills(
@Param('vendorCreditId') vendorCreditId: number,
) {
@@ -20,6 +37,7 @@ export class VendorCreditApplyBillsController {
}
@Post(':vendorCreditId/apply-to-bills')
@RequirePermission(VendorCreditAction.Edit, AbilitySubject.VendorCredit)
async applyVendorCreditToBills(
@Param('vendorCreditId') vendorCreditId: number,
@Body() applyCreditToBillsDTO: IVendorCreditApplyToInvoicesDTO,
@@ -31,6 +49,7 @@ export class VendorCreditApplyBillsController {
}
@Delete('applied-bills/:vendorCreditAppliedBillId')
@RequirePermission(VendorCreditAction.Edit, AbilitySubject.VendorCredit)
async deleteAppliedBillToVendorCredit(
@Param('vendorCreditAppliedBillId') vendorCreditAppliedBillId: number,
) {
@@ -40,6 +59,7 @@ export class VendorCreditApplyBillsController {
}
@Get(':vendorCreditId/applied-bills')
@RequirePermission(VendorCreditAction.View, AbilitySubject.VendorCredit)
async getAppliedBillsToVendorCredit(
@Param('vendorCreditId') vendorCreditId: number,
) {

View File

@@ -1,11 +1,27 @@
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { VendorCreditsRefundApplication } from './VendorCreditsRefund.application';
import { RefundVendorCredit } from './models/RefundVendorCredit';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { RefundVendorCreditDto } from './dtos/RefundVendorCredit.dto';
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 { VendorCreditAction } from '../VendorCredit/types/VendorCredit.types';
@Controller('vendor-credits')
@ApiTags('Vendor Credits Refunds')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class VendorCreditsRefundController {
constructor(
private readonly vendorCreditsRefundApplication: VendorCreditsRefundApplication,
@@ -17,6 +33,7 @@ export class VendorCreditsRefundController {
* @returns {Promise<IRefundVendorCreditPOJO[]>}
*/
@Get(':vendorCreditId/refund')
@RequirePermission(VendorCreditAction.View, AbilitySubject.VendorCredit)
@ApiOperation({ summary: 'Retrieve the vendor credit refunds graph.' })
public getVendorCreditRefunds(
@Param('vendorCreditId') vendorCreditId: string,
@@ -33,6 +50,7 @@ export class VendorCreditsRefundController {
* @returns {Promise<RefundVendorCredit>}
*/
@Post(':vendorCreditId/refund')
@RequirePermission(VendorCreditAction.Refund, AbilitySubject.VendorCredit)
@ApiOperation({ summary: 'Create a refund for the given vendor credit.' })
public async createRefundVendorCredit(
@Param('vendorCreditId') vendorCreditId: string,
@@ -50,6 +68,7 @@ export class VendorCreditsRefundController {
* @returns {Promise<void>}
*/
@Delete('refunds/:refundCreditId')
@RequirePermission(VendorCreditAction.Refund, AbilitySubject.VendorCredit)
@ApiOperation({ summary: 'Delete a refund for the given vendor credit.' })
public async deleteRefundVendorCredit(
@Param('refundCreditId') refundCreditId: string,

View File

@@ -7,6 +7,7 @@ import {
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { VendorsApplication } from './VendorsApplication.service';
import { VendorOpeningBalanceEditDto } from './dtos/VendorOpeningBalanceEdit.dto';
@@ -24,44 +25,56 @@ import {
BulkDeleteVendorsDto,
ValidateBulkDeleteVendorsResponseDto,
} 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')
@ApiTags('Vendors')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class VendorsController {
constructor(private vendorsApplication: VendorsApplication) {}
@Get()
@RequirePermission(VendorAction.View, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Retrieves the vendors.' })
getVendors(@Query() filterDTO: GetVendorsQueryDto) {
return this.vendorsApplication.getVendors(filterDTO);
}
@Get(':id')
@RequirePermission(VendorAction.View, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Retrieves the vendor details.' })
getVendor(@Param('id') vendorId: number) {
return this.vendorsApplication.getVendor(vendorId);
}
@Post()
@RequirePermission(VendorAction.Create, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Create a new vendor.' })
createVendor(@Body() vendorDTO: CreateVendorDto) {
return this.vendorsApplication.createVendor(vendorDTO);
}
@Put(':id')
@RequirePermission(VendorAction.Edit, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Edit the given vendor.' })
editVendor(@Param('id') vendorId: number, @Body() vendorDTO: EditVendorDto) {
return this.vendorsApplication.editVendor(vendorId, vendorDTO);
}
@Delete(':id')
@RequirePermission(VendorAction.Delete, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Delete the given vendor.' })
deleteVendor(@Param('id') vendorId: number) {
return this.vendorsApplication.deleteVendor(vendorId);
}
@Put(':id/opening-balance')
@RequirePermission(VendorAction.Edit, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Edit the given vendor opening balance.' })
editOpeningBalance(
@Param('id') vendorId: number,
@@ -74,6 +87,7 @@ export class VendorsController {
}
@Post('validate-bulk-delete')
@RequirePermission(VendorAction.Delete, AbilitySubject.Vendor)
@ApiOperation({
summary:
'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')
@RequirePermission(VendorAction.Delete, AbilitySubject.Vendor)
@ApiOperation({ summary: 'Deletes multiple vendors in bulk.' })
@ApiResponse({
status: 200,

View File

@@ -19,7 +19,7 @@ export const getAddMoneyInOptions = () => [
export const getAddMoneyOutOptions = () => [
{
name: intl.get('banking.owner_drawings'),
value: 'OwnerDrawing',
value: 'owner_drawing',
},
{
name: intl.get('banking.expenses'),
@@ -31,11 +31,11 @@ export const getAddMoneyOutOptions = () => [
},
];
export const TRANSACRIONS_TYPE = [
export const TRANSACTIONS_TYPE = [
'OwnerContribution',
'OtherIncome',
'TransferFromAccount',
'OnwersDrawing',
'OwnerDrawing',
'OtherExpense',
'TransferToAccount',
];

View File

@@ -92,7 +92,7 @@ function CategorizeTransactionFormSubContent() {
} else if (values.transactionType === 'transfer_to_account') {
return <CategorizeTransactionToAccount />;
// Owner drawings.
} else if (values.transactionType === 'OwnerDrawing') {
} else if (values.transactionType === 'owner_drawing') {
return <CategorizeTransactionOwnerDrawings />;
}
return null;

View File

@@ -23,6 +23,8 @@ import {
FFormGroup,
FTextArea,
FMoneyInputGroup,
Icon,
FDateInput,
} from '@/components';
import { CLASSES, ACCOUNT_TYPE, Features } from '@/constants';

View File

@@ -17,6 +17,7 @@ import {
FTextArea,
FInputGroup,
FDateInput,
Icon,
} from '@/components';
import { ACCOUNT_TYPE, CLASSES, Features } from '@/constants';
import {

View File

@@ -46,4 +46,8 @@ export const BranchRowDivider = styled.div`
height: 1px;
background: #ebf1f6;
margin-bottom: 15px;
.bp4-dark & {
background: var(--color-dark-gray5);
}
`;

View File

@@ -17,7 +17,7 @@ function MoneyOutContentFields() {
const transactionType = useMemo(() => {
switch (values.transaction_type) {
case 'OwnerDrawing':
case 'owner_drawing':
return <OwnerDrawingsFormFields />;
case 'other_expense':

View File

@@ -45,4 +45,8 @@ export const BranchRowDivider = styled.div`
height: 1px;
background: #ebf1f6;
margin-bottom: 15px;
.bp4-dark & {
background: var(--color-dark-gray5);
}
`;

View File

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

View File

@@ -15,7 +15,7 @@ import { useContactDetailDrawerContext } from './ContactDetailDrawerProvider';
import { withAlertActions } from '@/containers/Alert/withAlertActions';
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';
@@ -46,7 +46,7 @@ function ContactDetailActionsBar({
};
return (
<DashboardActionsBar>
<DrawerActionsBar>
<NavbarGroup>
<Button
className={Classes.MINIMAL}
@@ -63,7 +63,7 @@ function ContactDetailActionsBar({
onClick={safeCallback(onDeleteContact)}
/>
</NavbarGroup>
</DashboardActionsBar>
</DrawerActionsBar>
);
}

View File

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

View File

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

View File

@@ -57,7 +57,7 @@ function GlobalErrors({
if (globalErrors.access_denied) {
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,
onDismiss: () => {
globalErrorsSet({ access_denied: false });

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ const estimatePreviewCss = css`
export const EstimateSendMailReceiptPreview = () => {
return (
<Stack>
<Stack spacing={0}>
<EstimateSendMailPreviewHeader />
<Stack px={4} py={6}>

View File

@@ -7,7 +7,7 @@ import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
export function EstimateSendPdfPreviewConnected() {
return (
<Stack>
<Stack spacing={0}>
<EstimateSendMailPreviewHeader />
<Stack px={4} py={6}>

View File

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

View File

@@ -7,7 +7,7 @@ import { PaymentReceivedMailPreviewHeader } from './PaymentReceivedMailPreviewHe
export function PaymentReceivedSendMailPreviewPdf() {
return (
<Stack flex={1}>
<Stack flex={1} spacing={0}>
<PaymentReceivedMailPreviewHeader />
<Stack px={4} py={6}>
@@ -22,7 +22,6 @@ function PaymentReceivedSendPdfPreviewIframe() {
const { data, isLoading } = useGetPaymentReceiveHtml(
payload?.paymentReceivedId,
);
if (isLoading && data) {
return <Spinner size={20} />;
}

View File

@@ -18,7 +18,7 @@ const mailReceiptCss = css`
export function PaymentReceivedMailPreviewReceipt() {
return (
<Stack flex={1}>
<Stack flex={1} spacing={0}>
<PaymentReceivedMailPreviewHeader />
<Stack px={4} py={6}>

View File

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

View File

@@ -7,7 +7,7 @@ import { SendMailViewPreviewPdfIframe } from '../../Estimates/SendMailViewDrawer
export function ReceiptSendMailPdfPreview() {
return (
<Stack>
<Stack spacing={0}>
<ReceiptSendMailPreviewHeader />
<Stack px={4} py={6}>
@@ -26,7 +26,5 @@ function ReceiptSendPdfPreviewIframe() {
}
const iframeSrcDoc = data?.htmlContent;
console.log(data, 'data');
return <SendMailViewPreviewPdfIframe srcDoc={iframeSrcDoc} />;
}

View File

@@ -13,7 +13,7 @@ const receiptPreviewCss = css`
export function ReceiptSendMailPreview() {
return (
<Stack>
<Stack spacing={0}>
<ReceiptSendMailPreviewHeader />
<Stack px={4} py={6}>

View File

@@ -1,8 +1,8 @@
// @ts-nocheck
import React from 'react';
import { Intent, Tag } from '@blueprintjs/core';
import { Intent, Tag, Classes } from '@blueprintjs/core';
import { Align } from '@/constants';
import styled from 'styled-components';
import clsx from 'classnames';
const codeAccessor = (taxRate) => {
return (
@@ -28,13 +28,17 @@ const nameAccessor = (taxRate) => {
return (
<>
<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) => {
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,
} from '@blueprintjs/core';
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 { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
import { withAlertActions } from '@/containers/Alert/withAlertActions';
@@ -83,7 +83,7 @@ function TaxRateDetailsContentActionsBar({
};
return (
<DashboardActionsBar>
<DrawerActionsBar>
<NavbarGroup>
<Can I={TaxRateAction.Edit} a={AbilitySubject.TaxRate}>
<Button
@@ -137,7 +137,7 @@ function TaxRateDetailsContentActionsBar({
</Popover>
</Can>
</NavbarGroup>
</DashboardActionsBar>
</DrawerActionsBar>
);
}

View File

@@ -74,9 +74,13 @@ const TaxRateHeader = styled(`div`)`
const TaxRateAmount = styled('div')`
line-height: 1;
font-size: 30px;
color: #565b71;
font-weight: 600;
display: inline-block;
color: var(--x-color-amount-text, #565b71);
.bp4-dark & {
color: rgba(255, 255, 255, 0.9);
}
`;
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_')),
email: Yup.string().email().nullable(),
work_phone: Yup.number(),
personal_phone: Yup.number(),
work_phone: Yup.string().nullable(),
personal_phone: Yup.string().nullable(),
website: Yup.string().url().nullable(),
active: Yup.boolean(),
@@ -23,7 +23,7 @@ const Schema = Yup.object().shape({
billing_address_city: Yup.string().trim(),
billing_address_state: Yup.string().trim(),
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_1: Yup.string().trim(),
@@ -31,7 +31,7 @@ const Schema = Yup.object().shape({
shipping_address_city: Yup.string().trim(),
shipping_address_state: Yup.string().trim(),
shipping_address_postcode: Yup.string().nullable(),
shipping_address_phone: Yup.number(),
shipping_address_phone: Yup.string().nullable(),
opening_balance: Yup.number().nullable(),
currency_code: Yup.string(),

View File

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

View File

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