mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-10 18:01:59 +00:00
Compare commits
25 Commits
refactor-l
...
api-keys
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adb1bea374 | ||
|
|
5d96357042 | ||
|
|
9457b3cda1 | ||
|
|
84cb7693c8 | ||
|
|
9f6e9e85a5 | ||
|
|
83e698acf3 | ||
|
|
fa5c3bd955 | ||
|
|
0ca98c7ae4 | ||
|
|
0c0e1dc22e | ||
|
|
e7178a6575 | ||
|
|
6a39e9d71f | ||
|
|
9aa1ed93ca | ||
|
|
b8c9919799 | ||
|
|
e5701140e1 | ||
|
|
91976842a7 | ||
|
|
4d52059dba | ||
|
|
26c1f118c1 | ||
|
|
437bcb8854 | ||
|
|
f624cf7ae6 | ||
|
|
e057b4e2f0 | ||
|
|
c4668d7d22 | ||
|
|
88ef60ef28 | ||
|
|
bbf9ef9bc2 | ||
|
|
bcae2dae03 | ||
|
|
ff93168d72 |
@@ -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
0
packages/server/public/pdf/.gitignore
vendored
Normal file
33
packages/server/src/common/dtos/PaginatedResults.dto.ts
Normal file
33
packages/server/src/common/dtos/PaginatedResults.dto.ts
Normal 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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
7
packages/server/src/common/types/Objection.d.ts
vendored
Normal file
7
packages/server/src/common/types/Objection.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
}
|
||||
3
packages/server/src/i18n/en/branches.json
Normal file
3
packages/server/src/i18n/en/branches.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"head_branch": "Head Branch"
|
||||
}
|
||||
25
packages/server/src/i18n/en/transaction_type.json
Normal file
25
packages/server/src/i18n/en/transaction_type.json
Normal 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"
|
||||
}
|
||||
6
packages/server/src/i18n/en/vendor_credit.json
Normal file
6
packages/server/src/i18n/en/vendor_credit.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"view.draft": "Draft",
|
||||
"view.published": "Published",
|
||||
"view.open": "Open",
|
||||
"view.closed": "Closed"
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
|
||||
171
packages/server/src/modules/Accounts/dtos/AccountResponse.dto.ts
Normal file
171
packages/server/src/modules/Accounts/dtos/AccountResponse.dto.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -26,3 +26,5 @@ export const SendResetPasswordMailJob = 'SendResetPasswordMailJob';
|
||||
export const SendSignupVerificationMailQueue =
|
||||
'SendSignupVerificationMailQueue';
|
||||
export const SendSignupVerificationMailJob = 'SendSignupVerificationMailJob';
|
||||
|
||||
export const AuthApiKeyPrefix = 'bc_';
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 : '';
|
||||
};
|
||||
|
||||
26
packages/server/src/modules/Auth/AuthApiKeys.controllers.ts
Normal file
26
packages/server/src/modules/Auth/AuthApiKeys.controllers.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
13
packages/server/src/modules/Auth/api-key/AuthApiKey.guard.ts
Normal file
13
packages/server/src/modules/Auth/api-key/AuthApiKey.guard.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
24
packages/server/src/modules/Auth/api-key/MixedAuth.guard.ts
Normal file
24
packages/server/src/modules/Auth/api-key/MixedAuth.guard.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()],
|
||||
|
||||
72
packages/server/src/modules/Auth/models/ApiKey.model.ts
Normal file
72
packages/server/src/modules/Auth/models/ApiKey.model.ts
Normal 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');
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
|
||||
export class GetApiKeysTransformer extends Transformer {
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['tenantId'];
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { UncategorizedTransactionTransformer } from '@/modules/BankingCategorize/commands/UncategorizedTransaction.transformer';
|
||||
|
||||
export class ExcludedBankTransactionTransformer extends UncategorizedTransactionTransformer {}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
// );
|
||||
// };
|
||||
// }
|
||||
@@ -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
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ERRORS = {
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
|
||||
export const DEFAULT_VIEWS = [
|
||||
export const BillDefaultViews = [
|
||||
{
|
||||
name: 'Draft',
|
||||
slug: 'draft',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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()
|
||||
|
||||
202
packages/server/src/modules/Bills/dtos/BillResponse.dto.ts
Normal file
202
packages/server/src/modules/Bills/dtos/BillResponse.dto.ts
Normal 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;
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user