Compare commits

..

16 Commits

Author SHA1 Message Date
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
39 changed files with 191 additions and 128 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

@@ -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

@@ -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

@@ -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

@@ -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,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

@@ -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

@@ -85,9 +85,14 @@ export class TaxRatesController {
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),
},
},
},
},
})

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

@@ -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

@@ -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();
@@ -37,7 +38,7 @@ export default class InviteSendMainNotificationSubscribe {
const userId = authedUser.id;
this.sendInviteMailQueue.add(SendInviteUserMailJob, {
fromUser: user,
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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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]);