refactor(nestjs): bank transactions matching

This commit is contained in:
Ahmed Bouhuolia
2025-06-05 14:41:26 +02:00
parent f87bd341e9
commit 51988dba3b
43 changed files with 484 additions and 105 deletions

View File

@@ -2,14 +2,21 @@ import { Body, Controller, Delete, Param, Post, Query } from '@nestjs/common';
import { castArray, omit } from 'lodash';
import { BankingCategorizeApplication } from './BankingCategorize.application';
import { CategorizeBankTransactionRouteDto } from './dtos/CategorizeBankTransaction.dto';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('banking/categorize')
@ApiTags('banking-categorization')
export class BankingCategorizeController {
constructor(
private readonly bankingCategorizeApplication: BankingCategorizeApplication,
) {}
@Post()
@ApiOperation({ summary: 'Categorize bank transactions.' })
@ApiResponse({
status: 200,
description: 'The bank transactions have been categorized successfully.',
})
public categorizeTransaction(
@Body() body: CategorizeBankTransactionRouteDto,
) {
@@ -20,6 +27,11 @@ export class BankingCategorizeController {
}
@Delete('/bulk')
@ApiOperation({ summary: 'Uncategorize bank transactions.' })
@ApiResponse({
status: 200,
description: 'The bank transactions have been uncategorized successfully.',
})
public uncategorizeTransactionsBulk(
@Query() uncategorizedTransactionIds: number[] | number,
) {
@@ -29,6 +41,11 @@ export class BankingCategorizeController {
}
@Delete('/:id')
@ApiOperation({ summary: 'Uncategorize a bank transaction.' })
@ApiResponse({
status: 200,
description: 'The bank transaction has been uncategorized successfully.',
})
public uncategorizeTransaction(
@Param('id') uncategorizedTransactionId: number,
) {

View File

@@ -8,48 +8,106 @@ import {
IsOptional,
IsString,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
/**
* DTO for categorizing bank transactions
*/
export class CategorizeBankTransactionDto {
@ApiProperty({
description: 'The date of the bank transaction',
type: Date,
example: '2023-01-01T00:00:00.000Z',
})
@IsDateString()
@IsNotEmpty()
date: Date;
@ApiProperty({
description: 'ID of the credit account associated with this transaction',
type: Number,
example: 1001,
})
@IsInt()
@ToNumber()
@IsNotEmpty()
creditAccountId: number;
@ApiPropertyOptional({
description: 'Optional external reference number',
type: String,
example: 'REF-001',
})
@IsString()
@IsOptional()
referenceNo: string;
@ApiPropertyOptional({
description: 'Optional transaction number or reference',
type: String,
example: 'TRX-001',
})
@IsString()
@IsOptional()
transactionNumber: string;
@ApiProperty({
description: 'Type of bank transaction (e.g., deposit, withdrawal)',
type: String,
example: 'deposit',
})
@IsString()
@IsNotEmpty()
transactionType: string;
@ApiPropertyOptional({
description: 'Exchange rate for currency conversion',
type: Number,
default: 1,
example: 1.15,
})
@IsNumber()
@ToNumber()
@IsOptional()
exchangeRate: number = 1;
@ApiPropertyOptional({
description: 'Currency code for the transaction',
type: String,
example: 'USD',
})
@IsString()
@IsOptional()
currencyCode: string;
@ApiPropertyOptional({
description: 'Description of the bank transaction',
type: String,
example: 'Monthly rent payment',
})
@IsString()
@IsOptional()
description: string;
@ApiPropertyOptional({
description: 'ID of the branch where the transaction occurred',
type: Number,
example: 101,
})
@IsNumber()
@IsOptional()
branchId: number;
}
/**
* Extended DTO for categorizing bank transactions with IDs of uncategorized transactions
*/
export class CategorizeBankTransactionRouteDto extends CategorizeBankTransactionDto {
@ApiProperty({
description: 'Array of uncategorized transaction IDs to be categorized',
type: [Number],
example: [1001, 1002, 1003],
})
@IsArray()
uncategorizedTransactionIds: Array<number>;
}

View File

@@ -23,16 +23,12 @@ export class BankingMatchingController {
);
}
@Post('/match/:uncategorizedTransactionId')
@Post('/match')
@ApiOperation({ summary: 'Match the given uncategorized transaction.' })
async matchTransaction(
@Param('uncategorizedTransactionId')
uncategorizedTransactionId: number | number[],
@Body() matchedTransactions: MatchBankTransactionDto,
) {
async matchTransaction(@Body() matchedTransactions: MatchBankTransactionDto) {
return this.bankingMatchingApplication.matchTransaction(
uncategorizedTransactionId,
matchedTransactions,
matchedTransactions.uncategorizedTransactions,
matchedTransactions.matchedTransactions,
);
}

View File

@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import { GetMatchedTransactions } from './queries/GetMatchedTransactions.service';
import { MatchBankTransactions } from './commands/MatchTransactions';
import { UnmatchMatchedBankTransaction } from './commands/UnmatchMatchedTransaction.service';
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
import { GetMatchedTransactionsFilter } from './types';
import { MatchTransactionEntryDto } from './dtos/MatchBankTransaction.dto';
@Injectable()
export class BankingMatchingApplication {
@@ -31,17 +31,18 @@ export class BankingMatchingApplication {
/**
* Matches the given uncategorized transaction with the given system transaction.
* @param {number} uncategorizedTransactionId
* @param {IMatchTransactionDTO} matchTransactionsDTO
* @param {IMatchBankTransactionDto} matchedTransactionsDTO
* @returns {Promise<void>}
*/
public matchTransaction(
uncategorizedTransactionId: number | Array<number>,
matchedTransactions: MatchBankTransactionDto,
matchedTransactionsDto:
| MatchTransactionEntryDto
| Array<MatchTransactionEntryDto>,
): Promise<void> {
return this.matchTransactionService.matchTransaction(
uncategorizedTransactionId,
matchedTransactions,
matchedTransactionsDto,
);
}

View File

@@ -21,7 +21,7 @@ import { ServiceError } from '@/modules/Items/ServiceError';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { MatchBankTransactionDto } from '../dtos/MatchBankTransaction.dto';
import { MatchTransactionEntryDto } from '../dtos/MatchBankTransaction.dto';
@Injectable()
export class MatchBankTransactions {
@@ -107,16 +107,15 @@ export class MatchBankTransactions {
/**
* Matches the given uncategorized transaction to the given references.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
* @returns {Promise<void>}
*/
public async matchTransaction(
uncategorizedTransactionId: number | Array<number>,
matchedTransactionsDto: MatchBankTransactionDto,
matchedTransactionsDto: MatchTransactionEntryDto | Array<MatchTransactionEntryDto>,
): Promise<void> {
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
const matchedTransactions = matchedTransactionsDto.entries;
const matchedTransactions = castArray(matchedTransactionsDto);
// Validates the given matching transactions DTO.
await this.validate(uncategorizedTransactionIds, matchedTransactions);
@@ -131,7 +130,7 @@ export class MatchBankTransactions {
// Matches the given transactions under promise pool concurrency controlling.
await PromisePool.withConcurrency(10)
.for(matchedTransactions)
.process(async (matchedTransaction) => {
.process(async (matchedTransaction: MatchTransactionEntryDto) => {
const getMatchedTransactionsService =
this.matchedBankTransactions.registry.get(
matchedTransaction.referenceType,

View File

@@ -1,4 +1,5 @@
import {
ArrayMinSize,
IsArray,
IsNotEmpty,
IsNumber,
@@ -27,6 +28,10 @@ export class MatchTransactionEntryDto {
}
export class MatchBankTransactionDto {
@IsArray()
@ArrayMinSize(1)
uncategorizedTransactions: Array<number>
@IsArray()
@ValidateNested({ each: true })
@Type(() => MatchTransactionEntryDto)
@@ -37,5 +42,5 @@ export class MatchBankTransactionDto {
{ referenceType: 'SaleInvoice', referenceId: 2 },
],
})
entries: MatchTransactionEntryDto[];
matchedTransactions: MatchTransactionEntryDto[];
}

View File

@@ -1,14 +1,14 @@
import PromisePool from '@supercharge/promise-pool';
import { OnEvent } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common';
import {
IBankTransactionMatchedEventPayload,
IBankTransactionUnmatchedEventPayload,
} from '../types';
import PromisePool from '@supercharge/promise-pool';
import { OnEvent } from '@nestjs/event-emitter';
import { Account } from '@/modules/Accounts/models/Account.model';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class DecrementUncategorizedTransactionOnMatchingSubscriber {

View File

@@ -1,11 +1,12 @@
import { Body, Controller, Post } from '@nestjs/common';
import { PlaidWebhookDto } from './dtos/PlaidItem.dto';
import { ApiOperation } from '@nestjs/swagger';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { PlaidApplication } from './PlaidApplication';
import { PublicRoute } from '../Auth/guards/jwt.guard';
import { SetupPlaidItemTenantService } from './command/SetupPlaidItemTenant.service';
@Controller('banking/plaid')
@ApiTags('banking-plaid')
@PublicRoute()
export class BankingPlaidWebhooksController {
constructor(

View File

@@ -1,13 +1,13 @@
import { events } from '@/common/events/events';
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { Queue } from 'bullmq';
import { InjectQueue } from '@nestjs/bullmq';
import {
IPlaidItemCreatedEventPayload,
UpdateBankingPlaidTransitionsJob,
UpdateBankingPlaidTransitionsQueueJob,
} from '../types/BankingPlaid.types';
import { Queue } from 'bullmq';
import { InjectQueue } from '@nestjs/bullmq';
@Injectable()
export class PlaidUpdateTransactionsOnItemCreatedSubscriber {

View File

@@ -8,67 +8,145 @@ import {
IsOptional,
IsString,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateBankTransactionDto {
@ApiProperty({
description: 'The date of the bank transaction',
type: Date,
example: '2023-01-01T00:00:00.000Z',
})
@IsDateString()
@IsNotEmpty()
date: Date;
@ApiPropertyOptional({
description: 'Optional transaction number or reference',
type: String,
example: 'TRX-001',
})
@IsString()
@IsOptional()
transactionNumber?: string;
@ApiPropertyOptional({
description: 'Optional external reference number',
type: String,
example: 'REF-001',
})
@IsString()
@IsOptional()
referenceNo?: string;
@ApiProperty({
description: 'Type of bank transaction (e.g., deposit, withdrawal)',
type: String,
example: 'deposit',
})
@IsNotEmpty()
@IsString()
transactionType: string;
@ApiProperty({
description: 'Description of the bank transaction',
type: String,
example: 'Monthly rent payment',
})
@IsString()
description: string;
@ApiProperty({
description: 'Transaction amount',
type: Number,
example: 1000.5,
})
@IsNotEmpty()
@ToNumber()
@IsNumber()
amount: number;
@ApiProperty({
description: 'Exchange rate for currency conversion',
type: Number,
default: 1,
example: 1.15,
})
@ToNumber()
@IsNumber()
exchangeRate: number = 1;
@ApiPropertyOptional({
description: 'Currency code for the transaction',
type: String,
example: 'USD',
})
@IsString()
@IsOptional()
currencyCode: string;
@ApiProperty({
description: 'ID of the credit account associated with this transaction',
type: Number,
example: 1001,
})
@IsNotEmpty()
@ToNumber()
@IsInt()
@IsInt()
creditAccountId: number;
@ApiProperty({
description: 'ID of the cashflow account associated with this transaction',
type: Number,
example: 2001,
})
@IsNotEmpty()
@ToNumber()
@IsInt()
@IsInt()
cashflowAccountId: number;
@ApiProperty({
description: 'Whether the transaction should be published',
type: Boolean,
default: true,
})
@IsBoolean()
@IsOptional()
publish: boolean = true;
@ApiPropertyOptional({
description: 'ID of the branch where the transaction occurred',
type: Number,
example: 101,
})
@IsOptional()
@ToNumber()
@IsInt()
branchId?: number;
@ApiPropertyOptional({
description: 'Plaid transaction ID if imported from Plaid',
type: String,
example: 'plaid_trx_12345',
})
@IsOptional()
@IsString()
plaidTransactionId?: string;
@ApiPropertyOptional({
description: 'Plaid account ID if imported from Plaid',
type: String,
example: 'plaid_acc_67890',
})
@IsOptional()
@IsString()
plaidAccountId?: string;
@ApiPropertyOptional({
description:
'ID of the uncategorized transaction if this is categorizing an existing transaction',
type: Number,
example: 5001,
})
@IsOptional()
@IsInt()
uncategorizedTransactionId?: number;

View File

@@ -1,14 +1,8 @@
import * as moment from 'moment';
import * as R from 'ramda';
import type { Knex } from 'knex';
import { Model, raw } from 'objection';
import { castArray, difference, defaultTo } from 'lodash';
import * as moment from 'moment';
import * as R from 'ramda';
// import TenantModel from 'models/TenantModel';
// import BillSettings from './Bill.Settings';
// import ModelSetting from './ModelSetting';
// import CustomViewBaseModel from './CustomViewBaseModel';
// import { DEFAULT_VIEWS } from '@/services/Purchases/Bills/constants';
// import ModelSearchable from './ModelSearchable';
import { BaseModel, PaginationQueryBuilderType } from '@/models/Model';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { BillLandedCost } from '@/modules/BillLandedCosts/models/BillLandedCost';

View File

@@ -0,0 +1,33 @@
import { ToNumber } from '@/common/decorators/Validators';
import { IsArray, IsEnum, IsInt, IsOptional, IsString } from 'class-validator';
import { IFilterRole, ISortOrder } from '../DynamicFilter/DynamicFilter.types';
export class DynamicFilterQueryDto {
@IsOptional()
@ToNumber()
customViewId?: number;
@IsArray()
@IsOptional()
filterRoles?: IFilterRole[];
@IsOptional()
@IsString()
columnSortBy: string;
@IsString()
@IsOptional()
sortOrder: ISortOrder;
@IsString()
@IsOptional()
stringifiedFilterRoles?: string;
@IsString()
@IsOptional()
searchKeyword?: string;
@IsString()
@IsOptional()
viewSlug?: string;
}

View File

@@ -52,8 +52,6 @@ export class ExportResourceService {
const data = await this.getExportableData(resource);
const transformed = this.transformExportedData(resource, data);
console.log(format);
// Returns the csv, xlsx format.
if (format === ExportFormat.Csv || format === ExportFormat.Xlsx) {
const exportableColumns = this.getExportableColumns(resourceColumns);

View File

@@ -17,8 +17,8 @@ export class InventoryAdjustmentTransformer extends Transformer {
*/
formattedType(inventoryAdjustment: InventoryAdjustment) {
const types = {
increment: 'inventory_adjustment.type.increment',
decrement: 'inventory_adjustment.type.decrement',
increment: 'inventory_adjustment.increment',
decrement: 'inventory_adjustment.decrement',
};
return this.context.i18n.t(types[inventoryAdjustment.type] || '');
}

View File

@@ -1,15 +1,17 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsBoolean,
IsDate,
IsDateString,
IsEnum,
IsInt,
IsNotEmpty,
IsNumber,
IsOptional,
IsPositive,
IsString,
} from 'class-validator';
import { Type } from 'class-transformer';
import { Transform } from 'class-transformer';
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
import { parseBoolean } from '@/utils/parse-boolean';
enum IAdjustmentTypes {
INCREMENT = 'increment',
@@ -19,8 +21,7 @@ enum IAdjustmentTypes {
export class CreateQuickInventoryAdjustmentDto {
@ApiProperty({ description: 'Date of the inventory adjustment' })
@IsNotEmpty()
@IsDate()
@Type(() => Date)
@IsDateString()
date: Date;
@ApiProperty({ description: 'Type of adjustment', enum: IAdjustmentTypes })
@@ -30,7 +31,8 @@ export class CreateQuickInventoryAdjustmentDto {
@ApiProperty({ description: 'ID of the adjustment account' })
@IsNotEmpty()
@IsNumber()
@ToNumber()
@IsInt()
@IsPositive()
adjustmentAccountId: number;
@@ -40,47 +42,52 @@ export class CreateQuickInventoryAdjustmentDto {
reason: string;
@ApiProperty({ description: 'Description of the adjustment' })
@IsNotEmpty()
@IsOptional()
@IsString()
description: string;
@ApiProperty({ description: 'Reference number' })
@IsNotEmpty()
@IsOptional()
@IsString()
referenceNo: string;
@ApiProperty({ description: 'ID of the item being adjusted' })
@IsNotEmpty()
@ToNumber()
@IsNumber()
@IsPositive()
itemId: number;
@ApiProperty({ description: 'Quantity to adjust' })
@IsNotEmpty()
@ToNumber()
@IsNumber()
@IsPositive()
quantity: number;
@ApiProperty({ description: 'Cost of the item' })
@IsNotEmpty()
@IsOptional()
@ToNumber()
@IsNumber()
@IsPositive()
cost: number;
@ApiProperty({ description: 'Whether to publish the adjustment immediately' })
@IsNotEmpty()
@Transform((param) => parseBoolean(param.value, false))
@IsBoolean()
publish: boolean;
@ApiPropertyOptional({ description: 'ID of the warehouse (optional)' })
@IsOptional()
@IsNumber()
@ToNumber()
@IsInt()
@IsPositive()
warehouseId?: number;
@ApiPropertyOptional({ description: 'ID of the branch (optional)' })
@IsOptional()
@IsNumber()
@ToNumber()
@IsInt()
@IsPositive()
branchId?: number;
}

View File

@@ -0,0 +1,24 @@
import { Controller, Get, Query } from '@nestjs/common';
import { GetItemsInventoryValuationListService } from './queries/GetItemsInventoryValuationList.service';
import { GetInventoyItemsCostQueryDto } from './dtos/GetInventoryItemsCostQuery.dto';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
@Controller('inventory-cost')
@ApiTags('inventory-cost')
export class InventoryCostController {
constructor(
private readonly inventoryItemCost: GetItemsInventoryValuationListService,
) {}
@Get('items')
@ApiOperation({ summary: 'Get items inventory valuation list' })
async getItemsCost(
@Query() itemsCostsQueryDto: GetInventoyItemsCostQueryDto,
) {
const costs = await this.inventoryItemCost.getItemsInventoryValuationList(
itemsCostsQueryDto.itemsIds,
itemsCostsQueryDto.date,
);
return { costs };
}
}

View File

@@ -23,6 +23,8 @@ import { InventoryItemOpeningAvgCostService } from './commands/InventoryItemOpen
import { InventoryCostSubscriber } from './subscribers/InventoryCost.subscriber';
import { SaleInvoicesModule } from '../SaleInvoices/SaleInvoices.module';
import { ImportModule } from '../Import/Import.module';
import { GetItemsInventoryValuationListService } from './queries/GetItemsInventoryValuationList.service';
import { InventoryCostController } from './InventoryCost.controller';
const models = [
RegisterTenancyModel(InventoryCostLotTracker),
@@ -54,6 +56,7 @@ const models = [
InventoryItemCostService,
InventoryItemOpeningAvgCostService,
InventoryCostSubscriber,
GetItemsInventoryValuationListService
],
exports: [
...models,
@@ -61,5 +64,6 @@ const models = [
InventoryItemCostService,
InventoryComputeCostService,
],
controllers: [InventoryCostController]
})
export class InventoryCostModule {}

View File

@@ -0,0 +1,26 @@
import {
ArrayMinSize,
IsArray,
IsDateString,
IsNotEmpty,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class GetInventoyItemsCostQueryDto {
@IsDateString()
@IsNotEmpty()
@ApiProperty({
description: 'The date to get the inventory cost for',
example: '2021-01-01',
})
date: Date;
@IsArray()
@IsNotEmpty()
@ArrayMinSize(1)
@ApiProperty({
description: 'The ids of the items to get the inventory cost for',
example: [1, 2, 3],
})
itemsIds: Array<number>;
}

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { InventoryItemCostService } from '../commands/InventoryCosts.service';
import { IInventoryItemCostMeta } from '../types/InventoryCost.types';
@Injectable()
export class GetItemsInventoryValuationListService {
constructor(private readonly inventoryCost: InventoryItemCostService) {}
/**
* Retrieves the items inventory valuation list.
* @param {number[]} itemsId
* @param {Date} date
* @returns {Promise<IInventoryItemCostMeta[]>}
*/
public getItemsInventoryValuationList = async (
itemsId: number[],
date: Date,
): Promise<IInventoryItemCostMeta[]> => {
const itemsMap = await this.inventoryCost.getItemsInventoryValuation(
itemsId,
date,
);
return [...itemsMap.values()];
};
}

View File

@@ -7,6 +7,7 @@ import { IItemsFilter } from './types/Items.types';
import { ItemTransformer } from './Item.transformer';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { ISortOrder } from '../DynamicListing/DynamicFilter/DynamicFilter.types';
import { GetItemsQueryDto } from './dtos/GetItemsQuery.dto';
@Injectable()
export class GetItemsService {
@@ -32,7 +33,7 @@ export class GetItemsService {
* Retrieves items datatable list.
* @param {IItemsFilter} itemsFilter - Items filter.
*/
public async getItems(filterDto: Partial<IItemsFilter>) {
public async getItems(filterDto: Partial<GetItemsQueryDto>) {
const _filterDto = {
sortOrder: ISortOrder.DESC,
columnSortBy: 'created_at',

View File

@@ -23,6 +23,7 @@ import {
} from '@nestjs/swagger';
import { IItemsFilter } from './types/Items.types';
import { CreateItemDto, EditItemDto } from './dtos/Item.dto';
import { GetItemsQueryDto } from './dtos/GetItemsQuery.dto';
@Controller('/items')
@UseGuards(SubscriptionGuard)
@@ -99,7 +100,7 @@ export class ItemsController extends TenantController {
type: Boolean,
description: 'Filter for inactive items',
})
async getItems(@Query() filterDTO: IItemsFilter): Promise<any> {
async getItems(@Query() filterDTO: GetItemsQueryDto): Promise<any> {
return this.itemsApplication.getItems(filterDTO);
}

View File

@@ -12,6 +12,7 @@ import { Injectable } from '@nestjs/common';
import { GetItemsService } from './GetItems.service';
import { IItemsFilter } from './types/Items.types';
import { EditItemDto, CreateItemDto } from './dtos/Item.dto';
import { GetItemsQueryDto } from './dtos/GetItemsQuery.dto';
@Injectable()
export class ItemsApplicationService {
@@ -94,7 +95,7 @@ export class ItemsApplicationService {
* Retrieves the paginated filterable items list.
* @param {Partial<IItemsFilter>} filterDTO
*/
async getItems(filterDTO: Partial<IItemsFilter>) {
async getItems(filterDTO: Partial<GetItemsQueryDto>) {
return this.getItemsService.getItems(filterDTO);
}

View File

@@ -0,0 +1,22 @@
import { ToNumber } from '@/common/decorators/Validators';
import { DynamicFilterQueryDto } from '@/modules/DynamicListing/dtos/DynamicFilterQuery.dto';
import { parseBoolean } from '@/utils/parse-boolean';
import { Transform, Type } from 'class-transformer';
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
export class GetItemsQueryDto extends DynamicFilterQueryDto {
@IsOptional()
@IsInt()
@ToNumber()
page?: number;
@IsOptional()
@IsInt()
@ToNumber()
pageSize?: number;
@IsOptional()
@Transform((param) => parseBoolean(param.value, false))
@IsBoolean()
inactiveMode?: boolean;
}

View File

@@ -1,4 +1,4 @@
import async from 'async';
import * as async from 'async';
import { Knex } from 'knex';
import {
ILedger,

View File

@@ -1,5 +1,5 @@
import { Knex } from 'knex';
import async from 'async';
import * as async from 'async';
import { Inject, Injectable } from '@nestjs/common';
import { transformLedgerEntryToTransaction } from './utils';
import {
@@ -33,7 +33,7 @@ export class LedgerEntriesStorageService {
* @returns {Promise<void>}
*/
public saveEntries = async (ledger: ILedger, trx?: Knex.Transaction) => {
const saveEntryQueue = async.queue(this.saveEntryTask, 10);
const saveEntryQueue = async.queue(this.saveEntryTask.bind(this), 10);
const entries = ledger.filter(filterBlankEntry).getEntries();
entries.forEach((entry) => {

View File

@@ -1,4 +1,4 @@
import async from 'async';
import * as async from 'async';
import { Knex } from 'knex';
import { uniq } from 'lodash';
import {

View File

@@ -1,3 +1,4 @@
import * as moment from 'moment';
import { AccountTransaction } from "../Accounts/models/AccountTransaction.model";
import { ILedgerEntry } from "./types/Ledger.types";

View File

@@ -1,5 +1,5 @@
import { Knex } from 'knex';
import async from 'async';
import * as async from 'async';
import { Inject, Injectable } from '@nestjs/common';
import { PaymentReceivedGLEntries } from '../PaymentReceived/commands/PaymentReceivedGLEntries';
import { TenantModelProxy } from '../System/models/TenantBaseModel';

View File

@@ -1,7 +1,7 @@
import { Model, raw } from 'objection';
import { castArray } from 'lodash';
import * as moment from 'moment';
import * as R from 'ramda';
import { Model, raw } from 'objection';
import { castArray } from 'lodash';
import { MomentInput, unitOfTime } from 'moment';
import { defaultTo } from 'ramda';
import { TaxRateTransaction } from '@/modules/TaxRates/models/TaxRateTransaction.model';

View File

@@ -1,4 +1,4 @@
import moment from 'moment';
import * as moment from 'moment';
import { BaseModel } from '@/models/Model';
export class UserInvite extends BaseModel {

View File

@@ -1,6 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { IEditWarehouseDTO, IWarehouse } from '../Warehouse.types';
import { WarehouseValidator } from './WarehouseValidator.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';

View File

@@ -0,0 +1,18 @@
import { ToNumber } from '@/common/decorators/Validators';
import { IsInt, IsOptional, IsString } from 'class-validator';
export class GetWarehouseTransfersQueryDto {
@IsInt()
@ToNumber()
@IsOptional()
page: number;
@IsInt()
@ToNumber()
@IsOptional()
pageSize: number;
@IsString()
@IsOptional()
searchKeyword: string;
}

View File

@@ -13,6 +13,7 @@ import {
CreateWarehouseTransferDto,
EditWarehouseTransferDto,
} from './dtos/WarehouseTransfer.dto';
import { GetWarehouseTransfersQueryDto } from '../Warehouses/dtos/GetWarehouseTransfersQuery.dto';
@Injectable()
export class WarehouseTransferApplication {
@@ -86,7 +87,7 @@ export class WarehouseTransferApplication {
* @returns {Promise<IWarehouseTransfer>}
*/
public getWarehousesTransfers = (
filterDTO: IGetWarehousesTransfersFilterDTO,
filterDTO: GetWarehouseTransfersQueryDto,
) => {
return this.getWarehousesTransfersService.getWarehouseTransfers(filterDTO);
};

View File

@@ -15,6 +15,7 @@ import {
CreateWarehouseTransferDto,
EditWarehouseTransferDto,
} from './dtos/WarehouseTransfer.dto';
import { GetWarehouseTransfersQueryDto } from '../Warehouses/dtos/GetWarehouseTransfersQuery.dto';
@Controller('warehouse-transfers')
@ApiTags('warehouse-transfers')
@@ -129,7 +130,7 @@ export class WarehouseTransfersController {
description:
'The warehouse transfer transactions have been retrieved successfully.',
})
async getWarehousesTransfers(@Query() query: any) {
async getWarehousesTransfers(@Query() query: GetWarehouseTransfersQueryDto) {
const { warehousesTransfers, pagination, filter } =
await this.warehouseTransferApplication.getWarehousesTransfers(query);

View File

@@ -1,18 +1,18 @@
import { ToNumber } from '@/common/decorators/Validators';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsDate,
IsDecimal,
IsInt,
IsNotEmpty,
IsNumber,
IsOptional,
IsPositive,
IsString,
ValidateNested,
ArrayMinSize,
IsDateString,
} from 'class-validator';
export class WarehouseTransferEntryDto {
@@ -39,6 +39,7 @@ export class WarehouseTransferEntryDto {
export class CommandWarehouseTransferDto {
@IsNotEmpty()
@ToNumber()
@IsInt()
@ApiProperty({
description: 'The id of the warehouse to transfer from',
@@ -47,6 +48,7 @@ export class CommandWarehouseTransferDto {
fromWarehouseId: number;
@IsNotEmpty()
@ToNumber()
@IsInt()
@ApiProperty({
description: 'The id of the warehouse to transfer to',
@@ -55,7 +57,7 @@ export class CommandWarehouseTransferDto {
toWarehouseId: number;
@IsNotEmpty()
@IsDate()
@IsDateString()
@ApiProperty({
description: 'The date of the warehouse transfer',
example: '2021-01-01',

View File

@@ -0,0 +1,70 @@
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
export const WarehouseTransferMeta = {
defaultFilterField: 'name',
defaultSort: {
sortField: 'name',
sortOrder: 'DESC',
},
columns: {
date: {
name: 'warehouse_transfer.field.date',
type: 'date',
exportable: true,
},
transaction_number: {
name: 'warehouse_transfer.field.transaction_number',
type: 'text',
exportable: true,
},
status: {
name: 'warehouse_transfer.field.status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'in-transit', label: 'In Transit' },
{ key: 'transferred', label: 'Transferred' },
],
sortable: false,
},
created_at: {
name: 'warehouse_transfer.field.created_at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
},
fields: {
date: {
name: 'warehouse_transfer.field.date',
column: 'date',
columnType: 'date',
fieldType: 'date',
},
transaction_number: {
name: 'warehouse_transfer.field.transaction_number',
column: 'transaction_number',
fieldType: 'text',
},
status: {
name: 'warehouse_transfer.field.status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'in-transit', label: 'In Transit' },
{ key: 'transferred', label: 'Transferred' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortable: false,
},
created_at: {
name: 'warehouse_transfer.field.created_at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
},
};

View File

@@ -2,7 +2,10 @@ import { Model, mixin } from 'objection';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
import { WarehouseTransferEntry } from './WarehouseTransferEntry';
import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator';
import { WarehouseTransferMeta } from './WarehouseTransfer.meta';
@InjectModelMeta(WarehouseTransferMeta)
export class WarehouseTransfer extends TenantBaseModel {
public date!: Date;
public transferInitiatedAt!: Date;
@@ -14,7 +17,6 @@ export class WarehouseTransfer extends TenantBaseModel {
public fromWarehouse!: Warehouse;
public toWarehouse!: Warehouse;
/**
* Table name.
*/
@@ -104,7 +106,7 @@ export class WarehouseTransfer extends TenantBaseModel {
},
/**
*
*
*/
fromWarehouse: {
relation: Model.BelongsToOneRelation,
@@ -126,27 +128,13 @@ export class WarehouseTransfer extends TenantBaseModel {
};
}
/**
* Model settings.
*/
// static get meta() {
// return WarehouseTransferSettings;
// }
// /**
// * Retrieve the default custom views, roles and columns.
// */
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search roles.
*/
static get searchRoles() {
return [
// { fieldKey: 'name', comparator: 'contains' },
// { condition: 'or', fieldKey: 'code', comparator: 'like' },
{ fieldKey: 'name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'code', comparator: 'like' },
];
}
}

View File

@@ -6,6 +6,7 @@ import { TransformerInjectable } from '../../Transformer/TransformerInjectable.s
import { DynamicListService } from '../../DynamicListing/DynamicList.service';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
import { WarehouseTransfer } from '../models/WarehouseTransfer';
import { GetWarehouseTransfersQueryDto } from '@/modules/Warehouses/dtos/GetWarehouseTransfersQuery.dto';
@Injectable()
export class GetWarehouseTransfers {
@@ -30,16 +31,19 @@ export class GetWarehouseTransfers {
/**
* Retrieves warehouse transfers paginated list.
* @param {number} tenantId
* @param {IGetWarehousesTransfersFilterDTO} filterDTO
* @returns {}
* @param {IGetWarehousesTransfersFilterDTO} filterDTO
*/
public getWarehouseTransfers = async (
filterDTO: IGetWarehousesTransfersFilterDTO,
filterDTO: GetWarehouseTransfersQueryDto,
) => {
// Parses stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
const filter = this.parseListFilterDTO({
sortOrder: 'desc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...filterDTO,
});
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
this.warehouseTransferModel(),