Compare commits

..

9 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
66a2261e50 refactor(nestjs): wip 2025-05-28 21:32:48 +02:00
Ahmed Bouhuolia
c51347d3ec refactor(nestjs): wip import module 2025-05-28 17:01:46 +02:00
Ahmed Bouhuolia
b7a3c42074 refactor(nestjs): wip 2025-05-27 15:42:27 +02:00
Ahmed Bouhuolia
83c9392b74 refactor(nestjs): wip dtos validation schema 2025-05-26 17:04:53 +02:00
Ahmed Bouhuolia
24bf3dd06d refactor(nestjs): validation schema dtos 2025-05-25 23:39:54 +02:00
Ahmed Bouhuolia
2b3f98d8fe refactor(nestjs): hook the new endpoints 2025-05-22 19:55:55 +02:00
Ahmed Bouhuolia
4e64a9eadb refactor(nestjs): pdf templates 2025-05-22 13:36:10 +02:00
Ahmed Bouhuolia
0823bfc4e9 refactor(nestjs): contacts module 2025-05-20 23:55:39 +02:00
Ahmed Bouhuolia
99fe5a6b0d refactor(nestjs): Implement users module 2025-05-20 17:55:58 +02:00
162 changed files with 1817 additions and 1064 deletions

View File

@@ -0,0 +1,7 @@
import { registerAs } from '@nestjs/config';
export default registerAs('bankfeed', () => ({
enabled:
process.env.BANK_FEED_ENABLED === 'true' ||
process.env.BANK_FEED_ENABLED === 'yes',
}));

View File

@@ -13,6 +13,7 @@ import signupRestrictions from './signup-restrictions';
import jwt from './jwt';
import mail from './mail';
import loops from './loops';
import bankfeed from './bankfeed';
export const config = [
systemDatabase,
@@ -29,5 +30,6 @@ export const config = [
signupRestrictions,
jwt,
mail,
loops
loops,
bankfeed,
];

View File

@@ -0,0 +1,57 @@
/**
* Map to store all models that have been marked to prevent base currency mutation.
* Key is the model name, value is the model class.
*/
export const preventMutateBaseCurrencyModels = new Map<string, any>();
/**
* Decorator that marks an ORM model to prevent base currency mutation.
* When applied to a model class, it adds a static property `preventMutateBaseCurrency` set to true
* and registers the model in the preventMutateBaseCurrencyModels map.
*
* @returns {ClassDecorator} A decorator function that can be applied to a class.
*/
export function PreventMutateBaseCurrency(): ClassDecorator {
return (target: any) => {
// Set the static property on the model class
target.preventMutateBaseCurrency = true;
// Register the model in the map
const modelName = target.name;
preventMutateBaseCurrencyModels.set(modelName, target);
// Return the modified class
return target;
};
}
/**
* Get all registered models that prevent base currency mutation.
*
* @returns {Map<string, any>} Map of model names to model classes
*/
export function getPreventMutateBaseCurrencyModels(): Map<string, any> {
return preventMutateBaseCurrencyModels;
}
/**
* Check if a model is registered to prevent base currency mutation.
*
* @param {string} modelName - The name of the model to check
* @returns {boolean} True if the model is registered, false otherwise
*/
export function isModelPreventMutateBaseCurrency(modelName: string): boolean {
return preventMutateBaseCurrencyModels.has(modelName);
}
/**
* Get a specific model by name that prevents base currency mutation.
*
* @param {string} modelName - The name of the model to retrieve
* @returns {any | undefined} The model class if found, undefined otherwise
*/
export function getPreventMutateBaseCurrencyModel(
modelName: string,
): any | undefined {
return preventMutateBaseCurrencyModels.get(modelName);
}

View File

@@ -0,0 +1,32 @@
import { Transform } from 'class-transformer';
import { ValidateIf, ValidationOptions } from 'class-validator';
/**
* Decorator that converts the property value to a number.
* @returns PropertyDecorator
*/
export function ToNumber() {
return Transform(({ value, key }) => {
const defaultValue = null;
if (typeof value === 'number') {
return value;
}
// If value is an empty string or undefined/null, return it as-is (wont pass validation)
if (value === '' || value === null || value === undefined) {
return defaultValue;
}
const parsed = Number(value);
return !isNaN(parsed) ? parsed : value;
});
}
/**
* Validates if the property is not empty.
* @returns PropertyDecorator
*/
export function IsOptional(validationOptions?: ValidationOptions) {
return ValidateIf((_obj, value) => {
return value !== null && value !== undefined && value !== '';
}, validationOptions);
}

View File

@@ -70,7 +70,10 @@ export class SerializeInterceptor implements NestInterceptor<any, any> {
next: CallHandler<any>,
): Observable<any> {
const request = context.switchToHttp().getRequest();
// Transform both body and query parameters
request.body = this.strategy.in(request.body);
request.query = this.strategy.in(request.query);
// handle returns stream..
return next.handle().pipe(map(this.strategy.out));

View File

@@ -14,12 +14,15 @@ export class ValidationPipe implements PipeTransform<any> {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
const errors = await validate(object, {
// Strip validated object of any properties that do not have any decorators.
whitelist: true,
});
if (errors.length > 0) {
throw new BadRequestException(errors);
}
return value;
return object;
}
private toValidate(metatype: Function): boolean {

View File

@@ -0,0 +1,4 @@
{
"primary_warehouse": "Primary Warehouse"
}

View File

@@ -1,4 +1,4 @@
import FormData from 'form-data';
import * as FormData from 'form-data';
import { GotenbergUtils } from './GotenbergUtils';
import { PageProperties } from './_types';

View File

@@ -1,5 +1,5 @@
import FormData from 'form-data';
import Axios from 'axios';
import * as FormData from 'form-data';
import { Axios } from 'axios';
export class GotenbergUtils {
public static assert(condition: boolean, message: string): asserts condition {
@@ -10,12 +10,12 @@ export class GotenbergUtils {
public static async fetch(endpoint: string, data: FormData): Promise<Buffer> {
try {
const response = await Axios.post(endpoint, data, {
const response = await new Axios({
headers: {
...data.getHeaders(),
},
responseType: 'arraybuffer', // This ensures you get a Buffer bac
});
}).post(endpoint, data);
return response.data;
} catch (error) {
console.error(error);

View File

@@ -1,5 +1,5 @@
import { constants, createReadStream, PathLike, promises } from 'fs';
import FormData from 'form-data';
import * as FormData from 'form-data';
import { GotenbergUtils } from './GotenbergUtils';
import { IConverter, PageProperties } from './_types';
import { PdfFormat, ChromiumRoute } from './_types';

View File

@@ -1,4 +1,4 @@
import FormData from 'form-data';
import * as FormData from 'form-data';
import { IConverter, PageProperties, PdfFormat, ChromiumRoute } from './_types';
import { ConverterUtils } from './ConvertUtils';
import { Converter } from './Converter';

View File

@@ -13,9 +13,7 @@ export class MutateBaseCurrencyAccounts {
* Mutates the all accounts or the organziation.
* @param {string} currencyCode
*/
mutateAllAccountsCurrency = async (
currencyCode: string,
) => {
await Account.query().update({ currencyCode });
};
async mutateAllAccountsCurrency(currencyCode: string) {
await this.accountModel().query().update({ currencyCode });
}
}

View File

@@ -88,6 +88,8 @@ import { ViewsModule } from '../Views/Views.module';
import { CurrenciesModule } from '../Currencies/Currencies.module';
import { MiscellaneousModule } from '../Miscellaneous/Miscellaneous.module';
import { UsersModule } from '../UsersModule/Users.module';
import { ContactsModule } from '../Contacts/Contacts.module';
import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module';
@Module({
imports: [
@@ -185,6 +187,7 @@ import { UsersModule } from '../UsersModule/Users.module';
BankingTransactionsExcludeModule,
BankingTransactionsRegonizeModule,
BankingMatchingModule,
BankingPlaidModule,
TransactionsLockingModule,
SettingsModule,
FeaturesModule,
@@ -210,7 +213,8 @@ import { UsersModule } from '../UsersModule/Users.module';
ViewsModule,
CurrenciesModule,
MiscellaneousModule,
UsersModule
UsersModule,
ContactsModule
],
controllers: [AppController],
providers: [

View File

@@ -33,6 +33,7 @@ const models = [
@Module({
imports: [S3Module, ...models],
exports: [...models],
controllers: [AttachmentsController],
providers: [
DeleteAttachment,

View File

@@ -1,4 +1,4 @@
import path from 'path';
import * as path from 'path';
// import config from '@/config';
export const getUploadedObjectUri = (objectKey: string) => {

View File

@@ -12,6 +12,7 @@ import {
} from 'class-validator';
import { BankRuleComparator } from '../types';
import { ApiProperty } from '@nestjs/swagger';
import { ToNumber } from '@/common/decorators/Validators';
class BankRuleConditionDto {
@IsNotEmpty()
@@ -44,6 +45,8 @@ export class CommandBankRuleDto {
})
name: string;
@IsNotEmpty()
@ToNumber()
@IsInt()
@Min(0)
@ApiProperty({
@@ -53,6 +56,7 @@ export class CommandBankRuleDto {
order: number;
@IsOptional()
@ToNumber()
@IsInt()
@Min(0)
@ApiProperty({
@@ -61,6 +65,7 @@ export class CommandBankRuleDto {
})
applyIfAccountId?: number;
@IsNotEmpty()
@IsIn(['deposit', 'withdrawal'])
@ApiProperty({
description: 'The transaction type to apply the rule if',
@@ -82,11 +87,14 @@ export class CommandBankRuleDto {
@Type(() => BankRuleConditionDto)
@ApiProperty({
description: 'The conditions to apply the rule if',
example: [{ field: 'description', comparator: 'contains', value: 'Salary' }],
example: [
{ field: 'description', comparator: 'contains', value: 'Salary' },
],
})
conditions: BankRuleConditionDto[];
@IsString()
@IsNotEmpty()
@ApiProperty({
description: 'The category to assign the rule if',
example: 'Income:Salary',
@@ -95,6 +103,8 @@ export class CommandBankRuleDto {
@IsInt()
@Min(0)
@ToNumber()
@IsNotEmpty()
@ApiProperty({
description: 'The account ID to assign the rule if',
example: 1,

View File

@@ -1,8 +1,13 @@
import { Inject, Injectable } from '@nestjs/common';
import { Account } from '@/modules/Accounts/models/Account.model';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { BaseModel } from '@/models/Model';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Knex } from 'knex';
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
import { initialize } from 'objection';
import { MatchedBankTransaction } from '@/modules/BankingMatching/models/MatchedBankTransaction';
import { TenantModel } from '@/modules/System/models/TenantModel';
import { RecognizedBankTransaction } from '@/modules/BankingTranasctionsRegonize/models/RecognizedBankTransaction';
@Injectable()
export class GetBankAccountSummary {
@@ -14,6 +19,19 @@ export class GetBankAccountSummary {
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
typeof UncategorizedBankTransaction
>,
@Inject(MatchedBankTransaction.name)
private readonly matchedBankTransactionModel: TenantModelProxy<
typeof MatchedBankTransaction
>,
@Inject(RecognizedBankTransaction.name)
private readonly recognizedBankTransaction: TenantModelProxy<
typeof RecognizedBankTransaction
>,
@Inject(TENANCY_DB_CONNECTION)
private readonly tenantDb: () => Knex,
) {}
/**
@@ -27,6 +45,11 @@ export class GetBankAccountSummary {
.findById(bankAccountId)
.throwIfNotFound();
await initialize(this.tenantDb(), [
this.uncategorizedBankTransactionModel(),
this.matchedBankTransactionModel(),
this.recognizedBankTransaction(),
]);
const commonQuery = (q) => {
// Include just the given account.
q.where('accountId', bankAccountId);
@@ -37,11 +60,6 @@ export class GetBankAccountSummary {
// Only the not categorized.
q.modify('notCategorized');
};
interface UncategorizedTransactionsCount {
total: number;
}
// Retrieves the uncategorized transactions count of the given bank account.
const uncategorizedTranasctionsCount =
await this.uncategorizedBankTransactionModel()

View File

@@ -0,0 +1,22 @@
import { Body, Controller, Post } from '@nestjs/common';
import { PlaidApplication } from './PlaidApplication';
import { PlaidItemDto } from './dtos/PlaidItem.dto';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
@Controller('banking/plaid')
@ApiTags('banking-plaid')
export class BankingPlaidController {
constructor(private readonly plaidApplication: PlaidApplication) {}
@Post('link-token')
@ApiOperation({ summary: 'Get Plaid link token' })
getLinkToken() {
return this.plaidApplication.getLinkToken();
}
@Post('exchange-token')
@ApiOperation({ summary: 'Exchange Plaid access token' })
exchangeToken(@Body() itemDTO: PlaidItemDto) {
return this.plaidApplication.exchangeToken(itemDTO);
}
}

View File

@@ -15,6 +15,7 @@ import { PlaidItemService } from './command/PlaidItem';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
import { SystemPlaidItem } from './models/SystemPlaidItem';
import { BankingPlaidController } from './BankingPlaid.controller';
const models = [RegisterTenancyModel(PlaidItem)];
@@ -38,5 +39,6 @@ const models = [RegisterTenancyModel(PlaidItem)];
TenancyContext,
],
exports: [...models],
controllers: [BankingPlaidController]
})
export class BankingPlaidModule {}

View File

@@ -3,6 +3,7 @@ import { PlaidItemService } from './command/PlaidItem';
import { PlaidWebooks } from './command/PlaidWebhooks';
import { Injectable } from '@nestjs/common';
import { PlaidItemDTO } from './types/BankingPlaid.types';
import { PlaidItemDto } from './dtos/PlaidItem.dto';
@Injectable()
export class PlaidApplication {
@@ -25,7 +26,7 @@ export class PlaidApplication {
* @param {PlaidItemDTO} itemDTO
* @returns
*/
public exchangeToken(itemDTO: PlaidItemDTO): Promise<void> {
public exchangeToken(itemDTO: PlaidItemDto): Promise<void> {
return this.plaidItemService.item(itemDTO);
}

View File

@@ -6,11 +6,9 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { SystemPlaidItem } from '../models/SystemPlaidItem';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import {
IPlaidItemCreatedEventPayload,
PlaidItemDTO,
} from '../types/BankingPlaid.types';
import { IPlaidItemCreatedEventPayload } from '../types/BankingPlaid.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { PlaidItemDto } from '../dtos/PlaidItem.dto';
@Injectable()
export class PlaidItemService {
@@ -33,10 +31,10 @@ export class PlaidItemService {
/**
* Exchanges the public token to get access token and item id and then creates
* a new Plaid item.
* @param {PlaidItemDTO} itemDTO - Plaid item data transfer object.
* @param {PlaidItemDto} itemDTO - Plaid item data transfer object.
* @returns {Promise<void>}
*/
public async item(itemDTO: PlaidItemDTO): Promise<void> {
public async item(itemDTO: PlaidItemDto): Promise<void> {
const { publicToken, institutionId } = itemDTO;
const tenant = await this.tenancyContext.getTenant();

View File

@@ -15,6 +15,13 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class PlaidUpdateTransactions {
/**
* Constructor method.
* @param {PlaidSyncDb} plaidSync - Plaid sync service.
* @param {UnitOfWork} uow - Unit of work.
* @param {TenantModelProxy<typeof PlaidItem>} plaidItemModel - Plaid item model.
* @param {PlaidApi} plaidClient - Plaid client.
*/
constructor(
private readonly plaidSync: PlaidSyncDb,
private readonly uow: UnitOfWork,
@@ -28,8 +35,7 @@ export class PlaidUpdateTransactions {
/**
* Handles sync the Plaid item to Bigcaptial under UOW.
* @param {number} tenantId - Tenant id.
* @param {number} plaidItemId - Plaid item id.
* @param {string} plaidItemId - Plaid item id.
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
*/
public async updateTransactions(plaidItemId: string) {
@@ -44,9 +50,9 @@ export class PlaidUpdateTransactions {
* - New bank accounts.
* - Last accounts feeds updated at.
* - Turn on the accounts feed flag.
* @param {number} tenantId - Tenant ID.
* @param {string} plaidItemId - The Plaid ID for the item.
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
*/
public async updateTransactionsWork(
plaidItemId: string,

View File

@@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class PlaidItemDto {
@IsString()
@IsNotEmpty()
publicToken: string;
@IsString()
@IsNotEmpty()
institutionId: string;
}

View File

@@ -24,7 +24,7 @@ export class TriggerRecognizedTransactionsSubscriber {
* @param {IBankRuleEventEditedPayload} payload -
*/
@OnEvent(events.bankRules.onEdited)
private async recognizedTransactionsOnRuleEdited({
async recognizedTransactionsOnRuleEdited({
editRuleDTO,
oldBankRule,
bankRule,

View File

@@ -9,11 +9,8 @@ import {
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { BankingTransactionsApplication } from './BankingTransactionsApplication.service';
import {
IBankAccountsFilter,
ICashflowAccountTransactionsQuery,
} from './types/BankingTransactions.types';
import { CreateBankTransactionDto } from './dtos/CreateBankTransaction.dto';
import { GetBankTransactionsQueryDto } from './dtos/GetBankTranasctionsQuery.dto';
@Controller('banking/transactions')
@ApiTags('banking-transactions')
@@ -24,7 +21,7 @@ export class BankingTransactionsController {
@Get()
async getBankAccountTransactions(
@Query() query: ICashflowAccountTransactionsQuery,
@Query() query: GetBankTransactionsQueryDto,
) {
return this.bankingTransactionsApplication.getBankAccountTransactions(
query,

View File

@@ -0,0 +1,28 @@
import {
IsNotEmpty,
IsNumber,
IsNumberString,
IsOptional,
} from 'class-validator';
import { NumberFormatQueryDto } from './NumberFormatQuery.dto';
import { Type } from 'class-transformer';
export class GetBankTransactionsQueryDto {
@IsOptional()
@Type(() => Number)
@IsNumber()
page: number;
@IsOptional()
@Type(() => Number)
@IsNumber()
pageSize: number;
@IsNotEmpty()
@Type(() => Number)
@IsNumber()
accountId: number;
@IsOptional()
numberFormat: NumberFormatQueryDto;
}

View File

@@ -0,0 +1,26 @@
import { Type } from "class-transformer";
import { IsBoolean, IsEnum, IsNumber, IsOptional, IsPositive } from "class-validator";
export class NumberFormatQueryDto {
@Type(() => Number)
@IsNumber()
@IsPositive()
@IsOptional()
readonly precision: number;
@IsBoolean()
@IsOptional()
readonly divideOn1000: boolean;
@IsBoolean()
@IsOptional()
readonly showZero: boolean;
@IsEnum(['total', 'always', 'none'])
@IsOptional()
readonly formatMoney: 'total' | 'always' | 'none';
@IsEnum(['parentheses', 'mines'])
@IsOptional()
readonly negativeFormat: 'parentheses' | 'mines';
}

View File

@@ -1,9 +1,9 @@
/* eslint-disable global-require */
import * as moment from 'moment';
import { Model } from 'objection';
import { BaseModel } from '@/models/Model';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
export class UncategorizedBankTransaction extends BaseModel {
export class UncategorizedBankTransaction extends TenantBaseModel {
readonly amount!: number;
readonly date!: Date | string;
readonly categorized!: boolean;

View File

@@ -1,6 +1,6 @@
// @ts-nocheck
import R from 'ramda';
import moment from 'moment';
import * as R from 'ramda';
import * as moment from 'moment';
import { first, isEmpty } from 'lodash';
import {
ICashflowAccountTransaction,

View File

@@ -1,13 +1,16 @@
import { Injectable, Scope } from '@nestjs/common';
import { Inject, Injectable, Scope } from '@nestjs/common';
import { ICashflowAccountTransactionsQuery } from '../../types/BankingTransactions.types';
import {
groupMatchedBankTransactions,
groupUncategorizedTransactions,
} from './_utils';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { UncategorizedBankTransaction } from '../../models/UncategorizedBankTransaction';
import { MatchedBankTransaction } from '@/modules/BankingMatching/models/MatchedBankTransaction';
@Injectable({ scope: Scope.REQUEST })
export class GetBankAccountTransactionsRepository {
private models: any;
public query: ICashflowAccountTransactionsQuery;
public transactions: any;
public uncategorizedTransactions: any;
@@ -17,6 +20,28 @@ export class GetBankAccountTransactionsRepository {
public pagination: any;
public openingBalance: any;
/**
* @param {TenantModelProxy<typeof AccountTransaction>} accountTransactionModel - Account transaction model.
* @param {TenantModelProxy<typeof UncategorizedBankTransaction>} uncategorizedBankTransactionModel - Uncategorized transaction model
* @param {TenantModelProxy<typeof MatchedBankTransaction>} matchedBankTransactionModel - Matched bank transaction model.
*/
constructor(
@Inject(AccountTransaction.name)
private readonly accountTransactionModel: TenantModelProxy<
typeof AccountTransaction
>,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
typeof UncategorizedBankTransaction
>,
@Inject(MatchedBankTransaction.name)
private readonly matchedBankTransactionModel: TenantModelProxy<
typeof MatchedBankTransaction
>,
) {}
setQuery(query: ICashflowAccountTransactionsQuery) {
this.query = query;
}
@@ -37,9 +62,8 @@ export class GetBankAccountTransactionsRepository {
* @param {ICashflowAccountTransactionsQuery} query -
*/
async initCashflowAccountTransactions() {
const { AccountTransaction } = this.models;
const { results, pagination } = await AccountTransaction.query()
const { results, pagination } = await this.accountTransactionModel()
.query()
.where('account_id', this.query.accountId)
.orderBy([
{ column: 'date', order: 'desc' },
@@ -59,10 +83,9 @@ export class GetBankAccountTransactionsRepository {
* @return {Promise<number>}
*/
async initCashflowAccountOpeningBalance(): Promise<void> {
const { AccountTransaction } = this.models;
// Retrieve the opening balance of credit and debit balances.
const openingBalancesSubquery = AccountTransaction.query()
const openingBalancesSubquery = this.accountTransactionModel()
.query()
.where('account_id', this.query.accountId)
.orderBy([
{ column: 'date', order: 'desc' },
@@ -72,7 +95,8 @@ export class GetBankAccountTransactionsRepository {
.offset(this.pagination.pageSize * (this.pagination.page - 1));
// Sumation of credit and debit balance.
const openingBalances = await AccountTransaction.query()
const openingBalances = await this.accountTransactionModel()
.query()
.sum('credit as credit')
.sum('debit as debit')
.from(openingBalancesSubquery.as('T'))
@@ -87,14 +111,11 @@ export class GetBankAccountTransactionsRepository {
* Initialize the uncategorized transactions of the bank account.
*/
async initCategorizedTransactions() {
const { UncategorizedCashflowTransaction } = this.models;
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
const uncategorizedTransactions =
await UncategorizedCashflowTransaction.query().whereIn(
['categorizeRefType', 'categorizeRefId'],
refs,
);
await this.uncategorizedBankTransactionModel()
.query()
.whereIn(['categorizeRefType', 'categorizeRefId'], refs);
this.uncategorizedTransactions = uncategorizedTransactions;
this.uncategorizedTransactionsMapByRef = groupUncategorizedTransactions(
@@ -106,14 +127,11 @@ export class GetBankAccountTransactionsRepository {
* Initialize the matched bank transactions of the bank account.
*/
async initMatchedTransactions(): Promise<void> {
const { MatchedBankTransaction } = this.models;
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
const matchedBankTransactions =
await MatchedBankTransaction.query().whereIn(
['referenceType', 'referenceId'],
refs,
);
const matchedBankTransactions = await this.matchedBankTransactionModel()
.query()
.whereIn(['referenceType', 'referenceId'], refs);
this.matchedBankTransactions = matchedBankTransactions;
this.matchedBankTransactionsMapByRef = groupMatchedBankTransactions(
matchedBankTransactions,

View File

@@ -6,6 +6,7 @@ import {
Param,
Post,
Put,
Query,
} from '@nestjs/common';
import { BillPaymentsApplication } from './BillPaymentsApplication.service';
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
@@ -13,11 +14,16 @@ import {
CreateBillPaymentDto,
EditBillPaymentDto,
} from './dtos/BillPayment.dto';
import { GetBillPaymentsFilterDto } from './dtos/GetBillPaymentsFilter.dto';
import { BillPaymentsPages } from './commands/BillPaymentsPages.service';
@Controller('bill-payments')
@ApiTags('bill-payments')
export class BillPaymentsController {
constructor(private billPaymentsApplication: BillPaymentsApplication) {}
constructor(
private billPaymentsApplication: BillPaymentsApplication,
private billPaymentsPagesService: BillPaymentsPages,
) {}
@Post()
@ApiOperation({ summary: 'Create a new bill payment.' })
@@ -57,16 +63,22 @@ export class BillPaymentsController {
);
}
@Get(':billPaymentId')
@ApiOperation({ summary: 'Retrieves the bill payment details.' })
@Get('/new-page/entries')
@ApiOperation({
summary:
'Retrieves the payable entries of the new page once vendor be selected.',
})
@ApiParam({
name: 'billPaymentId',
name: 'vendorId',
required: true,
type: Number,
description: 'The bill payment id',
description: 'The vendor id',
})
public getBillPayment(@Param('billPaymentId') billPaymentId: string) {
return this.billPaymentsApplication.getBillPayment(Number(billPaymentId));
async getBillPaymentNewPageEntries(@Query('vendorId') vendorId: number) {
const entries =
await this.billPaymentsPagesService.getNewPageEntries(vendorId);
return entries;
}
@Get(':billPaymentId/bills')
@@ -80,4 +92,47 @@ export class BillPaymentsController {
public getPaymentBills(@Param('billPaymentId') billPaymentId: string) {
return this.billPaymentsApplication.getPaymentBills(Number(billPaymentId));
}
@Get('/:billPaymentId/edit-page')
@ApiOperation({
summary: 'Retrieves the edit page of the given bill payment.',
})
@ApiParam({
name: 'billPaymentId',
required: true,
type: Number,
description: 'The bill payment id',
})
public async getBillPaymentEditPage(
@Param('billPaymentId') billPaymentId: number,
) {
const billPaymentsWithEditEntries =
await this.billPaymentsPagesService.getBillPaymentEditPage(billPaymentId);
return billPaymentsWithEditEntries;
}
@Get(':billPaymentId')
@ApiOperation({ summary: 'Retrieves the bill payment details.' })
@ApiParam({
name: 'billPaymentId',
required: true,
type: Number,
description: 'The bill payment id',
})
public getBillPayment(@Param('billPaymentId') billPaymentId: string) {
return this.billPaymentsApplication.getBillPayment(Number(billPaymentId));
}
@Get()
@ApiOperation({ summary: 'Retrieves the bill payments list.' })
@ApiParam({
name: 'filterDTO',
required: true,
type: GetBillPaymentsFilterDto,
description: 'The bill payments filter dto',
})
public getBillPayments(@Query() filterDTO: GetBillPaymentsFilterDto) {
return this.billPaymentsApplication.getBillPayments(filterDTO);
}
}

View File

@@ -17,11 +17,13 @@ import { BillPaymentGLEntriesSubscriber } from './subscribers/BillPaymentGLEntri
import { LedgerModule } from '../Ledger/Ledger.module';
import { AccountsModule } from '../Accounts/Accounts.module';
import { BillPaymentsExportable } from './queries/BillPaymentsExportable';
import { GetBillPayments } from '../Bills/queries/GetBillPayments';
import { BillPaymentsImportable } from './commands/BillPaymentsImportable';
import { GetBillPaymentsService } from './queries/GetBillPayments.service';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { BillPaymentsPages } from './commands/BillPaymentsPages.service';
@Module({
imports: [LedgerModule, AccountsModule],
imports: [LedgerModule, AccountsModule, DynamicListModule],
providers: [
BillPaymentsApplication,
CreateBillPaymentService,
@@ -37,9 +39,10 @@ import { BillPaymentsImportable } from './commands/BillPaymentsImportable';
TenancyContext,
BillPaymentGLEntries,
BillPaymentGLEntriesSubscriber,
GetBillPayments,
BillPaymentsExportable,
BillPaymentsImportable,
GetBillPaymentsService,
BillPaymentsPages,
],
exports: [
BillPaymentValidators,

View File

@@ -2,11 +2,11 @@ import { Injectable } from '@nestjs/common';
import { CreateBillPaymentService } from './commands/CreateBillPayment.service';
import { DeleteBillPayment } from './commands/DeleteBillPayment.service';
import { EditBillPayment } from './commands/EditBillPayment.service';
// import { GetBillPayments } from './GetBillPayments';
import { GetBillPayment } from './queries/GetBillPayment.service';
import { GetPaymentBills } from './queries/GetPaymentBills.service';
import { GetBillPayments } from '../Bills/queries/GetBillPayments';
import { CreateBillPaymentDto, EditBillPaymentDto } from './dtos/BillPayment.dto';
import { GetBillPaymentsService } from './queries/GetBillPayments.service';
import { GetBillPaymentsFilterDto } from './dtos/GetBillPaymentsFilter.dto';
/**
* Bill payments application.
@@ -20,7 +20,7 @@ export class BillPaymentsApplication {
private deleteBillPaymentService: DeleteBillPayment,
private getBillPaymentService: GetBillPayment,
private getPaymentBillsService: GetPaymentBills,
private getBillPaymentsService: GetBillPayments,
private getBillPaymentsService: GetBillPaymentsService,
) {}
/**
@@ -58,9 +58,10 @@ export class BillPaymentsApplication {
/**
* Retrieves bill payments list.
* @param {GetBillPaymentsFilterDto} filterDTO - The given bill payments filter dto.
*/
public getBillPayments() {
// return this.getBillPaymentsService.getBillPayments(filterDTO);
public getBillPayments(filterDTO: GetBillPaymentsFilterDto) {
return this.getBillPaymentsService.getBillPayments(filterDTO);
}
/**

View File

@@ -1,75 +0,0 @@
// import { Inject, Service } from 'typedi';
// import * as R from 'ramda';
// import {
// IBillPayment,
// IBillPaymentsFilter,
// IPaginationMeta,
// IFilterMeta,
// } from '@/interfaces';
// import { BillPaymentTransformer } from './queries/BillPaymentTransformer';
// import DynamicListingService from '@/services/DynamicListing/DynamicListService';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
// @Service()
// export class GetBillPayments {
// @Inject()
// private tenancy: HasTenancyService;
// @Inject()
// private dynamicListService: DynamicListingService;
// @Inject()
// private transformer: TransformerInjectable;
// /**
// * Retrieve bill payment paginted and filterable list.
// * @param {number} tenantId
// * @param {IBillPaymentsFilter} billPaymentsFilter
// */
// public async getBillPayments(
// tenantId: number,
// filterDTO: IBillPaymentsFilter
// ): Promise<{
// billPayments: IBillPayment[];
// pagination: IPaginationMeta;
// filterMeta: IFilterMeta;
// }> {
// const { BillPayment } = this.tenancy.models(tenantId);
// // Parses filter DTO.
// const filter = this.parseListFilterDTO(filterDTO);
// // Dynamic list service.
// const dynamicList = await this.dynamicListService.dynamicList(
// tenantId,
// BillPayment,
// filter
// );
// const { results, pagination } = await BillPayment.query()
// .onBuild((builder) => {
// builder.withGraphFetched('vendor');
// builder.withGraphFetched('paymentAccount');
// dynamicList.buildQuery()(builder);
// filter?.filterQuery && filter?.filterQuery(builder);
// })
// .pagination(filter.page - 1, filter.pageSize);
// // Transformes the bill payments models to POJO.
// const billPayments = await this.transformer.transform(
// tenantId,
// results,
// new BillPaymentTransformer()
// );
// return {
// billPayments,
// pagination,
// filterMeta: dynamicList.getResponseMeta(),
// };
// }
// private parseListFilterDTO(filterDTO) {
// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
// }
// }

View File

@@ -8,7 +8,7 @@ import { ServiceError } from '../../Items/ServiceError';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export default class BillPaymentsPages {
export class BillPaymentsPages {
/**
* @param {TenantModelProxy<typeof Bill>} billModel - Bill model.
* @param {TenantModelProxy<typeof BillPayment>} billPaymentModel - Bill payment model.

View File

@@ -1,7 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
IsDate,
IsDateString,
IsNotEmpty,
IsNumber,
IsOptional,
@@ -9,25 +10,33 @@ import {
ValidateNested,
} from 'class-validator';
import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto';
import { ApiProperty } from '@nestjs/swagger';
import { ToNumber } from '@/common/decorators/Validators';
export class BillPaymentEntryDto {
@ToNumber()
@IsNumber()
@IsNotEmpty()
@ApiProperty({ description: 'The id of the bill', example: 1 })
billId: number;
@ToNumber()
@IsNumber()
@IsNotEmpty()
@ApiProperty({
description: 'The payment amount of the bill payment',
example: 100,
})
paymentAmount: number;
}
export class CommandBillPaymentDTO {
@ToNumber()
@IsNumber()
@IsNotEmpty()
@ApiProperty({
description: 'The id of the vendor',
example: 1,
})
@ApiProperty({ description: 'The id of the vendor', example: 1 })
vendorId: number;
@ToNumber()
@IsNumber()
@IsOptional()
@ApiProperty({
@@ -36,12 +45,10 @@ export class CommandBillPaymentDTO {
})
amount?: number;
@ToNumber()
@IsNumber()
@IsNotEmpty()
@ApiProperty({
description: 'The id of the payment account',
example: 1,
})
@ApiProperty({ description: 'The id of the payment account', example: 1 })
paymentAccountId: number;
@IsString()
@@ -52,8 +59,8 @@ export class CommandBillPaymentDTO {
})
paymentNumber?: string;
@IsDate()
@Type(() => Date)
@IsDateString()
@IsNotEmpty()
@ApiProperty({
description: 'The payment date of the bill payment',
example: '2021-01-01',
@@ -76,7 +83,6 @@ export class CommandBillPaymentDTO {
})
statement?: string;
@IsString()
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@@ -92,12 +98,10 @@ export class CommandBillPaymentDTO {
})
entries: BillPaymentEntryDto[];
@ToNumber()
@IsNumber()
@IsOptional()
@ApiProperty({
description: 'The id of the branch',
example: 1,
})
@ApiProperty({ description: 'The id of the branch', example: 1 })
branchId?: number;
@IsArray()

View File

@@ -0,0 +1,34 @@
import { Type } from 'class-transformer';
import { IsIn, IsJSON, IsNumber, IsOptional, IsString } from 'class-validator';
export class GetBillPaymentsFilterDto {
@IsOptional()
@IsNumber()
@Type(() => Number)
readonly customViewId?: number;
@IsOptional()
@IsJSON()
readonly stringifiedFilterRoles?: string;
@IsOptional()
readonly columnSortBy?: string;
@IsOptional()
@IsIn(['desc', 'asc'])
readonly sortOrder?: string;
@IsOptional()
@IsNumber()
@Type(() => Number)
readonly page?: number;
@IsOptional()
@IsNumber()
@Type(() => Number)
readonly pageSize?: number;
@IsOptional()
@IsString()
readonly searchKeyword?: string;
}

View File

@@ -6,6 +6,7 @@ import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.dec
import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator';
import { BillPaymentMeta } from './BillPayment.meta';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { Model } from 'objection';
@ImportableModel()
@ExportableModel()
@@ -60,103 +61,98 @@ export class BillPayment extends TenantBaseModel {
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Model settings.
*/
// static get meta() {
// return BillPaymentSettings;
// }
/**
* Relationship mapping.
*/
// static get relationMappings() {
// const BillPaymentEntry = require('models/BillPaymentEntry');
// const AccountTransaction = require('models/AccountTransaction');
// const Vendor = require('models/Vendor');
// const Account = require('models/Account');
// const Branch = require('models/Branch');
// const Document = require('models/Document');
static get relationMappings() {
const { BillPaymentEntry } = require('./BillPaymentEntry');
const {
AccountTransaction,
} = require('../../Accounts/models/AccountTransaction.model');
const { Vendor } = require('../../Vendors/models/Vendor');
const { Account } = require('../../Accounts/models/Account.model');
const { Branch } = require('../../Branches/models/Branch.model');
const { Document } = require('../../ChromiumlyTenancy/models/Document');
// return {
// entries: {
// relation: Model.HasManyRelation,
// modelClass: BillPaymentEntry.default,
// join: {
// from: 'bills_payments.id',
// to: 'bills_payments_entries.billPaymentId',
// },
// filter: (query) => {
// query.orderBy('index', 'ASC');
// },
// },
return {
entries: {
relation: Model.HasManyRelation,
modelClass: BillPaymentEntry,
join: {
from: 'bills_payments.id',
to: 'bills_payments_entries.billPaymentId',
},
filter: (query) => {
query.orderBy('index', 'ASC');
},
},
// vendor: {
// relation: Model.BelongsToOneRelation,
// modelClass: Vendor.default,
// join: {
// from: 'bills_payments.vendorId',
// to: 'contacts.id',
// },
// filter(query) {
// query.where('contact_service', 'vendor');
// },
// },
vendor: {
relation: Model.BelongsToOneRelation,
modelClass: Vendor,
join: {
from: 'bills_payments.vendorId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'vendor');
},
},
// paymentAccount: {
// relation: Model.BelongsToOneRelation,
// modelClass: Account.default,
// join: {
// from: 'bills_payments.paymentAccountId',
// to: 'accounts.id',
// },
// },
paymentAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'bills_payments.paymentAccountId',
to: 'accounts.id',
},
},
// transactions: {
// relation: Model.HasManyRelation,
// modelClass: AccountTransaction.default,
// join: {
// from: 'bills_payments.id',
// to: 'accounts_transactions.referenceId',
// },
// filter(builder) {
// builder.where('reference_type', 'BillPayment');
// },
// },
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction,
join: {
from: 'bills_payments.id',
to: 'accounts_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'BillPayment');
},
},
// /**
// * Bill payment may belongs to branch.
// */
// branch: {
// relation: Model.BelongsToOneRelation,
// modelClass: Branch.default,
// join: {
// from: 'bills_payments.branchId',
// to: 'branches.id',
// },
// },
/**
* Bill payment may belongs to branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch,
join: {
from: 'bills_payments.branchId',
to: 'branches.id',
},
},
// /**
// * Bill payment may has many attached attachments.
// */
// attachments: {
// relation: Model.ManyToManyRelation,
// modelClass: Document.default,
// join: {
// from: 'bills_payments.id',
// through: {
// from: 'document_links.modelId',
// to: 'document_links.documentId',
// },
// to: 'documents.id',
// },
// filter(query) {
// query.where('model_ref', 'BillPayment');
// },
// },
// };
// }
/**
* Bill payment may has many attached attachments.
*/
attachments: {
relation: Model.ManyToManyRelation,
modelClass: Document,
join: {
from: 'bills_payments.id',
through: {
from: 'document_links.modelId',
to: 'document_links.documentId',
},
to: 'documents.id',
},
filter(query) {
query.where('model_ref', 'BillPayment');
},
},
};
}
/**
* Retrieve the default custom views, roles and columns.

View File

@@ -1,15 +1,15 @@
import { Injectable } from "@nestjs/common";
import { BillPaymentsApplication } from "../BillPaymentsApplication.service";
import { Exportable } from "@/modules/Export/Exportable";
import { EXPORT_SIZE_LIMIT } from "@/modules/Export/constants";
import { ExportableService } from "@/modules/Export/decorators/ExportableModel.decorator";
import { BillPayment } from "../models/BillPayment";
import { Injectable } from '@nestjs/common';
import { BillPaymentsApplication } from '../BillPaymentsApplication.service';
import { Exportable } from '@/modules/Export/Exportable';
import { EXPORT_SIZE_LIMIT } from '@/modules/Export/constants';
import { ExportableService } from '@/modules/Export/decorators/ExportableModel.decorator';
import { BillPayment } from '../models/BillPayment';
@Injectable()
@ExportableService({ name: BillPayment.name })
export class BillPaymentsExportable extends Exportable {
constructor(
private readonly billPaymentsApplication: BillPaymentsApplication
private readonly billPaymentsApplication: BillPaymentsApplication,
) {
super();
}
@@ -30,13 +30,11 @@ export class BillPaymentsExportable extends Exportable {
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
filterQuery
filterQuery,
} as any;
return [];
// return this.billPaymentsApplication
// .billPayments(tenantId, parsedQuery)
// .then((output) => output.billPayments);
return this.billPaymentsApplication
.getBillPayments(parsedQuery)
.then((output) => output.billPayments);
}
}

View File

@@ -0,0 +1,67 @@
import * as R from 'ramda';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { BillPayment } from '../models/BillPayment';
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
import { BillPaymentTransformer } from './BillPaymentTransformer';
import { GetBillPaymentsFilterDto } from '../dtos/GetBillPaymentsFilter.dto';
@Injectable()
export class GetBillPaymentsService {
constructor(
private readonly dynamicListService: DynamicListService,
private readonly transformer: TransformerInjectable,
@Inject(BillPayment.name)
private readonly billPaymentModel: TenantModelProxy<typeof BillPayment>,
) {}
/**
* Retrieve bill payment paginted and filterable list.
* @param {GetBillPaymentsFilterDto} billPaymentsFilter
*/
public async getBillPayments(filterDTO: GetBillPaymentsFilterDto) {
const _filterDto = {
page: 1,
pageSize: 12,
filterRoles: [],
sortOrder: 'desc',
columnSortBy: 'created_at',
...filterDTO,
};
// Parses filter DTO.
const filter = this.parseListFilterDTO(_filterDto);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
BillPayment,
filter,
);
const { results, pagination } = await this.billPaymentModel()
.query()
.onBuild((builder) => {
builder.withGraphFetched('vendor');
builder.withGraphFetched('paymentAccount');
dynamicList.buildQuery()(builder);
filter?.filterQuery && filter?.filterQuery(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Transformes the bill payments models to POJO.
const billPayments = await this.transformer.transform(
results,
new BillPaymentTransformer(),
);
return {
billPayments,
pagination,
filterMeta: dynamicList.getResponseMeta(),
};
}
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -18,7 +18,7 @@ export class BillPaymentGLEntriesSubscriber {
* Handle bill payment writing journal entries once created.
*/
@OnEvent(events.billPayment.onCreated)
private async handleWriteJournalEntries({
async handleWriteJournalEntries({
billPayment,
trx,
}: IBillPaymentEventCreatedPayload) {
@@ -34,7 +34,7 @@ export class BillPaymentGLEntriesSubscriber {
* Handle bill payment re-writing journal entries once the payment transaction be edited.
*/
@OnEvent(events.billPayment.onEdited)
private async handleRewriteJournalEntriesOncePaymentEdited({
async handleRewriteJournalEntriesOncePaymentEdited({
billPayment,
trx,
}: IBillPaymentEventEditedPayload) {
@@ -48,7 +48,7 @@ export class BillPaymentGLEntriesSubscriber {
* Reverts journal entries once bill payment deleted.
*/
@OnEvent(events.billPayment.onDeleted)
private async handleRevertJournalEntries({
async handleRevertJournalEntries({
billPaymentId,
trx,
}: IBillPaymentEventDeletedPayload) {

View File

@@ -1,3 +1,4 @@
import { ToNumber } from '@/common/decorators/Validators';
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { Type } from 'class-transformer';
import {
@@ -5,14 +6,14 @@ import {
IsArray,
IsBoolean,
IsDate,
IsDateString,
IsEnum,
IsInt,
IsNotEmpty,
IsNumber,
IsOptional,
IsPositive,
IsString,
Min,
MinLength,
ValidateNested,
} from 'class-validator';
@@ -29,10 +30,12 @@ export class BillEntryDto extends ItemEntryDto {
class AttachmentDto {
@IsString()
@IsNotEmpty()
key: string;
}
export class CommandBillDto {
@IsOptional()
@IsString()
billNumber: string;
@@ -40,32 +43,36 @@ export class CommandBillDto {
@IsString()
referenceNo?: string;
@IsDate()
@Type(() => Date)
@IsNotEmpty()
@IsDateString()
billDate: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
@IsDateString()
dueDate?: Date;
@IsInt()
@IsNotEmpty()
vendorId: number;
@IsOptional()
@ToNumber()
@IsNumber()
@IsPositive()
exchangeRate?: number;
@IsOptional()
@ToNumber()
@IsInt()
warehouseId?: number;
@IsOptional()
@ToNumber()
@IsInt()
branchId?: number;
@IsOptional()
@ToNumber()
@IsInt()
projectId?: number;
@@ -74,9 +81,11 @@ export class CommandBillDto {
note?: string;
@IsBoolean()
@IsOptional()
open: boolean = false;
@IsBoolean()
@IsOptional()
isInclusiveTax: boolean = false;
@IsArray()
@@ -92,13 +101,17 @@ export class CommandBillDto {
attachments?: AttachmentDto[];
@IsEnum(DiscountType)
@IsOptional()
discountType: DiscountType = DiscountType.Amount;
@IsOptional()
@ToNumber()
@IsNumber()
@IsPositive()
discount?: number;
@IsOptional()
@ToNumber()
@IsNumber()
adjustment?: number;
}

View File

@@ -76,8 +76,13 @@ export class BranchesController {
status: 200,
description: 'The branches feature has been successfully activated.',
})
activateBranches() {
return this.branchesApplication.activateBranches();
async activateBranches() {
await this.branchesApplication.activateBranches();
return {
code: 200,
message: 'The branches activated successfully.',
};
}
@Put(':id/mark-as-primary')

View File

@@ -17,6 +17,8 @@ export class BranchesSettingsService {
const settingsStore = await this.settingsStore();
settingsStore.set({ group: 'features', key: Features.BRANCHES, value: 1 });
await settingsStore.save();
};
/**

View File

@@ -1,9 +1,9 @@
import { IsOptional } from '@/common/decorators/Validators';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsBoolean,
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
IsUrl,
} from 'class-validator';

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import path from 'path';
import * as path from 'path';
import { promises as fs } from 'fs';
import { PageProperties, PdfFormat } from '@/libs/Chromiumly/_types';
import { UrlConverter } from '@/libs/Chromiumly/UrlConvert';
@@ -40,7 +40,7 @@ export class ChromiumlyHtmlConvert {
const cleanup = async () => {
await fs.unlink(filePath);
await Document.query().where('key', filename).delete();
await this.documentModel().query().where('key', filename).delete();
};
return [filename, cleanup];
}

View File

@@ -1,4 +1,4 @@
import path from 'path';
import * as path from 'path';
export const PDF_FILE_SUB_DIR = '/pdf';
export const PDF_FILE_EXPIRE_IN = 40; // ms
@@ -9,6 +9,5 @@ export const getPdfFilesStorageDir = (filename: string) => {
export const getPdfFilePath = (filename: string) => {
const storageDir = getPdfFilesStorageDir(filename);
return path.join(global.__storage_dir, storageDir);
return path.join(global.__static_dirname, storageDir);
};

View File

@@ -0,0 +1,6 @@
export const ERRORS = {
CONTACT_ALREADY_ACTIVE: 'CONTACT_ALREADY_ACTIVE',
CONTACT_ALREADY_INACTIVE: 'CONTACT_ALREADY_INACTIVE'
}

View File

@@ -0,0 +1,45 @@
import {
Controller,
Get,
Query,
Param,
Post,
ParseIntPipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiParam } from '@nestjs/swagger';
import { GetContactsAutoCompleteQuery } from './dtos/GetContactsAutoCompleteQuery.dto';
import { GetAutoCompleteContactsService } from './queries/GetAutoCompleteContacts.service';
import { ActivateContactService } from './commands/ActivateContact.service';
import { InactivateContactService } from './commands/InactivateContact.service';
@Controller('contacts')
@ApiTags('contacts')
export class ContactsController {
constructor(
private readonly getAutoCompleteService: GetAutoCompleteContactsService,
private readonly activateContactService: ActivateContactService,
private readonly inactivateContactService: InactivateContactService,
) {}
@Get('auto-complete')
@ApiOperation({ summary: 'Get the auto-complete contacts' })
getAutoComplete(@Query() query: GetContactsAutoCompleteQuery) {
return this.getAutoCompleteService.autocompleteContacts(query);
}
@Post(':id/activate')
@ApiOperation({ summary: 'Activate a contact' })
@ApiParam({ name: 'id', type: 'number', description: 'Contact ID' })
async activateContact(@Param('id', ParseIntPipe) contactId: number) {
await this.activateContactService.activateContact(contactId);
return { id: contactId, activated: true };
}
@Post(':id/inactivate')
@ApiOperation({ summary: 'Inactivate a contact' })
@ApiParam({ name: 'id', type: 'number', description: 'Contact ID' })
async inactivateContact(@Param('id', ParseIntPipe) contactId: number) {
await this.inactivateContactService.inactivateContact(contactId);
return { id: contactId, inactivated: true };
}
}

View File

@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { GetAutoCompleteContactsService } from './queries/GetAutoCompleteContacts.service';
import { ContactsController } from './Contacts.controller';
import { ActivateContactService } from './commands/ActivateContact.service';
import { InactivateContactService } from './commands/InactivateContact.service';
@Module({
providers: [
GetAutoCompleteContactsService,
ActivateContactService,
InactivateContactService,
],
controllers: [ContactsController],
})
export class ContactsModule {}

View File

@@ -0,0 +1,14 @@
import { IFilterRole } from '../DynamicListing/DynamicFilter/DynamicFilter.types';
export interface IContactsAutoCompleteFilter {
limit: number;
keyword: string;
filterRoles?: IFilterRole[];
columnSortBy: string;
sortOrder: string;
}
export interface IContactAutoCompleteItem {
displayName: string;
contactService: string;
}

View File

@@ -0,0 +1,28 @@
import { ServiceError } from '@/modules/Items/ServiceError';
import { Contact } from '../models/Contact';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Inject, Injectable } from '@nestjs/common';
import { ERRORS } from '../Contacts.constants';
@Injectable()
export class ActivateContactService {
constructor(
@Inject(Contact.name)
private readonly contactModel: TenantModelProxy<typeof Contact>,
) {}
async activateContact(contactId: number) {
const contact = await this.contactModel()
.query()
.findById(contactId)
.throwIfNotFound();
if (contact.active) {
throw new ServiceError(ERRORS.CONTACT_ALREADY_ACTIVE);
}
await this.contactModel()
.query()
.findById(contactId)
.update({ active: true });
}
}

View File

@@ -0,0 +1,28 @@
import { Inject, Injectable } from '@nestjs/common';
import { ServiceError } from '@/modules/Items/ServiceError';
import { Contact } from '../models/Contact';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { ERRORS } from '../Contacts.constants';
@Injectable()
export class InactivateContactService {
constructor(
@Inject(Contact.name)
private readonly contactModel: TenantModelProxy<typeof Contact>,
) {}
async inactivateContact(contactId: number) {
const contact = await this.contactModel()
.query()
.findById(contactId)
.throwIfNotFound();
if (!contact.active) {
throw new ServiceError(ERRORS.CONTACT_ALREADY_INACTIVE);
}
await this.contactModel()
.query()
.findById(contactId)
.update({ active: false });
}
}

View File

@@ -0,0 +1,11 @@
import { IsNumber, IsOptional, IsString } from 'class-validator';
export class GetContactsAutoCompleteQuery {
@IsNumber()
@IsOptional()
limit: number;
@IsString()
@IsOptional()
keyword: string;
}

View File

@@ -0,0 +1,39 @@
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Contact } from '../models/Contact';
import { Inject, Injectable } from '@nestjs/common';
import { IContactsAutoCompleteFilter } from '../Contacts.types';
import { GetContactsAutoCompleteQuery } from '../dtos/GetContactsAutoCompleteQuery.dto';
@Injectable()
export class GetAutoCompleteContactsService {
constructor(
@Inject(Contact.name)
private readonly contactModel: TenantModelProxy<typeof Contact>,
) {}
/**
* Retrieve auto-complete contacts list.
* @param {number} tenantId -
* @param {IContactsAutoCompleteFilter} contactsFilter -
* @return {IContactAutoCompleteItem}
*/
async autocompleteContacts(queryDto: GetContactsAutoCompleteQuery) {
const _queryDto = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'display_name',
limit: 10,
...queryDto,
};
// Retrieve contacts list by the given query.
const contacts = await this.contactModel()
.query()
.onBuild((builder) => {
if (_queryDto.keyword) {
builder.where('display_name', 'LIKE', `%${_queryDto.keyword}%`);
}
builder.limit(_queryDto.limit);
});
return contacts;
}
}

View File

@@ -1,5 +1,5 @@
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Param, Post } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { ICreditNoteRefundDTO } from '../CreditNotes/types/CreditNotes.types';
import { CreditNotesRefundsApplication } from './CreditNotesRefundsApplication.service';
import { RefundCreditNote } from './models/RefundCreditNote';
@@ -12,6 +12,14 @@ export class CreditNoteRefundsController {
private readonly creditNotesRefundsApplication: CreditNotesRefundsApplication,
) {}
@Get(':creditNoteId/refunds')
@ApiOperation({ summary: 'Retrieve the credit note graph.' })
getCreditNoteRefunds(@Param('creditNoteId') creditNoteId: number) {
return this.creditNotesRefundsApplication.getCreditNoteRefunds(
creditNoteId,
);
}
/**
* Create a refund credit note.
* @param {number} creditNoteId - The credit note ID.

View File

@@ -6,6 +6,7 @@ import { RefundSyncCreditNoteBalanceService } from './commands/RefundSyncCreditN
import { CreditNotesRefundsApplication } from './CreditNotesRefundsApplication.service';
import { CreditNoteRefundsController } from './CreditNoteRefunds.controller';
import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
import { GetCreditNoteRefundsService } from './queries/GetCreditNoteRefunds.service';
@Module({
imports: [forwardRef(() => CreditNotesModule)],
@@ -15,10 +16,9 @@ import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
RefundCreditNoteService,
RefundSyncCreditNoteBalanceService,
CreditNotesRefundsApplication,
GetCreditNoteRefundsService,
],
exports: [
RefundSyncCreditNoteBalanceService
],
exports: [RefundSyncCreditNoteBalanceService],
controllers: [CreditNoteRefundsController],
})
export class CreditNoteRefundsModule {}

View File

@@ -5,16 +5,27 @@ import { DeleteRefundCreditNoteService } from './commands/DeleteRefundCreditNote
import { RefundCreditNoteService } from './commands/RefundCreditNote.service';
import { RefundSyncCreditNoteBalanceService } from './commands/RefundSyncCreditNoteBalance';
import { CreditNoteRefundDto } from './dto/CreditNoteRefund.dto';
import { GetCreditNoteRefundsService } from './queries/GetCreditNoteRefunds.service';
@Injectable()
export class CreditNotesRefundsApplication {
constructor(
private readonly createRefundCreditNoteService: CreateRefundCreditNoteService,
private readonly deleteRefundCreditNoteService: DeleteRefundCreditNoteService,
private readonly getCreditNoteRefundsService: GetCreditNoteRefundsService,
private readonly refundCreditNoteService: RefundCreditNoteService,
private readonly refundSyncCreditNoteBalanceService: RefundSyncCreditNoteBalanceService,
) {}
/**
* Retrieve the credit note graph.
* @param {number} creditNoteId - Credit note id.
* @returns {Promise<IRefundCreditNotePOJO[]>}
*/
public getCreditNoteRefunds(creditNoteId: number) {
return this.getCreditNoteRefundsService.getCreditNoteRefunds(creditNoteId);
}
/**
* Create a refund credit note.
* @param {number} creditNoteId - The credit note ID.

View File

@@ -6,7 +6,7 @@ import { IRefundCreditNotePOJO } from '../types/CreditNoteRefunds.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class ListCreditNoteRefunds {
export class GetCreditNoteRefundsService {
constructor(
private readonly transformer: TransformerInjectable,
@@ -18,7 +18,7 @@ export class ListCreditNoteRefunds {
/**
* Retrieve the credit note graph.
* @param {number} creditNoteId
* @param {number} creditNoteId - Credit note id.
* @returns {Promise<IRefundCreditNotePOJO[]>}
*/
public async getCreditNoteRefunds(

View File

@@ -7,6 +7,8 @@ import { GetCreditNotePdf } from './queries/GetCreditNotePdf.serivce';
import { ICreditNotesQueryDTO } from './types/CreditNotes.types';
import { GetCreditNotesService } from './queries/GetCreditNotes.service';
import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
import { GetCreditNoteState } from './queries/GetCreditNoteState.service';
import { GetCreditNoteService } from './queries/GetCreditNote.service';
@Injectable()
export class CreditNoteApplication {
@@ -17,6 +19,8 @@ export class CreditNoteApplication {
private readonly deleteCreditNoteService: DeleteCreditNoteService,
private readonly getCreditNotePdfService: GetCreditNotePdf,
private readonly getCreditNotesService: GetCreditNotesService,
private readonly getCreditNoteStateService: GetCreditNoteState,
private readonly getCreditNoteService: GetCreditNoteService
) {}
/**
@@ -76,4 +80,21 @@ export class CreditNoteApplication {
getCreditNotes(creditNotesQuery: ICreditNotesQueryDTO) {
return this.getCreditNotesService.getCreditNotesList(creditNotesQuery);
}
/**
* Retrieves the create/edit initial state of the credit note.
* @returns {Promise<ICreditNoteState>}
*/
getCreditNoteState() {
return this.getCreditNoteStateService.getCreditNoteState();
}
/**
* Retrieves the credit note.
* @param {number} creditNoteId
* @returns {Promise<CreditNote>}
*/
getCreditNote(creditNoteId: number) {
return this.getCreditNoteService.getCreditNote(creditNoteId);
}
}

View File

@@ -1,3 +1,4 @@
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
import {
Body,
Controller,
@@ -10,7 +11,6 @@ import {
} from '@nestjs/common';
import { CreditNoteApplication } from './CreditNoteApplication.service';
import { ICreditNotesQueryDTO } from './types/CreditNotes.types';
import { ApiTags } from '@nestjs/swagger';
import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
@Controller('credit-notes')
@@ -22,16 +22,42 @@ export class CreditNotesController {
constructor(private creditNoteApplication: CreditNoteApplication) {}
@Post()
@ApiOperation({ summary: 'Create a new credit note' })
@ApiResponse({ status: 201, description: 'Credit note successfully created' })
@ApiResponse({ status: 400, description: 'Invalid input data' })
createCreditNote(@Body() creditNoteDTO: CreateCreditNoteDto) {
return this.creditNoteApplication.createCreditNote(creditNoteDTO);
}
@Get('state')
@ApiOperation({ summary: 'Get credit note state' })
@ApiResponse({ status: 200, description: 'Returns the credit note state' })
getCreditNoteState() {
return this.creditNoteApplication.getCreditNoteState();
}
@Get(':id')
@ApiOperation({ summary: 'Get a specific credit note by ID' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Returns the credit note' })
@ApiResponse({ status: 404, description: 'Credit note not found' })
getCreditNote(@Param('id') creditNoteId: number) {
return this.creditNoteApplication.getCreditNote(creditNoteId);
}
@Get()
@ApiOperation({ summary: 'Get all credit notes' })
@ApiResponse({ status: 200, description: 'Returns a list of credit notes' })
getCreditNotes(@Query() creditNotesQuery: ICreditNotesQueryDTO) {
return this.creditNoteApplication.getCreditNotes(creditNotesQuery);
}
@Put(':id')
@ApiOperation({ summary: 'Update a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Credit note successfully updated' })
@ApiResponse({ status: 404, description: 'Credit note not found' })
@ApiResponse({ status: 400, description: 'Invalid input data' })
editCreditNote(
@Param('id') creditNoteId: number,
@Body() creditNoteDTO: EditCreditNoteDto,
@@ -43,11 +69,19 @@ export class CreditNotesController {
}
@Put(':id/open')
@ApiOperation({ summary: 'Open a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Credit note successfully opened' })
@ApiResponse({ status: 404, description: 'Credit note not found' })
openCreditNote(@Param('id') creditNoteId: number) {
return this.creditNoteApplication.openCreditNote(creditNoteId);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Credit note successfully deleted' })
@ApiResponse({ status: 404, description: 'Credit note not found' })
deleteCreditNote(@Param('id') creditNoteId: number) {
return this.creditNoteApplication.deleteCreditNote(creditNoteId);
}

View File

@@ -15,7 +15,7 @@ import { WarehousesModule } from '../Warehouses/Warehouses.module';
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module';
import { TemplateInjectableModule } from '../TemplateInjectable/TemplateInjectable.module';
import { GetCreditNote } from './queries/GetCreditNote.service';
import { GetCreditNoteService } from './queries/GetCreditNote.service';
import { CreditNoteBrandingTemplate } from './queries/CreditNoteBrandingTemplate.service';
import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module';
import { CreditNoteGLEntries } from './commands/CreditNoteGLEntries';
@@ -52,7 +52,7 @@ import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/Credit
],
providers: [
CreateCreditNoteService,
GetCreditNote,
GetCreditNoteService,
CommandCreditNoteDTOTransform,
EditCreditNoteService,
OpenCreditNoteService,
@@ -74,7 +74,7 @@ import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/Credit
],
exports: [
CreateCreditNoteService,
GetCreditNote,
GetCreditNoteService,
CommandCreditNoteDTOTransform,
EditCreditNoteService,
OpenCreditNoteService,

View File

@@ -1,3 +1,4 @@
import { ToNumber } from '@/common/decorators/Validators';
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
@@ -5,9 +6,10 @@ import {
ArrayMinSize,
IsArray,
IsBoolean,
IsDate,
IsDateString,
IsEnum,
IsInt,
IsNotEmpty,
IsNumber,
IsOptional,
IsPositive,
@@ -25,21 +27,25 @@ export class CreditNoteEntryDto extends ItemEntryDto {}
class AttachmentDto {
@IsString()
@IsNotEmpty()
key: string;
}
export class CommandCreditNoteDto {
@ToNumber()
@IsInt()
@IsNotEmpty()
@ApiProperty({ example: 1, description: 'The customer ID' })
customerId: number;
@IsOptional()
@ToNumber()
@IsPositive()
@ApiProperty({ example: 3.43, description: 'The exchange rate' })
exchangeRate?: number;
@IsDate()
@Type(() => Date)
@IsNotEmpty()
@IsDateString()
@ApiProperty({ example: '2021-09-01', description: 'The credit note date' })
creditNoteDate: Date;
@@ -64,26 +70,19 @@ export class CommandCreditNoteDto {
termsConditions?: string;
@IsBoolean()
@ApiProperty({
example: false,
description: 'The credit note is open',
})
@ApiProperty({ example: false, description: 'The credit note is open' })
open: boolean = false;
@IsOptional()
@ToNumber()
@IsInt()
@ApiProperty({
example: 1,
description: 'The warehouse ID',
})
@ApiProperty({ example: 1, description: 'The warehouse ID' })
warehouseId?: number;
@IsOptional()
@ToNumber()
@IsInt()
@ApiProperty({
example: 1,
description: 'The branch ID',
})
@ApiProperty({ example: 1, description: 'The branch ID' })
branchId?: number;
@IsArray()
@@ -91,14 +90,7 @@ export class CommandCreditNoteDto {
@Type(() => CreditNoteEntryDto)
@ArrayMinSize(1)
@ApiProperty({
example: [
{
itemId: 1,
quantity: 1,
rate: 10,
taxRateId: 1,
},
],
example: [{ itemId: 1, quantity: 1, rate: 10, taxRateId: 1 }],
description: 'The credit note entries',
})
entries: CreditNoteEntryDto[];
@@ -110,19 +102,15 @@ export class CommandCreditNoteDto {
attachments?: AttachmentDto[];
@IsOptional()
@ToNumber()
@IsInt()
@ApiProperty({
example: 1,
description: 'The pdf template ID',
})
@ApiProperty({ example: 1, description: 'The pdf template ID' })
pdfTemplateId?: number;
@IsOptional()
@ToNumber()
@IsNumber()
@ApiProperty({
example: 10,
description: 'The discount amount',
})
@ApiProperty({ example: 10, description: 'The discount amount' })
discount?: number;
@IsOptional()
@@ -135,6 +123,7 @@ export class CommandCreditNoteDto {
discountType?: DiscountType;
@IsOptional()
@ToNumber()
@IsNumber()
adjustment?: number;
}

View File

@@ -7,7 +7,7 @@ import { ServiceError } from '@/modules/Items/ServiceError';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetCreditNote {
export class GetCreditNoteService {
constructor(
private readonly transformer: TransformerInjectable,

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetCreditNote } from './GetCreditNote.service';
import { GetCreditNoteService } from './GetCreditNote.service';
import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate.service';
import { transformCreditNoteToPdfTemplate } from '../utils';
import { CreditNote } from '../models/CreditNote';
@@ -25,7 +25,7 @@ export class GetCreditNotePdf {
constructor(
private readonly chromiumlyTenancy: ChromiumlyTenancy,
private readonly templateInjectable: TemplateInjectable,
private readonly getCreditNoteService: GetCreditNote,
private readonly getCreditNoteService: GetCreditNoteService,
private readonly creditNoteBrandingTemplate: CreditNoteBrandingTemplate,
private readonly eventPublisher: EventEmitter2,

View File

@@ -0,0 +1,39 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
@Controller('credit-notes')
@ApiTags('credit-notes-apply-invoice')
export class CreditNotesApplyInvoiceController {
constructor(
private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices,
) {}
@Get(':creditNoteId/applied-invoices')
@ApiOperation({ summary: 'Applied credit note to invoices' })
@ApiResponse({
status: 200,
description: 'Credit note successfully applied to invoices',
})
@ApiResponse({ status: 404, description: 'Credit note not found' })
@ApiResponse({ status: 400, description: 'Invalid input data' })
appliedCreditNoteToInvoices(@Param('creditNoteId') creditNoteId: number) {
return this.getCreditNoteAssociatedAppliedInvoicesService.getCreditAssociatedAppliedInvoices(
creditNoteId,
);
}
@Post(':creditNoteId/apply-invoices')
@ApiOperation({ summary: 'Apply credit note to invoices' })
@ApiResponse({
status: 200,
description: 'Credit note successfully applied to invoices',
})
@ApiResponse({ status: 404, description: 'Credit note not found' })
@ApiResponse({ status: 400, description: 'Invalid input data' })
applyCreditNoteToInvoices(@Param('creditNoteId') creditNoteId: number) {
return this.getCreditNoteAssociatedAppliedInvoicesService.getCreditAssociatedAppliedInvoices(
creditNoteId,
);
}
}

View File

@@ -8,6 +8,7 @@ import { PaymentsReceivedModule } from '../PaymentReceived/PaymentsReceived.modu
import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.controller';
@Module({
providers: [
@@ -16,12 +17,11 @@ import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteA
CreditNoteApplyToInvoices,
CreditNoteApplySyncInvoicesCreditedAmount,
CreditNoteApplySyncCredit,
// GetCreditNoteAssociatedAppliedInvoices,
// GetCreditNoteAssociatedInvoicesToApply
],
exports: [
DeleteCustomerLinkedCreditNoteService,
GetCreditNoteAssociatedAppliedInvoices,
GetCreditNoteAssociatedInvoicesToApply,
],
exports: [DeleteCustomerLinkedCreditNoteService],
imports: [PaymentsReceivedModule, forwardRef(() => CreditNotesModule)],
controllers: [CreditNotesApplyInvoiceController],
})
export class CreditNotesApplyInvoiceModule {}

View File

@@ -7,9 +7,16 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetCreditNoteAssociatedAppliedInvoices {
/**
* @param {TransformerInjectable} transformer - The transformer service.
* @param {TenantModelProxy<typeof CreditNoteAppliedInvoice>} creditNoteAppliedInvoiceModel - The credit note applied invoice model.
* @param {TenantModelProxy<typeof CreditNote>} creditNoteModel - The credit note model.
*/
constructor(
private readonly transformer: TransformerInjectable,
private readonly creditNoteAppliedInvoiceModel: typeof CreditNoteAppliedInvoice,
@Inject(CreditNoteAppliedInvoice.name)
private readonly creditNoteAppliedInvoiceModel: TenantModelProxy<typeof CreditNoteAppliedInvoice>,
@Inject(CreditNote.name)
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
@@ -29,7 +36,7 @@ export class GetCreditNoteAssociatedAppliedInvoices {
.findById(creditNoteId)
.throwIfNotFound();
const appliedToInvoices = await this.creditNoteAppliedInvoiceModel
const appliedToInvoices = await this.creditNoteAppliedInvoiceModel()
.query()
.where('credit_note_id', creditNoteId)
.withGraphFetched('saleInvoice')

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { GetCreditNote } from '../../CreditNotes/queries/GetCreditNote.service';
import { GetCreditNoteService } from '../../CreditNotes/queries/GetCreditNote.service';
import { CreditNoteWithInvoicesToApplyTransformer } from './CreditNoteWithInvoicesToApplyTransformer';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@@ -10,11 +10,11 @@ export class GetCreditNoteAssociatedInvoicesToApply {
/**
* @param {TransformerInjectable} transformer - Transformer service.
* @param {GetCreditNote} getCreditNote - Get credit note service.
* @param {typeof SaleInvoice} saleInvoiceModel - Sale invoice model.
* @param {TenantModelProxy<typeof SaleInvoice>} saleInvoiceModel - Sale invoice model.
*/
constructor(
private transformer: TransformerInjectable,
private getCreditNote: GetCreditNote,
private getCreditNote: GetCreditNoteService,
@Inject(SaleInvoice.name)
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,

View File

@@ -37,15 +37,15 @@ export class CurrenciesController {
return this.currenciesApp.createCurrency(dto);
}
@Put(':code')
@Put(':id')
@ApiOperation({ summary: 'Edit an existing currency' })
@ApiParam({ name: 'id', type: Number, description: 'Currency ID' })
@ApiBody({ type: EditCurrencyDto })
@ApiOkResponse({ description: 'The currency has been successfully updated.' })
@ApiNotFoundResponse({ description: 'Currency not found.' })
@ApiBadRequestResponse({ description: 'Invalid input data.' })
edit(@Param('code') code: string, @Body() dto: EditCurrencyDto) {
return this.currenciesApp.editCurrency(code, dto);
edit(@Param('id') id: number, @Body() dto: EditCurrencyDto) {
return this.currenciesApp.editCurrency(id, dto);
}
@Delete(':code')

View File

@@ -27,8 +27,8 @@ export class CurrenciesApplication {
/**
* Edits an existing currency.
*/
public editCurrency(currencyCode: string, currencyDTO: EditCurrencyDto) {
return this.editCurrencyService.editCurrency(currencyCode, currencyDTO);
public editCurrency(currencyId: number, currencyDTO: EditCurrencyDto) {
return this.editCurrencyService.editCurrency(currencyId, currencyDTO);
}
/**

View File

@@ -12,21 +12,22 @@ export class EditCurrencyService {
/**
* Edit details of the given currency.
* @param {number} currencyCode - Currency code.
* @param {number} currencyId - Currency ID.
* @param {ICurrencyDTO} currencyDTO - Edit currency dto.
*/
public async editCurrency(
currencyCode: string,
currencyId: number,
currencyDTO: EditCurrencyDto,
): Promise<Currency> {
const foundCurrency = await this.currencyModel()
const foundCurrency = this.currencyModel()
.query()
.findOne('currencyCode', currencyCode)
.findById(currencyId)
.throwIfNotFound();
// Directly use the provided ID to update the currency
const currency = await this.currencyModel()
.query()
.patchAndFetchById(foundCurrency.id, {
.patchAndFetchById(currencyId, {
...currencyDTO,
});
return currency;

View File

@@ -1,12 +1,16 @@
import { IsString } from 'class-validator';
import { IsNotEmpty } from "class-validator";
import { IsString } from "class-validator";
export class CreateCurrencyDto {
@IsString()
@IsNotEmpty()
currencyName: string;
@IsString()
@IsNotEmpty()
currencyCode: string;
@IsString()
@IsNotEmpty()
currencySign: string;
}

View File

@@ -1,9 +1,12 @@
import { IsString } from 'class-validator';
import { IsNotEmpty } from "class-validator";
import { IsString } from "class-validator";
export class EditCurrencyDto {
@IsString()
@IsNotEmpty()
currencyName: string;
@IsString()
@IsNotEmpty()
currencySign: string;
}

View File

@@ -1,6 +1,6 @@
import { TenantModel } from "@/modules/System/models/TenantModel";
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
export class Currency extends TenantModel {
export class Currency extends TenantBaseModel {
public readonly currencySign: string;
public readonly currencyName: string;
public readonly currencyCode: string;
@@ -22,4 +22,4 @@ export class Currency extends TenantModel {
static get resourceable() {
return true;
}
}
}

View File

@@ -394,7 +394,5 @@ export abstract class DynamicFilterRoleAbstractor implements IDynamicFilter {
/**
* Retrieves the response meta.
*/
getResponseMeta() {
throw new Error('Method not implemented.');
}
getResponseMeta() {}
}

View File

@@ -1,10 +1,12 @@
import { ToNumber } from '@/common/decorators/Validators';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsDate,
IsDateString,
IsInt,
IsISO4217CurrencyCode,
IsNotEmpty,
IsNumber,
IsOptional,
@@ -23,10 +25,12 @@ export class ExpenseCategoryDto {
@IsNotEmpty()
index: number;
@IsInt()
@IsNotEmpty()
@ToNumber()
@IsInt()
expenseAccountId: number;
@ToNumber()
@IsNumber()
@IsOptional()
amount?: number;
@@ -40,6 +44,7 @@ export class ExpenseCategoryDto {
@IsOptional()
landedCost?: boolean;
@ToNumber()
@IsInt()
@IsOptional()
projectId?: number;
@@ -55,7 +60,7 @@ export class CommandExpenseDto {
})
referenceNo?: string;
@IsDate()
@IsDateString()
@IsNotEmpty()
@ApiProperty({
description: 'The payment date of the expense',
@@ -63,8 +68,9 @@ export class CommandExpenseDto {
})
paymentDate: Date;
@IsInt()
@IsNotEmpty()
@ToNumber()
@IsInt()
@ApiProperty({
description: 'The payment account id of the expense',
example: 1,
@@ -80,31 +86,22 @@ export class CommandExpenseDto {
})
description?: string;
@ToNumber()
@IsNumber()
@IsOptional()
@ApiProperty({
description: 'The exchange rate of the expense',
example: 1,
})
@ApiProperty({ description: 'The exchange rate of the expense', example: 1 })
exchangeRate?: number;
@IsString()
@MaxLength(3)
@IsOptional()
@IsISO4217CurrencyCode()
@ApiProperty({
description: 'The currency code of the expense',
example: 'USD',
})
currencyCode?: string;
@IsNumber()
@IsOptional()
@ApiProperty({
description: 'The exchange rate of the expense',
example: 1,
})
exchange_rate?: number;
@IsBoolean()
@IsOptional()
@ApiProperty({
@@ -113,14 +110,16 @@ export class CommandExpenseDto {
})
publish?: boolean;
@IsInt()
@IsOptional()
@ToNumber()
@IsInt()
@ApiProperty({
description: 'The payee id of the expense',
example: 1,
})
payeeId?: number;
@ToNumber()
@IsInt()
@IsOptional()
@ApiProperty({

View File

@@ -24,7 +24,7 @@ export class FeaturesConfigure {
},
{
name: Features.BankSyncing,
defaultValue: this.configService.get('bankSync.enabled') ?? false,
defaultValue: this.configService.get('bankfeed.enabled') ?? false,
},
];
}

View File

@@ -6,23 +6,28 @@ import { TenancyContext } from '../Tenancy/TenancyContext.service';
@Injectable()
export class ImportFileMeta {
/**
* @param {TransformerInjectable} transformer - Transformer injectable service.
* @param {TenancyContext} tenancyContext - Tenancy context service.
* @param {typeof ImportModel} importModel - Import model.
*/
constructor(
private readonly transformer: TransformerInjectable,
private readonly tenancyContext: TenancyContext,
@Inject(ImportModel.name)
private readonly importModel: () => typeof ImportModel,
private readonly importModel: typeof ImportModel,
) {}
/**
* Retrieves the import meta of the given import model id.
* @param {number} importId
* @param {string} importId - Import id.
*/
async getImportMeta(importId: string) {
const tenant = await this.tenancyContext.getTenant();
const tenantId = tenant.id;
const importFile = await this.importModel()
const importFile = await this.importModel
.query()
.where('tenantId', tenantId)
.findOne('importId', importId);

View File

@@ -74,11 +74,10 @@ export class EditItemCategoryService {
);
// Creates item category under unit-of-work evnirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
//
const itemCategory = await ItemCategory.query().patchAndFetchById(
itemCategoryId,
{ ...itemCategoryObj },
);
const itemCategory = await this.itemCategoryModel()
.query(trx)
.patchAndFetchById(itemCategoryId, { ...itemCategoryObj });
// Triggers `onItemCategoryEdited` event.
await this.eventEmitter.emitAsync(events.itemCategory.onEdited, {
oldItemCategory,

View File

@@ -1,6 +1,7 @@
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
import { IsOptional, IsString } from 'class-validator';
import { IsString } from 'class-validator';
import { IsNotEmpty } from 'class-validator';
class CommandItemCategoryDto {
@@ -17,16 +18,19 @@ class CommandItemCategoryDto {
})
description?: string;
@ToNumber()
@IsNumber()
@IsOptional()
@ApiProperty({ example: 1, description: 'The cost account ID' })
costAccountId?: number;
@ToNumber()
@IsNumber()
@IsOptional()
@ApiProperty({ example: 1, description: 'The sell account ID' })
sellAccountId?: number;
@ToNumber()
@IsNumber()
@IsOptional()
@ApiProperty({ example: 1, description: 'The inventory account ID' })

View File

@@ -1,7 +1,6 @@
import {
IsString,
IsIn,
IsOptional,
IsBoolean,
IsNumber,
IsInt,
@@ -9,11 +8,10 @@ import {
ValidateIf,
MaxLength,
Min,
Max,
IsNotEmpty,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
export class CommandItemDto {
@IsString()
@@ -23,6 +21,7 @@ export class CommandItemDto {
name: string;
@IsString()
@IsNotEmpty()
@IsIn(['service', 'non-inventory', 'inventory'])
@ApiProperty({
description: 'Item type',
@@ -52,6 +51,7 @@ export class CommandItemDto {
purchasable?: boolean;
@IsOptional()
@ToNumber()
@IsNumber({ maxDecimalPlaces: 3 })
@Min(0)
@ValidateIf((o) => o.purchasable === true)
@@ -64,6 +64,7 @@ export class CommandItemDto {
costPrice?: number;
@IsOptional()
@ToNumber()
@IsInt()
@Min(0)
@ValidateIf((o) => o.purchasable === true)
@@ -86,6 +87,7 @@ export class CommandItemDto {
sellable?: boolean;
@IsOptional()
@ToNumber()
@IsNumber({ maxDecimalPlaces: 3 })
@Min(0)
@ValidateIf((o) => o.sellable === true)
@@ -98,6 +100,7 @@ export class CommandItemDto {
sellPrice?: number;
@IsOptional()
@ToNumber()
@IsInt()
@Min(0)
@ValidateIf((o) => o.sellable === true)
@@ -110,6 +113,7 @@ export class CommandItemDto {
sellAccountId?: number;
@IsOptional()
@ToNumber()
@IsInt()
@Min(0)
@ValidateIf((o) => o.type === 'inventory')
@@ -140,6 +144,7 @@ export class CommandItemDto {
purchaseDescription?: string;
@IsOptional()
@ToNumber()
@IsInt()
@ApiProperty({
description: 'ID of the tax rate applied to sales',
@@ -149,6 +154,7 @@ export class CommandItemDto {
sellTaxRateId?: number;
@IsOptional()
@ToNumber()
@IsInt()
@ApiProperty({
description: 'ID of the tax rate applied to purchases',
@@ -158,6 +164,7 @@ export class CommandItemDto {
purchaseTaxRateId?: number;
@IsOptional()
@ToNumber()
@IsInt()
@Min(0)
@ApiProperty({
@@ -189,7 +196,6 @@ export class CommandItemDto {
@IsOptional()
@IsArray()
@Type(() => Number)
@IsInt({ each: true })
@ApiProperty({
description: 'IDs of media files associated with the item',

View File

@@ -5,10 +5,12 @@ import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.dec
import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator';
import { ItemMeta } from './Item.meta';
import { ImportableModel } from '@/modules/Import/decorators/Import.decorator';
import { PreventMutateBaseCurrency } from '@/common/decorators/LockMutateBaseCurrency.decorator';
@ExportableModel()
@ImportableModel()
@InjectModelMeta(ItemMeta)
@PreventMutateBaseCurrency()
export class Item extends TenantBaseModel {
public readonly quantityOnHand: number;
public readonly name: string;

View File

@@ -108,11 +108,7 @@ export class ManualJournalsController {
description: 'The manual journal details have been successfully retrieved.',
})
@ApiResponse({ status: 404, description: 'The manual journal not found.' })
public getManualJournals(
@Query() filterDto: Partial<IManualJournalsFilter>
) {
public getManualJournals(@Query() filterDto: Partial<IManualJournalsFilter>) {
return this.manualJournalsApplication.getManualJournals(filterDto);
}
}

View File

@@ -1,10 +1,13 @@
import { ToNumber } from '@/common/decorators/Validators';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsDate,
IsDateString,
IsInt,
IsNotEmpty,
IsNumber,
IsOptional,
IsPositive,
@@ -20,18 +23,21 @@ export class ManualJournalEntryDto {
index: number;
@ApiPropertyOptional({ description: 'Credit amount' })
@ToNumber()
@IsOptional()
@IsNumber()
@Min(0)
credit?: number;
@ApiPropertyOptional({ description: 'Debit amount' })
@ToNumber()
@IsOptional()
@IsNumber()
@Min(0)
debit?: number;
@ApiProperty({ description: 'Account ID' })
@IsNotEmpty()
@IsInt()
accountId: number;
@@ -41,16 +47,19 @@ export class ManualJournalEntryDto {
note?: string;
@ApiPropertyOptional({ description: 'Contact ID' })
@IsOptional()
@ToNumber()
@IsInt()
@IsOptional()
contactId?: number;
@ApiPropertyOptional({ description: 'Branch ID' })
@ToNumber()
@IsOptional()
@IsInt()
branchId?: number;
@ApiPropertyOptional({ description: 'Project ID' })
@ToNumber()
@IsOptional()
@IsInt()
projectId?: number;
@@ -64,8 +73,7 @@ class AttachmentDto {
export class CommandManualJournalDto {
@ApiProperty({ description: 'Journal date' })
@IsDate()
@Type(() => Date)
@IsDateString()
date: Date;
@ApiPropertyOptional({ description: 'Currency code' })
@@ -74,6 +82,7 @@ export class CommandManualJournalDto {
currencyCode?: string;
@ApiPropertyOptional({ description: 'Exchange rate' })
@ToNumber()
@IsOptional()
@IsNumber()
@IsPositive()
@@ -103,6 +112,7 @@ export class CommandManualJournalDto {
description?: string;
@ApiPropertyOptional({ description: 'Branch ID' })
@ToNumber()
@IsOptional()
@IsInt()
branchId?: number;

View File

@@ -11,13 +11,9 @@ import {
Put,
Get,
Body,
Req,
Res,
Next,
HttpCode,
Param,
} from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { BuildOrganizationService } from './commands/BuildOrganization.service';
import {
BuildOrganizationDto,
@@ -88,7 +84,7 @@ export class OrganizationController {
const abilities =
await this.orgBaseCurrencyLockingService.baseCurrencyMutateLocks();
return res.status(200).send({ abilities });
return { abilities };
}
@Put()

View File

@@ -1,6 +1,7 @@
// @ts-nocheck
import { Injectable } from '@nestjs/common';
import { isEmpty } from 'lodash';
import { Injectable } from '@nestjs/common';
import { getPreventMutateBaseCurrencyModels } from '@/common/decorators/LockMutateBaseCurrency.decorator';
import { ModuleRef } from '@nestjs/core';
interface MutateBaseCurrencyLockMeta {
modelName: string;
@@ -9,26 +10,27 @@ interface MutateBaseCurrencyLockMeta {
@Injectable()
export class OrganizationBaseCurrencyLocking {
constructor(private readonly moduleRef: ModuleRef) {}
/**
* Retrieves the tenant models that have prevented mutation base currency.
*/
private getModelsPreventsMutate = (tenantId: number) => {
const Models = this.tenancy.models(tenantId);
private getModelsPreventsMutate() {
const lockedModels = getPreventMutateBaseCurrencyModels();
const filteredEntries = Object.entries(Models).filter(
const filteredEntries = Array.from(lockedModels).filter(
([key, Model]) => !!Model.preventMutateBaseCurrency,
);
return Object.fromEntries(filteredEntries);
};
}
/**
* Detarmines the mutation base currency model is locked.
* @param {Model} Model
* @returns {Promise<MutateBaseCurrencyLockMeta | false>}
*/
private isModelMutateLocked = async (
private async isModelMutateLocked(
Model,
): Promise<MutateBaseCurrencyLockMeta | false> => {
): Promise<MutateBaseCurrencyLockMeta | false> {
const validateQuery = Model.query();
if (typeof Model?.modifiers?.preventMutateBaseCurrency !== 'undefined') {
@@ -45,20 +47,24 @@ export class OrganizationBaseCurrencyLocking {
pluralName: Model.pluralName,
}
: false;
};
}
/**
* Retrieves the base currency mutation locks of the tenant models.
* @param {number} tenantId
* @returns {Promise<MutateBaseCurrencyLockMeta[]>}
*/
public async baseCurrencyMutateLocks(
): Promise<MutateBaseCurrencyLockMeta[]> {
const PreventedModels = this.getModelsPreventsMutate(tenantId);
public async baseCurrencyMutateLocks(): Promise<
MutateBaseCurrencyLockMeta[]
> {
const PreventedModels = this.getModelsPreventsMutate();
const opers = Object.entries(PreventedModels).map(([ModelName, Model]) => {
const InjectedModelProxy = this.moduleRef.get(ModelName, {
strict: false,
});
const InjectedModel = InjectedModelProxy();
const opers = Object.entries(PreventedModels).map(([ModelName, Model]) =>
this.isModelMutateLocked(Model),
);
return this.isModelMutateLocked(InjectedModel);
});
const results = await Promise.all(opers);
return results.filter(
@@ -68,12 +74,11 @@ export class OrganizationBaseCurrencyLocking {
/**
* Detarmines the base currency mutation locked.
* @param {number} tenantId
* @returns {Promise<boolean>}
*/
public isBaseCurrencyMutateLocked = async (tenantId: number) => {
const locks = await this.baseCurrencyMutateLocks(tenantId);
public async isBaseCurrencyMutateLocked() {
const locks = await this.baseCurrencyMutateLocks();
return !isEmpty(locks);
};
}
}

View File

@@ -1,102 +0,0 @@
import { Inject, Service } from 'typedi';
import { ObjectId } from 'mongodb';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SeedMigration } from '@/lib/Seeder/SeedMigration';
import { Tenant } from '@/system/models';
import { ServiceError } from '@/exceptions';
import TenantDBManager from '@/services/Tenancy/TenantDBManager';
import config from '../../config';
import { ERRORS } from './constants';
import OrganizationService from './OrganizationService';
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
@Service()
export default class OrganizationUpgrade {
@Inject()
private organizationService: OrganizationService;
@Inject()
private tenantsManager: TenantsManagerService;
@Inject('agenda')
private agenda: any;
/**
* Upgrades the given organization database.
* @param {number} tenantId - Tenant id.
* @returns {Promise<void>}
*/
public upgradeJob = async (tenantId: number): Promise<void> => {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
// Validate tenant version.
this.validateTenantVersion(tenant);
// Initialize the tenant.
const seedContext = this.tenantsManager.getSeedMigrationContext(tenant);
// Database manager.
const dbManager = new TenantDBManager();
// Migrate the organization database schema.
await dbManager.migrate(tenant);
// Seeds the organization database data.
await new SeedMigration(seedContext.knex, seedContext).latest();
// Update the organization database version.
await this.organizationService.flagTenantDBBatch(tenantId);
// Remove the tenant job id.
await Tenant.markAsUpgraded(tenantId);
};
/**
* Running organization upgrade job.
* @param {number} tenantId - Tenant id.
* @return {Promise<void>}
*/
public upgrade = async (tenantId: number): Promise<{ jobId: string }> => {
const tenant = await Tenant.query().findById(tenantId);
// Validate tenant version.
this.validateTenantVersion(tenant);
// Validate tenant upgrade is not running.
this.validateTenantUpgradeNotRunning(tenant);
// Send welcome mail to the user.
const jobMeta = await this.agenda.now('organization-upgrade', {
tenantId,
});
// Transformes the mangodb id to string.
const jobId = new ObjectId(jobMeta.attrs._id).toString();
// Markes the tenant as currently building.
await Tenant.markAsUpgrading(tenantId, jobId);
return { jobId };
};
/**
* Validates the given tenant version.
* @param {ITenant} tenant
*/
private validateTenantVersion(tenant) {
if (tenant.databaseBatch >= config.databaseBatch) {
throw new ServiceError(ERRORS.TENANT_DATABASE_UPGRADED);
}
}
/**
* Validates the given tenant upgrade is not running.
* @param tenant
*/
private validateTenantUpgradeNotRunning(tenant) {
if (tenant.isUpgradeRunning) {
throw new ServiceError(ERRORS.TENANT_UPGRADE_IS_RUNNING);
}
}
}

View File

@@ -9,7 +9,7 @@ export class CommandOrganizationValidators {
constructor(
private readonly baseCurrencyMutateLocking: OrganizationBaseCurrencyLocking,
) {}
/**
* Validate mutate base currency ability.
* @param {Tenant} tenant -
@@ -23,9 +23,7 @@ export class CommandOrganizationValidators {
) {
if (tenant.isReady && newBaseCurrency !== oldBaseCurrency) {
const isLocked =
await this.baseCurrencyMutateLocking.isBaseCurrencyMutateLocked(
tenant.id,
);
await this.baseCurrencyMutateLocking.isBaseCurrencyMutateLocked();
if (isLocked) {
throw new ServiceError(ERRORS.BASE_CURRENCY_MUTATE_LOCKED);

View File

@@ -18,6 +18,7 @@ import {
CreatePaymentReceivedDto,
EditPaymentReceivedDto,
} from './dtos/PaymentReceived.dto';
import { PaymentsReceivedPagesService } from './queries/PaymentsReceivedPages.service';
@Injectable()
export class PaymentReceivesApplication {
@@ -31,6 +32,7 @@ export class PaymentReceivesApplication {
private sendPaymentReceiveMailNotification: SendPaymentReceiveMailNotification,
private getPaymentReceivePdfService: GetPaymentReceivedPdfService,
private getPaymentReceivedStateService: GetPaymentReceivedStateService,
private paymentsReceivedPagesService: PaymentsReceivedPagesService,
) {}
/**
@@ -147,4 +149,14 @@ export class PaymentReceivesApplication {
public getPaymentReceivedState() {
return this.getPaymentReceivedStateService.getPaymentReceivedState();
}
/**
* Retrieve the payment received edit page.
* @param {number} paymentReceiveId - Payment receive id.
*/
public getPaymentReceivedEditPage(paymentReceiveId: number) {
return this.paymentsReceivedPagesService.getPaymentReceiveEditPage(
paymentReceiveId,
);
}
}

View File

@@ -4,6 +4,7 @@ import {
Controller,
Delete,
Get,
Headers,
HttpCode,
Param,
ParseIntPipe,
@@ -13,11 +14,14 @@ import {
} from '@nestjs/common';
import { PaymentReceivesApplication } from './PaymentReceived.application';
import {
IPaymentReceivedCreateDTO,
IPaymentReceivedEditDTO,
IPaymentsReceivedFilter,
PaymentReceiveMailOptsDTO,
} from './types/PaymentReceived.types';
import {
CreatePaymentReceivedDto,
EditPaymentReceivedDto,
} from './dtos/PaymentReceived.dto';
import { AcceptType } from '@/constants/accept-type';
@Controller('payments-received')
@ApiTags('payments-received')
@@ -40,6 +44,20 @@ export class PaymentReceivesController {
);
}
@Get(':id/edit-page')
@ApiResponse({
status: 200,
description:
'The payment received edit page has been successfully retrieved.',
})
public getPaymentReceiveEditPage(
@Param('id', ParseIntPipe) paymentReceiveId: number,
) {
return this.paymentReceivesApplication.getPaymentReceivedEditPage(
paymentReceiveId,
);
}
@Get(':id/mail')
@ApiResponse({
status: 200,
@@ -57,7 +75,7 @@ export class PaymentReceivesController {
@Post()
@ApiOperation({ summary: 'Create a new payment received.' })
public createPaymentReceived(
@Body() paymentReceiveDTO: IPaymentReceivedCreateDTO,
@Body() paymentReceiveDTO: CreatePaymentReceivedDto,
) {
return this.paymentReceivesApplication.createPaymentReceived(
paymentReceiveDTO,
@@ -68,7 +86,7 @@ export class PaymentReceivesController {
@ApiOperation({ summary: 'Edit the given payment received.' })
public editPaymentReceive(
@Param('id', ParseIntPipe) paymentReceiveId: number,
@Body() paymentReceiveDTO: IPaymentReceivedEditDTO,
@Body() paymentReceiveDTO: EditPaymentReceivedDto,
) {
return this.paymentReceivesApplication.editPaymentReceive(
paymentReceiveId,
@@ -88,7 +106,9 @@ export class PaymentReceivesController {
@Get()
@ApiOperation({ summary: 'Retrieves the payment received list.' })
public getPaymentsReceived(@Query() filterDTO: Partial<IPaymentsReceivedFilter>) {
public getPaymentsReceived(
@Query() filterDTO: Partial<IPaymentsReceivedFilter>,
) {
return this.paymentReceivesApplication.getPaymentsReceived(filterDTO);
}
@@ -126,21 +146,16 @@ export class PaymentReceivesController {
})
public getPaymentReceive(
@Param('id', ParseIntPipe) paymentReceiveId: number,
@Headers('accept') acceptHeader: string,
) {
return this.paymentReceivesApplication.getPaymentReceive(paymentReceiveId);
}
@Get(':id/pdf')
@ApiOperation({ summary: 'Retrieves the payment received pdf.' })
@ApiResponse({
status: 200,
description: 'The payment received pdf has been successfully retrieved.',
})
public getPaymentReceivePdf(
@Param('id', ParseIntPipe) paymentReceivedId: number,
) {
return this.paymentReceivesApplication.getPaymentReceivePdf(
paymentReceivedId,
);
if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
return this.paymentReceivesApplication.getPaymentReceivePdf(
paymentReceiveId,
);
} else {
return this.paymentReceivesApplication.getPaymentReceive(
paymentReceiveId,
);
}
}
}

View File

@@ -36,6 +36,7 @@ import { SendPaymentReceivedMailProcessor } from './processors/PaymentReceivedMa
import { SEND_PAYMENT_RECEIVED_MAIL_QUEUE } from './constants';
import { PaymentsReceivedExportable } from './commands/PaymentsReceivedExportable';
import { PaymentsReceivedImportable } from './commands/PaymentsReceivedImportable';
import { PaymentsReceivedPagesService } from './queries/PaymentsReceivedPages.service';
@Module({
controllers: [PaymentReceivesController],
@@ -63,6 +64,7 @@ import { PaymentsReceivedImportable } from './commands/PaymentsReceivedImportabl
SendPaymentReceivedMailProcessor,
PaymentsReceivedExportable,
PaymentsReceivedImportable,
PaymentsReceivedPagesService
],
exports: [
PaymentReceivesApplication,

View File

@@ -1,32 +1,47 @@
import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsNotEmpty, ValidateNested } from 'class-validator';
import { IsString } from 'class-validator';
import { IsDateString, IsNumber, IsOptional } from 'class-validator';
import { IsInt } from 'class-validator';
import {
IsString,
IsDateString,
IsNumber,
IsOptional,
IsArray,
IsNotEmpty,
IsInt,
ValidateNested,
} from 'class-validator';
import { ToNumber } from '@/common/decorators/Validators';
import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto';
export class PaymentReceivedEntryDto {
@ToNumber()
@IsOptional()
@IsInt()
id?: number;
@IsOptional()
@ToNumber()
@IsInt()
@IsOptional()
index?: number;
@IsOptional()
@ToNumber()
@IsInt()
paymentReceiveId?: number;
@ToNumber()
@IsInt()
@IsNotEmpty()
invoiceId: number;
@IsNumber()
@ToNumber()
@IsInt()
@IsNotEmpty()
paymentAmount: number;
}
export class CommandPaymentReceivedDto {
@ToNumber()
@IsInt()
@IsNotEmpty()
@ApiProperty({ description: 'The id of the customer', example: 1 })
@@ -40,6 +55,7 @@ export class CommandPaymentReceivedDto {
paymentDate: Date | string;
@IsOptional()
@ToNumber()
@IsNumber()
@ApiProperty({
description: 'The amount of the payment received',
@@ -48,6 +64,7 @@ export class CommandPaymentReceivedDto {
amount?: number;
@IsOptional()
@ToNumber()
@IsNumber()
@ApiProperty({
description: 'The exchange rate of the payment received',
@@ -63,6 +80,7 @@ export class CommandPaymentReceivedDto {
})
referenceNo?: string;
@ToNumber()
@IsInt()
@IsNotEmpty()
@ApiProperty({
@@ -72,6 +90,7 @@ export class CommandPaymentReceivedDto {
depositAccountId: number;
@IsOptional()
@ToNumber()
@IsString()
@ApiProperty({
description: 'The payment receive number of the payment received',
@@ -97,6 +116,7 @@ export class CommandPaymentReceivedDto {
entries: PaymentReceivedEntryDto[];
@IsOptional()
@ToNumber()
@IsInt()
@ApiProperty({
description: 'The id of the branch',

View File

@@ -41,11 +41,10 @@ export class PaymentsReceivedPagesService {
/**
* Retrieve payment receive new page receivable entries.
* @param {number} tenantId - Tenant id.
* @param {number} vendorId - Vendor id.
* @return {IPaymentReceivePageEntry[]}
*/
public async getNewPageEntries(tenantId: number, customerId: number) {
public async getNewPageEntries(customerId: number) {
// Retrieve due invoices.
const entries = await this.saleInvoice()
.query()
@@ -62,10 +61,7 @@ export class PaymentsReceivedPagesService {
* @param {number} tenantId - Tenant id.
* @param {Integer} paymentReceiveId - Payment receive id.
*/
public async getPaymentReceiveEditPage(
tenantId: number,
paymentReceiveId: number,
): Promise<{
public async getPaymentReceiveEditPage(paymentReceiveId: number): Promise<{
paymentReceive: Omit<PaymentReceived, 'entries'>;
entries: IPaymentReceivePageEntry[];
}> {

View File

@@ -29,7 +29,7 @@ export class PdfTemplateApplication {
* @param {ICreateInvoicePdfTemplateDTO} invoiceTemplateDTO - The data transfer object containing the details for the new PDF template.
* @returns {Promise<any>}
*/
public async createPdfTemplate(
public createPdfTemplate(
templateName: string,
resource: string,
invoiceTemplateDTO: ICreateInvoicePdfTemplateDTO,
@@ -45,7 +45,7 @@ export class PdfTemplateApplication {
* Deletes a PDF template.
* @param {number} templateId - The ID of the template to delete.
*/
public async deletePdfTemplate(templateId: number) {
public deletePdfTemplate(templateId: number) {
return this.deletePdfTemplateService.deletePdfTemplate(templateId);
}
@@ -53,7 +53,7 @@ export class PdfTemplateApplication {
* Retrieves a specific PDF template.
* @param {number} templateId - The ID of the template to retrieve.
*/
public async getPdfTemplate(templateId: number) {
public getPdfTemplate(templateId: number) {
return this.getPdfTemplateService.getPdfTemplate(templateId);
}
@@ -61,7 +61,7 @@ export class PdfTemplateApplication {
* Retrieves all PDF templates.
* @param {string} resource - The resource type to filter templates.
*/
public async getPdfTemplates(query?: { resource?: string }) {
public getPdfTemplates(query?: { resource?: string }) {
return this.getPdfTemplatesService.getPdfTemplates(query);
}
@@ -70,7 +70,7 @@ export class PdfTemplateApplication {
* @param {number} templateId - The ID of the template to edit.
* @param {IEditPdfTemplateDTO} editDTO - The data transfer object containing the updates.
*/
public async editPdfTemplate(
public editPdfTemplate(
templateId: number,
editDTO: IEditPdfTemplateDTO,
) {
@@ -80,7 +80,7 @@ export class PdfTemplateApplication {
/**
* Gets the PDF template branding state.
*/
public async getPdfTemplateBrandingState() {
public getPdfTemplateBrandingState() {
return this.getPdfTemplateBrandingStateService.execute();
}
@@ -89,7 +89,7 @@ export class PdfTemplateApplication {
* @param {number} templateId - The ID of the PDF template to assign as default.
* @returns {Promise<any>}
*/
public async assignPdfTemplateAsDefault(templateId: number) {
public assignPdfTemplateAsDefault(templateId: number) {
return this.assignPdfTemplateDefaultService.assignDefaultTemplate(
templateId,
);
@@ -99,7 +99,7 @@ export class PdfTemplateApplication {
* Retrieves the organization branding attributes.
* @returns {Promise<CommonOrganizationBrandingAttributes>} The organization branding attributes.
*/
getOrganizationBrandingAttributes() {
public getOrganizationBrandingAttributes() {
return this.getOrganizationBrandingAttributesService.execute();
}
}

View File

@@ -47,6 +47,17 @@ export class PdfTemplatesController {
return this.pdfTemplateApplication.deletePdfTemplate(templateId);
}
@Get('/state')
@ApiOperation({ summary: 'Retrieves the PDF template branding state.' })
@ApiResponse({
status: 200,
description:
'The PDF template branding state has been successfully retrieved.',
})
async getPdfTemplateBrandingState() {
return this.pdfTemplateApplication.getPdfTemplateBrandingState();
}
@Get(':id')
@ApiOperation({ summary: 'Retrieves the PDF template details.' })
@ApiResponse({

View File

@@ -4,6 +4,7 @@ import { DeleteRoleService } from './commands/DeleteRole.service';
import { EditRoleService } from './commands/EditRole.service';
import { GetRoleService } from './queries/GetRole.service';
import { GetRolesService } from './queries/GetRoles.service';
import { RolePermissionsSchema } from './queries/RolePermissionsSchema';
@Injectable()
export class RolesApplication {
@@ -13,6 +14,7 @@ export class RolesApplication {
private readonly deleteRoleService: DeleteRoleService,
private readonly getRoleService: GetRoleService,
private readonly getRolesService: GetRolesService,
private readonly getRolePermissionsSchemaService: RolePermissionsSchema,
) {}
/**
@@ -59,4 +61,12 @@ export class RolesApplication {
async getRoles() {
return this.getRolesService.getRoles();
}
/**
* Gets the role permissions schema.
* @returns The role permissions schema.
*/
async getRolePermissionsSchema() {
return this.getRolePermissionsSchemaService.getRolePermissionsSchema();
}
}

View File

@@ -72,9 +72,7 @@ export class RolesController {
status: HttpStatus.OK,
description: 'Role deleted successfully',
})
async deleteRole(
@Param('id', ParseIntPipe) roleId: number,
) {
async deleteRole(@Param('id', ParseIntPipe) roleId: number) {
await this.rolesApp.deleteRole(roleId);
return {
@@ -83,24 +81,34 @@ export class RolesController {
};
}
@Get('permissions/schema')
@ApiOperation({ summary: 'Get role permissions schema' })
@ApiResponse({
status: HttpStatus.OK,
description: 'Role permissions schema',
})
async getRolePermissionsSchema() {
const schema = await this.rolesApp.getRolePermissionsSchema();
return schema;
}
@Get()
@ApiOperation({ summary: 'Get all roles' })
@ApiResponse({ status: HttpStatus.OK, description: 'List of all roles' })
async getRoles() {
const roles = await this.rolesApp.getRoles();
return { roles };
return roles;
}
@Get(':id')
@ApiOperation({ summary: 'Get a specific role by ID' })
@ApiParam({ name: 'id', description: 'Role ID' })
@ApiResponse({ status: HttpStatus.OK, description: 'Role details' })
async getRole(
@Param('id', ParseIntPipe) roleId: number,
) {
async getRole(@Param('id', ParseIntPipe) roleId: number) {
const role = await this.rolesApp.getRole(roleId);
return { role };
return role;
}
}

View File

@@ -9,6 +9,7 @@ import { Role } from './models/Role.model';
import { RolePermission } from './models/RolePermission.model';
import { RolesController } from './Roles.controller';
import { RolesApplication } from './Roles.application';
import { RolePermissionsSchema } from './queries/RolePermissionsSchema';
const models = [
RegisterTenancyModel(Role),
@@ -24,6 +25,7 @@ const models = [
GetRoleService,
GetRolesService,
RolesApplication,
RolePermissionsSchema
],
controllers: [RolesController],
exports: [...models],

View File

@@ -1,5 +1,5 @@
import { Ability } from '@casl/ability';
import LruCache from 'lru-cache';
import * as LruCache from 'lru-cache';
import { Role } from './models/Role.model';
import { RolePermission } from './models/RolePermission.model';

Some files were not shown because too many files have changed in this diff Show More