Compare commits

..

25 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
adb1bea374 feat: use the same Authorization header for jwt and api key 2025-07-02 08:30:53 +02:00
Ahmed Bouhuolia
5d96357042 feat: clean up items controller 2025-07-01 23:48:56 +02:00
Ahmed Bouhuolia
9457b3cda1 feat: api keys 2025-07-01 23:45:38 +02:00
Ahmed Bouhuolia
84cb7693c8 feat: api keys 2025-07-01 23:05:58 +02:00
Ahmed Bouhuolia
9f6e9e85a5 feat(server): endpoints swagger docs 2025-06-30 16:30:55 +02:00
Ahmed Bouhuolia
83e698acf3 fix:create customer/vendor 2025-06-29 16:55:02 +02:00
Ahmed Bouhuolia
fa5c3bd955 feat: deleteIfNoRelations 2025-06-28 22:35:29 +02:00
Ahmed Bouhuolia
0ca98c7ae4 fix: cycle dependecy 2025-06-27 02:18:01 +02:00
Ahmed Bouhuolia
0c0e1dc22e fix: invoice generate sharable link 2025-06-27 01:59:46 +02:00
Ahmed Bouhuolia
e7178a6575 fix: adjust contact balance 2025-06-26 17:04:46 +02:00
Ahmed Bouhuolia
6a39e9d71f feat: endpoints swagger document 2025-06-22 23:46:39 +02:00
Ahmed Bouhuolia
9aa1ed93ca feat: update endpoint swagger docs 2025-06-22 20:58:53 +02:00
Ahmed Bouhuolia
b8c9919799 fox: journal sheet 2025-06-21 21:10:05 +02:00
Ahmed Bouhuolia
e5701140e1 feat: swagger doc 2025-06-21 20:55:32 +02:00
Ahmed Bouhuolia
91976842a7 fix: AR/AP aging report 2025-06-21 20:15:42 +02:00
Ahmed Bouhuolia
4d52059dba feat: swagger document endpoints 2025-06-19 21:04:54 +02:00
Ahmed Bouhuolia
26c1f118c1 feat: more response docs 2025-06-19 00:49:43 +02:00
Ahmed Bouhuolia
437bcb8854 feat: models default views 2025-06-17 20:53:13 +02:00
Ahmed Bouhuolia
f624cf7ae6 feat: document more endpoints 2025-06-16 23:40:12 +02:00
Ahmed Bouhuolia
e057b4e2f0 feat: add swagger docs 2025-06-16 15:53:00 +02:00
Ahmed Bouhuolia
c4668d7d22 feat: add swagger docs for responses 2025-06-16 13:50:30 +02:00
Ahmed Bouhuolia
88ef60ef28 fix: delete inventory adjustment gl entries 2025-06-15 17:51:44 +02:00
Ahmed Bouhuolia
bbf9ef9bc2 fix: formatted transaction type 2025-06-15 15:22:19 +02:00
Ahmed Bouhuolia
bcae2dae03 feat: change the controllers tags 2025-06-13 01:57:53 +02:00
Ahmed Bouhuolia
ff93168d72 refactor(nestjs): landed cost 2025-06-11 14:04:37 +02:00
325 changed files with 13060 additions and 1414 deletions

View File

@@ -39,6 +39,7 @@
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^4.1.2",
"@nestjs/serve-static": "^5.0.3",
"@nestjs/swagger": "^7.4.2",
"@nestjs/throttler": "^6.2.1",
"@supercharge/promise-pool": "^3.2.0",
@@ -86,6 +87,7 @@
"object-hash": "^2.0.3",
"objection": "^3.1.5",
"passport": "^0.7.0",
"passport-headerapikey": "^1.2.2",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"plaid": "^10.3.0",

0
packages/server/public/pdf/.gitignore vendored Normal file
View File

View File

@@ -0,0 +1,33 @@
import { ApiProperty } from '@nestjs/swagger';
class Pagination {
@ApiProperty({
description: 'Total number of items across all pages',
example: 100,
})
total: number;
@ApiProperty({
description: 'Current page number (1-based)',
example: 1,
minimum: 1,
})
page: number;
@ApiProperty({
description: 'Number of items per page',
example: 10,
minimum: 1,
})
pageSize: number;
}
export class PaginatedResponseDto<TData> {
@ApiProperty({
description: 'Pagination metadata',
type: Pagination,
})
pagination: Pagination;
data: TData[];
}

View File

@@ -0,0 +1,9 @@
export class ModelHasRelationsError extends Error {
type: string;
constructor(type: string = 'ModelHasRelations', message?: string) {
message = message || `Entity has relations`;
super(message);
this.type = type;
}
}

View File

@@ -0,0 +1,27 @@
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpStatus,
} from '@nestjs/common';
import { Response } from 'express';
import { ModelHasRelationsError } from '../exceptions/ModelHasRelations.exception';
@Catch(ModelHasRelationsError)
export class ModelHasRelationsFilter implements ExceptionFilter {
catch(exception: ModelHasRelationsError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = HttpStatus.CONFLICT;
response.status(status).json({
errors: [
{
statusCode: status,
type: exception.type || 'MODEL_HAS_RELATIONS',
message: exception.message,
},
],
});
}
}

View File

@@ -0,0 +1,7 @@
import { QueryBuilder, Model } from 'objection';
declare module 'objection' {
interface QueryBuilder<M extends Model, R = M[]> {
deleteIfNoRelations(this: QueryBuilder<M, R>, ...args: any[]): Promise<any>;
}
}

View File

@@ -0,0 +1,3 @@
{
"head_branch": "Head Branch"
}

View File

@@ -0,0 +1,25 @@
{
"sale_invoice": "Sale invoice",
"sale_receipt": "Sale receipt",
"payment_received": "Payment received",
"bill": "Bill",
"bill_payment": "Payment made",
"vendor_opening_balance": "Vendor opening balance",
"customer_opening_balance": "Customer opening balance",
"inventory_adjustment": "Inventory adjustment",
"manual_journal": "Manual journal",
"expense": "Expense",
"owner_contribution": "Owner contribution",
"transfer_to_account": "Transfer to account",
"transfer_from_account": "Transfer from account",
"other_income": "Other income",
"other_expense": "Other expense",
"owner_drawing": "Owner drawing",
"invoice_write_off": "Invoice write-off",
"credit_note": "Credit Note",
"vendor_credit": "Vendor Credit",
"refund_credit_note": "Refund Credit Note",
"refund_vendor_credit": "Refund Vendor Credit",
"landed_cost": "Landed Cost",
"payment_made": "Payment made"
}

View File

@@ -0,0 +1,6 @@
{
"view.draft": "Draft",
"view.published": "Published",
"view.open": "Open",
"view.closed": "Closed"
}

View File

@@ -5,9 +5,11 @@ import * as path from 'path';
import './utils/moment-mysql';
import { AppModule } from './modules/App/App.module';
import { ServiceErrorFilter } from './common/filters/service-error.filter';
import { ModelHasRelationsFilter } from './common/filters/model-has-relations.filter';
import { ValidationPipe } from './common/pipes/ClassValidation.pipe';
import { ToJsonInterceptor } from './common/interceptors/to-json.interceptor';
global.__public_dirname = path.join(__dirname, '..', 'public');
global.__static_dirname = path.join(__dirname, '../static');
global.__views_dirname = path.join(global.__static_dirname, '/views');
global.__images_dirname = path.join(global.__static_dirname, '/images');
@@ -35,6 +37,7 @@ async function bootstrap() {
SwaggerModule.setup('swagger', app, documentFactory);
app.useGlobalFilters(new ServiceErrorFilter());
app.useGlobalFilters(new ModelHasRelationsFilter());
await app.listen(process.env.PORT ?? 3000);
}

View File

@@ -1,4 +1,5 @@
import { QueryBuilder, Model } from 'objection';
import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception';
interface PaginationResult<M extends Model> {
results: M[];
@@ -14,13 +15,13 @@ export type PaginationQueryBuilderType<M extends Model> = QueryBuilder<
PaginationResult<M>
>;
class PaginationQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<
M,
R
> {
export class PaginationQueryBuilder<
M extends Model,
R = M[],
> extends QueryBuilder<M, R> {
pagination(page: number, pageSize: number): PaginationQueryBuilderType<M> {
const query = super.page(page, pageSize);
return query.runAfter(({ results, total }) => {
return {
results,
@@ -32,12 +33,58 @@ class PaginationQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<
};
}) as unknown as PaginationQueryBuilderType<M>;
}
async deleteIfNoRelations({
type,
message,
}: {
type?: string;
message?: string;
} = {}) {
const relationMappings = this.modelClass().relationMappings;
const relationNames = Object.keys(relationMappings || {});
if (relationNames.length === 0) {
// No relations defined
return this.delete();
}
const recordQuery = this.clone();
relationNames.forEach((relationName: string) => {
recordQuery.withGraphFetched(relationName);
});
const record = await recordQuery;
const hasRelations = relationNames.some((name) => {
const val = record[name];
return Array.isArray(val) ? val.length > 0 : val != null;
});
if (!hasRelations) {
return this.clone().delete();
} else {
throw new ModelHasRelationsError(type, message);
}
}
}
export class BaseQueryBuilder<
M extends Model,
R = M[],
> extends PaginationQueryBuilder<M, R> {
changeAmount(whereAttributes, attribute, amount) {
const changeMethod = amount > 0 ? 'increment' : 'decrement';
return this.where(whereAttributes)[changeMethod](
attribute,
Math.abs(amount),
);
}
}
export class BaseModel extends Model {
public readonly id: number;
public readonly tableName: string;
QueryBuilderType!: PaginationQueryBuilder<this>;
static QueryBuilder = PaginationQueryBuilder;
QueryBuilderType!: BaseQueryBuilder<this>;
static QueryBuilder = BaseQueryBuilder;
}

View File

@@ -85,9 +85,7 @@ export class AccountTransformer extends Transformer {
* @returns {boolean}
*/
protected isFeedsPaused = (account: Account): boolean => {
// return account.plaidItem?.isPaused || false;
return false;
return account.plaidItem?.isPaused || false;
};
/**

View File

@@ -11,11 +11,25 @@ import {
import { AccountsApplication } from './AccountsApplication.service';
import { CreateAccountDTO } from './CreateAccount.dto';
import { EditAccountDTO } from './EditAccount.dto';
import { IAccountsFilter, IAccountsTransactionsFilter } from './Accounts.types';
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { IAccountsFilter } from './Accounts.types';
import {
ApiExtraModels,
ApiOperation,
ApiParam,
ApiResponse,
ApiTags,
getSchemaPath,
} from '@nestjs/swagger';
import { AccountResponseDto } from './dtos/AccountResponse.dto';
import { AccountTypeResponseDto } from './dtos/AccountTypeResponse.dto';
import { GetAccountTransactionResponseDto } from './dtos/GetAccountTransactionResponse.dto';
import { GetAccountTransactionsQueryDto } from './dtos/GetAccountTransactionsQuery.dto';
@Controller('accounts')
@ApiTags('accounts')
@ApiTags('Accounts')
@ApiExtraModels(AccountResponseDto)
@ApiExtraModels(AccountTypeResponseDto)
@ApiExtraModels(GetAccountTransactionResponseDto)
export class AccountsController {
constructor(private readonly accountsApplication: AccountsApplication) {}
@@ -105,6 +119,12 @@ export class AccountsController {
@ApiResponse({
status: 200,
description: 'The account types have been successfully retrieved.',
schema: {
type: 'array',
items: {
$ref: getSchemaPath(AccountTypeResponseDto),
},
},
})
async getAccountTypes() {
return this.accountsApplication.getAccountTypes();
@@ -115,8 +135,16 @@ export class AccountsController {
@ApiResponse({
status: 200,
description: 'The account transactions have been successfully retrieved.',
schema: {
type: 'array',
items: {
$ref: getSchemaPath(GetAccountTransactionResponseDto),
},
},
})
async getAccountTransactions(@Query() filter: IAccountsTransactionsFilter) {
async getAccountTransactions(
@Query() filter: GetAccountTransactionsQueryDto,
) {
return this.accountsApplication.getAccountsTransactions(filter);
}
@@ -125,6 +153,7 @@ export class AccountsController {
@ApiResponse({
status: 200,
description: 'The account details have been successfully retrieved.',
schema: { $ref: getSchemaPath(AccountResponseDto) },
})
@ApiResponse({ status: 404, description: 'The account not found.' })
@ApiParam({
@@ -142,6 +171,10 @@ export class AccountsController {
@ApiResponse({
status: 200,
description: 'The accounts have been successfully retrieved.',
schema: {
type: 'array',
items: { $ref: getSchemaPath(AccountResponseDto) },
},
})
async getAccounts(@Query() filter: Partial<IAccountsFilter>) {
return this.accountsApplication.getAccounts(filter);

View File

@@ -10,13 +10,10 @@ import { GetAccount } from './GetAccount.service';
import { ActivateAccount } from './ActivateAccount.service';
import { GetAccountTypesService } from './GetAccountTypes.service';
import { GetAccountTransactionsService } from './GetAccountTransactions.service';
import {
IAccountsFilter,
IAccountsTransactionsFilter,
IGetAccountTransactionPOJO,
} from './Accounts.types';
import { IAccountsFilter, IAccountsTransactionsFilter } from './Accounts.types';
import { GetAccountsService } from './GetAccounts.service';
import { IFilterMeta } from '@/interfaces/Model';
import { GetAccountTransactionResponseDto } from './dtos/GetAccountTransactionResponse.dto';
@Injectable()
export class AccountsApplication {
@@ -127,7 +124,7 @@ export class AccountsApplication {
*/
public getAccountsTransactions = (
filter: IAccountsTransactionsFilter,
): Promise<IGetAccountTransactionPOJO[]> => {
): Promise<Array<GetAccountTransactionResponseDto>> => {
return this.getAccountTransactionsService.getAccountsTransactions(filter);
};
}

View File

@@ -1,6 +1,5 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
// import { IAccountEventDeletedPayload } from '@/interfaces';
import { CommandAccountValidators } from './CommandAccountValidators.service';
import { Account } from './models/Account.model';
import { EventEmitter2 } from '@nestjs/event-emitter';
@@ -8,6 +7,7 @@ import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
import { IAccountEventDeletedPayload } from './Accounts.types';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { ERRORS } from './constants';
@Injectable()
export class DeleteAccount {
@@ -70,8 +70,12 @@ export class DeleteAccount {
await this.unassociateChildrenAccountsFromParent(accountId, trx);
// Deletes account by the given id.
await this.accountModel().query(trx).deleteById(accountId);
await this.accountModel()
.query(trx)
.findById(accountId)
.deleteIfNoRelations({
type: ERRORS.ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS,
});
// Triggers `onAccountDeleted` event.
await this.eventEmitter.emitAsync(events.accounts.onDeleted, {
accountId,

View File

@@ -6,6 +6,7 @@ import { TransformerInjectable } from '../Transformer/TransformerInjectable.serv
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { AccountResponseDto } from './dtos/AccountResponse.dto';
@Injectable()
export class GetAccount {
@@ -19,9 +20,10 @@ export class GetAccount {
/**
* Retrieve the given account details.
* @param {number} accountId
* @param {number} accountId - The account id.
* @returns {Promise<IAccount>} - The account details.
*/
public getAccount = async (accountId: number) => {
public async getAccount(accountId: number): Promise<AccountResponseDto> {
// Find the given account or throw not found error.
const account = await this.accountModel()
.query()
@@ -43,5 +45,5 @@ export class GetAccount {
await this.eventEmitter.emitAsync(events.accounts.onViewed, eventPayload);
return transformed;
};
}
}

View File

@@ -8,6 +8,7 @@ import { Account } from './models/Account.model';
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { GetAccountTransactionResponseDto } from './dtos/GetAccountTransactionResponse.dto';
@Injectable()
export class GetAccountTransactionsService {
@@ -29,7 +30,7 @@ export class GetAccountTransactionsService {
*/
public getAccountsTransactions = async (
filter: IAccountsTransactionsFilter,
): Promise<IGetAccountTransactionPOJO[]> => {
): Promise<Array<GetAccountTransactionResponseDto>> => {
// Retrieve the given account or throw not found error.
if (filter.accountId) {
await this.account().query().findById(filter.accountId).throwIfNotFound();

View File

@@ -34,7 +34,7 @@ export const DEFAULT_VIEW_COLUMNS = [
export const MAX_ACCOUNTS_CHART_DEPTH = 5;
// Accounts default views.
export const DEFAULT_VIEWS = [
export const AccountDefaultViews = [
{
name: 'Assets',
slug: 'assets',

View File

@@ -0,0 +1,171 @@
import { ApiProperty } from '@nestjs/swagger';
export class AccountResponseDto {
@ApiProperty({
description: 'The unique identifier of the account',
example: 1,
})
id: number;
@ApiProperty({
description: 'The name of the account',
example: 'Cash Account',
})
name: string;
@ApiProperty({
description: 'The slug of the account',
example: 'cash-account',
})
slug: string;
@ApiProperty({
description: 'The code of the account',
example: '1001',
})
code: string;
@ApiProperty({
description: 'The index of the account',
example: 1,
})
index: number;
@ApiProperty({
description: 'The type of the account',
example: 'bank',
})
accountType: string;
@ApiProperty({
description: 'The formatted account type label',
example: 'Bank Account',
})
accountTypeLabel: string;
@ApiProperty({
description: 'The parent account ID',
example: null,
})
parentAccountId: number | null;
@ApiProperty({
description: 'Whether the account is predefined',
example: false,
})
predefined: boolean;
@ApiProperty({
description: 'The currency code of the account',
example: 'USD',
})
currencyCode: string;
@ApiProperty({
description: 'Whether the account is active',
example: true,
})
active: boolean;
@ApiProperty({
description: 'The bank balance of the account',
example: 5000.0,
})
bankBalance: number;
@ApiProperty({
description: 'The formatted bank balance',
example: '$5,000.00',
})
bankBalanceFormatted: string;
@ApiProperty({
description: 'The last feeds update timestamp',
example: '2024-03-20T10:30:00Z',
})
lastFeedsUpdatedAt: string | Date | null;
@ApiProperty({
description: 'The formatted last feeds update timestamp',
example: 'Mar 20, 2024 10:30 AM',
})
lastFeedsUpdatedAtFormatted: string;
@ApiProperty({
description: 'The amount of the account',
example: 5000.0,
})
amount: number;
@ApiProperty({
description: 'The formatted amount',
example: '$5,000.00',
})
formattedAmount: string;
@ApiProperty({
description: 'The Plaid item ID',
example: 'plaid-item-123',
})
plaidItemId: string;
@ApiProperty({
description: 'The Plaid account ID',
example: 'plaid-account-456',
})
plaidAccountId: string | null;
@ApiProperty({
description: 'Whether the feeds are active',
example: true,
})
isFeedsActive: boolean;
@ApiProperty({
description: 'Whether the account is syncing owner',
example: true,
})
isSyncingOwner: boolean;
@ApiProperty({
description: 'Whether the feeds are paused',
example: false,
})
isFeedsPaused: boolean;
@ApiProperty({
description: 'The account normal',
example: 'debit',
})
accountNormal: string;
@ApiProperty({
description: 'The formatted account normal',
example: 'Debit',
})
accountNormalFormatted: string;
@ApiProperty({
description: 'The flatten name with all dependant accounts names',
example: 'Assets: Cash Account',
})
flattenName: string;
@ApiProperty({
description: 'The account level in the hierarchy',
example: 2,
})
accountLevel?: number;
@ApiProperty({
description: 'The creation timestamp',
example: '2024-03-20T10:00:00Z',
})
createdAt: Date;
@ApiProperty({
description: 'The update timestamp',
example: '2024-03-20T10:30:00Z',
})
updatedAt: Date;
}

View File

@@ -0,0 +1,51 @@
import { ApiProperty, ApiExtraModels } from '@nestjs/swagger';
export class AccountTypeResponseDto {
@ApiProperty({
description: 'The display label for the account type',
example: 'Cash',
})
label: string;
@ApiProperty({
description: 'The unique key for the account type',
example: 'cash',
})
key: string;
@ApiProperty({
description: 'The normal balance type for the account',
example: 'debit',
})
normal: string;
@ApiProperty({
description: 'The parent type of the account',
example: 'current-asset',
})
parentType: string;
@ApiProperty({
description: 'The root type of the account',
example: 'asset',
})
rootType: string;
@ApiProperty({
description: 'Whether the account type supports multiple currencies',
example: true,
})
multiCurrency: boolean;
@ApiProperty({
description: 'Whether the account type appears on the balance sheet',
example: true,
})
balanceSheet: boolean;
@ApiProperty({
description: 'Whether the account type appears on the income sheet',
example: false,
})
incomeSheet: boolean;
}

View File

@@ -0,0 +1,114 @@
import { ApiProperty } from '@nestjs/swagger';
export class GetAccountTransactionResponseDto {
/**
* The transaction date (ISO string or Date).
*/
@ApiProperty({
description: 'The transaction date (ISO string or Date)',
example: '2024-01-01',
})
date: string | Date;
/**
* The formatted transaction date (string).
*/
@ApiProperty({
description: 'The formatted transaction date',
example: '01 Jan 2024',
})
formattedDate: string;
/**
* The transaction type (referenceType from model).
*/
@ApiProperty({
description: 'The transaction type (referenceType from model)',
example: 'INVOICE',
})
transactionType: string;
/**
* The transaction id (referenceId from model).
*/
@ApiProperty({
description: 'The transaction id (referenceId from model)',
example: 123,
})
transactionId: number;
/**
* The formatted transaction type (translated string).
*/
@ApiProperty({
description: 'The formatted transaction type (translated string)',
example: 'Invoice',
})
transactionTypeFormatted: string;
/**
* The credit amount (number).
*/
@ApiProperty({ description: 'The credit amount', example: 100 })
credit: number;
/**
* The debit amount (number).
*/
@ApiProperty({ description: 'The debit amount', example: 50 })
debit: number;
/**
* The formatted credit amount (string, e.g. currency formatted).
*/
@ApiProperty({
description: 'The formatted credit amount (e.g. currency formatted)',
example: '100.00 USD',
})
formattedCredit: string;
/**
* The formatted debit amount (string, e.g. currency formatted).
*/
@ApiProperty({
description: 'The formatted debit amount (e.g. currency formatted)',
example: '50.00 USD',
})
formattedDebit: string;
/**
* The foreign currency credit (number, credit * exchangeRate).
*/
@ApiProperty({
description: 'The foreign currency credit (credit * exchangeRate)',
example: 120,
})
fcCredit: number;
/**
* The foreign currency debit (number, debit * exchangeRate).
*/
@ApiProperty({
description: 'The foreign currency debit (debit * exchangeRate)',
example: 60,
})
fcDebit: number;
/**
* The formatted foreign currency credit (string).
*/
@ApiProperty({
description: 'The formatted foreign currency credit',
example: '120.00 EUR',
})
formattedFcCredit: string;
/**
* The formatted foreign currency debit (string).
*/
@ApiProperty({
description: 'The formatted foreign currency debit',
example: '60.00 EUR',
})
formattedFcDebit: string;
}

View File

@@ -0,0 +1,23 @@
import { IsInt, IsOptional } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { ToNumber } from '@/common/decorators/Validators';
export class GetAccountTransactionsQueryDto {
@ApiPropertyOptional({
type: Number,
description: 'ID of the account to fetch transactions for',
})
@IsOptional()
@IsInt()
@ToNumber()
accountId?: number;
@ApiPropertyOptional({
type: Number,
description: 'Maximum number of transactions to return',
})
@IsOptional()
@IsInt()
@ToNumber()
limit?: number;
}

View File

@@ -14,10 +14,13 @@ import { ExportableModel } from '../../Export/decorators/ExportableModel.decorat
import { AccountMeta } from './Account.meta';
import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator';
import { ImportableModel } from '@/modules/Import/decorators/Import.decorator';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { AccountDefaultViews } from '../constants';
@ExportableModel()
@ImportableModel()
@InjectModelMeta(AccountMeta)
@InjectModelDefaultViews(AccountDefaultViews)
export class Account extends TenantBaseModel {
public name!: string;
public slug!: string;
@@ -227,7 +230,7 @@ export class Account extends TenantBaseModel {
to: 'accounts_transactions.accountId',
},
},
/**
* Account may has many items as cost account.
*/
@@ -422,20 +425,6 @@ export class Account extends TenantBaseModel {
});
}
/**
* Model settings.
*/
// static get meta() {
// return AccountSettings;
// }
/**
* Retrieve the default custom views, roles and columns.
*/
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search roles.
*/

View File

@@ -4,6 +4,7 @@ import { unitOfTime } from 'moment';
import { isEmpty, castArray } from 'lodash';
import { BaseModel } from '@/models/Model';
import { Account } from './Account.model';
import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils';
// import { getTransactionTypeLabel } from '@/utils/transactions-types';
export class AccountTransaction extends BaseModel {
@@ -19,7 +20,6 @@ export class AccountTransaction extends BaseModel {
public readonly date: Date | string;
public readonly transactionType: string;
public readonly currencyCode: string;
public readonly referenceTypeFormatted: string;
public readonly transactionNumber!: string;
public readonly referenceNumber!: string;
public readonly note!: string;
@@ -72,13 +72,13 @@ export class AccountTransaction extends BaseModel {
return this.debit * this.exchangeRate;
}
// /**
// * Retrieve formatted reference type.
// * @return {string}
// */
// get referenceTypeFormatted() {
// return getTransactionTypeLabel(this.referenceType, this.transactionType);
// }
/**
* Retrieve formatted reference type.
* @return {string}
*/
get referenceTypeFormatted() {
return getTransactionTypeLabel(this.referenceType, this.transactionType);
}
/**
* Model modifiers.

View File

@@ -3,6 +3,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { join } from 'path';
import { ServeStaticModule } from '@nestjs/serve-static';
import { RedisModule } from '@liaoliaots/nestjs-redis';
import {
AcceptLanguageResolver,
@@ -92,9 +93,14 @@ import { ContactsModule } from '../Contacts/Contacts.module';
import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module';
import { BankingCategorizeModule } from '../BankingCategorize/BankingCategorize.module';
import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.module';
import { BillLandedCostsModule } from '../BillLandedCosts/BillLandedCosts.module';
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: join(__dirname, '../../..', 'public'),
serveRoot: '/public',
}),
ConfigModule.forRoot({
envFilePath: '.env',
load: config,
@@ -174,6 +180,7 @@ import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.
SaleEstimatesModule,
SaleReceiptsModule,
BillsModule,
BillLandedCostsModule,
ManualJournalsModule,
CreditNotesModule,
VendorCreditsModule,
@@ -218,7 +225,7 @@ import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.
CurrenciesModule,
MiscellaneousModule,
UsersModule,
ContactsModule
ContactsModule,
],
controllers: [AppController],
providers: [

View File

@@ -26,3 +26,5 @@ export const SendResetPasswordMailJob = 'SendResetPasswordMailJob';
export const SendSignupVerificationMailQueue =
'SendSignupVerificationMailQueue';
export const SendSignupVerificationMailJob = 'SendSignupVerificationMailJob';
export const AuthApiKeyPrefix = 'bc_';

View File

@@ -34,7 +34,10 @@ export class AuthController {
@UseGuards(LocalAuthGuard)
@ApiOperation({ summary: 'Sign in a user' })
@ApiBody({ type: AuthSigninDto })
async signin(@Request() req: Request & { user: SystemUser }, @Body() signinDto: AuthSigninDto) {
async signin(
@Request() req: Request & { user: SystemUser },
@Body() signinDto: AuthSigninDto,
) {
const { user } = req;
const tenant = await this.tenantModel.query().findById(user.tenantId);
@@ -68,7 +71,6 @@ export class AuthController {
return this.authApp.signUpConfirm(email, token);
}
@Post('/send_reset_password')
@ApiOperation({ summary: 'Send reset password email' })
@ApiBody({

View File

@@ -32,11 +32,22 @@ import { AuthedController } from './Authed.controller';
import { GetAuthenticatedAccount } from './queries/GetAuthedAccount.service';
import { TenancyModule } from '../Tenancy/Tenancy.module';
import { EnsureUserVerifiedGuard } from './guards/EnsureUserVerified.guard';
import { ApiKeyAuthGuard } from './api-key/AuthApiKey.guard';
import { MixedAuthGuard } from './api-key/MixedAuth.guard';
import { ApiKeyStrategy } from './api-key/AuthApiKey.strategy';
import { ApiKeyModel } from './models/ApiKey.model';
import { AuthApiKeysController } from './AuthApiKeys.controllers';
import { AuthApiKeyAuthorizeService } from './commands/AuthApiKeyAuthorization.service';
import { GenerateApiKey } from './commands/GenerateApiKey.service';
import { GetApiKeysService } from './queries/GetApiKeys.service';
const models = [InjectSystemModel(PasswordReset)];
const models = [
InjectSystemModel(PasswordReset),
InjectSystemModel(ApiKeyModel),
];
@Module({
controllers: [AuthController, AuthedController],
controllers: [AuthController, AuthedController, AuthApiKeysController],
imports: [
MailModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
@@ -70,9 +81,15 @@ const models = [InjectSystemModel(PasswordReset)];
SendSignupVerificationMailProcessor,
GetAuthMetaService,
GetAuthenticatedAccount,
ApiKeyAuthGuard,
ApiKeyStrategy,
AuthApiKeyAuthorizeService,
GenerateApiKey,
GetApiKeysService,
JwtAuthGuard,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
useClass: MixedAuthGuard,
},
{
provide: APP_GUARD,

View File

@@ -1,4 +1,5 @@
import * as bcrypt from 'bcrypt';
import { AuthApiKeyPrefix } from './Auth.constants';
export const hashPassword = (password: string): Promise<string> =>
new Promise((resolve) => {
@@ -8,3 +9,12 @@ export const hashPassword = (password: string): Promise<string> =>
});
});
});
/**
* Extracts and validates an API key from the Authorization header
* @param {string} authorization - Full authorization header content.
*/
export const getAuthApiKey = (authorization: string) => {
const apiKey = authorization.toLowerCase().replace('bearer ', '').trim();
return apiKey.startsWith(AuthApiKeyPrefix) ? apiKey : '';
};

View File

@@ -0,0 +1,26 @@
import { Controller, Post, Param, Get, Put } from '@nestjs/common';
import { GenerateApiKey } from './commands/GenerateApiKey.service';
import { GetApiKeysService } from './queries/GetApiKeys.service';
@Controller('api-keys')
export class AuthApiKeysController {
constructor(
private readonly getApiKeysService: GetApiKeysService,
private readonly generateApiKeyService: GenerateApiKey,
) {}
@Post('generate')
async generate() {
return this.generateApiKeyService.generate();
}
@Put(':id/revoke')
async revoke(@Param('id') id: number) {
return this.generateApiKeyService.revoke(id);
}
@Get()
async getApiKeys() {
return this.getApiKeysService.getApiKeys();
}
}

View File

@@ -0,0 +1,13 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class ApiKeyAuthGuard extends AuthGuard('apiKey') {
constructor() {
super();
}
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
}

View File

@@ -0,0 +1,26 @@
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { AuthApiKeyAuthorizeService } from '../commands/AuthApiKeyAuthorization.service';
@Injectable()
export class ApiKeyStrategy extends PassportStrategy(
HeaderAPIKeyStrategy,
'apiKey',
) {
constructor(
private readonly authApiKeyAuthorizeService: AuthApiKeyAuthorizeService,
) {
super(
{
header: 'Authorization',
prefix: 'Bearer ',
},
false,
);
}
validate(apiKey: string): unknown {
return this.authApiKeyAuthorizeService.authorize(apiKey);
}
}

View File

@@ -0,0 +1,24 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { JwtAuthGuard } from '../guards/jwt.guard';
import { ApiKeyAuthGuard } from './AuthApiKey.guard';
import { getAuthApiKey } from '../Auth.utils';
// mixed-auth.guard.ts
@Injectable()
export class MixedAuthGuard implements CanActivate {
constructor(
private jwtGuard: JwtAuthGuard,
private apiKeyGuard: ApiKeyAuthGuard,
) {}
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const apiKey = getAuthApiKey(request.headers['authorization'] || '');
if (apiKey) {
return this.apiKeyGuard.canActivate(context);
} else {
return this.jwtGuard.canActivate(context);
}
}
}

View File

@@ -0,0 +1,50 @@
import { Inject, Injectable } from '@nestjs/common';
import { ApiKeyModel } from '../models/ApiKey.model';
import { ClsService } from 'nestjs-cls';
import { TenantModel } from '@/modules/System/models/TenantModel';
@Injectable()
export class AuthApiKeyAuthorizeService {
constructor(
private readonly clsService: ClsService,
@Inject(ApiKeyModel.name)
private readonly apikeyModel: typeof ApiKeyModel,
@Inject(TenantModel.name)
private readonly tenantModel: typeof TenantModel,
) {}
/**
* Authenticate using the given api key.
*/
async authorize(apiKey: string): Promise<boolean> {
const apiKeyRecord = await this.apikeyModel
.query()
.findOne({ key: apiKey });
if (!apiKeyRecord) {
return false;
}
if (apiKeyRecord.revoked) {
return false;
}
if (
apiKeyRecord.expiresAt &&
new Date(apiKeyRecord.expiresAt) < new Date()
) {
return false;
}
const tenant = await this.tenantModel
.query()
.findById(apiKeyRecord.tenantId);
if (!tenant) return false;
this.clsService.set('tenantId', tenant.id);
this.clsService.set('organizationId', tenant.organizationId);
this.clsService.set('userId', apiKeyRecord.userId);
return true;
}
}

View File

@@ -0,0 +1,51 @@
import { Inject, Injectable } from '@nestjs/common';
import * as crypto from 'crypto';
import { ApiKeyModel } from '../models/ApiKey.model';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { AuthApiKeyPrefix } from '../Auth.constants';
@Injectable()
export class GenerateApiKey {
constructor(
private readonly tenancyContext: TenancyContext,
@Inject(ApiKeyModel.name)
private readonly apiKeyModel: typeof ApiKeyModel,
) {}
/**
* Generates a new secure API key for the current tenant and system user.
* The key is saved in the database and returned (only the key and id for security).
* @returns {Promise<{ key: string; id: number }>} The generated API key and its database id.
*/
async generate() {
const tenant = await this.tenancyContext.getTenant();
const user = await this.tenancyContext.getSystemUser();
// Generate a secure random API key
const key = `${AuthApiKeyPrefix}${crypto.randomBytes(48).toString('hex')}`;
// Save the API key to the database
const apiKeyRecord = await this.apiKeyModel.query().insert({
key,
tenantId: tenant.id,
userId: user.id,
createdAt: new Date(),
revokedAt: null,
});
// Return the created API key (not the full record for security)
return { key: apiKeyRecord.key, id: apiKeyRecord.id };
}
/**
* Revokes an API key by setting its revokedAt timestamp.
* @param {number} apiKeyId - The id of the API key to revoke.
* @returns {Promise<{ id: number; revoked: boolean }>} The id of the revoked API key and a revoked flag.
*/
async revoke(apiKeyId: number) {
// Set the revoked flag to true for the given API key
await ApiKeyModel.query()
.findById(apiKeyId)
.patch({ revokedAt: new Date() });
return { id: apiKeyId, revoked: true };
}
}

View File

@@ -1,10 +1,16 @@
import { IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class AuthSigninDto {
@ApiProperty({ example: 'password123', description: 'User password' })
@IsNotEmpty()
@IsString()
password: string;
@ApiProperty({
example: 'user@example.com',
description: 'User email address',
})
@IsNotEmpty()
@IsString()
email: string;

View File

@@ -1,19 +1,27 @@
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class AuthSignupDto {
@ApiProperty({ example: 'John', description: 'User first name' })
@IsNotEmpty()
@IsString()
firstName: string;
@ApiProperty({ example: 'Doe', description: 'User last name' })
@IsNotEmpty()
@IsString()
lastName: string;
@ApiProperty({
example: 'john.doe@example.com',
description: 'User email address',
})
@IsNotEmpty()
@IsString()
@IsEmail()
email: string;
@ApiProperty({ example: 'password123', description: 'User password' })
@IsNotEmpty()
@IsString()
password: string;

View File

@@ -16,6 +16,7 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
}
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_ROUTE,
[context.getHandler(), context.getClass()],

View File

@@ -0,0 +1,72 @@
import { SystemModel } from '@/modules/System/models/SystemModel';
import { Model } from 'objection';
export class ApiKeyModel extends SystemModel {
readonly key: string;
readonly name?: string;
readonly createdAt: Date;
readonly expiresAt?: Date;
readonly revokedAt?: Date;
readonly userId: number;
readonly tenantId: number;
get revoked() {
return !!this.revokedAt;
}
static get virtualAttributes() {
return ['revoked'];
}
/**
* Table name
*/
static get tableName() {
return 'api_keys';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt'];
}
/**
* Relation mappings for Objection.js
*/
static get relationMappings() {
const { SystemUser } = require('../../System/models/SystemUser');
const { TenantModel } = require('../../System/models/TenantModel');
return {
user: {
relation: Model.BelongsToOneRelation,
modelClass: SystemUser,
join: {
from: 'api_keys.userId',
to: 'users.id',
},
},
tenant: {
relation: Model.BelongsToOneRelation,
modelClass: TenantModel,
join: {
from: 'api_keys.tenantId',
to: 'tenants.id',
},
},
};
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
notRevoked(query) {
query.whereNull('revokedAt');
},
};
}
}

View File

@@ -0,0 +1,29 @@
import { Inject, Injectable } from '@nestjs/common';
import { ApiKeyModel } from '../models/ApiKey.model';
import { GetApiKeysTransformer } from './GetApiKeys.transformer';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class GetApiKeysService {
constructor(
private readonly injectableTransformer: TransformerInjectable,
private readonly tenancyContext: TenancyContext,
@Inject(ApiKeyModel.name)
private readonly apiKeyModel: typeof ApiKeyModel,
) {}
async getApiKeys() {
const tenant = await this.tenancyContext.getTenant();
const apiKeys = await this.apiKeyModel
.query()
.modify('notRevoked')
.where({ tenantId: tenant.id });
return this.injectableTransformer.transform(
apiKeys,
new GetApiKeysTransformer(),
);
}
}

View File

@@ -0,0 +1,7 @@
import { Transformer } from '@/modules/Transformer/Transformer';
export class GetApiKeysTransformer extends Transformer {
public excludeAttributes = (): string[] => {
return ['tenantId'];
};
}

View File

@@ -1,4 +1,10 @@
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import {
ApiOperation,
ApiTags,
ApiResponse,
getSchemaPath,
ApiExtraModels,
} from '@nestjs/swagger';
import {
Body,
Controller,
@@ -9,20 +15,28 @@ import {
Put,
} from '@nestjs/common';
import { BankRulesApplication } from './BankRulesApplication';
import { BankRule } from './models/BankRule';
import { CreateBankRuleDto } from './dtos/BankRule.dto';
import { EditBankRuleDto } from './dtos/BankRule.dto';
import { BankRuleResponseDto } from './dtos/BankRuleResponse.dto';
@Controller('banking/rules')
@ApiTags('bank-rules')
@ApiTags('Bank Rules')
@ApiExtraModels(BankRuleResponseDto)
export class BankRulesController {
constructor(private readonly bankRulesApplication: BankRulesApplication) {}
@Post()
@ApiOperation({ summary: 'Create a new bank rule.' })
@ApiResponse({
status: 201,
description: 'The bank rule has been successfully created.',
schema: {
$ref: getSchemaPath(BankRuleResponseDto),
},
})
async createBankRule(
@Body() createRuleDTO: CreateBankRuleDto,
): Promise<BankRule> {
): Promise<BankRuleResponseDto> {
return this.bankRulesApplication.createBankRule(createRuleDTO);
}
@@ -37,19 +51,36 @@ export class BankRulesController {
@Delete(':id')
@ApiOperation({ summary: 'Delete the given bank rule.' })
@ApiResponse({
status: 200,
description: 'The bank rule has been successfully deleted.',
})
async deleteBankRule(@Param('id') ruleId: number): Promise<void> {
return this.bankRulesApplication.deleteBankRule(ruleId);
}
@Get(':id')
@ApiOperation({ summary: 'Retrieves the bank rule details.' })
async getBankRule(@Param('id') ruleId: number): Promise<any> {
@ApiResponse({
status: 200,
description: 'The bank rule details have been successfully retrieved.',
schema: { $ref: getSchemaPath(BankRuleResponseDto) },
})
async getBankRule(@Param('id') ruleId: number): Promise<BankRuleResponseDto> {
return this.bankRulesApplication.getBankRule(ruleId);
}
@Get()
@ApiOperation({ summary: 'Retrieves the bank rules.' })
async getBankRules(): Promise<any> {
@ApiResponse({
status: 200,
description: 'The bank rules have been successfully retrieved.',
schema: {
type: 'array',
items: { $ref: getSchemaPath(BankRuleResponseDto) },
},
})
async getBankRules(): Promise<BankRuleResponseDto[]> {
return this.bankRulesApplication.getBankRules();
}
}

View File

@@ -0,0 +1,140 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BankRuleAssignCategory, BankRuleConditionType } from '../types';
class BankRuleConditionResponseDto {
@ApiProperty({
description: 'The unique identifier of the bank rule condition',
example: 1,
})
id: number;
@ApiProperty({
description: 'The field to check in the condition',
example: 'description',
enum: ['description', 'amount'],
})
field: string;
@ApiProperty({
description: 'The comparison operator to use',
example: 'contains',
enum: [
'equals',
'equal',
'contains',
'not_contain',
'bigger',
'bigger_or_equal',
'smaller',
'smaller_or_equal',
],
})
comparator: string;
@ApiProperty({
description: 'The value to compare against',
example: 'Salary',
})
value: string;
}
export class BankRuleResponseDto {
@ApiProperty({
description: 'The unique identifier of the bank rule',
example: 1,
})
id: number;
@ApiProperty({
description: 'The name of the bank rule',
example: 'Monthly Salary',
})
name: string;
@ApiProperty({
description: 'The order in which the rule should be applied',
example: 1,
})
order: number;
@ApiProperty({
description: 'The account ID to apply the rule if',
example: 1,
})
applyIfAccountId: number;
@ApiProperty({
description: 'The transaction type to apply the rule if',
example: 'deposit',
enum: ['deposit', 'withdrawal'],
})
applyIfTransactionType: string;
@ApiProperty({
description: 'The conditions type to apply the rule if',
example: 'and',
enum: ['and', 'or'],
})
conditionsType: BankRuleConditionType;
@ApiProperty({
description: 'The conditions to apply the rule if',
type: [BankRuleConditionResponseDto],
example: [
{
id: 1,
field: 'description',
comparator: 'contains',
value: 'Salary',
},
],
})
@Type(() => BankRuleConditionResponseDto)
conditions: BankRuleConditionResponseDto[];
@ApiProperty({
description: 'The category to assign the rule if',
example: 'InterestIncome',
enum: [
'InterestIncome',
'OtherIncome',
'Deposit',
'Expense',
'OwnerDrawings',
],
})
assignCategory: BankRuleAssignCategory;
@ApiProperty({
description: 'The account ID to assign the rule if',
example: 1,
})
assignAccountId: number;
@ApiProperty({
description: 'The payee to assign the rule if',
example: 'Employer Inc.',
required: false,
})
assignPayee?: string;
@ApiProperty({
description: 'The memo to assign the rule if',
example: 'Monthly Salary',
required: false,
})
assignMemo?: string;
@ApiProperty({
description: 'The creation timestamp of the bank rule',
example: '2024-03-20T10:00:00Z',
})
createdAt: string;
@ApiProperty({
description: 'The last update timestamp of the bank rule',
example: '2024-03-20T10:00:00Z',
})
updatedAt: string;
}

View File

@@ -2,8 +2,9 @@ import { BaseModel } from '@/models/Model';
import { Model } from 'objection';
import { BankRuleCondition } from './BankRuleCondition';
import { BankRuleAssignCategory, BankRuleConditionType } from '../types';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
export class BankRule extends BaseModel {
export class BankRule extends TenantBaseModel {
public readonly id!: number;
public readonly name!: string;
public readonly order!: number;
@@ -17,6 +18,9 @@ export class BankRule extends BaseModel {
public readonly conditions!: BankRuleCondition[];
public readonly createdAt: string;
public readonly updatedAt: string;
/**
* Table name
*/
@@ -28,7 +32,7 @@ export class BankRule extends BaseModel {
* Timestamps columns.
*/
static get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -4,7 +4,7 @@ import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ICashflowAccountsFilter } from './types/BankAccounts.types';
@Controller('banking/accounts')
@ApiTags('banking-accounts')
@ApiTags('Bank Accounts')
export class BankAccountsController {
constructor(private bankAccountsApplication: BankAccountsApplication) {}

View File

@@ -5,7 +5,7 @@ import { CategorizeBankTransactionRouteDto } from './dtos/CategorizeBankTransact
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('banking/categorize')
@ApiTags('banking-categorization')
@ApiTags('Banking Categorization')
export class BankingCategorizeController {
constructor(
private readonly bankingCategorizeApplication: BankingCategorizeApplication,

View File

@@ -5,7 +5,7 @@ import { GetMatchedTransactionsFilter } from './types';
import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
@Controller('banking/matching')
@ApiTags('banking-transactions-matching')
@ApiTags('Banking Transactions Matching')
export class BankingMatchingController {
constructor(
private readonly bankingMatchingApplication: BankingMatchingApplication,

View File

@@ -4,7 +4,7 @@ import { PlaidItemDto } from './dtos/PlaidItem.dto';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
@Controller('banking/plaid')
@ApiTags('banking-plaid')
@ApiTags('Banking Plaid')
export class BankingPlaidController {
constructor(private readonly plaidApplication: PlaidApplication) {}

View File

@@ -1,26 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class PlaidItemDto {
@IsString()
@IsNotEmpty()
@ApiProperty({ example: '123', description: 'The public token' })
publicToken: string;
@IsString()
@IsNotEmpty()
@ApiProperty({ example: '123', description: 'The institution ID' })
institutionId: string;
}
export class PlaidWebhookDto {
@IsString()
@IsNotEmpty()
@ApiProperty({ example: '123', description: 'The Plaid item ID' })
itemId: string;
@IsString()
@IsNotEmpty()
@ApiProperty({ example: '123', description: 'The Plaid webhook type' })
webhookType: string;
@IsString()
@IsNotEmpty()
@ApiProperty({ example: '123', description: 'The Plaid webhook code' })
webhookCode: string;
}

View File

@@ -1,15 +1,42 @@
import { Controller, Get, Param, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import {
ApiTags,
ApiOperation,
ApiParam,
ApiQuery,
ApiResponse,
ApiExtraModels,
getSchemaPath,
} from '@nestjs/swagger';
import { RecognizedTransactionsApplication } from './RecognizedTransactions.application';
import { GetRecognizedTransactionResponseDto } from './dtos/GetRecognizedTransactionResponse.dto';
@Controller('banking/recognized')
@ApiTags('banking-recognized')
@ApiTags('Banking Recognized Transactions')
@ApiExtraModels(GetRecognizedTransactionResponseDto)
export class BankingRecognizedTransactionsController {
constructor(
private readonly recognizedTransactionsApplication: RecognizedTransactionsApplication,
) {}
@Get(':recognizedTransactionId')
@ApiOperation({ summary: 'Get recognized transaction' })
@ApiParam({
name: 'recognizedTransactionId',
description: 'The ID of the recognized transaction',
type: 'number',
})
@ApiResponse({
status: 200,
description: 'Returns the recognized transaction details',
schema: {
$ref: getSchemaPath(GetRecognizedTransactionResponseDto),
},
})
@ApiResponse({
status: 404,
description: 'Recognized transaction not found',
})
async getRecognizedTransaction(
@Param('recognizedTransactionId') recognizedTransactionId: number,
) {
@@ -19,7 +46,25 @@ export class BankingRecognizedTransactionsController {
}
@Get()
@ApiOperation({ summary: 'Get a list of recognized transactions' })
@ApiQuery({
name: 'query',
required: false,
description: 'Query parameters for filtering recognized transactions',
})
@ApiResponse({
status: 200,
description: 'Returns a list of recognized transactions',
schema: {
type: 'array',
items: {
$ref: getSchemaPath(GetRecognizedTransactionResponseDto),
},
},
})
async getRecognizedTransactions(@Query() query: any) {
return this.recognizedTransactionsApplication.getRecognizedTransactions(query);
return this.recognizedTransactionsApplication.getRecognizedTransactions(
query,
);
}
}

View File

@@ -0,0 +1,117 @@
import { ApiProperty } from '@nestjs/swagger';
export class GetRecognizedTransactionResponseDto {
@ApiProperty({
description: 'The unique identifier of the uncategorized transaction',
example: 123,
})
uncategorizedTransactionId: number;
@ApiProperty({
description: 'The reference number of the transaction',
example: 'TRX-2024-001',
})
referenceNo: string;
@ApiProperty({
description: 'The description of the transaction',
example: 'Payment for invoice #123',
})
description: string;
@ApiProperty({
description: 'The payee of the transaction',
example: 'John Doe',
})
payee: string;
@ApiProperty({
description: 'The amount of the transaction',
example: 1500.75,
})
amount: number;
@ApiProperty({
description: 'The formatted amount of the transaction',
example: '$1,500.75',
})
formattedAmount: string;
@ApiProperty({
description: 'The date of the transaction',
example: '2024-04-01',
})
date: string;
@ApiProperty({
description: 'The formatted date of the transaction',
example: 'Apr 1, 2024',
})
formattedDate: string;
@ApiProperty({ description: 'The assigned account ID', example: 10 })
assignedAccountId: number;
@ApiProperty({
description: 'The assigned account name',
example: 'Bank Account',
})
assignedAccountName: string;
@ApiProperty({ description: 'The assigned account code', example: '1001' })
assignedAccountCode: string;
@ApiProperty({ description: 'The assigned payee', example: 'Jane Smith' })
assignedPayee: string;
@ApiProperty({ description: 'The assigned memo', example: 'Office supplies' })
assignedMemo: string;
@ApiProperty({
description: 'The assigned category',
example: 'Office Expenses',
})
assignedCategory: string;
@ApiProperty({
description: 'The formatted assigned category',
example: 'Other Income',
})
assignedCategoryFormatted: string;
@ApiProperty({ description: 'The withdrawal amount', example: 500 })
withdrawal: number;
@ApiProperty({ description: 'The deposit amount', example: 1000 })
deposit: number;
@ApiProperty({
description: 'Whether this is a deposit transaction',
example: true,
})
isDepositTransaction: boolean;
@ApiProperty({
description: 'Whether this is a withdrawal transaction',
example: false,
})
isWithdrawalTransaction: boolean;
@ApiProperty({
description: 'The formatted deposit amount',
example: '$1,000.00',
})
formattedDepositAmount: string;
@ApiProperty({
description: 'The formatted withdrawal amount',
example: '$500.00',
})
formattedWithdrawalAmount: string;
@ApiProperty({ description: 'The bank rule ID', example: 'BR-001' })
bankRuleId: string;
@ApiProperty({ description: 'The bank rule name', example: 'Salary Rule' })
bankRuleName: string;
}

View File

@@ -1,5 +1,4 @@
import { ACCOUNT_TYPE } from "@/constants/accounts";
import { ACCOUNT_TYPE } from '@/constants/accounts';
export const ERRORS = {
CASHFLOW_TRANSACTION_TYPE_INVALID: 'CASHFLOW_TRANSACTION_TYPE_INVALID',
@@ -111,35 +110,34 @@ export const BankTransactionsSampleData = [
},
];
export const CashflowTransactionTypes = {
OtherIncome: 'Other income',
OtherExpense: 'Other expense',
OwnerDrawing: 'Owner drawing',
OwnerContribution: 'Owner contribution',
TransferToAccount: 'Transfer to account',
TransferFromAccount: 'Transfer from account',
OtherIncome: 'transaction_type.other_income',
OtherExpense: 'transaction_type.other_expense',
OwnerDrawing: 'transaction_type.owner_drawing',
OwnerContribution: 'transaction_type.owner_contribution',
TransferToAccount: 'transaction_type.transfer_to_account',
TransferFromAccount: 'transaction_type.transfer_from_account',
};
export const TransactionTypes = {
SaleInvoice: 'Sale invoice',
SaleReceipt: 'Sale receipt',
PaymentReceive: 'Payment received',
Bill: 'Bill',
BillPayment: 'Payment made',
VendorOpeningBalance: 'Vendor opening balance',
CustomerOpeningBalance: 'Customer opening balance',
InventoryAdjustment: 'Inventory adjustment',
ManualJournal: 'Manual journal',
Journal: 'Manual journal',
Expense: 'Expense',
OwnerContribution: 'Owner contribution',
TransferToAccount: 'Transfer to account',
TransferFromAccount: 'Transfer from account',
OtherIncome: 'Other income',
OtherExpense: 'Other expense',
OwnerDrawing: 'Owner drawing',
InvoiceWriteOff: 'Invoice write-off',
SaleInvoice: 'transaction_type.sale_invoice',
SaleReceipt: 'transaction_type.sale_receipt',
PaymentReceive: 'transaction_type.payment_received',
Bill: 'transaction_type.bill',
BillPayment: 'transaction_type.payment_made',
VendorOpeningBalance: 'transaction_type.vendor_opening_balance',
CustomerOpeningBalance: 'transaction_type.customer_opening_balance',
InventoryAdjustment: 'transaction_type.inventory_adjustment',
ManualJournal: 'transaction_type.manual_journal',
Journal: 'transaction_type.manual_journal',
Expense: 'transaction_type.expense',
OwnerContribution: 'transaction_type.owner_contribution',
TransferToAccount: 'transaction_type.transfer_to_account',
TransferFromAccount: 'transaction_type.transfer_from_account',
OtherIncome: 'transaction_type.other_income',
OtherExpense: 'transaction_type.other_expense',
OwnerDrawing: 'transaction_type.owner_drawing',
InvoiceWriteOff: 'transaction_type.invoice_write_off',
CreditNote: 'transaction_type.credit_note',
VendorCredit: 'transaction_type.vendor_credit',
RefundCreditNote: 'transaction_type.refund_credit_note',

View File

@@ -1,10 +1,19 @@
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiQuery,
getSchemaPath,
ApiExtraModels,
} from '@nestjs/swagger';
import { BankingTransactionsApplication } from '../BankingTransactionsApplication.service';
import { GetPendingTransactionsQueryDto } from '../dtos/GetPendingTransactionsQuery.dto';
import { GetPendingTransactionResponseDto } from '../dtos/GetPendingTransactionResponse.dto';
@Controller('banking/pending')
@ApiTags('banking-pending')
@ApiTags('Banking Pending Transactions')
@ApiExtraModels(GetPendingTransactionResponseDto)
export class BankingPendingTransactionsController {
constructor(
private readonly bankingTransactionsApplication: BankingTransactionsApplication,
@@ -15,6 +24,9 @@ export class BankingPendingTransactionsController {
@ApiResponse({
status: 200,
description: 'Returns a list of pending bank account transactions',
schema: {
$ref: getSchemaPath(GetPendingTransactionResponseDto),
},
})
@ApiQuery({
name: 'page',

View File

@@ -7,13 +7,25 @@ import {
Post,
Query,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery, ApiBody } from '@nestjs/swagger';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiParam,
ApiQuery,
ApiBody,
getSchemaPath,
ApiExtraModels,
} from '@nestjs/swagger';
import { BankingTransactionsApplication } from '../BankingTransactionsApplication.service';
import { CreateBankTransactionDto } from '../dtos/CreateBankTransaction.dto';
import { GetBankTransactionsQueryDto } from '../dtos/GetBankTranasctionsQuery.dto';
import { BankTransactionResponseDto } from '../dtos/BankTransactionResponse.dto';
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
@Controller('banking/transactions')
@ApiTags('banking-transactions')
@ApiTags('Banking Transactions')
@ApiExtraModels(BankTransactionResponseDto, PaginatedResponseDto)
export class BankingTransactionsController {
constructor(
private readonly bankingTransactionsApplication: BankingTransactionsApplication,
@@ -24,6 +36,21 @@ export class BankingTransactionsController {
@ApiResponse({
status: 200,
description: 'Returns a list of bank account transactions',
schema: {
allOf: [
{
$ref: getSchemaPath(PaginatedResponseDto),
},
{
properties: {
data: {
type: 'array',
items: { $ref: getSchemaPath(BankTransactionResponseDto) },
},
},
},
],
},
})
@ApiQuery({
name: 'page',
@@ -89,6 +116,9 @@ export class BankingTransactionsController {
@ApiResponse({
status: 200,
description: 'Returns the bank transaction details',
schema: {
$ref: getSchemaPath(BankTransactionResponseDto),
},
})
@ApiResponse({
status: 404,

View File

@@ -10,7 +10,7 @@ import { GetUncategorizedTransactionsQueryDto } from '../dtos/GetUncategorizedTr
import { BankingTransactionsApplication } from '../BankingTransactionsApplication.service';
@Controller('banking/uncategorized')
@ApiTags('banking-uncategorized')
@ApiTags('Banking Uncategorized Transactions')
export class BankingUncategorizedTransactionsController {
constructor(
private readonly bankingTransactionsApplication: BankingTransactionsApplication,

View File

@@ -0,0 +1,129 @@
import { ApiProperty } from '@nestjs/swagger';
export class BankTransactionResponseDto {
@ApiProperty({
description: 'The withdrawal amount',
example: 1000.5,
type: Number,
})
withdrawal: number;
@ApiProperty({
description: 'The deposit amount',
example: 2000.75,
type: Number,
})
deposit: number;
@ApiProperty({
description: 'The running balance after the transaction',
example: 3000.25,
type: Number,
})
runningBalance: number;
@ApiProperty({
description: 'Formatted withdrawal amount with currency symbol',
example: '$1,000.50',
type: String,
})
formattedWithdrawal: string;
@ApiProperty({
description: 'Formatted deposit amount with currency symbol',
example: '$2,000.75',
type: String,
})
formattedDeposit: string;
@ApiProperty({
description: 'Formatted running balance with currency symbol',
example: '$3,000.25',
type: String,
})
formattedRunningBalance: string;
@ApiProperty({
description: 'Unique transaction number',
example: 'TRX-2024-001',
type: String,
})
transactionNumber: string;
@ApiProperty({
description: 'Reference number for the transaction',
example: 'REF-2024-001',
type: String,
})
referenceNumber: string;
@ApiProperty({
description: 'ID of the reference entity',
example: 12345,
type: Number,
})
referenceId: number;
@ApiProperty({
description: 'Type of the reference entity',
example: 'INVOICE',
type: String,
})
referenceType: string;
@ApiProperty({
description: 'Formatted transaction type',
example: 'Bank Transfer',
type: String,
})
formattedTransactionType: string;
@ApiProperty({
description: 'Current balance',
example: 5000.0,
type: Number,
})
balance: number;
@ApiProperty({
description: 'Formatted balance with currency symbol',
example: '$5,000.00',
type: String,
})
formattedBalance: string;
@ApiProperty({
description: 'Transaction date',
example: '2024-03-20T10:30:00Z',
type: Date,
})
date: Date;
@ApiProperty({
description: 'Formatted transaction date',
example: 'March 20, 2024',
type: String,
})
formattedDate: string;
@ApiProperty({
description: 'Transaction status',
example: 'COMPLETED',
type: String,
})
status: string;
@ApiProperty({
description: 'Formatted transaction status',
example: 'Completed',
type: String,
})
formattedStatus: string;
@ApiProperty({
description: 'ID of the uncategorized transaction',
example: 67890,
type: Number,
})
uncategorizedTransactionId: number;
}

View File

@@ -0,0 +1,76 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNumber, IsBoolean, IsDateString } from 'class-validator';
export class GetPendingTransactionResponseDto {
@ApiProperty({ description: 'Transaction amount' })
@IsNumber()
amount: number;
@ApiProperty({ description: 'Transaction date' })
@IsDateString()
date: Date | string;
@ApiProperty({ description: 'Bank account ID' })
@IsNumber()
accountId: number;
@ApiProperty({ description: 'Transaction reference number', required: false })
@IsString()
referenceNo: string;
@ApiProperty({ description: 'Payee', required: false })
@IsString()
payee: string;
@ApiProperty({ description: 'Transaction description', required: false })
@IsString()
description: string;
@ApiProperty({ description: 'Plaid transaction ID', required: false })
@IsString()
plaidTransactionId: string;
@ApiProperty({ description: 'Recognized transaction ID', required: false })
@IsNumber()
recognizedTransactionId: number;
@ApiProperty({ description: 'Is transaction pending?' })
@IsBoolean()
pending: boolean;
@ApiProperty({ description: 'Transaction currency code' })
@IsString()
currencyCode: string;
@ApiProperty({ description: 'Withdrawal amount' })
@IsNumber()
withdrawal: number;
@ApiProperty({ description: 'Deposit amount' })
@IsNumber()
deposit: number;
@ApiProperty({ description: 'Is deposit transaction?' })
@IsBoolean()
isDepositTransaction: boolean;
@ApiProperty({ description: 'Is withdrawal transaction?' })
@IsBoolean()
isWithdrawalTransaction: boolean;
@ApiProperty({ description: 'Formatted amount' })
@IsString()
formattedAmount: string;
@ApiProperty({ description: 'Formatted date' })
@IsString()
formattedDate: string;
@ApiProperty({ description: 'Formatted deposit amount' })
@IsString()
formattedDepositAmount: string;
@ApiProperty({ description: 'Formatted withdrawal amount' })
@IsString()
formattedWithdrawalAmount: string;
}

View File

@@ -1,26 +1,55 @@
import { Type } from "class-transformer";
import { IsBoolean, IsEnum, IsNumber, IsOptional, IsPositive } from "class-validator";
import { Type } from 'class-transformer';
import {
IsBoolean,
IsEnum,
IsNumber,
IsOptional,
IsPositive,
} from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class NumberFormatQueryDto {
@ApiPropertyOptional({
description: 'Number of decimal places to display',
example: 2,
})
@Type(() => Number)
@IsNumber()
@IsPositive()
@IsOptional()
readonly precision: number;
@ApiPropertyOptional({
description: 'Whether to divide the number by 1000',
example: false,
})
@IsBoolean()
@IsOptional()
readonly divideOn1000: boolean;
@ApiPropertyOptional({
description: 'Whether to show zero values',
example: true,
})
@IsBoolean()
@IsOptional()
readonly showZero: boolean;
@ApiPropertyOptional({
description: 'How to format money values',
example: 'total',
enum: ['total', 'always', 'none'],
})
@IsEnum(['total', 'always', 'none'])
@IsOptional()
readonly formatMoney: 'total' | 'always' | 'none';
@ApiPropertyOptional({
description: 'How to format negative numbers',
example: 'parentheses',
enum: ['parentheses', 'mines'],
})
@IsEnum(['parentheses', 'mines'])
@IsOptional()
readonly negativeFormat: 'parentheses' | 'mines';
}
}

View File

@@ -109,24 +109,10 @@ export class BankAccount extends TenantBaseModel {
isParentType(parentType) {
return AccountTypesUtils.isParentTypeEqualsKey(
this.accountType,
parentType
parentType,
);
}
// /**
// * Model settings.
// */
// static get meta() {
// return CashflowAccountSettings;
// }
// /**
// * Retrieve the default custom views, roles and columns.
// */
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search roles.
*/

View File

@@ -3,11 +3,13 @@ import { getBankAccountTransactionsDefaultQuery } from './_utils';
import { GetBankAccountTransactionsRepository } from './GetBankAccountTransactionsRepo.service';
import { GetBankAccountTransactions } from './GetBankAccountTransactions';
import { GetBankTransactionsQueryDto } from '../../dtos/GetBankTranasctionsQuery.dto';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class GetBankAccountTransactionsService {
constructor(
private readonly getBankAccountTransactionsRepository: GetBankAccountTransactionsRepository,
private readonly i18nService: I18nService
) {}
/**
@@ -30,6 +32,7 @@ export class GetBankAccountTransactionsService {
const report = new GetBankAccountTransactions(
this.getBankAccountTransactionsRepository,
parsedQuery,
this.i18nService
);
const transactions = report.reportData();
const pagination = this.getBankAccountTransactionsRepository.pagination;

View File

@@ -11,11 +11,13 @@ import { FinancialSheet } from '@/modules/FinancialStatements/common/FinancialSh
import { formatBankTransactionsStatus } from './_utils';
import { GetBankAccountTransactionsRepository } from './GetBankAccountTransactionsRepo.service';
import { runningBalance } from '@/utils/running-balance';
import { I18nService } from 'nestjs-i18n';
export class GetBankAccountTransactions extends FinancialSheet {
private runningBalance: any;
private query: ICashflowAccountTransactionsQuery;
private repo: GetBankAccountTransactionsRepository;
private i18n: I18nService;
/**
* Constructor method.
@@ -26,11 +28,14 @@ export class GetBankAccountTransactions extends FinancialSheet {
constructor(
repo: GetBankAccountTransactionsRepository,
query: ICashflowAccountTransactionsQuery,
i18n: I18nService,
) {
super();
this.repo = repo;
this.query = query;
this.i18n = i18n;
this.runningBalance = runningBalance(this.repo.openingBalance);
}
@@ -104,7 +109,7 @@ export class GetBankAccountTransactions extends FinancialSheet {
referenceId: transaction.referenceId,
referenceType: transaction.referenceType,
formattedTransactionType: transaction.referenceTypeFormatted,
formattedTransactionType: this.i18n.t(transaction.referenceTypeFormatted),
transactionNumber: transaction.transactionNumber,
referenceNumber: transaction.referenceNumber,

View File

@@ -10,10 +10,18 @@ import {
} from '@nestjs/common';
import { ExcludeBankTransactionsApplication } from './ExcludeBankTransactionsApplication';
import { ExcludedBankTransactionsQuery } from './types/BankTransactionsExclude.types';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import {
ApiExtraModels,
ApiOperation,
ApiResponse,
ApiTags,
getSchemaPath,
} from '@nestjs/swagger';
import { GetExcludedBankTransactionResponseDto } from './dtos/GetExcludedBankTransactionResponse.dto';
@Controller('banking/exclude')
@ApiTags('banking-transactions')
@ApiTags('Banking Transactions')
@ApiExtraModels(GetExcludedBankTransactionResponseDto)
export class BankingTransactionsExcludeController {
constructor(
private readonly excludeBankTransactionsApplication: ExcludeBankTransactionsApplication,
@@ -35,6 +43,17 @@ export class BankingTransactionsExcludeController {
@Get()
@ApiOperation({ summary: 'Retrieves the excluded bank transactions.' })
@ApiResponse({
status: 200,
description:
'The excluded bank transactions has been retrieved successfully.',
schema: {
type: 'array',
items: {
$ref: getSchemaPath(GetExcludedBankTransactionResponseDto),
},
},
})
public getExcludedBankTransactions(
@Query() query: ExcludedBankTransactionsQuery,
) {

View File

@@ -0,0 +1,205 @@
import { ApiProperty, ApiExtraModels } from '@nestjs/swagger';
import {
IsNumber,
IsString,
IsBoolean,
IsDateString,
IsOptional,
} from 'class-validator';
@ApiExtraModels()
export class GetExcludedBankTransactionResponseDto {
@ApiProperty({
description:
'Transaction amount (positive for deposit, negative for withdrawal)',
})
@IsNumber()
amount: number;
@ApiProperty({ description: 'Transaction date (ISO string or Date)' })
@IsDateString()
date: string | Date;
@ApiProperty({ description: 'ID of the associated bank account' })
@IsNumber()
accountId: number;
@ApiProperty({
description: 'Reference number for the transaction',
required: false,
})
@IsString()
@IsOptional()
referenceNo?: string;
@ApiProperty({ description: 'Payee name', required: false })
@IsString()
@IsOptional()
payee?: string;
@ApiProperty({ description: 'Transaction description', required: false })
@IsString()
@IsOptional()
description?: string;
@ApiProperty({ description: 'Plaid transaction ID', required: false })
@IsString()
@IsOptional()
plaidTransactionId?: string;
@ApiProperty({
description: 'Whether the transaction is pending',
required: false,
})
@IsBoolean()
@IsOptional()
pending?: boolean;
@ApiProperty({
description: 'ID of the recognized transaction, if any',
required: false,
})
@IsNumber()
@IsOptional()
recognizedTransactionId?: number;
@ApiProperty({
description: 'Categorization reference type',
required: false,
})
@IsString()
@IsOptional()
categorizeRefType?: string;
@ApiProperty({ description: 'Categorization reference ID', required: false })
@IsNumber()
@IsOptional()
categorizeRefId?: number;
@ApiProperty({
description: 'Formatted amount (localized string)',
required: false,
})
@IsString()
@IsOptional()
formattedAmount?: string;
@ApiProperty({ description: 'Formatted transaction date', required: false })
@IsString()
@IsOptional()
formattedDate?: string;
@ApiProperty({ description: 'Formatted deposit amount', required: false })
@IsString()
@IsOptional()
formattedDepositAmount?: string;
@ApiProperty({ description: 'Formatted withdrawal amount', required: false })
@IsString()
@IsOptional()
formattedWithdrawalAmount?: string;
@ApiProperty({ description: 'Withdrawal amount', required: false })
@IsNumber()
@IsOptional()
withdrawal?: number;
@ApiProperty({ description: 'Deposit amount', required: false })
@IsNumber()
@IsOptional()
deposit?: number;
@ApiProperty({ description: 'True if deposit transaction', required: false })
@IsBoolean()
@IsOptional()
isDepositTransaction?: boolean;
@ApiProperty({
description: 'True if withdrawal transaction',
required: false,
})
@IsBoolean()
@IsOptional()
isWithdrawalTransaction?: boolean;
@ApiProperty({
description: 'True if transaction is recognized',
required: false,
})
@IsBoolean()
@IsOptional()
isRecognized?: boolean;
@ApiProperty({
description: 'True if transaction is excluded',
required: false,
})
@IsBoolean()
@IsOptional()
isExcluded?: boolean;
@ApiProperty({
description: 'True if transaction is pending',
required: false,
})
@IsBoolean()
@IsOptional()
isPending?: boolean;
// Recognized transaction fields (from transformer)
@ApiProperty({
description: 'Assigned account ID from recognized transaction',
required: false,
})
@IsNumber()
@IsOptional()
assignedAccountId?: number;
@ApiProperty({
description: 'Assigned account name from recognized transaction',
required: false,
})
@IsString()
@IsOptional()
assignedAccountName?: string;
@ApiProperty({
description: 'Assigned account code from recognized transaction',
required: false,
})
@IsString()
@IsOptional()
assignedAccountCode?: string;
@ApiProperty({
description: 'Assigned payee from recognized transaction',
required: false,
})
@IsString()
@IsOptional()
assignedPayee?: string;
@ApiProperty({
description: 'Assigned memo from recognized transaction',
required: false,
})
@IsString()
@IsOptional()
assignedMemo?: string;
@ApiProperty({
description: 'Assigned category from recognized transaction',
required: false,
})
@IsString()
@IsOptional()
assignedCategory?: string;
@ApiProperty({
description: 'Assigned formatted category from recognized transaction',
required: false,
})
@IsString()
@IsOptional()
assignedCategoryFormatted?: string;
}

View File

@@ -0,0 +1,3 @@
import { UncategorizedTransactionTransformer } from '@/modules/BankingCategorize/commands/UncategorizedTransaction.transformer';
export class ExcludedBankTransactionTransformer extends UncategorizedTransactionTransformer {}

View File

@@ -1,9 +1,9 @@
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { ExcludedBankTransactionsQuery } from '../types/BankTransactionsExclude.types';
import { UncategorizedTransactionTransformer } from '@/modules/BankingCategorize/commands/UncategorizedTransaction.transformer';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { ExcludedBankTransactionTransformer } from './ExcludedBankTransaction.transformer';
@Injectable()
export class GetExcludedBankTransactionsService {
@@ -60,7 +60,7 @@ export class GetExcludedBankTransactionsService {
const data = await this.transformer.transform(
results,
new UncategorizedTransactionTransformer(),
new ExcludedBankTransactionTransformer(),
);
return { data, pagination };
}

View File

@@ -1,11 +1,10 @@
import { Inject } from '@nestjs/common';
import { difference, sumBy } from 'lodash';
import {
ILandedCostItemDTO,
ILandedCostDTO,
IBillLandedCostTransaction,
ILandedCostTransaction,
ILandedCostTransactionEntry,
LandedCostTransactionModel,
LandedCostTransactionType,
} from './types/BillLandedCosts.types';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { BillLandedCost } from './models/BillLandedCost';
@@ -14,13 +13,19 @@ import { CONFIG, ERRORS } from './utils';
import { ItemEntry } from '../TransactionItemEntry/models/ItemEntry';
import { Bill } from '../Bills/models/Bill';
import { TransactionLandedCost } from './commands/TransctionLandedCost.service';
import {
AllocateBillLandedCostDto,
AllocateBillLandedCostItemDto,
} from './dtos/AllocateBillLandedCost.dto';
export class BaseLandedCostService {
@Inject()
public readonly transactionLandedCost: TransactionLandedCost;
protected readonly transactionLandedCost: TransactionLandedCost;
@Inject(BillLandedCost.name)
private readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost>;
protected readonly billLandedCostModel: TenantModelProxy<
typeof BillLandedCost
>;
/**
* Validates allocate cost items association with the purchase invoice entries.
@@ -29,7 +34,7 @@ export class BaseLandedCostService {
*/
protected validateAllocateCostItems = (
purchaseInvoiceEntries: ItemEntry[],
landedCostItems: ILandedCostItemDTO[],
landedCostItems: AllocateBillLandedCostItemDto[],
): void => {
// Purchase invoice entries items ids.
const purchaseInvoiceItems = purchaseInvoiceEntries.map((e) => e.id);
@@ -55,7 +60,7 @@ export class BaseLandedCostService {
* @returns
*/
protected transformToBillLandedCost(
landedCostDTO: ILandedCostDTO,
landedCostDTO: AllocateBillLandedCostDto,
bill: Bill,
costTransaction: ILandedCostTransaction,
costTransactionEntry: ILandedCostTransactionEntry,
@@ -88,20 +93,18 @@ export class BaseLandedCostService {
* @param {transactionId} transactionId -
*/
public getLandedCostOrThrowError = async (
transactionType: string,
transactionType: LandedCostTransactionType,
transactionId: number,
) => {
const Model = this.transactionLandedCost.getModel(
transactionType,
);
const model = await Model.query().findById(transactionId);
const Model = await this.transactionLandedCost.getModel(transactionType);
const model = await Model().query().findById(transactionId);
if (!model) {
throw new ServiceError(ERRORS.LANDED_COST_TRANSACTION_NOT_FOUND);
}
return this.transactionLandedCost.transformToLandedCost(
transactionType,
model,
model as LandedCostTransactionModel,
);
};
@@ -117,13 +120,11 @@ export class BaseLandedCostService {
transactionId: number,
transactionEntryId: number,
): Promise<any> => {
const Model = this.transactionLandedCost.getModel(
tenantId,
transactionType,
);
const Model = await this.transactionLandedCost.getModel(transactionType);
const relation = CONFIG.COST_TYPES[transactionType].entries;
const entry = await Model.relatedQuery(relation)
const entry = await Model()
.relatedQuery(relation)
.for(transactionId)
.findOne('id', transactionEntryId)
.where('landedCost', true)
@@ -139,7 +140,7 @@ export class BaseLandedCostService {
throw new ServiceError(ERRORS.LANDED_COST_ENTRY_NOT_FOUND);
}
return this.transactionLandedCost.transformToLandedCostEntry(
transactionType,
transactionType as LandedCostTransactionType,
entry,
);
};
@@ -150,7 +151,7 @@ export class BaseLandedCostService {
* @returns {number}
*/
protected getAllocateItemsCostTotal = (
landedCostDTO: ILandedCostDTO,
landedCostDTO: AllocateBillLandedCostDto,
): number => {
return sumBy(landedCostDTO.items, 'cost');
};

View File

@@ -1,25 +1,30 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { TransactionLandedCostEntriesService } from './TransactionLandedCostEntries.service';
import { AllocateLandedCostService } from './commands/AllocateLandedCost.service';
import { LandedCostGLEntriesSubscriber } from './commands/LandedCostGLEntries.subscriber';
import { LandedCostGLEntries } from './commands/LandedCostGLEntries.service';
// import { LandedCostGLEntries } from './commands/LandedCostGLEntries.service';
import { LandedCostSyncCostTransactions } from './commands/LandedCostSyncCostTransactions.service';
import { LandedCostSyncCostTransactionsSubscriber } from './commands/LandedCostSyncCostTransactions.subscriber';
import { BillAllocatedLandedCostTransactions } from './commands/BillAllocatedLandedCostTransactions.service';
import { BillAllocateLandedCostController } from './LandedCost.controller';
import { RevertAllocatedLandedCost } from './commands/RevertAllocatedLandedCost.service';
import LandedCostTranasctions from './commands/LandedCostTransactions.service';
import { LandedCostTranasctions } from './commands/LandedCostTransactions.service';
import { LandedCostInventoryTransactions } from './commands/LandedCostInventoryTransactions.service';
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
import { TransactionLandedCost } from './commands/TransctionLandedCost.service';
import { ExpenseLandedCost } from './commands/ExpenseLandedCost.service';
import { BillLandedCost } from './commands/BillLandedCost.service';
@Module({
imports: [InventoryCostModule],
imports: [forwardRef(() => InventoryCostModule)],
providers: [
AllocateLandedCostService,
TransactionLandedCostEntriesService,
BillAllocatedLandedCostTransactions,
LandedCostGLEntriesSubscriber,
LandedCostGLEntries,
TransactionLandedCost,
BillLandedCost,
ExpenseLandedCost,
LandedCostSyncCostTransactions,
RevertAllocatedLandedCost,
LandedCostInventoryTransactions,

View File

@@ -7,32 +7,45 @@ import {
Post,
Query,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { AllocateBillLandedCostDto } from './dtos/AllocateBillLandedCost.dto';
import { AllocateLandedCostService } from './commands/AllocateLandedCost.service';
import { BillAllocatedLandedCostTransactions } from './commands/BillAllocatedLandedCostTransactions.service';
import { RevertAllocatedLandedCost } from './commands/RevertAllocatedLandedCost.service';
import { LandedCostTranasctions } from './commands/LandedCostTransactions.service';
import { LandedCostTransactionsQueryDto } from './dtos/LandedCostTransactionsQuery.dto';
@ApiTags('Landed Cost')
@Controller('landed-cost')
export class BillAllocateLandedCostController {
constructor(
private allocateLandedCost: AllocateLandedCostService,
private billAllocatedCostTransactions: BillAllocatedLandedCostTransactions,
private revertAllocatedLandedCost: RevertAllocatedLandedCost,
private landedCostTranasctions: LandedCostTranasctions,
private landedCostTransactions: LandedCostTranasctions,
) {}
@Get('/transactions')
@ApiOperation({ summary: 'Get landed cost transactions' })
@ApiResponse({
status: 200,
description: 'List of landed cost transactions.',
})
async getLandedCostTransactions(
@Query('transaction_type') transactionType: string,
@Query() query: LandedCostTransactionsQueryDto,
) {
const transactions =
await this.landedCostTranasctions.getLandedCostTransactions(transactionType);
await this.landedCostTransactions.getLandedCostTransactions(query);
return transactions;
}
@Post('/bills/:billId/allocate')
@ApiOperation({ summary: 'Allocate landed cost to bill items' })
@ApiResponse({
status: 201,
description: 'Landed cost allocated successfully.',
})
public async calculateLandedCost(
@Param('billId') billId: number,
@Body() landedCostDTO: AllocateBillLandedCostDto,
@@ -48,37 +61,37 @@ export class BillAllocateLandedCostController {
}
@Delete('/:allocatedLandedCostId')
@ApiOperation({ summary: 'Delete allocated landed cost' })
@ApiResponse({
status: 200,
description: 'Allocated landed cost deleted successfully.',
})
public async deleteAllocatedLandedCost(
@Param('allocatedLandedCostId') allocatedLandedCostId: number,
) {
await this.revertAllocatedLandedCost.deleteAllocatedLandedCost(
allocatedLandedCostId,
);
return {
id: allocatedLandedCostId,
message: 'The allocated landed cost are delete successfully.',
};
}
public async listLandedCosts(
) {
const transactions =
await this.landedCostTranasctions.getLandedCostTransactions(query);
return transactions;
};
@Get('/bills/:billId/transactions')
@ApiOperation({ summary: 'Get bill landed cost transactions' })
@ApiResponse({
status: 200,
description: 'List of bill landed cost transactions.',
})
async getBillLandedCostTransactions(@Param('billId') billId: number) {
const transactions =
const data =
await this.billAllocatedCostTransactions.getBillLandedCostTransactions(
billId,
);
return {
billId,
transactions,
data,
};
}
}

View File

@@ -23,7 +23,7 @@ export class AllocateLandedCostService extends BaseLandedCostService {
private readonly billModel: TenantModelProxy<typeof Bill>,
@Inject(BillLandedCost.name)
private readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost>
protected readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost>
) {
super();
}
@@ -54,7 +54,7 @@ export class AllocateLandedCostService extends BaseLandedCostService {
const amount = this.getAllocateItemsCostTotal(allocateCostDTO);
// Retrieve the purchase invoice or throw not found error.
const bill = await Bill.query()
const bill = await this.billModel().query()
.findById(billId)
.withGraphFetched('entries')
.throwIfNotFound();

View File

@@ -6,6 +6,8 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Bill } from '@/modules/Bills/models/Bill';
import { BillLandedCost } from '../models/BillLandedCost';
import { IBillLandedCostTransaction } from '../types/BillLandedCosts.types';
import { ModelObject } from 'objection';
import { formatNumber } from '@/utils/format-number';
@Injectable()
export class BillAllocatedLandedCostTransactions {
@@ -23,19 +25,17 @@ export class BillAllocatedLandedCostTransactions {
/**
* Retrieve the bill associated landed cost transactions.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<IBillLandedCostTransaction>}
*/
public getBillLandedCostTransactions = async (
billId: number,
): Promise<IBillLandedCostTransaction> => {
): Promise<Array<IBillLandedCostTransaction>> => {
// Retrieve the given bill id or throw not found service error.
const bill = await this.billModel()
.query()
.findById(billId)
.throwIfNotFound();
// Retrieve the bill associated allocated landed cost with bill and expense entry.
const landedCostTransactions = await this.billLandedCostModel()
.query()
@@ -45,11 +45,8 @@ export class BillAllocatedLandedCostTransactions {
.withGraphFetched('allocatedFromExpenseEntry.expenseAccount')
.withGraphFetched('bill');
const transactionsJson = this.i18nService.i18nApply(
[[qim.$each, 'allocationMethodFormatted']],
landedCostTransactions.map((a) => a.toJSON()),
tenantId,
);
const transactionsJson = landedCostTransactions.map((a) => a.toJSON());
return this.transformBillLandedCostTransactions(transactionsJson);
};
@@ -59,7 +56,7 @@ export class BillAllocatedLandedCostTransactions {
* @returns
*/
private transformBillLandedCostTransactions = (
landedCostTransactions: IBillLandedCostTransaction[],
landedCostTransactions: ModelObject<BillLandedCost>[],
) => {
return landedCostTransactions.map(this.transformBillLandedCostTransaction);
};
@@ -70,15 +67,16 @@ export class BillAllocatedLandedCostTransactions {
* @returns
*/
private transformBillLandedCostTransaction = (
transaction: IBillLandedCostTransaction,
) => {
const getTransactionName = R.curry(this.condBillLandedTransactionName)(
transaction: ModelObject<BillLandedCost>,
): IBillLandedCostTransaction => {
const name = this.condBillLandedTransactionName(
transaction.fromTransactionType,
transaction,
);
const description = this.condBillLandedTransactionDescription(
transaction.fromTransactionType,
transaction,
);
const getTransactionDesc = R.curry(
this.condBillLandedTransactionDescription,
)(transaction.fromTransactionType);
return {
formattedAmount: formatNumber(transaction.amount, {
currencyCode: transaction.currencyCode,
@@ -87,8 +85,8 @@ export class BillAllocatedLandedCostTransactions {
'allocatedFromBillEntry',
'allocatedFromExpenseEntry',
]),
name: getTransactionName(transaction),
description: getTransactionDesc(transaction),
name,
description,
formattedLocalAmount: formatNumber(transaction.localAmount, {
currencyCode: 'USD',
}),

View File

@@ -0,0 +1,61 @@
import { isEmpty } from 'lodash';
import {
ILandedCostTransactionEntry,
ILandedCostTransaction,
} from '../types/BillLandedCosts.types';
import { Injectable } from '@nestjs/common';
import { Bill } from '@/modules/Bills/models/Bill';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { Item } from '@/modules/Items/models/Item';
import { ModelObject } from 'objection';
@Injectable()
export class BillLandedCost {
/**
* Retrieve the landed cost transaction from the given bill transaction.
* @param {IBill} bill - Bill transaction.
* @returns {ILandedCostTransaction} - Landed cost transaction.
*/
public transformToLandedCost = (
bill: ModelObject<Bill>,
): ILandedCostTransaction => {
const name = bill.billNumber || bill.referenceNo;
return {
id: bill.id,
name,
allocatedCostAmount: bill.allocatedCostAmount,
amount: bill.landedCostAmount,
unallocatedCostAmount: bill.unallocatedCostAmount,
transactionType: 'Bill',
currencyCode: bill.currencyCode,
exchangeRate: bill.exchangeRate,
...(!isEmpty(bill.entries) && {
entries: bill.entries.map(this.transformToLandedCostEntry),
}),
};
};
/**
* Transformes bill entry to landed cost entry.
* @param {IBill} bill - Bill model.
* @param {IItemEntry} billEntry - Bill entry.
* @return {ILandedCostTransactionEntry}
*/
public transformToLandedCostEntry(
billEntry: ItemEntry & { item: Item },
): ILandedCostTransactionEntry {
return {
id: billEntry.id,
name: billEntry.item.name,
code: billEntry.item.code,
amount: billEntry.amount,
unallocatedCostAmount: billEntry.unallocatedCostAmount,
allocatedCostAmount: billEntry.allocatedCostAmount,
description: billEntry.description,
costAccountId: billEntry.costAccountId || billEntry.item.costAccountId,
};
}
}

View File

@@ -0,0 +1,59 @@
import { Expense } from '@/modules/Expenses/models/Expense.model';
import { Injectable } from '@nestjs/common';
import { isEmpty } from 'lodash';
import { ModelObject } from 'objection';
import {
ILandedCostTransaction,
ILandedCostTransactionEntry,
} from '../types/BillLandedCosts.types';
import { ExpenseCategory } from '@/modules/Expenses/models/ExpenseCategory.model';
import { Account } from '@/modules/Accounts/models/Account.model';
@Injectable()
export class ExpenseLandedCost {
/**
* Retrieve the landed cost transaction from the given expense transaction.
* @param {IExpense} expense
* @returns {ILandedCostTransaction}
*/
public transformToLandedCost = (
expense: ModelObject<Expense>,
): ILandedCostTransaction => {
const name = 'EXP-100';
return {
id: expense.id,
name,
amount: expense.landedCostAmount,
allocatedCostAmount: expense.allocatedCostAmount,
unallocatedCostAmount: expense.unallocatedCostAmount,
transactionType: 'Expense',
currencyCode: expense.currencyCode,
exchangeRate: expense.exchangeRate || 1,
...(!isEmpty(expense.categories) && {
entries: expense.categories.map(this.transformToLandedCostEntry),
}),
};
};
/**
* Transformes expense entry to landed cost entry.
* @param {IExpenseCategory & { expenseAccount: IAccount }} expenseEntry -
* @return {ILandedCostTransactionEntry}
*/
public transformToLandedCostEntry = (
expenseEntry: ExpenseCategory & { expenseAccount: Account },
): ILandedCostTransactionEntry => {
return {
id: expenseEntry.id,
name: expenseEntry.expenseAccount.name,
code: expenseEntry.expenseAccount.code,
amount: expenseEntry.amount,
description: expenseEntry.description,
allocatedCostAmount: expenseEntry.allocatedCostAmount,
unallocatedCostAmount: expenseEntry.unallocatedCostAmount,
costAccountId: expenseEntry.expenseAccount.id,
};
};
}

View File

@@ -1,234 +1,236 @@
import * as R from 'ramda';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { BaseLandedCostService } from '../BaseLandedCost.service';
import { BillLandedCost } from '../models/BillLandedCost';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Bill } from '@/modules/Bills/models/Bill';
import { BillLandedCostEntry } from '../models/BillLandedCostEntry';
import { ILedger, ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Ledger } from '@/modules/Ledger/Ledger';
// import * as R from 'ramda';
// import { Knex } from 'knex';
// import { Inject, Injectable } from '@nestjs/common';
// import { BaseLandedCostService } from '../BaseLandedCost.service';
// import { BillLandedCost } from '../models/BillLandedCost';
// import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
// import { Bill } from '@/modules/Bills/models/Bill';
// import { BillLandedCostEntry } from '../models/BillLandedCostEntry';
// import { ILedger, ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
// import { Ledger } from '@/modules/Ledger/Ledger';
// import { AccountNormal } from '@/interfaces/Account';
// import { ILandedCostTransactionEntry } from '../types/BillLandedCosts.types';
@Injectable()
export class LandedCostGLEntries extends BaseLandedCostService {
constructor(
private readonly journalService: JournalPosterService,
private readonly ledgerRepository: LedgerRepository,
// @Injectable()
// export class LandedCostGLEntries extends BaseLandedCostService {
// constructor(
// private readonly journalService: JournalPosterService,
// private readonly ledgerRepository: LedgerRepository,
@Inject(BillLandedCost.name)
private readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost>,
) {
super();
}
// @Inject(BillLandedCost.name)
// private readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost>,
// ) {
// super();
// }
/**
* Retrieves the landed cost GL common entry.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @returns
*/
private getLandedCostGLCommonEntry = (
bill: Bill,
allocatedLandedCost: BillLandedCost
) => {
return {
date: bill.billDate,
currencyCode: allocatedLandedCost.currencyCode,
exchangeRate: allocatedLandedCost.exchangeRate,
// /**
// * Retrieves the landed cost GL common entry.
// * @param {IBill} bill
// * @param {IBillLandedCost} allocatedLandedCost
// * @returns
// */
// private getLandedCostGLCommonEntry = (
// bill: Bill,
// allocatedLandedCost: BillLandedCost
// ) => {
// return {
// date: bill.billDate,
// currencyCode: allocatedLandedCost.currencyCode,
// exchangeRate: allocatedLandedCost.exchangeRate,
transactionType: 'LandedCost',
transactionId: allocatedLandedCost.id,
transactionNumber: bill.billNumber,
// transactionType: 'LandedCost',
// transactionId: allocatedLandedCost.id,
// transactionNumber: bill.billNumber,
referenceNumber: bill.referenceNo,
// referenceNumber: bill.referenceNo,
credit: 0,
debit: 0,
};
};
// credit: 0,
// debit: 0,
// };
// };
/**
* Retrieves the landed cost GL inventory entry.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @param {IBillLandedCostEntry} allocatedEntry
* @returns {ILedgerEntry}
*/
private getLandedCostGLInventoryEntry = (
bill: Bill,
allocatedLandedCost: BillLandedCost,
allocatedEntry: BillLandedCostEntry
): ILedgerEntry => {
const commonEntry = this.getLandedCostGLCommonEntry(
bill,
allocatedLandedCost
);
return {
...commonEntry,
debit: allocatedLandedCost.localAmount,
accountId: allocatedEntry.itemEntry.item.inventoryAccountId,
index: 1,
accountNormal: AccountNormal.DEBIT,
};
};
// /**
// * Retrieves the landed cost GL inventory entry.
// * @param {IBill} bill
// * @param {IBillLandedCost} allocatedLandedCost
// * @param {IBillLandedCostEntry} allocatedEntry
// * @returns {ILedgerEntry}
// */
// private getLandedCostGLInventoryEntry = (
// bill: Bill,
// allocatedLandedCost: BillLandedCost,
// allocatedEntry: BillLandedCostEntry
// ): ILedgerEntry => {
// const commonEntry = this.getLandedCostGLCommonEntry(
// bill,
// allocatedLandedCost
// );
// return {
// ...commonEntry,
// debit: allocatedLandedCost.localAmount,
// accountId: allocatedEntry.itemEntry.item.inventoryAccountId,
// index: 1,
// accountNormal: AccountNormal.DEBIT,
// };
// };
/**
* Retrieves the landed cost GL cost entry.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @returns {ILedgerEntry}
*/
private getLandedCostGLCostEntry = (
bill: Bill,
allocatedLandedCost: BillLandedCost,
fromTransactionEntry: ILandedCostTransactionEntry
): ILedgerEntry => {
const commonEntry = this.getLandedCostGLCommonEntry(
bill,
allocatedLandedCost
);
return {
...commonEntry,
credit: allocatedLandedCost.localAmount,
accountId: fromTransactionEntry.costAccountId,
index: 2,
accountNormal: AccountNormal.CREDIT,
};
};
// /**
// * Retrieves the landed cost GL cost entry.
// * @param {IBill} bill
// * @param {IBillLandedCost} allocatedLandedCost
// * @param {ILandedCostTransactionEntry} fromTransactionEntry
// * @returns {ILedgerEntry}
// */
// private getLandedCostGLCostEntry = (
// bill: Bill,
// allocatedLandedCost: BillLandedCost,
// fromTransactionEntry: ILandedCostTransactionEntry
// ): ILedgerEntry => {
// const commonEntry = this.getLandedCostGLCommonEntry(
// bill,
// allocatedLandedCost
// );
// return {
// ...commonEntry,
// credit: allocatedLandedCost.localAmount,
// accountId: fromTransactionEntry.costAccountId,
// index: 2,
// accountNormal: AccountNormal.CREDIT,
// };
// };
/**
* Retrieve allocated landed cost entry GL entries.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @param {IBillLandedCostEntry} allocatedEntry
* @returns {ILedgerEntry}
*/
private getLandedCostGLAllocateEntry = R.curry(
(
bill: Bill,
allocatedLandedCost: BillLandedCost,
fromTransactionEntry: LandedCostTransactionEntry,
allocatedEntry: BillLandedCostEntry
): ILedgerEntry[] => {
const inventoryEntry = this.getLandedCostGLInventoryEntry(
bill,
allocatedLandedCost,
allocatedEntry
);
const costEntry = this.getLandedCostGLCostEntry(
bill,
allocatedLandedCost,
fromTransactionEntry
);
return [inventoryEntry, costEntry];
}
);
// /**
// * Retrieve allocated landed cost entry GL entries.
// * @param {IBill} bill
// * @param {IBillLandedCost} allocatedLandedCost
// * @param {ILandedCostTransactionEntry} fromTransactionEntry
// * @param {IBillLandedCostEntry} allocatedEntry
// * @returns {ILedgerEntry}
// */
// private getLandedCostGLAllocateEntry = R.curry(
// (
// bill: Bill,
// allocatedLandedCost: BillLandedCost,
// fromTransactionEntry: ILandedCostTransactionEntry,
// allocatedEntry: BillLandedCostEntry
// ): ILedgerEntry[] => {
// const inventoryEntry = this.getLandedCostGLInventoryEntry(
// bill,
// allocatedLandedCost,
// allocatedEntry
// );
// const costEntry = this.getLandedCostGLCostEntry(
// bill,
// allocatedLandedCost,
// fromTransactionEntry
// );
// return [inventoryEntry, costEntry];
// }
// );
/**
* Compose the landed cost GL entries.
* @param {BillLandedCost} allocatedLandedCost
* @param {Bill} bill
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @returns {ILedgerEntry[]}
*/
public getLandedCostGLEntries = (
allocatedLandedCost: BillLandedCost,
bill: Bill,
fromTransactionEntry: LandedCostTransactionEntry
): ILedgerEntry[] => {
const getEntry = this.getLandedCostGLAllocateEntry(
bill,
allocatedLandedCost,
fromTransactionEntry
);
return allocatedLandedCost.allocateEntries.map(getEntry).flat();
};
// /**
// * Compose the landed cost GL entries.
// * @param {BillLandedCost} allocatedLandedCost
// * @param {Bill} bill
// * @param {ILandedCostTransactionEntry} fromTransactionEntry
// * @returns {ILedgerEntry[]}
// */
// public getLandedCostGLEntries = (
// allocatedLandedCost: BillLandedCost,
// bill: Bill,
// fromTransactionEntry: ILandedCostTransactionEntry
// ): ILedgerEntry[] => {
// const getEntry = this.getLandedCostGLAllocateEntry(
// bill,
// allocatedLandedCost,
// fromTransactionEntry
// );
// return allocatedLandedCost.allocateEntries.map(getEntry).flat();
// };
/**
* Retrieves the landed cost GL ledger.
* @param {IBillLandedCost} allocatedLandedCost
* @param {Bill} bill
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @returns {ILedger}
*/
public getLandedCostLedger = (
allocatedLandedCost: BillLandedCost,
bill: Bill,
fromTransactionEntry: LandedCostTransactionEntry
): ILedger => {
const entries = this.getLandedCostGLEntries(
allocatedLandedCost,
bill,
fromTransactionEntry
);
return new Ledger(entries);
};
// /**
// * Retrieves the landed cost GL ledger.
// * @param {BillLandedCost} allocatedLandedCost
// * @param {Bill} bill
// * @param {ILandedCostTransactionEntry} fromTransactionEntry
// * @returns {ILedger}
// */
// public getLandedCostLedger = (
// allocatedLandedCost: BillLandedCost,
// bill: Bill,
// fromTransactionEntry: ILandedCostTransactionEntry
// ): ILedger => {
// const entries = this.getLandedCostGLEntries(
// allocatedLandedCost,
// bill,
// fromTransactionEntry
// );
// return new Ledger(entries);
// };
/**
* Writes landed cost GL entries to the storage layer.
* @param {number} tenantId -
*/
public writeLandedCostGLEntries = async (
allocatedLandedCost: BillLandedCost,
bill: Bill,
fromTransactionEntry: ILandedCostTransactionEntry,
trx?: Knex.Transaction
) => {
const ledgerEntries = this.getLandedCostGLEntries(
allocatedLandedCost,
bill,
fromTransactionEntry
);
await this.ledgerRepository.saveLedgerEntries(ledgerEntries, trx);
};
// /**
// * Writes landed cost GL entries to the storage layer.
// * @param {number} tenantId -
// */
// public writeLandedCostGLEntries = async (
// allocatedLandedCost: BillLandedCost,
// bill: Bill,
// fromTransactionEntry: ILandedCostTransactionEntry,
// trx?: Knex.Transaction
// ) => {
// const ledgerEntries = this.getLandedCostGLEntries(
// allocatedLandedCost,
// bill,
// fromTransactionEntry
// );
// await this.ledgerRepository.saveLedgerEntries(ledgerEntries, trx);
// };
/**
* Generates and writes GL entries of the given landed cost.
* @param {number} billLandedCostId
* @param {Knex.Transaction} trx
*/
public createLandedCostGLEntries = async (
billLandedCostId: number,
trx?: Knex.Transaction
) => {
// Retrieve the bill landed cost transacion with associated
// allocated entries and items.
const allocatedLandedCost = await this.billLandedCostModel().query(trx)
.findById(billLandedCostId)
.withGraphFetched('bill')
.withGraphFetched('allocateEntries.itemEntry.item');
// /**
// * Generates and writes GL entries of the given landed cost.
// * @param {number} billLandedCostId
// * @param {Knex.Transaction} trx
// */
// public createLandedCostGLEntries = async (
// billLandedCostId: number,
// trx?: Knex.Transaction
// ) => {
// // Retrieve the bill landed cost transacion with associated
// // allocated entries and items.
// const allocatedLandedCost = await this.billLandedCostModel().query(trx)
// .findById(billLandedCostId)
// .withGraphFetched('bill')
// .withGraphFetched('allocateEntries.itemEntry.item');
// Retrieve the allocated from transactione entry.
const transactionEntry = await this.getLandedCostEntry(
allocatedLandedCost.fromTransactionType,
allocatedLandedCost.fromTransactionId,
allocatedLandedCost.fromTransactionEntryId
);
// Writes the given landed cost GL entries to the storage layer.
await this.writeLandedCostGLEntries(
allocatedLandedCost,
allocatedLandedCost.bill,
transactionEntry,
trx
);
};
// // Retrieve the allocated from transactione entry.
// const transactionEntry = await this.getLandedCostEntry(
// allocatedLandedCost.fromTransactionType,
// allocatedLandedCost.fromTransactionId,
// allocatedLandedCost.fromTransactionEntryId
// );
// // Writes the given landed cost GL entries to the storage layer.
// await this.writeLandedCostGLEntries(
// allocatedLandedCost,
// allocatedLandedCost.bill,
// transactionEntry,
// trx
// );
// };
/**
* Reverts GL entries of the given allocated landed cost transaction.
* @param {number} tenantId
* @param {number} landedCostId
* @param {Knex.Transaction} trx
*/
public revertLandedCostGLEntries = async (
landedCostId: number,
trx: Knex.Transaction
) => {
await this.journalService.revertJournalTransactions(
landedCostId,
'LandedCost',
trx
);
};
}
// /**
// * Reverts GL entries of the given allocated landed cost transaction.
// * @param {number} tenantId
// * @param {number} landedCostId
// * @param {Knex.Transaction} trx
// */
// public revertLandedCostGLEntries = async (
// landedCostId: number,
// trx: Knex.Transaction
// ) => {
// await this.journalService.revertJournalTransactions(
// landedCostId,
// 'LandedCost',
// trx
// );
// };
// }

View File

@@ -3,15 +3,14 @@ import {
IAllocatedLandedCostDeletedPayload,
} from '../types/BillLandedCosts.types';
import { OnEvent } from '@nestjs/event-emitter';
import { LandedCostGLEntries } from './LandedCostGLEntries.service';
// import { LandedCostGLEntries } from './LandedCostGLEntries.service';
import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
@Injectable()
export class LandedCostGLEntriesSubscriber {
constructor(
private readonly billLandedCostGLEntries: LandedCostGLEntries,
) {}
constructor() // private readonly billLandedCostGLEntries: LandedCostGLEntries,
{}
/**
* Writes GL entries once landed cost transaction created.
@@ -22,11 +21,11 @@ export class LandedCostGLEntriesSubscriber {
billLandedCost,
trx,
}: IAllocatedLandedCostCreatedPayload) {
await this.billLandedCostGLEntries.createLandedCostGLEntries(
billLandedCost.id,
trx
);
};
// await this.billLandedCostGLEntries.createLandedCostGLEntries(
// billLandedCost.id,
// trx
// );
}
/**
* Reverts GL entries associated to landed cost transaction once deleted.
@@ -37,9 +36,9 @@ export class LandedCostGLEntriesSubscriber {
oldBillLandedCost,
trx,
}: IAllocatedLandedCostDeletedPayload) {
await this.billLandedCostGLEntries.revertLandedCostGLEntries(
oldBillLandedCost.id,
trx
);
};
}
// await this.billLandedCostGLEntries.revertLandedCostGLEntries(
// oldBillLandedCost.id,
// trx
// );
}
}

View File

@@ -11,9 +11,8 @@ export class LandedCostSyncCostTransactions {
/**
* Allocate the landed cost amount to cost transactions.
* @param {number} tenantId -
* @param {string} transactionType
* @param {number} transactionId
* @param {string} transactionType - Transaction type.
* @param {number} transactionId - Transaction id.
*/
public incrementLandedCostAmount = async (
transactionType: string,
@@ -22,18 +21,18 @@ export class LandedCostSyncCostTransactions {
amount: number,
trx?: Knex.Transaction
): Promise<void> => {
const Model = this.transactionLandedCost.getModel(
const Model = await this.transactionLandedCost.getModel(
transactionType
);
const relation = CONFIG.COST_TYPES[transactionType].entries;
// Increment the landed cost transaction amount.
await Model.query(trx)
await Model().query(trx)
.where('id', transactionId)
.increment('allocatedCostAmount', amount);
// Increment the landed cost entry.
await Model.relatedQuery(relation, trx)
await Model().relatedQuery(relation, trx)
.for(transactionId)
.where('id', transactionEntryId)
.increment('allocatedCostAmount', amount);
@@ -54,18 +53,18 @@ export class LandedCostSyncCostTransactions {
amount: number,
trx?: Knex.Transaction
) => {
const Model = this.transactionLandedCost.getModel(
const Model = await this.transactionLandedCost.getModel(
transactionType
);
const relation = CONFIG.COST_TYPES[transactionType].entries;
// Decrement the allocate cost amount of cost transaction.
await Model.query(trx)
await Model().query(trx)
.where('id', transactionId)
.decrement('allocatedCostAmount', amount);
// Decrement the allocated cost amount cost transaction entry.
await Model.relatedQuery(relation, trx)
await Model().relatedQuery(relation, trx)
.for(transactionId)
.where('id', transactionEntryId)
.decrement('allocatedCostAmount', amount);

View File

@@ -1,70 +1,73 @@
import { Inject, Service } from 'typedi';
import { Injectable } from '@nestjs/common';
import { ref } from 'objection';
import { curry, pipe, map } from 'lodash/fp';
import * as R from 'ramda';
import {
ILandedCostTransactionsQueryDTO,
ILandedCostTransaction,
ILandedCostTransactionDOJO,
ILandedCostTransactionEntry,
ILandedCostTransactionEntryDOJO,
} from '@/interfaces';
import TransactionLandedCost from './TransctionLandedCost';
import { formatNumber } from 'utils';
} from '../types/BillLandedCosts.types';
import { TransactionLandedCost } from './TransctionLandedCost.service';
import { formatNumber } from '@/utils/format-number';
import { LandedCostTransactionsQueryDto } from '../dtos/LandedCostTransactionsQuery.dto';
@Service()
export default class LandedCostTranasctions {
@Inject()
private transactionLandedCost: TransactionLandedCost;
@Injectable()
export class LandedCostTranasctions {
constructor(private readonly transactionLandedCost: TransactionLandedCost) {}
/**
* Retrieve the landed costs based on the given query.
* @param {number} tenantId
* @param {ILandedCostTransactionsQueryDTO} query
* @param {LandedCostTransactionsQueryDto} query -
* @returns {Promise<ILandedCostTransaction[]>}
*/
public getLandedCostTransactions = async (
query: ILandedCostTransactionsQueryDTO
query: LandedCostTransactionsQueryDto,
): Promise<ILandedCostTransaction[]> => {
const { transactionType } = query;
const Model = this.transactionLandedCost.getModel(
query.transactionType
const Model = await this.transactionLandedCost.getModel(
query.transactionType,
);
// Retrieve the model entities.
const transactions = await Model.query().onBuild((q) => {
q.where('allocated_cost_amount', '<', ref('landed_cost_amount'));
const transactions = await Model()
.query()
.onBuild((q) => {
q.where('allocated_cost_amount', '<', ref('landed_cost_amount'));
if (query.transactionType === 'Bill') {
q.withGraphFetched('entries.item');
} else if (query.transactionType === 'Expense') {
q.withGraphFetched('categories.expenseAccount');
}
});
const transformLandedCost =
this.transactionLandedCost.transformToLandedCost(transactionType);
if (query.transactionType === 'Bill') {
q.withGraphFetched('entries.item');
} else if (query.transactionType === 'Expense') {
q.withGraphFetched('categories.expenseAccount');
}
});
const transformLandedCost = curry(
this.transactionLandedCost.transformToLandedCost,
)(transactionType);
return R.compose(
return pipe(
this.transformLandedCostTransactions,
R.map(transformLandedCost)
R.map(transformLandedCost),
)(transactions);
};
/**
*
* @param transactions
* @returns
* Transformes the landed cost transactions.
* @param {ILandedCostTransaction[]} transactions
* @returns {ILandedCostTransactionDOJO[]}
*/
public transformLandedCostTransactions = (
transactions: ILandedCostTransaction[]
transactions: ILandedCostTransaction[],
) => {
return R.map(this.transformLandedCostTransaction)(transactions);
};
/**
* Transformes the landed cost transaction.
* @param {ILandedCostTransaction} transaction
* @param {ILandedCostTransaction} transaction - Landed cost transaction.
* @returns {ILandedCostTransactionDOJO}
*/
public transformLandedCostTransaction = (
transaction: ILandedCostTransaction
transaction: ILandedCostTransaction,
): ILandedCostTransactionDOJO => {
const { currencyCode } = transaction;
@@ -74,57 +77,60 @@ export default class LandedCostTranasctions {
// Formatted transaction unallocated cost amount.
const formattedUnallocatedCostAmount = formatNumber(
transaction.unallocatedCostAmount,
{ currencyCode }
{ currencyCode },
);
// Formatted transaction allocated cost amount.
const formattedAllocatedCostAmount = formatNumber(
transaction.allocatedCostAmount,
{ currencyCode }
{ currencyCode },
);
const transformLandedCostEntry = R.curry(this.transformLandedCostEntry)(
transaction,
);
const entries = R.map<
ILandedCostTransactionEntry,
ILandedCostTransactionEntryDOJO
>(transformLandedCostEntry)(transaction.entries);
return {
...transaction,
formattedAmount,
formattedUnallocatedCostAmount,
formattedAllocatedCostAmount,
entries: R.map(this.transformLandedCostEntry(transaction))(
transaction.entries
),
entries,
};
};
/**
*
* @param {ILandedCostTransaction} transaction
* @param {ILandedCostTransactionEntry} entry
* Transformes the landed cost transaction entry.
* @param {ILandedCostTransaction} transaction - Landed cost transaction.
* @param {ILandedCostTransactionEntry} entry - Landed cost transaction entry.
* @returns {ILandedCostTransactionEntryDOJO}
*/
public transformLandedCostEntry = R.curry(
(
transaction: ILandedCostTransaction,
entry: ILandedCostTransactionEntry
): ILandedCostTransactionEntryDOJO => {
const { currencyCode } = transaction;
public transformLandedCostEntry = (
transaction: ILandedCostTransaction,
entry: ILandedCostTransactionEntry,
): ILandedCostTransactionEntryDOJO => {
const { currencyCode } = transaction;
// Formatted entry amount.
const formattedAmount = formatNumber(entry.amount, { currencyCode });
// Formatted entry amount.
const formattedAmount = formatNumber(entry.amount, { currencyCode });
// Formatted entry unallocated cost amount.
const formattedUnallocatedCostAmount = formatNumber(
entry.unallocatedCostAmount,
{ currencyCode }
);
// Formatted entry allocated cost amount.
const formattedAllocatedCostAmount = formatNumber(
entry.allocatedCostAmount,
{ currencyCode }
);
return {
...entry,
formattedAmount,
formattedUnallocatedCostAmount,
formattedAllocatedCostAmount,
};
}
);
}
// Formatted entry unallocated cost amount.
const formattedUnallocatedCostAmount = formatNumber(
entry.unallocatedCostAmount,
{ currencyCode },
);
// Formatted entry allocated cost amount.
const formattedAllocatedCostAmount = formatNumber(
entry.allocatedCostAmount,
{ currencyCode },
);
return {
...entry,
formattedAmount,
formattedUnallocatedCostAmount,
formattedAllocatedCostAmount,
};
};
}

View File

@@ -7,7 +7,6 @@ import { events } from '@/common/events/events';
import { IAllocatedLandedCostDeletedPayload } from '../types/BillLandedCosts.types';
import { BillLandedCostEntry } from '../models/BillLandedCostEntry';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { BillLandedCost } from '../models/BillLandedCost';
@Injectable()
export class RevertAllocatedLandedCost extends BaseLandedCostService {
@@ -15,11 +14,6 @@ export class RevertAllocatedLandedCost extends BaseLandedCostService {
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
@Inject(BillLandedCost.name)
private readonly billLandedCostModel: TenantModelProxy<
typeof BillLandedCost
>,
@Inject(BillLandedCostEntry.name)
private readonly billLandedCostEntryModel: TenantModelProxy<
typeof BillLandedCostEntry

View File

@@ -3,35 +3,46 @@ import { Model } from 'objection';
import {
ILandedCostTransaction,
ILandedCostTransactionEntry,
LandedCostTransactionModel,
LandedCostTransactionType,
} from '../types/BillLandedCosts.types';
import { Injectable } from '@nestjs/common';
import { BillLandedCost } from '../models/BillLandedCost';
import { Bill } from '@/modules/Bills/models/Bill';
import { Expense } from '@/modules/Expenses/models/Expense.model';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
import { sanitizeModelName } from '@/utils/sanitize-model-name';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { ExpenseLandedCost } from './ExpenseLandedCost.service';
import { BillLandedCost } from './BillLandedCost.service';
import { ERRORS } from '../utils';
import { ExpenseLandedCost } from '../models/ExpenseLandedCost';
@Injectable()
export class TransactionLandedCost {
constructor(
private readonly billLandedCost: BillLandedCost,
private readonly expenseLandedCost: ExpenseLandedCost,
private readonly moduleRef: ModuleRef,
) {}
/**
* Retrieve the cost transaction code model.
* @param {number} tenantId - Tenant id.
* @param {string} transactionType - Transaction type.
* @returns
*/
public getModel = (tenantId: number, transactionType: string): Model => {
const Models = this.tenancy.models(tenantId);
const Model = Models[transactionType];
public getModel = async (
transactionType: string,
): Promise<TenantModelProxy<typeof Model>> => {
const contextId = ContextIdFactory.create();
const modelName = sanitizeModelName(transactionType);
if (!Model) {
const instance = await this.moduleRef.resolve(modelName, contextId, {
strict: false,
});
if (!instance) {
throw new ServiceError(ERRORS.COST_TYPE_UNDEFINED);
}
return Model;
return instance;
};
/**
@@ -40,10 +51,10 @@ export class TransactionLandedCost {
* @param {IBill|IExpense} transaction - Expense or bill transaction.
* @returns {ILandedCostTransaction}
*/
public transformToLandedCost = R.curry(
public transformToLandedCost =
(
transactionType: string,
transaction: Bill | Expense,
transactionType: LandedCostTransactionType,
transaction: LandedCostTransactionModel,
): ILandedCostTransaction => {
return R.compose(
R.when(
@@ -54,9 +65,8 @@ export class TransactionLandedCost {
R.always(transactionType === 'Expense'),
this.expenseLandedCost.transformToLandedCost,
),
)(transaction);
},
);
)(transaction) as ILandedCostTransaction;
};
/**
* Transformes the given expense or bill entry to landed cost transaction entry.
@@ -65,7 +75,7 @@ export class TransactionLandedCost {
* @returns {ILandedCostTransactionEntry}
*/
public transformToLandedCostEntry = (
transactionType: 'Bill' | 'Expense',
transactionType: LandedCostTransactionType,
transactionEntry,
): ILandedCostTransactionEntry => {
return R.compose(
@@ -77,6 +87,6 @@ export class TransactionLandedCost {
R.always(transactionType === 'Expense'),
this.expenseLandedCost.transformToLandedCostEntry,
),
)(transactionEntry);
)(transactionEntry) as ILandedCostTransactionEntry;
};
}

View File

@@ -10,8 +10,9 @@ import {
} from 'class-validator';
import { Type } from 'class-transformer';
import { ToNumber } from '@/common/decorators/Validators';
import { LandedCostTransactionType } from '../types/BillLandedCosts.types';
class AllocateBillLandedCostItemDto {
export class AllocateBillLandedCostItemDto {
@IsInt()
@ToNumber()
entryId: number;
@@ -26,7 +27,7 @@ export class AllocateBillLandedCostDto {
transactionId: number;
@IsIn(['Expense', 'Bill'])
transactionType: string;
transactionType: LandedCostTransactionType;
@IsInt()
transactionEntryId: number;

View File

@@ -0,0 +1,14 @@
import { IsDateString, IsEnum, IsIn, IsNotEmpty, IsOptional, IsString } from "class-validator";
import { LandedCostTransactionType } from "../types/BillLandedCosts.types";
export class LandedCostTransactionsQueryDto {
@IsString()
@IsNotEmpty()
@IsIn(['Expense', 'Bill'])
transactionType: LandedCostTransactionType;
@IsDateString()
@IsOptional()
date: string;
}

View File

@@ -1,5 +1,7 @@
import { Knex } from 'knex';
import { Bill } from '@/modules/Bills/models/Bill';
import { ModelObject } from 'objection';
import { Expense } from '@/modules/Expenses/models/Expense.model';
export interface ILandedCostItemDTO {
entryId: number;
@@ -140,3 +142,7 @@ interface ICommonEntryDTO {
export interface ICommonLandedCostEntryDTO extends ICommonEntryDTO {
landedCost?: boolean;
}
export type LandedCostTransactionType = 'Bill' | 'Expense';
export type LandedCostTransactionModel = Bill | Expense;

View File

@@ -1,5 +1,7 @@
import { IItemEntry, IBillLandedCostTransactionEntry } from '@/interfaces';
import { transformToMap } from 'utils';
import { ModelObject } from 'objection';
import { transformToMap } from '@/utils/transform-to-key';
import { IBillLandedCostTransactionEntry } from './types/BillLandedCosts.types';
import { ItemEntry } from '../TransactionItemEntry/models/ItemEntry';
export const ERRORS = {
COST_TYPE_UNDEFINED: 'COST_TYPE_UNDEFINED',
@@ -23,8 +25,8 @@ export const ERRORS = {
*/
export const mergeLocatedWithBillEntries = (
locatedEntries: IBillLandedCostTransactionEntry[],
billEntries: IItemEntry[]
): (IBillLandedCostTransactionEntry & { entry: IItemEntry })[] => {
billEntries: ModelObject<ItemEntry>[]
): (IBillLandedCostTransactionEntry & { entry: ModelObject<ItemEntry> })[] => {
const billEntriesByEntryId = transformToMap(billEntries, 'id');
return locatedEntries.map((entry) => ({

View File

@@ -9,16 +9,27 @@ import {
Query,
} from '@nestjs/common';
import { BillPaymentsApplication } from './BillPaymentsApplication.service';
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import {
ApiExtraModels,
ApiOperation,
ApiParam,
ApiResponse,
ApiTags,
getSchemaPath,
} from '@nestjs/swagger';
import {
CreateBillPaymentDto,
EditBillPaymentDto,
} from './dtos/BillPayment.dto';
import { GetBillPaymentsFilterDto } from './dtos/GetBillPaymentsFilter.dto';
import { BillPaymentsPages } from './commands/BillPaymentsPages.service';
import { BillPaymentResponseDto } from './dtos/BillPaymentResponse.dto';
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
@Controller('bill-payments')
@ApiTags('bill-payments')
@ApiTags('Bill Payments')
@ApiExtraModels(BillPaymentResponseDto)
@ApiExtraModels(PaginatedResponseDto)
export class BillPaymentsController {
constructor(
private billPaymentsApplication: BillPaymentsApplication,
@@ -114,6 +125,13 @@ export class BillPaymentsController {
@Get(':billPaymentId')
@ApiOperation({ summary: 'Retrieves the bill payment details.' })
@ApiResponse({
status: 200,
description: 'The bill payment details have been successfully retrieved.',
schema: {
$ref: getSchemaPath(BillPaymentResponseDto),
},
})
@ApiParam({
name: 'billPaymentId',
required: true,
@@ -126,6 +144,23 @@ export class BillPaymentsController {
@Get()
@ApiOperation({ summary: 'Retrieves the bill payments list.' })
@ApiResponse({
status: 200,
description: 'The bill payments have been successfully retrieved.',
schema: {
allOf: [
{ $ref: getSchemaPath(PaginatedResponseDto) },
{
properties: {
data: {
type: 'array',
items: { $ref: getSchemaPath(BillPaymentResponseDto) },
},
},
},
],
},
})
@ApiParam({
name: 'filterDTO',
required: true,

View File

@@ -14,7 +14,7 @@ export const ERRORS = {
WITHDRAWAL_ACCOUNT_CURRENCY_INVALID: 'WITHDRAWAL_ACCOUNT_CURRENCY_INVALID',
};
export const DEFAULT_VIEWS = [];
export const BillPaymentDefaultViews = [];
export const BillsPaymentsSampleData = [
{

View File

@@ -0,0 +1,140 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto';
// Minimal Bill response for entry
class BillResponseDto {
@ApiProperty({ description: 'The bill ID', example: 1 })
id: number;
@ApiProperty({ description: 'The bill number', example: 'BILL-001' })
billNo: string;
@ApiProperty({
description: 'The formatted bill date',
example: '2024-01-01',
})
formattedBillDate: string;
@ApiProperty({ description: 'The formatted due date', example: '2024-01-15' })
formattedDueDate: string;
@ApiProperty({
description: 'The formatted total amount',
example: '1,000.00 USD',
})
totalFormatted: string;
}
export class BillPaymentEntryResponseDto {
@ApiProperty({
description: 'The payment amount formatted',
example: '100.00',
})
paymentAmountFormatted: string;
@ApiProperty({ description: 'The bill details', type: BillResponseDto })
@Type(() => BillResponseDto)
bill: BillResponseDto;
}
export class BillPaymentResponseDto {
@ApiProperty({
description: 'The unique identifier of the bill payment',
example: 1,
})
id: number;
@ApiProperty({ description: 'The vendor ID', example: 1 })
vendorId: number;
@ApiProperty({ description: 'The amount paid', example: 100 })
amount: number;
@ApiProperty({
description: 'The currency code',
example: 'USD',
required: false,
})
currencyCode?: string;
@ApiProperty({ description: 'The payment account ID', example: 2 })
paymentAccountId: number;
@ApiProperty({
description: 'The payment number',
example: 'PAY-2024-001',
required: false,
})
paymentNumber?: string;
@ApiProperty({ description: 'The payment date', example: '2024-01-01' })
paymentDate: string;
@ApiProperty({
description: 'The formatted payment date',
example: '2024-01-01',
})
formattedPaymentDate: string;
@ApiProperty({
description: 'The exchange rate',
example: 1,
required: false,
})
exchangeRate?: number;
@ApiProperty({
description: 'Statement or note',
example: 'Payment for January bills',
required: false,
})
statement?: string;
@ApiProperty({
description: 'Reference number',
example: 'REF-123',
required: false,
})
reference?: string;
@ApiProperty({ description: 'The branch ID', example: 1, required: false })
branchId?: number;
@ApiProperty({ description: 'The formatted amount', example: '100.00 USD' })
formattedAmount: string;
@ApiProperty({
description: 'The date when the payment was created',
example: '2024-01-01T12:00:00Z',
})
createdAt: Date;
@ApiProperty({
description: 'The formatted created at date',
example: '2024-01-01',
})
formattedCreatedAt: string;
@ApiProperty({
description: 'The date when the payment was last updated',
example: '2024-01-02T12:00:00Z',
required: false,
})
updatedAt?: Date;
@ApiProperty({
description: 'The entries of the bill payment',
type: [BillPaymentEntryResponseDto],
})
@Type(() => BillPaymentEntryResponseDto)
entries: BillPaymentEntryResponseDto[];
@ApiProperty({
description: 'The attachments of the bill payment',
type: [AttachmentLinkDto],
required: false,
})
@Type(() => AttachmentLinkDto)
attachments?: AttachmentLinkDto[];
}

View File

@@ -1,3 +1,4 @@
import { Model } from 'objection';
import { BillPaymentEntry } from './BillPaymentEntry';
import { Vendor } from '@/modules/Vendors/models/Vendor';
import { Document } from '@/modules/ChromiumlyTenancy/models/Document';
@@ -6,11 +7,13 @@ 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';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { BillPaymentDefaultViews } from '../constants';
@ImportableModel()
@ExportableModel()
@InjectModelMeta(BillPaymentMeta)
@InjectModelDefaultViews(BillPaymentDefaultViews)
export class BillPayment extends TenantBaseModel {
vendorId: number;
amount: number;
@@ -61,7 +64,7 @@ export class BillPayment extends TenantBaseModel {
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Relationship mapping.
*/
@@ -154,13 +157,6 @@ export class BillPayment extends TenantBaseModel {
};
}
/**
* Retrieve the default custom views, roles and columns.
*/
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search attributes.
*/

View File

@@ -8,6 +8,7 @@ import { OpenBillService } from './commands/OpenBill.service';
import { Injectable } from '@nestjs/common';
import { GetBillsService } from './queries/GetBills.service';
import { CreateBillDto, EditBillDto } from './dtos/Bill.dto';
import { GetBillPaymentTransactionsService } from './queries/GetBillPayments';
// import { GetBillPayments } from './queries/GetBillPayments';
// import { GetBills } from './queries/GetBills';
@@ -21,7 +22,7 @@ export class BillsApplication {
private getDueBillsService: GetDueBills,
private openBillService: OpenBillService,
private getBillsService: GetBillsService,
// private getBillPaymentsService: GetBillPayments,
private getBillPaymentTransactionsService: GetBillPaymentTransactionsService,
) {}
/**
@@ -71,7 +72,6 @@ export class BillsApplication {
/**
* Open the given bill.
* @param {number} tenantId
* @param {number} billId
* @returns {Promise<void>}
*/
@@ -91,10 +91,11 @@ export class BillsApplication {
/**
* Retrieve the specific bill associated payment transactions.
* @param {number} tenantId
* @param {number} billId
*/
// public getBillPayments(billId: number) {
// return this.getBillPaymentsService.getBillPayments(billId);
// }
public getBillPaymentTransactions(billId: number) {
return this.getBillPaymentTransactionsService.getBillPaymentTransactions(
billId,
);
}
}

View File

@@ -23,7 +23,7 @@ export const ERRORS = {
export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [
export const BillDefaultViews = [
{
name: 'Draft',
slug: 'draft',

View File

@@ -1,4 +1,11 @@
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import {
ApiExtraModels,
ApiOperation,
ApiParam,
ApiResponse,
ApiTags,
getSchemaPath,
} from '@nestjs/swagger';
import {
Controller,
Post,
@@ -12,9 +19,13 @@ import {
import { BillsApplication } from './Bills.application';
import { IBillsFilter } from './Bills.types';
import { CreateBillDto, EditBillDto } from './dtos/Bill.dto';
import { BillResponseDto } from './dtos/BillResponse.dto';
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
@Controller('bills')
@ApiTags('bills')
@ApiTags('Bills')
@ApiExtraModels(BillResponseDto)
@ApiExtraModels(PaginatedResponseDto)
export class BillsController {
constructor(private billsApplication: BillsApplication) {}
@@ -50,6 +61,23 @@ export class BillsController {
@Get()
@ApiOperation({ summary: 'Retrieves the bills.' })
@ApiResponse({
status: 200,
description: 'The bill details has been retrieved successfully',
schema: {
allOf: [
{ $ref: getSchemaPath(PaginatedResponseDto) },
{
properties: {
data: {
type: 'array',
items: { $ref: getSchemaPath(BillResponseDto) },
},
},
},
],
},
})
@ApiParam({
name: 'id',
required: true,
@@ -60,8 +88,29 @@ export class BillsController {
return this.billsApplication.getBills(filterDTO);
}
@Get(':id/payment-transactions')
@ApiOperation({
summary: 'Retrieve the specific bill associated payment transactions.',
})
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The bill id',
})
getBillPaymentTransactions(@Param('id') billId: number) {
return this.billsApplication.getBillPaymentTransactions(billId);
}
@Get(':id')
@ApiOperation({ summary: 'Retrieves the bill details.' })
@ApiResponse({
status: 200,
description: 'The bill details have been successfully retrieved.',
schema: {
$ref: getSchemaPath(BillResponseDto),
},
})
@ApiParam({
name: 'id',
required: true,

View File

@@ -28,6 +28,7 @@ import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
import { BillsExportable } from './commands/BillsExportable';
import { BillsImportable } from './commands/BillsImportable';
import { GetBillPaymentTransactionsService } from './queries/GetBillPayments';
@Module({
imports: [
@@ -60,7 +61,8 @@ import { BillsImportable } from './commands/BillsImportable';
BillInventoryTransactions,
BillWriteInventoryTransactionsSubscriber,
BillsExportable,
BillsImportable
BillsImportable,
GetBillPaymentTransactionsService,
],
controllers: [BillsController],
exports: [BillsExportable, BillsImportable],

View File

@@ -1,6 +1,7 @@
import { ToNumber } from '@/common/decorators/Validators';
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
import {
ArrayMinSize,
IsArray,
@@ -23,93 +24,187 @@ enum DiscountType {
}
export class BillEntryDto extends ItemEntryDto {
@ApiProperty({
description: 'Flag indicating whether the entry contributes to landed cost',
example: true,
required: false,
})
@IsOptional()
@IsBoolean()
landedCost?: boolean;
}
class AttachmentDto {
@ApiProperty({
description: 'Storage key of the attachment file',
example: 'attachments/bills/receipt.pdf',
})
@IsString()
@IsNotEmpty()
key: string;
}
export class CommandBillDto {
@ApiProperty({
description: 'Unique bill number',
example: 'BILL-2024-001',
required: false,
})
@IsOptional()
@IsString()
billNumber: string;
@ApiProperty({
description: 'Reference number',
example: 'PO-2024-001',
required: false,
})
@IsOptional()
@IsString()
referenceNo?: string;
@ApiProperty({
description: 'Date the bill was issued',
example: '2024-03-15',
})
@IsNotEmpty()
@IsDateString()
billDate: Date;
@ApiProperty({
description: 'Date the bill is due',
example: '2024-04-15',
required: false,
})
@IsOptional()
@IsDateString()
dueDate?: Date;
@ApiProperty({
description: 'Vendor identifier',
example: 1001,
})
@IsInt()
@IsNotEmpty()
vendorId: number;
@ApiProperty({
description: 'Exchange rate applied to bill amounts',
example: 1.25,
required: false,
})
@IsOptional()
@ToNumber()
@IsNumber()
@IsPositive()
exchangeRate?: number;
@ApiProperty({
description: 'Warehouse identifier',
example: 101,
required: false,
})
@IsOptional()
@ToNumber()
@IsInt()
warehouseId?: number;
@ApiProperty({
description: 'Branch identifier',
example: 201,
required: false,
})
@IsOptional()
@ToNumber()
@IsInt()
branchId?: number;
@ApiProperty({
description: 'Project identifier',
example: 301,
required: false,
})
@IsOptional()
@ToNumber()
@IsInt()
projectId?: number;
@ApiProperty({
description: 'Additional notes about the bill',
example: 'Office supplies and equipment for Q2 2024',
required: false,
})
@IsOptional()
@IsString()
note?: string;
@ApiProperty({
description: 'Indicates if the bill is open',
example: true,
required: false,
})
@IsBoolean()
@IsOptional()
open: boolean = false;
@ApiProperty({
description: 'Indicates if tax is inclusive in prices',
example: false,
required: false,
})
@IsBoolean()
@IsOptional()
isInclusiveTax: boolean = false;
@ApiProperty({
description: 'Bill line items',
type: () => BillEntryDto,
isArray: true,
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => BillEntryDto)
@ArrayMinSize(1)
entries: BillEntryDto[];
@ApiProperty({
description: 'File attachments associated with the bill',
type: () => AttachmentDto,
isArray: true,
required: false,
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AttachmentDto)
attachments?: AttachmentDto[];
@ApiProperty({
description: 'Type of discount applied',
example: DiscountType.Amount,
enum: DiscountType,
required: false,
})
@IsEnum(DiscountType)
@IsOptional()
discountType: DiscountType = DiscountType.Amount;
@ApiProperty({
description: 'Discount value',
example: 100,
required: false,
})
@IsOptional()
@ToNumber()
@IsNumber()
@IsPositive()
discount?: number;
@ApiProperty({
description: 'Adjustment value',
example: 50,
required: false,
})
@IsOptional()
@ToNumber()
@IsNumber()

View File

@@ -0,0 +1,202 @@
import { ApiProperty } from '@nestjs/swagger';
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto';
import { DiscountType } from '@/common/types/Discount';
export class BillResponseDto {
@ApiProperty({
description: 'The unique identifier of the bill',
example: 1,
})
id: number;
@ApiProperty({
description: 'The bill number',
example: 'BILL-2024-001',
})
billNumber: string;
@ApiProperty({
description: 'The date the bill was issued',
example: '2024-03-15T00:00:00Z',
})
billDate: Date;
@ApiProperty({
description: 'The due date of the bill',
example: '2024-04-15T00:00:00Z',
})
dueDate: Date;
@ApiProperty({
description: 'The reference number',
example: 'PO-2024-001',
required: false,
})
referenceNo?: string;
@ApiProperty({
description: 'The ID of the vendor',
example: 1001,
})
vendorId: number;
@ApiProperty({
description: 'The exchange rate for currency conversion',
example: 1.25,
required: false,
})
exchangeRate?: number;
@ApiProperty({
description: 'The currency code',
example: 'USD',
required: false,
})
currencyCode?: string;
@ApiProperty({
description: 'Additional notes about the bill',
example: 'Office supplies and equipment for Q2 2024',
required: false,
})
note?: string;
@ApiProperty({
description: 'Whether tax is inclusive in the item rates',
example: false,
required: false,
})
isInclusiveTax?: boolean;
@ApiProperty({
description: 'The line items of the bill',
type: [ItemEntryDto],
})
entries: ItemEntryDto[];
@ApiProperty({
description: 'The ID of the warehouse',
example: 101,
required: false,
})
warehouseId?: number;
@ApiProperty({
description: 'The ID of the branch',
example: 201,
required: false,
})
branchId?: number;
@ApiProperty({
description: 'The ID of the project',
example: 301,
required: false,
})
projectId?: number;
@ApiProperty({
description: 'The attachments of the bill',
type: [AttachmentLinkDto],
required: false,
})
attachments?: AttachmentLinkDto[];
@ApiProperty({
description: 'The discount value',
example: 100,
required: false,
})
discount?: number;
@ApiProperty({
description: 'The type of discount (percentage or fixed)',
enum: DiscountType,
example: DiscountType.Amount,
required: false,
})
discountType?: DiscountType;
@ApiProperty({
description: 'The adjustment amount',
example: 50,
required: false,
})
adjustment?: number;
@ApiProperty({
description: 'The total amount of tax withheld',
example: 50,
required: false,
})
taxAmountWithheld?: number;
@ApiProperty({
description: 'The balance of the bill',
example: 1000,
})
balance: number;
@ApiProperty({
description: 'The amount paid',
example: 500,
})
paymentAmount: number;
@ApiProperty({
description: 'The amount credited',
example: 0,
required: false,
})
creditedAmount?: number;
@ApiProperty({
description: 'The subtotal amount before tax and adjustments',
example: 900,
})
subtotal: number;
@ApiProperty({
description: 'The total amount including tax and adjustments',
example: 1000,
})
total: number;
@ApiProperty({
description: 'The due amount remaining to be paid',
example: 500,
})
dueAmount: number;
@ApiProperty({
description: 'Whether the bill is overdue',
example: false,
})
isOverdue: boolean;
@ApiProperty({
description: 'Whether the bill is partially paid',
example: true,
})
isPartiallyPaid: boolean;
@ApiProperty({
description: 'Whether the bill is fully paid',
example: false,
})
isFullyPaid: boolean;
@ApiProperty({
description: 'The date when the bill was created',
example: '2024-03-15T00:00:00Z',
})
createdAt: Date;
@ApiProperty({
description: 'The date when the bill was last updated',
example: '2024-03-16T00:00:00Z',
required: false,
})
updatedAt?: Date;
}

View File

@@ -11,9 +11,12 @@ import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator';
import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator';
import { BillMeta } from './Bill.meta';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { BillDefaultViews } from '../Bills.constants';
@ExportableModel()
@InjectModelMeta(BillMeta)
@InjectModelDefaultViews(BillDefaultViews)
export class Bill extends TenantBaseModel {
public amount: number;
public paymentAmount: number;
@@ -486,7 +489,9 @@ export class Bill extends TenantBaseModel {
TaxRateTransaction,
} = require('../../TaxRates/models/TaxRateTransaction.model');
const { Document } = require('../../ChromiumlyTenancy/models/Document');
const { MatchedBankTransaction } = require('../../BankingMatching/models/MatchedBankTransaction');
const {
MatchedBankTransaction,
} = require('../../BankingMatching/models/MatchedBankTransaction');
return {
vendor: {
@@ -631,13 +636,6 @@ export class Bill extends TenantBaseModel {
[changeMethod]('payment_amount', Math.abs(amount));
}
/**
* Retrieve the default custom views, roles and columns.
*/
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search attributes.
*/

View File

@@ -2,12 +2,13 @@ import { Inject, Injectable } from '@nestjs/common';
import { BillPaymentEntry } from '@/modules/BillPayments/models/BillPaymentEntry';
import { BillPaymentTransactionTransformer } from '@/modules/BillPayments/queries/BillPaymentTransactionTransformer';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetBillPayments {
export class GetBillPaymentTransactionsService {
constructor(
@Inject(BillPaymentEntry.name)
private billPaymentEntryModel: typeof BillPaymentEntry,
private billPaymentEntryModel: TenantModelProxy<typeof BillPaymentEntry>,
private transformer: TransformerInjectable,
) {}
@@ -15,8 +16,8 @@ export class GetBillPayments {
* Retrieve the specific bill associated payment transactions.
* @param {number} billId - Bill id.
*/
public getBillPayments = async (billId: number) => {
const billsEntries = await this.billPaymentEntryModel
public getBillPaymentTransactions = async (billId: number) => {
const billsEntries = await this.billPaymentEntryModel()
.query()
.where('billId', billId)
.withGraphJoined('payment.paymentAccount')

View File

@@ -9,10 +9,18 @@ import {
} from '@nestjs/common';
import { BranchesApplication } from './BranchesApplication.service';
import { CreateBranchDto, EditBranchDto } from './dtos/Branch.dto';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import {
ApiExtraModels,
ApiOperation,
ApiResponse,
ApiTags,
getSchemaPath,
} from '@nestjs/swagger';
import { BranchResponseDto } from './dtos/BranchResponse.dto';
@Controller('branches')
@ApiTags('branches')
@ApiTags('Branches')
@ApiExtraModels(BranchResponseDto)
export class BranchesController {
constructor(private readonly branchesApplication: BranchesApplication) {}
@@ -21,6 +29,12 @@ export class BranchesController {
@ApiResponse({
status: 200,
description: 'The branches have been successfully retrieved.',
schema: {
type: 'array',
items: {
$ref: getSchemaPath(BranchResponseDto),
},
},
})
getBranches() {
return this.branchesApplication.getBranches();
@@ -31,6 +45,9 @@ export class BranchesController {
@ApiResponse({
status: 200,
description: 'The branch details have been successfully retrieved.',
schema: {
$ref: getSchemaPath(BranchResponseDto),
},
})
@ApiResponse({ status: 404, description: 'The branch not found.' })
getBranch(@Param('id') id: string) {
@@ -42,6 +59,9 @@ export class BranchesController {
@ApiResponse({
status: 200,
description: 'The branch has been successfully created.',
schema: {
$ref: getSchemaPath(BranchResponseDto),
},
})
@ApiResponse({ status: 404, description: 'The branch not found.' })
createBranch(@Body() createBranchDTO: CreateBranchDto) {
@@ -53,6 +73,9 @@ export class BranchesController {
@ApiResponse({
status: 200,
description: 'The branch has been successfully edited.',
schema: {
$ref: getSchemaPath(BranchResponseDto),
},
})
@ApiResponse({ status: 404, description: 'The branch not found.' })
editBranch(@Param('id') id: string, @Body() editBranchDTO: EditBranchDto) {
@@ -90,6 +113,9 @@ export class BranchesController {
@ApiResponse({
status: 200,
description: 'The branch has been successfully marked as primary.',
schema: {
$ref: getSchemaPath(BranchResponseDto),
},
})
@ApiResponse({ status: 404, description: 'The branch not found.' })
markBranchAsPrimary(@Param('id') id: string) {

View File

@@ -39,9 +39,6 @@ export class DeleteBranchService {
.query()
.findById(branchId)
.throwIfNotFound();
// .queryAndThrowIfHasRelations({
// type: ERRORS.BRANCH_HAS_ASSOCIATED_TRANSACTIONS,
// });
// Authorize the branch before deleting.
await this.authorize(branchId);
@@ -54,8 +51,10 @@ export class DeleteBranchService {
trx,
} as IBranchDeletePayload);
await this.branchModel().query().findById(branchId).delete();
await this.branchModel().query().findById(branchId).deleteIfNoRelations({
type: ERRORS.BRANCH_HAS_ASSOCIATED_TRANSACTIONS,
message: 'Branch has associated transactions',
});
// Triggers `onBranchCreate` event.
await this.eventPublisher.emitAsync(events.warehouse.onEdited, {
oldBranch,

View File

@@ -0,0 +1,75 @@
import { ApiProperty } from '@nestjs/swagger';
export class BranchResponseDto {
@ApiProperty({
description: 'Branch ID',
example: 1,
})
id: number;
@ApiProperty({
description: 'Branch name',
example: 'Main Branch',
})
name: string;
@ApiProperty({
description: 'Branch code',
example: 'BR001',
})
code: string;
@ApiProperty({
description: 'Branch address',
example: '123 Main Street',
})
address: string;
@ApiProperty({
description: 'Branch city',
example: 'New York',
})
city: string;
@ApiProperty({
description: 'Branch country',
example: 'USA',
})
country: string;
@ApiProperty({
description: 'Branch phone number',
example: '+1-555-123-4567',
})
phoneNumber: string;
@ApiProperty({
description: 'Branch email',
example: 'branch@example.com',
})
email: string;
@ApiProperty({
description: 'Branch website',
example: 'https://www.example.com/branch',
})
website: string;
@ApiProperty({
description: 'Whether this is the primary branch',
example: true,
})
primary: boolean;
@ApiProperty({
description: 'Creation timestamp',
example: '2024-03-20T10:00:00Z',
})
createdAt: Date;
@ApiProperty({
description: 'Last update timestamp',
example: '2024-03-20T10:00:00Z',
})
updatedAt: Date;
}

View File

@@ -9,5 +9,5 @@ export const getPdfFilesStorageDir = (filename: string) => {
export const getPdfFilePath = (filename: string) => {
const storageDir = getPdfFilesStorageDir(filename);
return path.join(global.__static_dirname, storageDir);
return path.join(global.__public_dirname, storageDir);
};

View File

@@ -13,7 +13,7 @@ import { ActivateContactService } from './commands/ActivateContact.service';
import { InactivateContactService } from './commands/InactivateContact.service';
@Controller('contacts')
@ApiTags('contacts')
@ApiTags('Contacts')
export class ContactsController {
constructor(
private readonly getAutoCompleteService: GetAutoCompleteContactsService,

View File

@@ -6,7 +6,7 @@ import { RefundCreditNote } from './models/RefundCreditNote';
import { CreditNoteRefundDto } from './dto/CreditNoteRefund.dto';
@Controller('credit-notes')
@ApiTags('credit-notes-refunds')
@ApiTags('Credit Note Refunds')
export class CreditNoteRefundsController {
constructor(
private readonly creditNotesRefundsApplication: CreditNotesRefundsApplication,

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