Compare commits

...

15 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
88ff5db0f3 Merge pull request #989 from bigcapitalhq/fix/organization-date-formats-and-address-fields
fix(organization): align date formats and fix address field naming
2026-02-24 22:45:10 +02:00
Ahmed Bouhuolia
f35e85c3d2 fix(organization): align date formats and fix address field naming
- Fix date format mismatch between Miscellaneous and Organization constants
- Fix default date format casing ('DD MMM yyyy' -> 'DD MMM YYYY')
- Rename address fields from address_1/address_2 to address1/address2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 22:42:29 +02:00
Ahmed Bouhuolia
29decf9c5a Merge pull request #987 from bigcapitalhq/feat/contact-address-country-fields
fix: country and address fields of customer and vendor forms
2026-02-24 20:55:23 +02:00
Ahmed Bouhuolia
f149ff43b4 feat(contacts): add country field to customer and vendor address forms
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 20:53:14 +02:00
Ahmed Bouhuolia
fb05af8c00 Merge pull request #979 from yk-a11y/fix/credit-note-apply-invoice-validation
fix: validate credit note per-entry amount against each invoice due amount
2026-02-24 02:55:47 +02:00
Ahmed Bouhuolia
688b1bfb56 fix(server): add invoices Map for validateInvoicesRemainingAmount 2026-02-24 02:52:28 +02:00
Ahmed Bouhuolia
0f8147daff Merge branch 'develop' into fix/credit-note-apply-invoice-validation 2026-02-24 02:42:23 +02:00
Ahmed Bouhuolia
96b24d4fb9 Merge pull request #982 from bigcapitalhq/fix/credit-notes-applied-invoice-delete
fix: Add DELETE endpoint for credit notes applied invoices
2026-02-24 02:17:37 +02:00
Ahmed Bouhuolia
2a87103bc8 fix: Add DELETE endpoint for credit notes applied invoices
- Add missing DELETE /credit-notes/applied-invoices/:id endpoint
- Fix CreditNotesApplyInvoice controller to use correct service methods
- Add missing GetCreditNoteAssociatedInvoicesToApply endpoint
- Add proper DTO for ApplyCreditNoteToInvoices
- Update frontend creditNote hook to use correct API paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 02:15:32 +02:00
Ahmed Bouhuolia
238b60144f Merge pull request #981 from bigcapitalhq/fix/register-verify-dark-mode
fix: add dark mode support to email confirmation UI
2026-02-23 01:10:50 +02:00
Ahmed Bouhuolia
fcee85e358 fix: add dark mode support to email confirmation UI
Refactored RegisterVerify component to use xstyled for styling
with proper dark mode color values instead of hardcoded light theme colors.
2026-02-23 00:48:32 +02:00
Ahmed Bouhuolia
64a10053e3 Merge pull request #980 from bigcapitalhq/fix-signup-verification
fix: signup confirmation
2026-02-23 00:39:45 +02:00
Ahmed Bouhuolia
ce9f2a238f fix: signup confirmation 2026-02-23 00:37:56 +02:00
Yong ke Weng
75b98c39d8 fix: validate credit note per-entry amount against each invoice due amount
The `validateInvoicesRemainingAmount` method was incorrectly comparing the
total credit amount (sum of all entries) against each individual invoice's
due amount. This caused valid credit note applications to be rejected when
applying to multiple invoices where the total exceeded any single invoice's
due amount.

Changed the validation to compare each invoice's due amount against only the
specific entry amount being applied to that invoice.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 09:50:39 -05:00
Ahmed Bouhuolia
80e545072d Merge pull request #975 from bigcapitalhq/fix/localize-financial-reports
fix: localize hardcoded strings in financial reports
2026-02-18 22:09:05 +02:00
35 changed files with 242 additions and 130 deletions

View File

@@ -1,5 +1,5 @@
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import bluebird from 'bluebird'; import * as bluebird from 'bluebird';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import {
validateLinkModelEntryExists, validateLinkModelEntryExists,
@@ -53,7 +53,8 @@ export class LinkAttachment {
const foundLinkModel = await LinkModel().query(trx).findById(modelId); const foundLinkModel = await LinkModel().query(trx).findById(modelId);
validateLinkModelEntryExists(foundLinkModel); validateLinkModelEntryExists(foundLinkModel);
const foundLinks = await this.documentLinkModel().query(trx) const foundLinks = await this.documentLinkModel()
.query(trx)
.where('modelRef', modelRef) .where('modelRef', modelRef)
.where('modelId', modelId) .where('modelId', modelId)
.where('documentId', foundFile.id); .where('documentId', foundFile.id);

View File

@@ -65,7 +65,7 @@ export class AuthController {
return this.authApp.signUp(signupDto); return this.authApp.signUp(signupDto);
} }
@Post('/signup/confirm') @Post('/signup/verify')
@ApiOperation({ summary: 'Confirm user signup' }) @ApiOperation({ summary: 'Confirm user signup' })
@ApiBody({ @ApiBody({
schema: { schema: {

View File

@@ -7,17 +7,13 @@ import {
import { GetAuthenticatedAccount } from './queries/GetAuthedAccount.service'; import { GetAuthenticatedAccount } from './queries/GetAuthedAccount.service';
import { Controller, Get, Post } from '@nestjs/common'; import { Controller, Get, Post } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler'; import { Throttle } from '@nestjs/throttler';
import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards'; import { TenantAgnosticRoute } from '../Tenancy/TenancyGlobal.guard';
import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard';
import { AuthenticationApplication } from './AuthApplication.sevice'; import { AuthenticationApplication } from './AuthApplication.sevice';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { IgnoreUserVerifiedRoute } from './guards/EnsureUserVerified.guard'; import { IgnoreUserVerifiedRoute } from './guards/EnsureUserVerified.guard';
@Controller('/auth') @Controller('/auth')
@ApiTags('Auth') @ApiTags('Auth')
@ApiExcludeController() @TenantAgnosticRoute()
@IgnoreTenantSeededRoute()
@IgnoreTenantInitializedRoute()
@IgnoreUserVerifiedRoute() @IgnoreUserVerifiedRoute()
@Throttle({ auth: {} }) @Throttle({ auth: {} })
export class AuthedController { export class AuthedController {

View File

@@ -13,7 +13,6 @@ import {
IAuthSignedUpEventPayload, IAuthSignedUpEventPayload,
IAuthSigningUpEventPayload, IAuthSigningUpEventPayload,
} from '../Auth.interfaces'; } from '../Auth.interfaces';
import { defaultTo } from 'ramda';
import { ERRORS } from '../Auth.constants'; import { ERRORS } from '../Auth.constants';
import { hashPassword } from '../Auth.utils'; import { hashPassword } from '../Auth.utils';
import { ClsService } from 'nestjs-cls'; import { ClsService } from 'nestjs-cls';
@@ -51,7 +50,7 @@ export class AuthSignupService {
const signupConfirmation = this.configService.get('signupConfirmation'); const signupConfirmation = this.configService.get('signupConfirmation');
const verifyTokenCrypto = crypto.randomBytes(64).toString('hex'); const verifyTokenCrypto = crypto.randomBytes(64).toString('hex');
const verifiedEnabed = defaultTo(signupConfirmation.enabled, false); const verifiedEnabed = signupConfirmation.enabled ?? false;
const verifyToken = verifiedEnabed ? verifyTokenCrypto : ''; const verifyToken = verifiedEnabed ? verifyTokenCrypto : '';
const verified = !verifiedEnabed; const verified = !verifiedEnabed;

View File

@@ -4,7 +4,6 @@ import { SystemUser } from '@/modules/System/models/SystemUser';
import { ServiceError } from '@/modules/Items/ServiceError'; import { ServiceError } from '@/modules/Items/ServiceError';
import { ERRORS } from '../Auth.constants'; import { ERRORS } from '../Auth.constants';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { ModelObject } from 'objection';
import { ISignUpConfigmResendedEventPayload } from '../Auth.interfaces'; import { ISignUpConfigmResendedEventPayload } from '../Auth.interfaces';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';

View File

@@ -10,7 +10,7 @@ export interface IContactAddress {
billingAddressCity: string; billingAddressCity: string;
billingAddressCountry: string; billingAddressCountry: string;
billingAddressEmail: string; billingAddressEmail: string;
billingAddressZipcode: string; billingAddressPostcode: string;
billingAddressPhone: string; billingAddressPhone: string;
billingAddressState: string; billingAddressState: string;
@@ -19,7 +19,7 @@ export interface IContactAddress {
shippingAddressCity: string; shippingAddressCity: string;
shippingAddressCountry: string; shippingAddressCountry: string;
shippingAddressEmail: string; shippingAddressEmail: string;
shippingAddressZipcode: string; shippingAddressPostcode: string;
shippingAddressPhone: string; shippingAddressPhone: string;
shippingAddressState: string; shippingAddressState: string;
} }
@@ -29,7 +29,7 @@ export interface IContactAddressDTO {
billingAddressCity?: string; billingAddressCity?: string;
billingAddressCountry?: string; billingAddressCountry?: string;
billingAddressEmail?: string; billingAddressEmail?: string;
billingAddressZipcode?: string; billingAddressPostcode?: string;
billingAddressPhone?: string; billingAddressPhone?: string;
billingAddressState?: string; billingAddressState?: string;
@@ -38,7 +38,7 @@ export interface IContactAddressDTO {
shippingAddressCity?: string; shippingAddressCity?: string;
shippingAddressCountry?: string; shippingAddressCountry?: string;
shippingAddressEmail?: string; shippingAddressEmail?: string;
shippingAddressZipcode?: string; shippingAddressPostcode?: string;
shippingAddressPhone?: string; shippingAddressPhone?: string;
shippingAddressState?: string; shippingAddressState?: string;
} }

View File

@@ -1,6 +1,7 @@
import { import {
Body, Body,
Controller, Controller,
Delete,
Get, Get,
Param, Param,
Post, Post,
@@ -14,6 +15,10 @@ import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard'; import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types'; import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types'; import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
import { CreditNoteApplyToInvoices } from './commands/CreditNoteApplyToInvoices.service';
import { DeleteCreditNoteApplyToInvoices } from './commands/DeleteCreditNoteApplyToInvoices.service';
import { ApplyCreditNoteToInvoicesDto } from './dtos/ApplyCreditNoteToInvoices.dto';
@Controller('credit-notes') @Controller('credit-notes')
@ApiTags('Credit Notes Apply Invoice') @ApiTags('Credit Notes Apply Invoice')
@@ -22,6 +27,9 @@ import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
export class CreditNotesApplyInvoiceController { export class CreditNotesApplyInvoiceController {
constructor( constructor(
private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices, private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices,
private readonly getCreditNoteAssociatedInvoicesToApplyService: GetCreditNoteAssociatedInvoicesToApply,
private readonly creditNoteApplyToInvoicesService: CreditNoteApplyToInvoices,
private readonly deleteCreditNoteApplyToInvoicesService: DeleteCreditNoteApplyToInvoices,
) {} ) {}
@Get(':creditNoteId/applied-invoices') @Get(':creditNoteId/applied-invoices')
@@ -39,6 +47,23 @@ export class CreditNotesApplyInvoiceController {
); );
} }
@Get(':creditNoteId/apply-invoices')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get credit note associated invoices to apply' })
@ApiResponse({
status: 200,
description: 'Credit note associated invoices to apply',
})
@ApiResponse({ status: 404, description: 'Credit note not found' })
@ApiResponse({ status: 400, description: 'Invalid input data' })
getCreditNoteAssociatedInvoicesToApply(
@Param('creditNoteId') creditNoteId: number,
) {
return this.getCreditNoteAssociatedInvoicesToApplyService.getCreditAssociatedInvoicesToApply(
creditNoteId,
);
}
@Post(':creditNoteId/apply-invoices') @Post(':creditNoteId/apply-invoices')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote) @RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Apply credit note to invoices' }) @ApiOperation({ summary: 'Apply credit note to invoices' })
@@ -48,9 +73,32 @@ export class CreditNotesApplyInvoiceController {
}) })
@ApiResponse({ status: 404, description: 'Credit note not found' }) @ApiResponse({ status: 404, description: 'Credit note not found' })
@ApiResponse({ status: 400, description: 'Invalid input data' }) @ApiResponse({ status: 400, description: 'Invalid input data' })
applyCreditNoteToInvoices(@Param('creditNoteId') creditNoteId: number) { applyCreditNoteToInvoices(
return this.getCreditNoteAssociatedAppliedInvoicesService.getCreditAssociatedAppliedInvoices( @Param('creditNoteId') creditNoteId: number,
@Body() applyDto: ApplyCreditNoteToInvoicesDto,
) {
return this.creditNoteApplyToInvoicesService.applyCreditNoteToInvoices(
creditNoteId, creditNoteId,
applyDto,
);
}
@Delete('applied-invoices/:applyCreditToInvoicesId')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Delete applied credit note to invoice' })
@ApiResponse({
status: 200,
description: 'Credit note application successfully deleted',
})
@ApiResponse({
status: 404,
description: 'Credit note application not found',
})
deleteApplyCreditNoteToInvoices(
@Param('applyCreditToInvoicesId') applyCreditToInvoicesId: number,
) {
return this.deleteCreditNoteApplyToInvoicesService.deleteApplyCreditNoteToInvoices(
applyCreditToInvoicesId,
); );
} }
} }

View File

@@ -9,6 +9,8 @@ import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service'; import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service'; import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.controller'; import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.controller';
import { CreditNoteApplySyncCreditSubscriber } from './subscribers/CreditNoteApplySyncCreditSubscriber';
import { CreditNoteApplySyncInvoicesCreditedAmountSubscriber } from './subscribers/CreditNoteApplySyncInvoicesSubscriber';
@Module({ @Module({
providers: [ providers: [
@@ -19,6 +21,8 @@ import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.con
CreditNoteApplySyncCredit, CreditNoteApplySyncCredit,
GetCreditNoteAssociatedAppliedInvoices, GetCreditNoteAssociatedAppliedInvoices,
GetCreditNoteAssociatedInvoicesToApply, GetCreditNoteAssociatedInvoicesToApply,
CreditNoteApplySyncCreditSubscriber,
CreditNoteApplySyncInvoicesCreditedAmountSubscriber,
], ],
exports: [DeleteCustomerLinkedCreditNoteService], exports: [DeleteCustomerLinkedCreditNoteService],
imports: [PaymentsReceivedModule, forwardRef(() => CreditNotesModule)], imports: [PaymentsReceivedModule, forwardRef(() => CreditNotesModule)],

View File

@@ -1,6 +1,6 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Injectable, Inject } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import { ICreditNoteAppliedToInvoice } from '../types/CreditNoteApplyInvoice.types'; import { ICreditNoteAppliedToInvoice } from '../types/CreditNoteApplyInvoice.types';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice'; import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice'; import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice';

View File

@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { sumBy } from 'lodash'; import { sumBy } from 'lodash';
import { import {
ICreditNoteAppliedToInvoice,
ICreditNoteAppliedToInvoiceModel, ICreditNoteAppliedToInvoiceModel,
IApplyCreditToInvoicesDTO, IApplyCreditToInvoicesDTO,
IApplyCreditToInvoicesCreatedPayload, IApplyCreditToInvoicesCreatedPayload,
@@ -17,6 +18,7 @@ import { CreditNote } from '@/modules/CreditNotes/models/CreditNote';
import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice'; import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice';
import { CommandCreditNoteDTOTransform } from '@/modules/CreditNotes/commands/CommandCreditNoteDTOTransform.service'; import { CommandCreditNoteDTOTransform } from '@/modules/CreditNotes/commands/CommandCreditNoteDTOTransform.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { ApplyCreditNoteToInvoicesDto } from '../dtos/ApplyCreditNoteToInvoices.dto';
@Injectable() @Injectable()
export class CreditNoteApplyToInvoices { export class CreditNoteApplyToInvoices {
@@ -48,7 +50,7 @@ export class CreditNoteApplyToInvoices {
*/ */
public async applyCreditNoteToInvoices( public async applyCreditNoteToInvoices(
creditNoteId: number, creditNoteId: number,
applyCreditToInvoicesDTO: IApplyCreditToInvoicesDTO, applyCreditToInvoicesDTO: ApplyCreditNoteToInvoicesDto,
): Promise<CreditNoteAppliedInvoice[]> { ): Promise<CreditNoteAppliedInvoice[]> {
// Saves the credit note or throw not found service error. // Saves the credit note or throw not found service error.
const creditNote = await this.creditNoteModel() const creditNote = await this.creditNoteModel()
@@ -71,7 +73,7 @@ export class CreditNoteApplyToInvoices {
// Validate invoices has remaining amount to apply. // Validate invoices has remaining amount to apply.
this.validateInvoicesRemainingAmount( this.validateInvoicesRemainingAmount(
appliedInvoicesEntries, appliedInvoicesEntries,
creditNoteAppliedModel.amount, creditNoteAppliedModel.entries,
); );
// Validate the credit note remaining amount. // Validate the credit note remaining amount.
this.creditNoteDTOTransform.validateCreditRemainingAmount( this.creditNoteDTOTransform.validateCreditRemainingAmount(
@@ -122,18 +124,20 @@ export class CreditNoteApplyToInvoices {
}; };
/** /**
* Validate the invoice remaining amount. * Validate each invoice has sufficient remaining amount for the applied credit.
* @param {ISaleInvoice[]} invoices * @param {ISaleInvoice[]} invoices
* @param {number} amount * @param {ICreditNoteAppliedToInvoice[]} entries
*/ */
private validateInvoicesRemainingAmount = ( private validateInvoicesRemainingAmount = (
invoices: SaleInvoice[], invoices: SaleInvoice[],
amount: number, entries: ICreditNoteAppliedToInvoice[],
) => { ) => {
const invalidInvoices = invoices.filter( const invoiceMap = new Map(invoices.map((inv) => [inv.id, inv]));
(invoice) => invoice.dueAmount < amount, const invalidEntries = entries.filter((entry) => {
); const invoice = invoiceMap.get(entry.invoiceId);
if (invalidInvoices.length > 0) { return invoice != null && invoice.dueAmount < entry.amount;
});
if (invalidEntries.length > 0) {
throw new ServiceError(ERRORS.INVOICES_HAS_NO_REMAINING_AMOUNT); throw new ServiceError(ERRORS.INVOICES_HAS_NO_REMAINING_AMOUNT);
} }
}; };

View File

@@ -0,0 +1,38 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsInt,
IsNotEmpty,
IsNumber,
ValidateNested,
} from 'class-validator';
export class ApplyCreditNoteInvoiceEntryDto {
@IsNotEmpty()
@IsInt()
@ApiProperty({ description: 'Invoice ID to apply credit to', example: 1 })
invoiceId: number;
@IsNotEmpty()
@IsNumber()
@ApiProperty({ description: 'Amount to apply', example: 100.5 })
amount: number;
}
export class ApplyCreditNoteToInvoicesDto {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => ApplyCreditNoteInvoiceEntryDto)
@ApiProperty({
description: 'Entries of invoice ID and amount to apply',
type: [ApplyCreditNoteInvoiceEntryDto],
example: [
{ invoice_id: 1, amount: 100.5 },
{ invoice_id: 2, amount: 50 },
],
})
entries: ApplyCreditNoteInvoiceEntryDto[];
}

View File

@@ -8,7 +8,7 @@ import { CreditNoteApplySyncInvoicesCreditedAmount } from '../commands/CreditNot
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
@Injectable() @Injectable()
export default class CreditNoteApplySyncInvoicesCreditedAmountSubscriber { export class CreditNoteApplySyncInvoicesCreditedAmountSubscriber {
constructor( constructor(
private readonly syncInvoicesWithCreditNote: CreditNoteApplySyncInvoicesCreditedAmount, private readonly syncInvoicesWithCreditNote: CreditNoteApplySyncInvoicesCreditedAmount,
) {} ) {}

View File

@@ -29,6 +29,7 @@ export interface IApplyCreditToInvoicesDeletedPayload {
export interface ICreditNoteAppliedToInvoice { export interface ICreditNoteAppliedToInvoice {
amount: number; amount: number;
creditNoteId: number; creditNoteId: number;
invoiceId: number;
} }
export interface ICreditNoteAppliedToInvoiceModel { export interface ICreditNoteAppliedToInvoiceModel {
amount: number; amount: number;

View File

@@ -27,10 +27,10 @@ export class ContactAddressDto {
@IsEmail() @IsEmail()
billingAddressEmail?: string; billingAddressEmail?: string;
@ApiProperty({ required: false, description: 'Billing address zipcode' }) @ApiProperty({ required: false, description: 'Billing address postcode' })
@IsOptional() @IsOptional()
@IsString() @IsString()
billingAddressZipcode?: string; billingAddressPostcode?: string;
@ApiProperty({ required: false, description: 'Billing address phone' }) @ApiProperty({ required: false, description: 'Billing address phone' })
@IsOptional() @IsOptional()
@@ -67,10 +67,10 @@ export class ContactAddressDto {
@IsEmail() @IsEmail()
shippingAddressEmail?: string; shippingAddressEmail?: string;
@ApiProperty({ required: false, description: 'Shipping address zipcode' }) @ApiProperty({ required: false, description: 'Shipping address postcode' })
@IsOptional() @IsOptional()
@IsString() @IsString()
shippingAddressZipcode?: string; shippingAddressPostcode?: string;
@ApiProperty({ required: false, description: 'Shipping address phone' }) @ApiProperty({ required: false, description: 'Shipping address phone' })
@IsOptional() @IsOptional()

View File

@@ -1,18 +1,15 @@
import currencies from 'js-money/lib/currency'; import currencies from 'js-money/lib/currency';
export const DATE_FORMATS = [ export const DATE_FORMATS = [
'MM.dd.yy', 'MM/DD/YY',
'dd.MM.yy', 'DD/MM/YY',
'yy.MM.dd', 'YY/MM/DD',
'MM.dd.yyyy', 'MM/DD/yyyy',
'dd.MM.yyyy', 'DD/MM/yyyy',
'yyyy.MM.dd', 'yyyy/MM/DD',
'MM/DD/YYYY', 'DD MMM YYYY',
'M/D/YYYY', 'DD MMMM YYYY',
'dd MMM YYYY', 'MMMM DD, YYYY',
'dd MMMM YYYY',
'MMMM dd, YYYY',
'EEE, MMMM dd, YYYY',
]; ];
export const MONTHS = [ export const MONTHS = [
'january', 'january',

View File

@@ -28,6 +28,7 @@ import { UpdateOrganizationService } from './commands/UpdateOrganization.service
import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard'; import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard';
import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards'; import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards';
import { IgnoreTenantModelsInitialize } from '../Tenancy/TenancyInitializeModels.guard'; import { IgnoreTenantModelsInitialize } from '../Tenancy/TenancyInitializeModels.guard';
import { IgnoreUserVerifiedRoute } from '../Auth/guards/EnsureUserVerified.guard';
import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob.service'; import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob.service';
import { OrganizationBaseCurrencyLocking } from './Organization/OrganizationBaseCurrencyLocking.service'; import { OrganizationBaseCurrencyLocking } from './Organization/OrganizationBaseCurrencyLocking.service';
import { import {
@@ -93,6 +94,7 @@ export class OrganizationController {
@Get('current') @Get('current')
@HttpCode(200) @HttpCode(200)
@IgnoreUserVerifiedRoute()
@ApiOperation({ summary: 'Get current organization' }) @ApiOperation({ summary: 'Get current organization' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -12,6 +12,6 @@ export const transformBuildDto = (
): BuildOrganizationDto => { ): BuildOrganizationDto => {
return { return {
...buildDTO, ...buildDTO,
dateFormat: defaultTo(buildDTO.dateFormat, 'DD MMM yyyy'), dateFormat: defaultTo(buildDTO.dateFormat, 'DD MMM YYYY'),
}; };
}; };

View File

@@ -8,6 +8,7 @@ import {
import { TenancyContext } from './TenancyContext.service'; import { TenancyContext } from './TenancyContext.service';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants'; import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants';
import { IS_TENANT_AGNOSTIC } from './TenancyGlobal.guard';
export const IS_IGNORE_TENANT_INITIALIZED = 'IS_IGNORE_TENANT_INITIALIZED'; export const IS_IGNORE_TENANT_INITIALIZED = 'IS_IGNORE_TENANT_INITIALIZED';
export const IgnoreTenantInitializedRoute = () => export const IgnoreTenantInitializedRoute = () =>
@@ -35,8 +36,12 @@ export class EnsureTenantIsInitializedGuard implements CanActivate {
IS_PUBLIC_ROUTE, IS_PUBLIC_ROUTE,
[context.getHandler(), context.getClass()], [context.getHandler(), context.getClass()],
); );
const isTenantAgnostic = this.reflector.getAllAndOverride<boolean>(
IS_TENANT_AGNOSTIC,
[context.getHandler(), context.getClass()],
);
// Skip the guard early if the route marked as public or ignored. // Skip the guard early if the route marked as public or ignored.
if (isPublic || isIgnoreEnsureTenantInitialized) { if (isPublic || isIgnoreEnsureTenantInitialized || isTenantAgnostic) {
return true; return true;
} }
const tenant = await this.tenancyContext.getTenant(); const tenant = await this.tenancyContext.getTenant();

View File

@@ -9,6 +9,7 @@ import {
import { TenancyContext } from './TenancyContext.service'; import { TenancyContext } from './TenancyContext.service';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants'; import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants';
import { IS_TENANT_AGNOSTIC } from './TenancyGlobal.guard';
export const IS_IGNORE_TENANT_SEEDED = 'IS_IGNORE_TENANT_SEEDED'; export const IS_IGNORE_TENANT_SEEDED = 'IS_IGNORE_TENANT_SEEDED';
export const IgnoreTenantSeededRoute = () => export const IgnoreTenantSeededRoute = () =>
@@ -36,7 +37,12 @@ export class EnsureTenantIsSeededGuard implements CanActivate {
context.getHandler(), context.getHandler(),
context.getClass(), context.getClass(),
]); ]);
if (isPublic || isIgnoreEnsureTenantSeeded) { const isTenantAgnostic = this.reflector.getAllAndOverride<boolean>(
IS_TENANT_AGNOSTIC,
[context.getHandler(), context.getClass()],
);
// Skip the guard early if the route marked as public, tenant agnostic or ignored.
if (isPublic || isIgnoreEnsureTenantSeeded || isTenantAgnostic) {
return true; return true;
} }
const tenant = await this.tenancyContext.getTenant(); const tenant = await this.tenancyContext.getTenant();

View File

@@ -1,4 +1,4 @@
import Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { IVendorCreditAppliedBill } from '../types/VendorCreditApplyBills.types'; import { IVendorCreditAppliedBill } from '../types/VendorCreditApplyBills.types';

View File

@@ -48,7 +48,10 @@ export class EditVendorDto extends ContactAddressDto {
@IsString() @IsString()
personalPhone?: string; personalPhone?: string;
@ApiProperty({ required: false, description: 'Additional notes about the vendor' }) @ApiProperty({
required: false,
description: 'Additional notes about the vendor',
})
@IsOptional() @IsOptional()
@IsString() @IsString()
note?: string; note?: string;

View File

@@ -1,18 +0,0 @@
.root {
text-align: center;
}
.title{
font-size: 18px;
font-weight: 600;
margin-bottom: 0.5rem;
color: #252A31;
}
.description{
margin-bottom: 1rem;
font-size: 15px;
line-height: 1.45;
color: #404854;
}

View File

@@ -1,12 +1,13 @@
// @ts-nocheck // @ts-nocheck
import { Button, Intent } from '@blueprintjs/core'; import { Button, Intent } from '@blueprintjs/core';
import { x } from '@xstyled/emotion';
import AuthInsider from './AuthInsider'; import AuthInsider from './AuthInsider';
import { AuthInsiderCard } from './_components'; import { AuthInsiderCard } from './_components';
import styles from './RegisterVerify.module.scss';
import { AppToaster, Stack } from '@/components'; import { AppToaster, Stack } from '@/components';
import { useAuthActions, useAuthUserVerifyEmail } from '@/hooks/state'; import { useAuthActions, useAuthUserVerifyEmail } from '@/hooks/state';
import { useAuthSignUpVerifyResendMail } from '@/hooks/query'; import { useAuthSignUpVerifyResendMail } from '@/hooks/query';
import { AuthContainer } from './AuthContainer'; import { AuthContainer } from './AuthContainer';
import { useIsDarkMode } from '@/hooks/useDarkMode';
export default function RegisterVerify() { export default function RegisterVerify() {
const { setLogout } = useAuthActions(); const { setLogout } = useAuthActions();
@@ -14,6 +15,7 @@ export default function RegisterVerify() {
useAuthSignUpVerifyResendMail(); useAuthSignUpVerifyResendMail();
const emailAddress = useAuthUserVerifyEmail(); const emailAddress = useAuthUserVerifyEmail();
const isDarkMode = useIsDarkMode();
const handleResendMailBtnClick = () => { const handleResendMailBtnClick = () => {
resendSignUpVerifyMail() resendSignUpVerifyMail()
@@ -37,12 +39,24 @@ export default function RegisterVerify() {
return ( return (
<AuthContainer> <AuthContainer>
<AuthInsider> <AuthInsider>
<AuthInsiderCard className={styles.root}> <AuthInsiderCard textAlign="center">
<h2 className={styles.title}>Please verify your email</h2> <x.h2
<p className={styles.description}> fontSize="18px"
fontWeight={600}
mb="0.5rem"
color={isDarkMode ? 'rgba(255, 255, 255, 0.85)' : '#252A31'}
>
Please verify your email
</x.h2>
<x.p
mb="1rem"
fontSize="15px"
lineHeight="1.45"
color={isDarkMode ? 'rgba(255, 255, 255, 0.7)' : '#404854'}
>
We sent an email to <strong>{emailAddress}</strong> Click the link We sent an email to <strong>{emailAddress}</strong> Click the link
inside to get started. inside to get started.
</p> </x.p>
<Stack spacing={4}> <Stack spacing={4}>
<Button <Button

View File

@@ -27,20 +27,20 @@ const CustomerBillingAddress = ({}) => {
{/*------------ Billing Address 1 -----------*/} {/*------------ Billing Address 1 -----------*/}
<FFormGroup <FFormGroup
name={'billing_address_1'} name={'billing_address1'}
label={<T id={'address_line_1'} />} label={<T id={'address_line_1'} />}
inline={true} inline={true}
> >
<FTextArea name={'billing_address_1'} /> <FTextArea name={'billing_address1'} />
</FFormGroup> </FFormGroup>
{/*------------ Billing Address 2 -----------*/} {/*------------ Billing Address 2 -----------*/}
<FFormGroup <FFormGroup
name={'billing_address_2'} name={'billing_address2'}
label={<T id={'address_line_2'} />} label={<T id={'address_line_2'} />}
inline={true} inline={true}
> >
<FTextArea name={'billing_address_2'} /> <FTextArea name={'billing_address2'} />
</FFormGroup> </FFormGroup>
{/*------------ Billing Address city -----------*/} {/*------------ Billing Address city -----------*/}
<FFormGroup <FFormGroup
@@ -93,20 +93,20 @@ const CustomerBillingAddress = ({}) => {
{/*------------ Shipping Address 1 -----------*/} {/*------------ Shipping Address 1 -----------*/}
<FFormGroup <FFormGroup
name={'shipping_address_1'} name={'shipping_address1'}
label={<T id={'address_line_1'} />} label={<T id={'address_line_1'} />}
inline={true} inline={true}
> >
<FTextArea name={'shipping_address_1'} /> <FTextArea name={'shipping_address1'} />
</FFormGroup> </FFormGroup>
{/*------------ Shipping Address 2 -----------*/} {/*------------ Shipping Address 2 -----------*/}
<FFormGroup <FFormGroup
name={'shipping_address_2'} name={'shipping_address2'}
label={<T id={'address_line_2'} />} label={<T id={'address_line_2'} />}
inline={true} inline={true}
> >
<FTextArea name={'shipping_address_2'} /> <FTextArea name={'shipping_address2'} />
</FFormGroup> </FFormGroup>
{/*------------ Shipping Address city -----------*/} {/*------------ Shipping Address city -----------*/}

View File

@@ -25,16 +25,16 @@ const Schema = Yup.object().shape({
note: Yup.string().trim(), note: Yup.string().trim(),
billing_address_country: Yup.string().trim(), billing_address_country: Yup.string().trim(),
billing_address_1: Yup.string().trim(), billing_address1: Yup.string().trim(),
billing_address_2: Yup.string().trim(), billing_address2: Yup.string().trim(),
billing_address_city: Yup.string().trim(), billing_address_city: Yup.string().trim(),
billing_address_state: Yup.string().trim(), billing_address_state: Yup.string().trim(),
billing_address_postcode: Yup.string().nullable(), billing_address_postcode: Yup.string().nullable(),
billing_address_phone: Yup.string().nullable(), billing_address_phone: Yup.string().nullable(),
shipping_address_country: Yup.string().trim(), shipping_address_country: Yup.string().trim(),
shipping_address_1: Yup.string().trim(), shipping_address1: Yup.string().trim(),
shipping_address_2: Yup.string().trim(), shipping_address2: Yup.string().trim(),
shipping_address_city: Yup.string().trim(), shipping_address_city: Yup.string().trim(),
shipping_address_state: Yup.string().trim(), shipping_address_state: Yup.string().trim(),
shipping_address_postcode: Yup.string().nullable(), shipping_address_postcode: Yup.string().nullable(),

View File

@@ -8,7 +8,7 @@ import styled from 'styled-components';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema'; import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema';
import { compose, transformToForm, saveInvoke } from '@/utils'; import { compose, transformToForm, saveInvoke, parseBoolean } from '@/utils';
import { useCustomerFormContext } from './CustomerFormProvider'; import { useCustomerFormContext } from './CustomerFormProvider';
import { defaultInitialValues } from './utils'; import { defaultInitialValues } from './utils';
@@ -60,7 +60,10 @@ function CustomerFormFormik({
// Handles the form submit. // Handles the form submit.
const handleFormSubmit = (values, formArgs) => { const handleFormSubmit = (values, formArgs) => {
const { setSubmitting, resetForm } = formArgs; const { setSubmitting, resetForm } = formArgs;
const formValues = { ...values }; const formValues = {
...values,
active: parseBoolean(values.active, true),
};
const onSuccess = (res) => { const onSuccess = (res) => {
AppToaster.show({ AppToaster.show({

View File

@@ -55,7 +55,7 @@ export default function CustomerFormPrimarySection({}) {
label={<T id={'company_name'} />} label={<T id={'company_name'} />}
inline={true} inline={true}
> >
<InputGroup name={'company_name'} /> <FInputGroup name={'company_name'} />
</FFormGroup> </FFormGroup>
{/*----------- Display Name -----------*/} {/*----------- Display Name -----------*/}

View File

@@ -23,16 +23,16 @@ export const defaultInitialValues = {
active: true, active: true,
billing_address_country: '', billing_address_country: '',
billing_address_1: '', billing_address1: '',
billing_address_2: '', billing_address2: '',
billing_address_city: '', billing_address_city: '',
billing_address_state: '', billing_address_state: '',
billing_address_postcode: '', billing_address_postcode: '',
billing_address_phone: '', billing_address_phone: '',
shipping_address_country: '', shipping_address_country: '',
shipping_address_1: '', shipping_address1: '',
shipping_address_2: '', shipping_address2: '',
shipping_address_city: '', shipping_address_city: '',
shipping_address_state: '', shipping_address_state: '',
shipping_address_postcode: '', shipping_address_postcode: '',

View File

@@ -111,12 +111,12 @@ export default function PreferencesGeneralForm({ isSubmitting }) {
> >
<Stack> <Stack>
<FInputGroup <FInputGroup
name={'address.address_1'} name={'address.address1'}
placeholder={'Address 1'} placeholder={'Address 1'}
fastField fastField
/> />
<FInputGroup <FInputGroup
name={'address.address_2'} name={'address.address2'}
placeholder={'Address 2'} placeholder={'Address 2'}
fastField fastField
/> />

View File

@@ -18,16 +18,16 @@ const Schema = Yup.object().shape({
note: Yup.string().trim(), note: Yup.string().trim(),
billing_address_country: Yup.string().trim(), billing_address_country: Yup.string().trim(),
billing_address_1: Yup.string().trim(), billing_address1: Yup.string().trim(),
billing_address_2: Yup.string().trim(), billing_address2: Yup.string().trim(),
billing_address_city: Yup.string().trim(), billing_address_city: Yup.string().trim(),
billing_address_state: Yup.string().trim(), billing_address_state: Yup.string().trim(),
billing_address_postcode: Yup.string().nullable(), billing_address_postcode: Yup.string().nullable(),
billing_address_phone: Yup.string().nullable(), billing_address_phone: Yup.string().nullable(),
shipping_address_country: Yup.string().trim(), shipping_address_country: Yup.string().trim(),
shipping_address_1: Yup.string().trim(), shipping_address1: Yup.string().trim(),
shipping_address_2: Yup.string().trim(), shipping_address2: Yup.string().trim(),
shipping_address_city: Yup.string().trim(), shipping_address_city: Yup.string().trim(),
shipping_address_state: Yup.string().trim(), shipping_address_state: Yup.string().trim(),
shipping_address_postcode: Yup.string().nullable(), shipping_address_postcode: Yup.string().nullable(),

View File

@@ -21,7 +21,7 @@ import VendorFloatingActions from './VendorFloatingActions';
import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization'; import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization';
import { useVendorFormContext } from './VendorFormProvider'; import { useVendorFormContext } from './VendorFormProvider';
import { compose, transformToForm, safeInvoke } from '@/utils'; import { compose, transformToForm, safeInvoke, parseBoolean } from '@/utils';
import { defaultInitialValues } from './utils'; import { defaultInitialValues } from './utils';
import '@/style/pages/Vendors/Form.scss'; import '@/style/pages/Vendors/Form.scss';
@@ -69,7 +69,10 @@ function VendorFormFormik({
// Handles the form submit. // Handles the form submit.
const handleFormSubmit = (values, form) => { const handleFormSubmit = (values, form) => {
const { setSubmitting, resetForm } = form; const { setSubmitting, resetForm } = form;
const requestForm = { ...values }; const requestForm = {
...values,
active: parseBoolean(values.active, true),
};
setSubmitting(true); setSubmitting(true);

View File

@@ -22,16 +22,16 @@ export const defaultInitialValues = {
active: true, active: true,
billing_address_country: '', billing_address_country: '',
billing_address_1: '', billing_address1: '',
billing_address_2: '', billing_address2: '',
billing_address_city: '', billing_address_city: '',
billing_address_state: '', billing_address_state: '',
billing_address_postcode: '', billing_address_postcode: '',
billing_address_phone: '', billing_address_phone: '',
shipping_address_country: '', shipping_address_country: '',
shipping_address_1: '', shipping_address1: '',
shipping_address_2: '', shipping_address2: '',
shipping_address_city: '', shipping_address_city: '',
shipping_address_state: '', shipping_address_state: '',
shipping_address_postcode: '', shipping_address_postcode: '',

View File

@@ -128,7 +128,7 @@ export const useAuthMetadata = (props = {}) => {
* Resend the mail of signup verification. * Resend the mail of signup verification.
*/ */
export const useAuthSignUpVerifyResendMail = (props) => { export const useAuthSignUpVerifyResendMail = (props) => {
const apiRequest = useAuthApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation(
() => apiRequest.post(AuthRoute.SignupVerifyResend), () => apiRequest.post(AuthRoute.SignupVerifyResend),

View File

@@ -58,16 +58,13 @@ export function useCreateCreditNote(props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation((values) => apiRequest.post('credit-notes', values), {
(values) => apiRequest.post('credit-notes', values),
{
onSuccess: (res, values) => { onSuccess: (res, values) => {
// Common invalidate queries. // Common invalidate queries.
commonInvalidateQueries(queryClient); commonInvalidateQueries(queryClient);
}, },
...props, ...props,
}, });
);
} }
/** /**
@@ -218,8 +215,7 @@ export function useCreateRefundCreditNote(props) {
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation(
([id, values]) => ([id, values]) => apiRequest.post(`credit-notes/${id}/refunds`, values),
apiRequest.post(`credit-notes/${id}/refunds`, values),
{ {
onSuccess: (res, [id, values]) => { onSuccess: (res, [id, values]) => {
// Common invalidate queries. // Common invalidate queries.
@@ -240,9 +236,7 @@ export function useDeleteRefundCreditNote(props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation((id) => apiRequest.delete(`credit-notes/refunds/${id}`), {
(id) => apiRequest.delete(`credit-notes/refunds/${id}`),
{
onSuccess: (res, id) => { onSuccess: (res, id) => {
// Common invalidate queries. // Common invalidate queries.
commonInvalidateQueries(queryClient); commonInvalidateQueries(queryClient);
@@ -251,8 +245,7 @@ export function useDeleteRefundCreditNote(props) {
queryClient.invalidateQueries([t.CREDIT_NOTE, id]); queryClient.invalidateQueries([t.CREDIT_NOTE, id]);
}, },
...props, ...props,
}, });
);
} }
/** /**
@@ -301,7 +294,7 @@ export function useReconcileCreditNote(id, props, requestProps) {
[t.RECONCILE_CREDIT_NOTE, id], [t.RECONCILE_CREDIT_NOTE, id],
{ {
method: 'get', method: 'get',
url: `credit-notes/${id}/applied-invoices`, url: `credit-notes/${id}/apply-invoices`,
...requestProps, ...requestProps,
}, },
{ {

View File

@@ -84,6 +84,20 @@ export const handleBooleanChange = (handler) => {
return (event) => handler(event.target.checked); return (event) => handler(event.target.checked);
}; };
/**
* Parses a value to boolean (handles 1, 0, '1', '0', true, false).
* @param {*} value
* @param {boolean} defaultValue - value when empty/unknown
* @returns {boolean}
*/
export const parseBoolean = (value, defaultValue = false) => {
if (typeof value === 'boolean') return value;
if (value === 1 || value === '1') return true;
if (value === 0 || value === '0') return false;
if (value == null || value === '') return defaultValue;
return Boolean(value);
};
/** Event handler that exposes the target element's value as a string. */ /** Event handler that exposes the target element's value as a string. */
export const handleStringChange = (handler) => { export const handleStringChange = (handler) => {
return (event) => handler(event.target.value); return (event) => handler(event.target.value);