Merge branch 'develop' into feat/financial-audit-trail
This commit is contained in:
@@ -4,7 +4,7 @@ FROM node:18.16.0-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm
|
||||
RUN npm install -g pnpm@8.10.2
|
||||
RUN npm install -g pnpm@9.0.5
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache python3 build-base chromium
|
||||
@@ -15,18 +15,13 @@ ENV PYTHON=/usr/bin/python3
|
||||
# Copy package files for dependency installation
|
||||
COPY --chown=node:node package.json pnpm-lock.yaml pnpm-workspace.yaml lerna.json ./
|
||||
COPY --chown=node:node packages/server/package.json ./packages/server/
|
||||
COPY --chown=node:node shared/bigcapital-utils/package.json ./shared/bigcapital-utils/
|
||||
COPY --chown=node:node shared/pdf-templates/package.json ./shared/pdf-templates/
|
||||
COPY --chown=node:node shared/email-components/package.json ./shared/email-components/
|
||||
COPY --chown=node:node shared ./shared
|
||||
|
||||
# Install all dependencies (including devDependencies for build)
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source code
|
||||
COPY --chown=node:node ./packages/server ./packages/server
|
||||
COPY --chown=node:node ./shared/bigcapital-utils ./shared/bigcapital-utils
|
||||
COPY --chown=node:node ./shared/pdf-templates ./shared/pdf-templates
|
||||
COPY --chown=node:node ./shared/email-components ./shared/email-components
|
||||
|
||||
# Build NestJS application
|
||||
RUN pnpm run build:server --skip-nx-cache
|
||||
@@ -37,7 +32,7 @@ FROM node:18.16.0-alpine AS production
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm for production
|
||||
RUN npm install -g pnpm@8.10.2
|
||||
RUN npm install -g pnpm@9.0.5
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
@@ -52,9 +47,7 @@ ENV PYTHON=/usr/bin/python3
|
||||
# Copy package files for production dependency installation
|
||||
COPY --chown=nodejs:nodejs package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY --chown=nodejs:nodejs packages/server/package.json ./packages/server/
|
||||
COPY --chown=nodejs:nodejs shared/bigcapital-utils/package.json ./shared/bigcapital-utils/
|
||||
COPY --chown=nodejs:nodejs shared/pdf-templates/package.json ./shared/pdf-templates/
|
||||
COPY --chown=nodejs:nodejs shared/email-components/package.json ./shared/email-components/
|
||||
COPY --chown=nodejs:nodejs shared ./shared
|
||||
|
||||
# Copy .husky directory (needed for husky install command)
|
||||
COPY --chown=nodejs:nodejs .husky ./.husky
|
||||
@@ -78,10 +71,8 @@ COPY --from=builder --chown=nodejs:nodejs /app/packages/server/static ./packages
|
||||
# Copy database migration files (needed for running migrations)
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/src/database ./packages/server/src/database
|
||||
|
||||
# Copy built shared packages (dist folders and package.json for module resolution)
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/shared/bigcapital-utils/dist ./shared/bigcapital-utils/dist
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/shared/pdf-templates/dist ./shared/pdf-templates/dist
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/shared/email-components/dist ./shared/email-components/dist
|
||||
# Copy all shared packages from builder so newly added shared packages are included automatically
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/shared ./shared
|
||||
|
||||
# Set runtime environment variables (these should be provided at runtime via docker-compose or k8s)
|
||||
ENV NODE_ENV=production
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json --watchAll",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json --forceExit",
|
||||
"cli": "ts-node -r tsconfig-paths/register src/cli.ts",
|
||||
"cli:system:migrate:latest": "ts-node -r tsconfig-paths/register src/cli.ts system:migrate:latest",
|
||||
"cli:system:migrate:rollback": "ts-node -r tsconfig-paths/register src/cli.ts system:migrate:rollback",
|
||||
@@ -27,6 +27,7 @@
|
||||
"cli:tenants:migrate:rollback": "ts-node -r tsconfig-paths/register src/cli.ts tenants:migrate:rollback",
|
||||
"cli:tenants:migrate:make": "ts-node -r tsconfig-paths/register src/cli.ts tenants:migrate:make",
|
||||
"cli:tenants:list": "ts-node -r tsconfig-paths/register src/cli.ts tenants:list",
|
||||
"openapi:export": "ts-node -r tsconfig-paths/register src/cli.ts openapi:export",
|
||||
"cli:system:seed:latest": "ts-node -r tsconfig-paths/register src/cli.ts system:seed:latest",
|
||||
"cli:tenants:seed:latest": "ts-node -r tsconfig-paths/register src/cli.ts tenants:seed:latest"
|
||||
},
|
||||
@@ -36,6 +37,7 @@
|
||||
"@bigcapital/email-components": "workspace:*",
|
||||
"@bigcapital/pdf-templates": "workspace:*",
|
||||
"@bigcapital/utils": "workspace:*",
|
||||
"@bigcapital/sdk-ts": "workspace:*",
|
||||
"@bull-board/api": "^5.22.0",
|
||||
"@bull-board/express": "^5.22.0",
|
||||
"@bull-board/nestjs": "^5.22.0",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// <reference path="./common/types/Objection.d.ts" />
|
||||
import { CommandFactory } from 'nest-commander';
|
||||
import { CLIModule } from './modules/CLI/CLI.module';
|
||||
|
||||
|
||||
@@ -6,4 +6,5 @@ export default registerAs('s3', () => ({
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
bucket: process.env.S3_BUCKET,
|
||||
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,7 @@ export const ACCOUNT_TYPE = {
|
||||
INVENTORY: 'inventory',
|
||||
OTHER_CURRENT_ASSET: 'other-current-asset',
|
||||
FIXED_ASSET: 'fixed-asset',
|
||||
NON_CURRENT_ASSET: 'none-current-asset',
|
||||
NON_CURRENT_ASSET: 'non-current-asset',
|
||||
|
||||
ACCOUNTS_PAYABLE: 'accounts-payable',
|
||||
CREDIT_CARD: 'credit-card',
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.alterTable('contacts', table => {
|
||||
table.string('code').nullable().unique();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.alterTable('contacts', table => {
|
||||
table.dropColumn('code');
|
||||
});
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Fix account type typos in the database.
|
||||
*
|
||||
* This migration corrects the following typos:
|
||||
* - 'none-current-asset' -> 'non-current-asset'
|
||||
*
|
||||
* Related GitHub issue: #1041
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex('accounts')
|
||||
.where('account_type', 'none-current-asset')
|
||||
.update({ account_type: 'non-current-asset' });
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex('accounts')
|
||||
.where('account_type', 'non-current-asset')
|
||||
.update({ account_type: 'none-current-asset' });
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.alterTable('documents', (table) => {
|
||||
table.unique('key');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable('documents', (table) => {
|
||||
table.dropUnique('key');
|
||||
});
|
||||
};
|
||||
@@ -9,7 +9,7 @@
|
||||
"non_current_assets": "Non-Current Assets",
|
||||
"liabilities_and_equity": "Liabilities and Equity",
|
||||
"liabilities": "Liabilities",
|
||||
"current_liabilties": "Current Liabilties",
|
||||
"current_liabilities": "Current Liabilities",
|
||||
"long_term_liabilities": "Long-Term Liabilities",
|
||||
"non_current_liabilities": "Non-Current Liabilities",
|
||||
"equity": "Equity",
|
||||
|
||||
@@ -9,5 +9,10 @@
|
||||
"net_cash_financing": "Net cash provided by financing activities",
|
||||
"cash_beginning_period": "Cash at beginning of period",
|
||||
"net_cash_increase": "NET CASH INCREASE FOR PERIOD",
|
||||
"cash_end_period": "CASH AT END OF PERIOD"
|
||||
"cash_end_period": "CASH AT END OF PERIOD",
|
||||
"account_name": "Account name",
|
||||
"total": "Total",
|
||||
"sheet_name": "Statement of Cash Flow",
|
||||
"from_date": "From",
|
||||
"to_date": "To"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"account_name": "Account name",
|
||||
"total": "Total",
|
||||
"percentage_column": "% of Column"
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"view.draft": "Draft",
|
||||
"view.published": "Published",
|
||||
"view.open": "Open",
|
||||
"view.closed": "Closed",
|
||||
|
||||
"field.customer": "Customer",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.credit_note_date": "Credit Note Date",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"opening_balance": "Opening balance",
|
||||
"closing_balance": "Closing balance",
|
||||
"date": "Date",
|
||||
"transaction_type": "Transaction type",
|
||||
"transaction_number": "Transaction #",
|
||||
"quantity": "Quantity",
|
||||
"rate": "Rate",
|
||||
"total": "Total",
|
||||
"value": "Value",
|
||||
"profit_margin": "Profit Margin",
|
||||
"running_quantity": "Running quantity",
|
||||
"running_value": "Running Value"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"opening_balance": "Opening balance",
|
||||
"closing_balance": "Closing balance"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"account": "Account",
|
||||
"debit": "Debit",
|
||||
"credit": "Credit",
|
||||
"total": "Total"
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { QueryBuilder, Model } from 'objection';
|
||||
import { QueryBuilder, Model, mixin } from 'objection';
|
||||
import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception';
|
||||
import { withDateSessionMixin } from './withDateSessionMixin';
|
||||
|
||||
interface PaginationResult<M extends Model> {
|
||||
results: M[];
|
||||
@@ -69,6 +70,7 @@ export class PaginationQueryBuilder<
|
||||
dependentRelationNames.forEach((relationName: string) => {
|
||||
recordQuery.withGraphFetched(relationName);
|
||||
});
|
||||
|
||||
const record = await recordQuery;
|
||||
|
||||
const hasRelations = dependentRelationNames.some((name) => {
|
||||
@@ -97,7 +99,7 @@ export class BaseQueryBuilder<
|
||||
}
|
||||
}
|
||||
|
||||
export class BaseModel extends Model {
|
||||
export class BaseModel extends mixin(Model, [withDateSessionMixin]) {
|
||||
public readonly id: number;
|
||||
public readonly tableName: string;
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as moment from 'moment';
|
||||
import { Model } from 'objection';
|
||||
|
||||
type Constructor<T = {}> = new (...args: any[]) => T;
|
||||
|
||||
export const withDateSessionMixin = <T extends Constructor<Model>>(BaseModel: T) => {
|
||||
return class DateSession extends BaseModel {
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
}
|
||||
|
||||
get timestamps() {
|
||||
return [];
|
||||
}
|
||||
|
||||
$beforeUpdate(opt, context) {
|
||||
const maybePromise = super.$beforeUpdate(opt, context);
|
||||
|
||||
return Promise.resolve(maybePromise).then(() => {
|
||||
const key = this.timestamps[1];
|
||||
|
||||
if (key && !this[key]) {
|
||||
this[key] = moment().format('YYYY/MM/DD HH:mm:ss');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$beforeInsert(context) {
|
||||
const maybePromise = super.$beforeInsert(context);
|
||||
|
||||
return Promise.resolve(maybePromise).then(() => {
|
||||
const key = this.timestamps[0];
|
||||
|
||||
if (key && !this[key]) {
|
||||
this[key] = moment().format('YYYY/MM/DD HH:mm:ss');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -412,7 +412,7 @@ export const ACCOUNT_TYPE = {
|
||||
INVENTORY: 'inventory',
|
||||
OTHER_CURRENT_ASSET: 'other-current-asset',
|
||||
FIXED_ASSET: 'fixed-asset',
|
||||
NON_CURRENT_ASSET: 'none-current-asset',
|
||||
NON_CURRENT_ASSET: 'non-current-asset',
|
||||
|
||||
ACCOUNTS_PAYABLE: 'accounts-payable',
|
||||
CREDIT_CARD: 'credit-card',
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ParseIntPipe,
|
||||
Put,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AccountsApplication } from './AccountsApplication.service';
|
||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||
@@ -32,6 +33,11 @@ import {
|
||||
BulkDeleteDto,
|
||||
ValidateBulkDeleteResponseDto,
|
||||
} from '@/common/dtos/BulkDelete.dto';
|
||||
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||
import { AccountAction } from './Accounts.types';
|
||||
|
||||
@Controller('accounts')
|
||||
@ApiTags('Accounts')
|
||||
@@ -40,11 +46,13 @@ import {
|
||||
@ApiExtraModels(GetAccountTransactionResponseDto)
|
||||
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||
export class AccountsController {
|
||||
constructor(private readonly accountsApplication: AccountsApplication) { }
|
||||
|
||||
@Post('validate-bulk-delete')
|
||||
@HttpCode(200)
|
||||
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.',
|
||||
@@ -67,6 +75,7 @@ export class AccountsController {
|
||||
|
||||
@Post('bulk-delete')
|
||||
@HttpCode(200)
|
||||
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
|
||||
@ApiOperation({ summary: 'Deletes multiple accounts in bulk.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -81,6 +90,7 @@ export class AccountsController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermission(AccountAction.CREATE, AbilitySubject.Account)
|
||||
@ApiOperation({ summary: 'Create an account' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -91,6 +101,7 @@ export class AccountsController {
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
|
||||
@ApiOperation({ summary: 'Edit the given account.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -111,6 +122,7 @@ export class AccountsController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
|
||||
@ApiOperation({ summary: 'Delete the given account.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -129,6 +141,7 @@ export class AccountsController {
|
||||
|
||||
@Post(':id/activate')
|
||||
@HttpCode(200)
|
||||
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
|
||||
@ApiOperation({ summary: 'Activate the given account.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -147,6 +160,7 @@ export class AccountsController {
|
||||
|
||||
@Post(':id/inactivate')
|
||||
@HttpCode(200)
|
||||
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
|
||||
@ApiOperation({ summary: 'Inactivate the given account.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -164,6 +178,7 @@ export class AccountsController {
|
||||
}
|
||||
|
||||
@Get('types')
|
||||
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
|
||||
@ApiOperation({ summary: 'Retrieves the account types.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -180,6 +195,7 @@ export class AccountsController {
|
||||
}
|
||||
|
||||
@Get('transactions')
|
||||
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
|
||||
@ApiOperation({ summary: 'Retrieves the account transactions.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -198,6 +214,7 @@ export class AccountsController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
|
||||
@ApiOperation({ summary: 'Retrieves the account details.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -216,6 +233,7 @@ export class AccountsController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
|
||||
@ApiOperation({ summary: 'Retrieves the accounts.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { AccountsExportable } from './AccountsExportable.service';
|
||||
import { AccountsImportable } from './AccountsImportable.service';
|
||||
import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service';
|
||||
import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service';
|
||||
import { AccountsSettingsService } from './AccountsSettings.service';
|
||||
|
||||
const models = [RegisterTenancyModel(BankAccount)];
|
||||
|
||||
@@ -29,6 +30,7 @@ const models = [RegisterTenancyModel(BankAccount)];
|
||||
controllers: [AccountsController],
|
||||
providers: [
|
||||
AccountsApplication,
|
||||
AccountsSettingsService,
|
||||
CreateAccountService,
|
||||
TenancyContext,
|
||||
CommandAccountValidators,
|
||||
@@ -49,9 +51,10 @@ const models = [RegisterTenancyModel(BankAccount)];
|
||||
exports: [
|
||||
AccountRepository,
|
||||
CreateAccountService,
|
||||
AccountsSettingsService,
|
||||
...models,
|
||||
AccountsExportable,
|
||||
AccountsImportable
|
||||
AccountsImportable,
|
||||
],
|
||||
})
|
||||
export class AccountsModule {}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SettingsStore } from '../Settings/SettingsStore';
|
||||
import { SETTINGS_PROVIDER } from '../Settings/Settings.types';
|
||||
|
||||
export interface IAccountsSettings {
|
||||
accountCodeRequired: boolean;
|
||||
accountCodeUnique: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AccountsSettingsService {
|
||||
constructor(
|
||||
@Inject(SETTINGS_PROVIDER)
|
||||
private readonly settingsStore: () => SettingsStore,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves account settings (account code required, account code unique).
|
||||
*/
|
||||
public async getAccountsSettings(): Promise<IAccountsSettings> {
|
||||
const settingsStore = await this.settingsStore();
|
||||
return {
|
||||
accountCodeRequired: settingsStore.get(
|
||||
{ group: 'accounts', key: 'account_code_required' },
|
||||
false,
|
||||
),
|
||||
accountCodeUnique: settingsStore.get(
|
||||
{ group: 'accounts', key: 'account_code_unique' },
|
||||
true,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,20 @@ export class CommandAccountValidators {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if account code is missing or blank when required.
|
||||
* @param {string|undefined} code - Account code.
|
||||
*/
|
||||
public validateAccountCodeRequiredOrThrow(code: string | undefined) {
|
||||
const trimmed = typeof code === 'string' ? code.trim() : '';
|
||||
if (!trimmed) {
|
||||
throw new ServiceError(
|
||||
ERRORS.ACCOUNT_CODE_REQUIRED,
|
||||
'Account code is required.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the account name uniquiness.
|
||||
* @param {string} accountName - Account name.
|
||||
|
||||
@@ -15,6 +15,7 @@ import { events } from '@/common/events/events';
|
||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||
import { PartialModelObject } from 'objection';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { AccountsSettingsService } from './AccountsSettings.service';
|
||||
|
||||
@Injectable()
|
||||
export class CreateAccountService {
|
||||
@@ -32,6 +33,7 @@ export class CreateAccountService {
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validator: CommandAccountValidators,
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
private readonly accountsSettings: AccountsSettingsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -43,14 +45,21 @@ export class CreateAccountService {
|
||||
baseCurrency: string,
|
||||
params?: CreateAccountParams,
|
||||
) => {
|
||||
const { accountCodeRequired, accountCodeUnique } =
|
||||
await this.accountsSettings.getAccountsSettings();
|
||||
|
||||
// Validate account code required when setting is enabled.
|
||||
if (accountCodeRequired) {
|
||||
this.validator.validateAccountCodeRequiredOrThrow(accountDTO.code);
|
||||
}
|
||||
// Validate the account code uniquiness when setting is enabled.
|
||||
if (accountCodeUnique && accountDTO.code?.trim()) {
|
||||
await this.validator.isAccountCodeUniqueOrThrowError(accountDTO.code);
|
||||
}
|
||||
// Validate account name uniquiness.
|
||||
if (!params.ignoreUniqueName) {
|
||||
await this.validator.validateAccountNameUniquiness(accountDTO.name);
|
||||
}
|
||||
// Validate the account code uniquiness.
|
||||
if (accountDTO.code) {
|
||||
await this.validator.isAccountCodeUniqueOrThrowError(accountDTO.code);
|
||||
}
|
||||
// Retrieve the account type meta or throw service error if not found.
|
||||
this.validator.getAccountTypeOrThrowError(accountDTO.accountType);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { EditAccountDTO } from './EditAccount.dto';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { AccountsSettingsService } from './AccountsSettings.service';
|
||||
|
||||
@Injectable()
|
||||
export class EditAccount {
|
||||
@@ -17,7 +18,8 @@ export class EditAccount {
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
) { }
|
||||
private readonly accountsSettings: AccountsSettingsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize the account editing.
|
||||
@@ -30,6 +32,24 @@ export class EditAccount {
|
||||
accountDTO: EditAccountDTO,
|
||||
oldAccount: Account,
|
||||
) => {
|
||||
const { accountCodeRequired, accountCodeUnique } =
|
||||
await this.accountsSettings.getAccountsSettings();
|
||||
|
||||
// Validate account code required when setting is enabled.
|
||||
if (accountCodeRequired) {
|
||||
this.validator.validateAccountCodeRequiredOrThrow(accountDTO.code);
|
||||
}
|
||||
// Validate the account code uniquiness when setting is enabled.
|
||||
if (
|
||||
accountCodeUnique &&
|
||||
accountDTO.code?.trim() &&
|
||||
accountDTO.code !== oldAccount.code
|
||||
) {
|
||||
await this.validator.isAccountCodeUniqueOrThrowError(
|
||||
accountDTO.code,
|
||||
oldAccount.id,
|
||||
);
|
||||
}
|
||||
// Validate account name uniquiness.
|
||||
await this.validator.validateAccountNameUniquiness(
|
||||
accountDTO.name,
|
||||
@@ -40,13 +60,6 @@ export class EditAccount {
|
||||
oldAccount,
|
||||
accountDTO,
|
||||
);
|
||||
// Validate the account code not exists on the storage.
|
||||
if (accountDTO.code && accountDTO.code !== oldAccount.code) {
|
||||
await this.validator.isAccountCodeUniqueOrThrowError(
|
||||
accountDTO.code,
|
||||
oldAccount.id,
|
||||
);
|
||||
}
|
||||
// Retrieve the parent account of throw not found service error.
|
||||
if (accountDTO.parentAccountId) {
|
||||
const parentAccount = await this.validator.getParentAccountOrThrowError(
|
||||
|
||||
@@ -3,6 +3,7 @@ export const ERRORS = {
|
||||
ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found',
|
||||
PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found',
|
||||
ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique',
|
||||
ACCOUNT_CODE_REQUIRED: 'account_code_required',
|
||||
ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue',
|
||||
PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type',
|
||||
ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed',
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { IsBoolean, IsEnum, IsOptional } from 'class-validator';
|
||||
import { IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, Min } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { parseBoolean } from '@/utils/parse-boolean';
|
||||
import { IAccountsStructureType } from '../Accounts.types';
|
||||
import { IFilterRole, ISortOrder } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
import { ToNumber } from '@/common/decorators/Validators';
|
||||
|
||||
export class GetAccountsQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
@@ -23,5 +25,93 @@ export class GetAccountsQueryDto {
|
||||
@IsOptional()
|
||||
@IsEnum(IAccountsStructureType)
|
||||
structure?: IAccountsStructureType;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Custom view ID',
|
||||
type: Number,
|
||||
example: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
customViewId?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Filter roles array',
|
||||
type: Array,
|
||||
isArray: true,
|
||||
})
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
filterRoles?: IFilterRole[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Column to sort by',
|
||||
type: String,
|
||||
example: 'created_at',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
columnSortBy?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Sort order',
|
||||
enum: ISortOrder,
|
||||
example: ISortOrder.DESC,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(ISortOrder)
|
||||
sortOrder?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Stringified filter roles',
|
||||
type: String,
|
||||
example: '{"fieldKey":"root_type","value":"asset"}',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
stringifiedFilterRoles?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Search keyword',
|
||||
type: String,
|
||||
example: 'bank account',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
searchKeyword?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'View slug',
|
||||
type: String,
|
||||
example: 'assets',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
viewSlug?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Page number',
|
||||
type: Number,
|
||||
example: 1,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Page size',
|
||||
type: Number,
|
||||
example: 25,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { createBullBoardAuthMiddleware } from '@/middleware/bull-board-auth.midd
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ClsModule, ClsService } from 'nestjs-cls';
|
||||
import { ClsModule } from 'nestjs-cls';
|
||||
import { AppController } from './App.controller';
|
||||
import { AppService } from './App.service';
|
||||
import { ItemsModule } from '../Items/Items.module';
|
||||
@@ -99,6 +99,7 @@ import { UsersModule } from '../UsersModule/Users.module';
|
||||
import { ContactsModule } from '../Contacts/Contacts.module';
|
||||
import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module';
|
||||
import { BankingCategorizeModule } from '../BankingCategorize/BankingCategorize.module';
|
||||
import { ExchangeRatesModule } from '../ExchangeRates/ExchangeRates.module';
|
||||
import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.module';
|
||||
import { BillLandedCostsModule } from '../BillLandedCosts/BillLandedCosts.module';
|
||||
import { SocketModule } from '../Socket/Socket.module';
|
||||
@@ -124,7 +125,7 @@ import { AppThrottleModule } from './AppThrottle.module';
|
||||
useFactory: () => ({
|
||||
fallbackLanguage: 'en',
|
||||
loaderOptions: {
|
||||
path: join(__dirname, '/../../i18n/'),
|
||||
path: join(__dirname, '../../i18n/'),
|
||||
watch: true,
|
||||
},
|
||||
}),
|
||||
@@ -169,9 +170,6 @@ import { AppThrottleModule } from './AppThrottle.module';
|
||||
global: true,
|
||||
middleware: {
|
||||
mount: true,
|
||||
setup: (cls: ClsService, req: Request, res: Response) => {
|
||||
cls.set('organizationId', req.headers['organization-id']);
|
||||
},
|
||||
generateId: true,
|
||||
saveReq: true,
|
||||
},
|
||||
@@ -258,6 +256,7 @@ import { AppThrottleModule } from './AppThrottle.module';
|
||||
ContactsModule,
|
||||
SocketModule,
|
||||
EEModule,
|
||||
ExchangeRatesModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import * as multerS3 from 'multer-s3';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { S3_CLIENT, S3Module } from "../S3/S3.module";
|
||||
import { DeleteAttachment } from "./DeleteAttachment";
|
||||
import { GetAttachment } from "./GetAttachment";
|
||||
import { getAttachmentPresignedUrl } from "./GetAttachmentPresignedUrl";
|
||||
import { GetAttachmentPresignedUrl } from "./GetAttachmentPresignedUrl";
|
||||
import { LinkAttachment } from "./LinkAttachment";
|
||||
import { UnlinkAttachment } from "./UnlinkAttachment";
|
||||
import { ValidateAttachments } from "./ValidateAttachments";
|
||||
@@ -15,6 +17,8 @@ import { AttachmentsOnPaymentsReceived } from "./events/AttachmentsOnPaymentsRec
|
||||
import { AttachmentsOnManualJournals } from "./events/AttachmentsOnManualJournals";
|
||||
import { AttachmentsOnVendorCredits } from "./events/AttachmentsOnVendorCredits";
|
||||
import { AttachmentsOnSaleInvoiceCreated } from "./events/AttachmentsOnSaleInvoice";
|
||||
import { AttachmentsOnSaleReceipt } from "./events/AttachmentsOnSaleReceipts";
|
||||
import { AttachmentsOnSaleEstimates } from "./events/AttachmentsOnSaleEstimates";
|
||||
import { AttachmentsController } from "./Attachments.controller";
|
||||
import { RegisterTenancyModel } from "../Tenancy/TenancyModels/Tenancy.module";
|
||||
import { DocumentModel } from "./models/Document.model";
|
||||
@@ -33,12 +37,12 @@ const models = [
|
||||
|
||||
@Module({
|
||||
imports: [S3Module, ...models],
|
||||
exports: [...models],
|
||||
exports: [...models, GetAttachmentPresignedUrl],
|
||||
controllers: [AttachmentsController],
|
||||
providers: [
|
||||
DeleteAttachment,
|
||||
GetAttachment,
|
||||
getAttachmentPresignedUrl,
|
||||
GetAttachmentPresignedUrl,
|
||||
LinkAttachment,
|
||||
UnlinkAttachment,
|
||||
ValidateAttachments,
|
||||
@@ -50,13 +54,19 @@ const models = [
|
||||
AttachmentsOnManualJournals,
|
||||
AttachmentsOnVendorCredits,
|
||||
AttachmentsOnSaleInvoiceCreated,
|
||||
AttachmentsOnSaleReceipt,
|
||||
AttachmentsOnSaleEstimates,
|
||||
AttachmentsApplication,
|
||||
UploadDocument,
|
||||
AttachmentUploadPipeline,
|
||||
{
|
||||
provide: MULTER_MODULE_OPTIONS,
|
||||
inject: [ConfigService, S3_CLIENT],
|
||||
useFactory: (configService: ConfigService, s3: S3Client) => ({
|
||||
inject: [ConfigService, S3_CLIENT, ClsService],
|
||||
useFactory: (
|
||||
configService: ConfigService,
|
||||
s3: S3Client,
|
||||
cls: ClsService,
|
||||
) => ({
|
||||
storage: multerS3({
|
||||
s3,
|
||||
bucket: configService.get('s3.bucket'),
|
||||
@@ -65,7 +75,11 @@ const models = [
|
||||
cb(null, { fieldName: file.fieldname });
|
||||
},
|
||||
key: function (req, file, cb) {
|
||||
cb(null, Date.now().toString());
|
||||
const organizationId = cls.get<string>('organizationId');
|
||||
if (!organizationId) {
|
||||
return cb(new Error('Tenant context required for upload.'), undefined as any);
|
||||
}
|
||||
cb(null, `${organizationId}/${randomUUID()}`);
|
||||
},
|
||||
acl: function(req, file, cb) {
|
||||
// Conditionally set file to public or private based on isPublic flag
|
||||
|
||||
@@ -31,6 +31,9 @@ import { AttachmentUploadPipeline } from './S3UploadPipeline';
|
||||
import { FileInterceptor } from '@/common/interceptors/file.interceptor';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||
import { AttachmentAction } from './Attachments.types';
|
||||
|
||||
@ApiTags('Attachments')
|
||||
@Controller('/attachments')
|
||||
@@ -86,6 +89,7 @@ export class AttachmentsController {
|
||||
@ApiOperation({ summary: 'Get attachment by ID' })
|
||||
@ApiParam({ name: 'id', description: 'Attachment ID' })
|
||||
@ApiResponse({ status: 200, description: 'Returns the attachment file' })
|
||||
@RequirePermission(AttachmentAction.View, AbilitySubject.Attachment)
|
||||
async getAttachment(
|
||||
@Res() res: Response,
|
||||
@Param('id') documentId: string,
|
||||
@@ -93,11 +97,12 @@ export class AttachmentsController {
|
||||
const data = await this.attachmentsApplication.get(documentId);
|
||||
|
||||
const byte = await data.Body.transformToByteArray();
|
||||
const extension = mime.extension(data.ContentType);
|
||||
const contentType = data.ContentType || 'application/octet-stream';
|
||||
const extension = mime.extension(contentType) || 'bin';
|
||||
const buffer = Buffer.from(byte);
|
||||
|
||||
res.set('Content-Disposition', `filename="${documentId}.${extension}"`);
|
||||
res.set('Content-Type', data.ContentType);
|
||||
res.set('Content-Type', contentType);
|
||||
res.send(buffer);
|
||||
}
|
||||
|
||||
@@ -111,6 +116,7 @@ export class AttachmentsController {
|
||||
status: 200,
|
||||
description: 'The document has been deleted successfully',
|
||||
})
|
||||
@RequirePermission(AttachmentAction.Delete, AbilitySubject.Attachment)
|
||||
async deleteAttachment(@Param('id') documentId: string) {
|
||||
await this.attachmentsApplication.delete(documentId);
|
||||
|
||||
@@ -184,6 +190,7 @@ export class AttachmentsController {
|
||||
status: 200,
|
||||
description: 'Returns the presigned URL for the attachment',
|
||||
})
|
||||
@RequirePermission(AttachmentAction.View, AbilitySubject.Attachment)
|
||||
async getAttachmentPresignedUrl(@Param('id') documentKey: string) {
|
||||
const presignedUrl =
|
||||
await this.attachmentsApplication.getPresignedUrl(documentKey);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
export interface AttachmentLinkDTO {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export enum AttachmentAction {
|
||||
View = 'View',
|
||||
Delete = 'Delete',
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DeleteAttachment } from './DeleteAttachment';
|
||||
import { GetAttachment } from './GetAttachment';
|
||||
import { LinkAttachment } from './LinkAttachment';
|
||||
import { UnlinkAttachment } from './UnlinkAttachment';
|
||||
import { getAttachmentPresignedUrl } from './GetAttachmentPresignedUrl';
|
||||
import { GetAttachmentPresignedUrl } from './GetAttachmentPresignedUrl';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentsApplication {
|
||||
@@ -14,7 +14,7 @@ export class AttachmentsApplication {
|
||||
private readonly getDocumentService: GetAttachment,
|
||||
private readonly linkDocumentService: LinkAttachment,
|
||||
private readonly unlinkDocumentService: UnlinkAttachment,
|
||||
private readonly getPresignedUrlService: getAttachmentPresignedUrl,
|
||||
private readonly getPresignedUrlService: GetAttachmentPresignedUrl,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,17 +31,17 @@ export class DeleteAttachment {
|
||||
* @param {string} filekey
|
||||
*/
|
||||
async delete(filekey: string): Promise<void> {
|
||||
const foundDocument = await this.documentModel()
|
||||
.query()
|
||||
.findOne('key', filekey)
|
||||
.throwIfNotFound();
|
||||
|
||||
const params = {
|
||||
Bucket: this.configService.get('s3.bucket'),
|
||||
Key: filekey,
|
||||
};
|
||||
await this.s3Client.send(new DeleteObjectCommand(params));
|
||||
|
||||
const foundDocument = await this.documentModel()
|
||||
.query()
|
||||
.findOne('key', filekey)
|
||||
.throwIfNotFound();
|
||||
|
||||
await this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Delete all document links
|
||||
await this.documentLinkModel()
|
||||
|
||||
@@ -2,6 +2,8 @@ import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { S3_CLIENT } from '../S3/S3.module';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { DocumentModel } from './models/Document.model';
|
||||
|
||||
@Injectable()
|
||||
export class GetAttachment {
|
||||
@@ -10,13 +12,21 @@ export class GetAttachment {
|
||||
|
||||
@Inject(S3_CLIENT)
|
||||
private readonly s3: S3Client,
|
||||
|
||||
@Inject(DocumentModel.name)
|
||||
private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
|
||||
) {}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves data of the given document key.
|
||||
* @param {string} filekey
|
||||
*/
|
||||
async getAttachment(filekey: string) {
|
||||
await this.documentModel()
|
||||
.query()
|
||||
.findOne('key', filekey)
|
||||
.throwIfNotFound();
|
||||
|
||||
const params = {
|
||||
Bucket: this.configService.get('s3.bucket'),
|
||||
Key: filekey,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { DocumentModel } from './models/Document.model';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { S3_CLIENT } from '../S3/S3.module';
|
||||
|
||||
@Injectable()
|
||||
export class getAttachmentPresignedUrl {
|
||||
export class GetAttachmentPresignedUrl {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
|
||||
@@ -24,7 +24,10 @@ export class getAttachmentPresignedUrl {
|
||||
* @returns {string}
|
||||
*/
|
||||
async getPresignedUrl(key: string) {
|
||||
const foundDocument = await this.documentModel().query().findOne({ key });
|
||||
const foundDocument = await this.documentModel()
|
||||
.query()
|
||||
.findOne({ key })
|
||||
.throwIfNotFound();
|
||||
const config = this.configService.get('s3');
|
||||
|
||||
let ResponseContentDisposition = 'attachment';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import bluebird from 'bluebird';
|
||||
import * as bluebird from 'bluebird';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
validateLinkModelEntryExists,
|
||||
@@ -53,7 +53,8 @@ export class LinkAttachment {
|
||||
const foundLinkModel = await LinkModel().query(trx).findById(modelId);
|
||||
validateLinkModelEntryExists(foundLinkModel);
|
||||
|
||||
const foundLinks = await this.documentLinkModel().query(trx)
|
||||
const foundLinks = await this.documentLinkModel()
|
||||
.query(trx)
|
||||
.where('modelRef', modelRef)
|
||||
.where('modelId', modelId)
|
||||
.where('documentId', foundFile.id);
|
||||
@@ -70,7 +71,7 @@ export class LinkAttachment {
|
||||
|
||||
/**
|
||||
* Links the given file keys to the given model type and id.
|
||||
* @param {string[]} filekeys - File keys.
|
||||
* @param {string[]} filekeys - File keys.
|
||||
* @param {string} modelRef - Model reference.
|
||||
* @param {number} modelId - Model id.
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
|
||||
@@ -44,7 +44,7 @@ export class UnlinkAttachment {
|
||||
validateLinkModelExists(attachableModel);
|
||||
|
||||
const LinkModel = this.moduleRef.get(modelRef, { strict: false });
|
||||
const foundLinkModel = await LinkModel.query(trx).findById(modelId);
|
||||
const foundLinkModel = await LinkModel().query(trx).findById(modelId);
|
||||
validateLinkModelEntryExists(foundLinkModel);
|
||||
|
||||
const document = await this.documentModel().query(trx).findOne('key', filekey);
|
||||
|
||||
@@ -55,7 +55,7 @@ export class AttachmentsOnPaymentsReceived {
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'PaymentReceive',
|
||||
'PaymentReceived',
|
||||
paymentReceive.id,
|
||||
trx,
|
||||
);
|
||||
@@ -76,7 +76,7 @@ export class AttachmentsOnPaymentsReceived {
|
||||
);
|
||||
await this.unlinkAttachmentService.unlinkUnpresentedKeys(
|
||||
keys,
|
||||
'PaymentReceive',
|
||||
'PaymentReceived',
|
||||
oldPaymentReceive.id,
|
||||
trx,
|
||||
);
|
||||
@@ -100,7 +100,7 @@ export class AttachmentsOnPaymentsReceived {
|
||||
);
|
||||
await this.linkAttachmentService.bulkLink(
|
||||
keys,
|
||||
'PaymentReceive',
|
||||
'PaymentReceived',
|
||||
oldPaymentReceive.id,
|
||||
trx,
|
||||
);
|
||||
@@ -117,7 +117,7 @@ export class AttachmentsOnPaymentsReceived {
|
||||
trx,
|
||||
}: IPaymentReceivedDeletingPayload) {
|
||||
await this.unlinkAttachmentService.unlinkAllModelKeys(
|
||||
'PaymentReceive',
|
||||
'PaymentReceived',
|
||||
oldPaymentReceive.id,
|
||||
trx,
|
||||
);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import * as path from 'path';
|
||||
// import config from '@/config';
|
||||
|
||||
export const getUploadedObjectUri = (objectKey: string) => {
|
||||
return '';
|
||||
// return new URL(
|
||||
// path.join(config.s3.bucket, objectKey),
|
||||
// config.s3.endpoint
|
||||
// ).toString();
|
||||
};
|
||||
@@ -14,12 +14,19 @@ import {
|
||||
ApiOperation,
|
||||
ApiBody,
|
||||
ApiParam,
|
||||
ApiExcludeController,
|
||||
ApiResponse,
|
||||
ApiExtraModels,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
import { PublicRoute } from './guards/jwt.guard';
|
||||
import { AuthenticationApplication } from './AuthApplication.sevice';
|
||||
import { AuthSignupDto } from './dtos/AuthSignup.dto';
|
||||
import { AuthSigninDto } from './dtos/AuthSignin.dto';
|
||||
import { AuthSignupVerifyDto } from './dtos/AuthSignupVerify.dto';
|
||||
import { AuthSendResetPasswordDto } from './dtos/AuthSendResetPassword.dto';
|
||||
import { AuthResetPasswordDto } from './dtos/AuthResetPassword.dto';
|
||||
import { AuthSigninResponseDto } from './dtos/AuthSigninResponse.dto';
|
||||
import { AuthMetaResponseDto } from './dtos/AuthMetaResponse.dto';
|
||||
import { LocalAuthGuard } from './guards/Local.guard';
|
||||
import { AuthSigninService } from './commands/AuthSignin.service';
|
||||
import { TenantModel } from '../System/models/TenantModel';
|
||||
@@ -27,7 +34,7 @@ import { SystemUser } from '../System/models/SystemUser';
|
||||
|
||||
@Controller('/auth')
|
||||
@ApiTags('Auth')
|
||||
@ApiExcludeController()
|
||||
@ApiExtraModels(AuthSigninResponseDto, AuthMetaResponseDto)
|
||||
@PublicRoute()
|
||||
@Throttle({ auth: {} })
|
||||
export class AuthController {
|
||||
@@ -43,10 +50,15 @@ export class AuthController {
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@ApiOperation({ summary: 'Sign in a user' })
|
||||
@ApiBody({ type: AuthSigninDto })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Sign-in successful. Returns access token and tenant/organization IDs.',
|
||||
schema: { $ref: getSchemaPath(AuthSigninResponseDto) },
|
||||
})
|
||||
async signin(
|
||||
@Request() req: Request & { user: SystemUser },
|
||||
@Body() signinDto: AuthSigninDto,
|
||||
) {
|
||||
): Promise<AuthSigninResponseDto> {
|
||||
const { user } = req;
|
||||
const tenant = await this.tenantModel.query().findById(user.tenantId);
|
||||
|
||||
@@ -61,59 +73,47 @@ export class AuthController {
|
||||
@Post('/signup')
|
||||
@ApiOperation({ summary: 'Sign up a new user' })
|
||||
@ApiBody({ type: AuthSignupDto })
|
||||
@ApiResponse({ status: 201, description: 'Sign-up initiated. Check email for confirmation.' })
|
||||
signup(@Request() req: Request, @Body() signupDto: AuthSignupDto) {
|
||||
return this.authApp.signUp(signupDto);
|
||||
}
|
||||
|
||||
@Post('/signup/confirm')
|
||||
@Post('/signup/verify')
|
||||
@ApiOperation({ summary: 'Confirm user signup' })
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: { type: 'string', example: 'user@example.com' },
|
||||
token: { type: 'string', example: 'confirmation-token' },
|
||||
},
|
||||
},
|
||||
})
|
||||
signupConfirm(@Body('email') email: string, @Body('token') token: string) {
|
||||
return this.authApp.signUpConfirm(email, token);
|
||||
@ApiBody({ type: AuthSignupVerifyDto })
|
||||
@ApiResponse({ status: 200, description: 'Signup confirmed successfully.' })
|
||||
signupConfirm(@Body() body: AuthSignupVerifyDto) {
|
||||
return this.authApp.signUpConfirm(body.email, body.token);
|
||||
}
|
||||
|
||||
@Post('/send_reset_password')
|
||||
@ApiOperation({ summary: 'Send reset password email' })
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: { type: 'string', example: 'user@example.com' },
|
||||
},
|
||||
},
|
||||
})
|
||||
sendResetPassword(@Body('email') email: string) {
|
||||
return this.authApp.sendResetPassword(email);
|
||||
@ApiBody({ type: AuthSendResetPasswordDto })
|
||||
@ApiResponse({ status: 200, description: 'Reset password email sent if the account exists.' })
|
||||
sendResetPassword(@Body() body: AuthSendResetPasswordDto) {
|
||||
return this.authApp.sendResetPassword(body.email);
|
||||
}
|
||||
|
||||
@Post('/reset_password/:token')
|
||||
@ApiOperation({ summary: 'Reset password using token' })
|
||||
@ApiParam({ name: 'token', description: 'Reset password token' })
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
password: { type: 'string', example: 'new-password' },
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiParam({ name: 'token', description: 'Reset password token from email link' })
|
||||
@ApiBody({ type: AuthResetPasswordDto })
|
||||
@ApiResponse({ status: 200, description: 'Password reset successfully.' })
|
||||
resetPassword(
|
||||
@Param('token') token: string,
|
||||
@Body('password') password: string,
|
||||
@Body() body: AuthResetPasswordDto,
|
||||
) {
|
||||
return this.authApp.resetPassword(token, password);
|
||||
return this.authApp.resetPassword(token, body.password);
|
||||
}
|
||||
|
||||
@Get('/meta')
|
||||
meta() {
|
||||
@ApiOperation({ summary: 'Get auth metadata (e.g. signup disabled)' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Auth metadata for the login/signup page.',
|
||||
schema: { $ref: getSchemaPath(AuthMetaResponseDto) },
|
||||
})
|
||||
meta(): Promise<AuthMetaResponseDto> {
|
||||
return this.authApp.getAuthMeta();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export class AuthenticationMailMesssages {
|
||||
* @returns {Mail}
|
||||
*/
|
||||
resetPasswordMessage(user: ModelObject<SystemUser>, token: string) {
|
||||
const baseURL = this.configService.get('baseURL');
|
||||
const baseURL = this.configService.get('app.baseUrl');
|
||||
|
||||
return new Mail()
|
||||
.setSubject('Bigcapital - Password Reset')
|
||||
@@ -54,7 +54,7 @@ export class AuthenticationMailMesssages {
|
||||
* @returns {Mail}
|
||||
*/
|
||||
signupVerificationMail(email: string, fullName: string, token: string) {
|
||||
const baseURL = this.configService.get('baseURL');
|
||||
const baseURL = this.configService.get('app.baseUrl');
|
||||
const verifyUrl = `${baseURL}/auth/email_confirmation?token=${token}&email=${email}`;
|
||||
|
||||
return new Mail()
|
||||
|
||||
@@ -7,17 +7,13 @@ import {
|
||||
import { GetAuthenticatedAccount } from './queries/GetAuthedAccount.service';
|
||||
import { Controller, Get, Post } from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards';
|
||||
import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard';
|
||||
import { TenantAgnosticRoute } from '../Tenancy/TenancyGlobal.guard';
|
||||
import { AuthenticationApplication } from './AuthApplication.sevice';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { IgnoreUserVerifiedRoute } from './guards/EnsureUserVerified.guard';
|
||||
|
||||
@Controller('/auth')
|
||||
@ApiTags('Auth')
|
||||
@ApiExcludeController()
|
||||
@IgnoreTenantSeededRoute()
|
||||
@IgnoreTenantInitializedRoute()
|
||||
@TenantAgnosticRoute()
|
||||
@IgnoreUserVerifiedRoute()
|
||||
@Throttle({ auth: {} })
|
||||
export class AuthedController {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ClsService } from 'nestjs-cls';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||
import { ModelObject } from 'objection';
|
||||
import { JwtPayload } from '../Auth.interfaces';
|
||||
import { InvalidEmailPasswordException } from '../exceptions/InvalidEmailPassword.exception';
|
||||
@@ -12,6 +13,10 @@ export class AuthSigninService {
|
||||
constructor(
|
||||
@Inject(SystemUser.name)
|
||||
private readonly systemUserModel: typeof SystemUser,
|
||||
|
||||
@Inject(TenantModel.name)
|
||||
private readonly tenantModel: typeof TenantModel,
|
||||
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly clsService: ClsService,
|
||||
) { }
|
||||
@@ -49,6 +54,7 @@ export class AuthSigninService {
|
||||
*/
|
||||
async verifyPayload(payload: JwtPayload): Promise<any> {
|
||||
let user: SystemUser;
|
||||
let tenant: TenantModel | undefined;
|
||||
|
||||
try {
|
||||
user = await this.systemUserModel
|
||||
@@ -56,8 +62,14 @@ export class AuthSigninService {
|
||||
.findOne({ email: payload.sub })
|
||||
.throwIfNotFound();
|
||||
|
||||
tenant = await this.tenantModel
|
||||
.query()
|
||||
.findById(user.tenantId)
|
||||
.throwIfNotFound();
|
||||
|
||||
this.clsService.set('tenantId', user.tenantId);
|
||||
this.clsService.set('userId', user.id);
|
||||
this.clsService.set('organizationId', tenant.organizationId);
|
||||
} catch (error) {
|
||||
throw new UserNotFoundException(String(payload.sub));
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
IAuthSignedUpEventPayload,
|
||||
IAuthSigningUpEventPayload,
|
||||
} from '../Auth.interfaces';
|
||||
import { defaultTo } from 'ramda';
|
||||
import { ERRORS } from '../Auth.constants';
|
||||
import { hashPassword } from '../Auth.utils';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
@@ -51,10 +50,10 @@ export class AuthSignupService {
|
||||
const signupConfirmation = this.configService.get('signupConfirmation');
|
||||
|
||||
const verifyTokenCrypto = crypto.randomBytes(64).toString('hex');
|
||||
const verifiedEnabed = defaultTo(signupConfirmation.enabled, false);
|
||||
const verifiedEnabed = signupConfirmation.enabled ?? false;
|
||||
const verifyToken = verifiedEnabed ? verifyTokenCrypto : '';
|
||||
const verified = !verifiedEnabed;
|
||||
|
||||
|
||||
const inviteAcceptedAt = moment().format('YYYY-MM-DD');
|
||||
|
||||
// Triggers signin up event.
|
||||
|
||||
@@ -4,7 +4,6 @@ import { SystemUser } from '@/modules/System/models/SystemUser';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { ERRORS } from '../Auth.constants';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ModelObject } from 'objection';
|
||||
import { ISignUpConfigmResendedEventPayload } from '../Auth.interfaces';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AuthMetaResponseDto {
|
||||
@ApiProperty({ description: 'Whether signup is disabled' })
|
||||
signupDisabled: boolean;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AuthResetPasswordDto {
|
||||
@ApiProperty({
|
||||
example: 'new-password',
|
||||
description: 'New password',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AuthSendResetPasswordDto {
|
||||
@ApiProperty({
|
||||
example: 'user@example.com',
|
||||
description: 'User email address to send reset link to',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
email: string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AuthSigninResponseDto {
|
||||
@ApiProperty({ description: 'JWT access token' })
|
||||
accessToken: string;
|
||||
|
||||
@ApiProperty({ description: 'Organization ID' })
|
||||
organizationId: string;
|
||||
|
||||
@ApiProperty({ description: 'Tenant ID' })
|
||||
tenantId: number;
|
||||
|
||||
@ApiProperty({ description: 'User ID' })
|
||||
userId: number;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AuthSignupVerifyDto {
|
||||
@ApiProperty({
|
||||
example: 'user@example.com',
|
||||
description: 'User email address',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'confirmation-token',
|
||||
description: 'Signup confirmation token from email',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
token: string;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import { ToNumber } from '@/common/decorators/Validators';
|
||||
|
||||
class BankRuleConditionDto {
|
||||
@IsNotEmpty()
|
||||
@IsIn(['description', 'amount'])
|
||||
@IsIn(['description', 'amount', 'payee'])
|
||||
field: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -11,12 +11,6 @@ export class BankAccountsController {
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Retrieve the bank accounts.' })
|
||||
@ApiQuery({
|
||||
name: 'query',
|
||||
description: 'Query parameters for the bank accounts list.',
|
||||
type: BankAccountsQueryDto,
|
||||
required: false,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'List of bank accounts retrieved successfully.',
|
||||
|
||||
@@ -2,7 +2,14 @@ import { Body, Controller, Delete, Param, Post, Query } from '@nestjs/common';
|
||||
import { castArray, omit } from 'lodash';
|
||||
import { BankingCategorizeApplication } from './BankingCategorize.application';
|
||||
import { CategorizeBankTransactionRouteDto } from './dtos/CategorizeBankTransaction.dto';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiBody,
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
|
||||
@Controller('banking/categorize')
|
||||
@@ -15,6 +22,7 @@ export class BankingCategorizeController {
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Categorize bank transactions.' })
|
||||
@ApiBody({ type: CategorizeBankTransactionRouteDto })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The bank transactions have been categorized successfully.',
|
||||
@@ -29,21 +37,38 @@ export class BankingCategorizeController {
|
||||
}
|
||||
|
||||
@Delete('/bulk')
|
||||
@ApiOperation({ summary: 'Uncategorize bank transactions.' })
|
||||
@ApiOperation({ summary: 'Uncategorize bank transactions in bulk.' })
|
||||
@ApiQuery({
|
||||
name: 'uncategorizedTransactionIds',
|
||||
required: true,
|
||||
type: [Number],
|
||||
isArray: true,
|
||||
description: 'Array of uncategorized transaction IDs to uncategorize',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The bank transactions have been uncategorized successfully.',
|
||||
})
|
||||
public uncategorizeTransactionsBulk(
|
||||
@Query() uncategorizedTransactionIds: number[] | number,
|
||||
@Query('uncategorizedTransactionIds')
|
||||
uncategorizedTransactionIds: number[] | number,
|
||||
) {
|
||||
const ids = castArray(uncategorizedTransactionIds).map((id) =>
|
||||
Number(id),
|
||||
);
|
||||
return this.bankingCategorizeApplication.uncategorizeTransactionsBulk(
|
||||
castArray(uncategorizedTransactionIds),
|
||||
ids,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@ApiOperation({ summary: 'Uncategorize a bank transaction.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'Uncategorized transaction ID to uncategorize',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The bank transaction has been uncategorized successfully.',
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiOperation,
|
||||
ApiQuery,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
import { BankingMatchingApplication } from './BankingMatchingApplication';
|
||||
import { GetMatchedTransactionsFilter } from './types';
|
||||
import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
|
||||
import { GetMatchedTransactionsQueryDto } from './dtos/GetMatchedTransactionsQuery.dto';
|
||||
import { GetMatchedTransactionsResponseDto } from './dtos/GetMatchedTransactionsResponse.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
|
||||
@Controller('banking/matching')
|
||||
@ApiTags('Banking Transactions Matching')
|
||||
@ApiExtraModels(GetMatchedTransactionsResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
export class BankingMatchingController {
|
||||
constructor(
|
||||
@@ -15,13 +24,25 @@ export class BankingMatchingController {
|
||||
|
||||
@Get('matched')
|
||||
@ApiOperation({ summary: 'Retrieves the matched transactions.' })
|
||||
@ApiQuery({
|
||||
name: 'uncategorizedTransactionIds',
|
||||
required: true,
|
||||
type: [Number],
|
||||
isArray: true,
|
||||
description: 'Uncategorized transaction IDs to match',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Matched transactions (perfect and possible matches).',
|
||||
schema: { $ref: getSchemaPath(GetMatchedTransactionsResponseDto) },
|
||||
})
|
||||
async getMatchedTransactions(
|
||||
@Query('uncategorizedTransactionIds') uncategorizedTransactionIds: number[],
|
||||
@Query() filter: GetMatchedTransactionsFilter,
|
||||
@Query() filter: GetMatchedTransactionsQueryDto,
|
||||
) {
|
||||
return this.bankingMatchingApplication.getMatchedTransactions(
|
||||
uncategorizedTransactionIds,
|
||||
filter,
|
||||
uncategorizedTransactionIds ?? [],
|
||||
filter as any,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsOptional, IsString, IsNumber, Min, Max } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class GetMatchedTransactionsQueryDto {
|
||||
@ApiPropertyOptional({ description: 'Filter from date', example: '2024-01-01' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
fromDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter to date', example: '2024-12-31' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
toDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Minimum amount', example: 0 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
minAmount?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Maximum amount', example: 10000 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Max(Number.MAX_SAFE_INTEGER)
|
||||
maxAmount?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Transaction type filter',
|
||||
example: 'SaleInvoice',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
transactionType?: string;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class MatchedTransactionItemDto {
|
||||
@ApiProperty({ description: 'Transaction amount', example: 100.5 })
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({ description: 'Formatted amount', example: '$100.50' })
|
||||
amountFormatted: string;
|
||||
|
||||
@ApiProperty({ description: 'Transaction date', example: '2024-01-15' })
|
||||
date: string;
|
||||
|
||||
@ApiProperty({ description: 'Formatted date', example: 'Jan 15, 2024' })
|
||||
dateFormatted: string;
|
||||
|
||||
@ApiProperty({ description: 'Reference number', example: 'REF-001' })
|
||||
referenceNo: string;
|
||||
|
||||
@ApiProperty({ description: 'Transaction number', example: 'TXN-001' })
|
||||
transactionNo: string;
|
||||
|
||||
@ApiProperty({ description: 'Transaction ID', example: 1 })
|
||||
transactionId: number;
|
||||
|
||||
@ApiProperty({ description: 'Transaction type', example: 'SaleInvoice' })
|
||||
transactionType: string;
|
||||
}
|
||||
|
||||
export class GetMatchedTransactionsResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Perfect matches (amount and date match)',
|
||||
type: [MatchedTransactionItemDto],
|
||||
})
|
||||
perfectMatches: MatchedTransactionItemDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Possible matches (candidates)',
|
||||
type: [MatchedTransactionItemDto],
|
||||
})
|
||||
possibleMatches: MatchedTransactionItemDto[];
|
||||
|
||||
@ApiProperty({ description: 'Total pending amount', example: 500 })
|
||||
totalPending: number;
|
||||
}
|
||||
@@ -30,7 +30,12 @@ export class MatchTransactionEntryDto {
|
||||
export class MatchBankTransactionDto {
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
uncategorizedTransactions: Array<number>
|
||||
@ApiProperty({
|
||||
description: 'Uncategorized transaction IDs to match',
|
||||
type: [Number],
|
||||
example: [1, 2],
|
||||
})
|
||||
uncategorizedTransactions: Array<number>;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
|
||||
+14
-6
@@ -8,13 +8,14 @@ import {
|
||||
ApiExtraModels,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import { RecognizedTransactionsApplication } from './RecognizedTransactions.application';
|
||||
import { GetRecognizedTransactionResponseDto } from './dtos/GetRecognizedTransactionResponse.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
|
||||
@Controller('banking/recognized')
|
||||
@ApiTags('Banking Recognized Transactions')
|
||||
@ApiExtraModels(GetRecognizedTransactionResponseDto)
|
||||
@ApiExtraModels(GetRecognizedTransactionResponseDto, PaginatedResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
export class BankingRecognizedTransactionsController {
|
||||
constructor(
|
||||
@@ -58,10 +59,17 @@ export class BankingRecognizedTransactionsController {
|
||||
status: 200,
|
||||
description: 'Returns a list of recognized transactions',
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: getSchemaPath(GetRecognizedTransactionResponseDto),
|
||||
},
|
||||
allOf: [
|
||||
{ $ref: getSchemaPath(PaginatedResponseDto) },
|
||||
{
|
||||
properties: {
|
||||
data: {
|
||||
type: 'array',
|
||||
items: { $ref: getSchemaPath(GetRecognizedTransactionResponseDto) },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
async getRecognizedTransactions(@Query() query: any) {
|
||||
|
||||
@@ -15,8 +15,13 @@ export const RecognizeUncategorizedTransactionsJob =
|
||||
export const RecognizeUncategorizedTransactionsQueue =
|
||||
'recognize-uncategorized-transactions-queue';
|
||||
|
||||
|
||||
export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload {
|
||||
ruleId: number,
|
||||
transactionsCriteria: any;
|
||||
transactionsCriteria?: RecognizeTransactionsCriteria;
|
||||
/**
|
||||
* When true, first reverts recognized transactions before recognizing again.
|
||||
* Used when a bank rule is edited to ensure transactions previously recognized
|
||||
* by lower-priority rules are re-evaluated against the updated rule.
|
||||
*/
|
||||
shouldRevert?: boolean;
|
||||
}
|
||||
+4
@@ -93,6 +93,10 @@ export class RecognizeTranasctionsService {
|
||||
q.whereIn('id', rulesIds);
|
||||
}
|
||||
q.withGraphFetched('conditions');
|
||||
|
||||
// Order by the 'order' field to ensure higher priority rules (lower order values)
|
||||
// are matched first.
|
||||
q.orderBy('order', 'asc');
|
||||
});
|
||||
|
||||
const bankRulesByAccountId = transformToMapBy(
|
||||
|
||||
+3
@@ -69,10 +69,13 @@ export class TriggerRecognizedTransactionsSubscriber {
|
||||
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
|
||||
const payload = {
|
||||
ruleId: bankRule.id,
|
||||
shouldRevert: true,
|
||||
...tenantPayload,
|
||||
} as RecognizeUncategorizedTransactionsJobPayload;
|
||||
|
||||
// Re-recognize the transactions based on the new rules.
|
||||
// Setting shouldRevert to true ensures that transactions previously recognized
|
||||
// by this or lower-priority rules are re-evaluated against the updated rule.
|
||||
await this.recognizeTransactionsQueue.add(
|
||||
RecognizeUncategorizedTransactionsJob,
|
||||
payload,
|
||||
|
||||
+13
-1
@@ -3,6 +3,7 @@ import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Scope } from '@nestjs/common';
|
||||
import { ClsService, UseCls } from 'nestjs-cls';
|
||||
import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
|
||||
import { RevertRecognizedTransactionsService } from '../commands/RevertRecognizedTransactions.service';
|
||||
import {
|
||||
RecognizeUncategorizedTransactionsJobPayload,
|
||||
RecognizeUncategorizedTransactionsQueue,
|
||||
@@ -15,10 +16,12 @@ import {
|
||||
export class RegonizeTransactionsPrcessor extends WorkerHost {
|
||||
/**
|
||||
* @param {RecognizeTranasctionsService} recognizeTranasctionsService -
|
||||
* @param {RevertRecognizedTransactionsService} revertRecognizedTransactionsService -
|
||||
* @param {ClsService} clsService -
|
||||
*/
|
||||
constructor(
|
||||
private readonly recognizeTranasctionsService: RecognizeTranasctionsService,
|
||||
private readonly revertRecognizedTransactionsService: RevertRecognizedTransactionsService,
|
||||
private readonly clsService: ClsService,
|
||||
) {
|
||||
super();
|
||||
@@ -29,12 +32,21 @@ export class RegonizeTransactionsPrcessor extends WorkerHost {
|
||||
*/
|
||||
@UseCls()
|
||||
async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) {
|
||||
const { ruleId, transactionsCriteria } = job.data;
|
||||
const { ruleId, transactionsCriteria, shouldRevert } = job.data;
|
||||
|
||||
this.clsService.set('organizationId', job.data.organizationId);
|
||||
this.clsService.set('userId', job.data.userId);
|
||||
|
||||
try {
|
||||
// If shouldRevert is true, first revert recognized transactions before re-recognizing.
|
||||
// This is used when a bank rule is edited to ensure transactions previously recognized
|
||||
// by lower-priority rules are re-evaluated against the updated rule.
|
||||
if (shouldRevert) {
|
||||
await this.revertRecognizedTransactionsService.revertRecognizedTransactions(
|
||||
ruleId,
|
||||
transactionsCriteria,
|
||||
);
|
||||
}
|
||||
await this.recognizeTranasctionsService.recognizeTransactions(
|
||||
ruleId,
|
||||
transactionsCriteria,
|
||||
|
||||
@@ -24,6 +24,7 @@ import { GetBankAccountsService } from './queries/GetBankAccounts.service';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
import { BankAccount } from './models/BankAccount';
|
||||
import { LedgerModule } from '../Ledger/Ledger.module';
|
||||
import { TenancyModule } from '../Tenancy/Tenancy.module';
|
||||
import { GetBankAccountTransactionsService } from './queries/GetBankAccountTransactions/GetBankAccountTransactions.service';
|
||||
import { GetBankAccountTransactionsRepository } from './queries/GetBankAccountTransactions/GetBankAccountTransactionsRepo.service';
|
||||
import { GetUncategorizedTransactions } from './queries/GetUncategorizedTransactions';
|
||||
@@ -46,6 +47,7 @@ const models = [
|
||||
LedgerModule,
|
||||
BranchesModule,
|
||||
DynamicListModule,
|
||||
TenancyModule,
|
||||
...models,
|
||||
],
|
||||
controllers: [
|
||||
|
||||
@@ -27,7 +27,7 @@ export enum CASHFLOW_DIRECTION {
|
||||
}
|
||||
|
||||
export enum CASHFLOW_TRANSACTION_TYPE {
|
||||
ONWERS_DRAWING = 'OwnerDrawing',
|
||||
OWNERS_DRAWING = 'OwnerDrawing',
|
||||
OWNER_CONTRIBUTION = 'OwnerContribution',
|
||||
OTHER_INCOME = 'OtherIncome',
|
||||
TRANSFER_FROM_ACCOUNT = 'TransferFromAccount',
|
||||
@@ -36,7 +36,7 @@ export enum CASHFLOW_TRANSACTION_TYPE {
|
||||
}
|
||||
|
||||
export const CASHFLOW_TRANSACTION_TYPE_META = {
|
||||
[`${CASHFLOW_TRANSACTION_TYPE.ONWERS_DRAWING}`]: {
|
||||
[`${CASHFLOW_TRANSACTION_TYPE.OWNERS_DRAWING}`]: {
|
||||
type: 'OwnerDrawing',
|
||||
direction: CASHFLOW_DIRECTION.OUT,
|
||||
creditType: [ACCOUNT_TYPE.EQUITY],
|
||||
|
||||
+14
-3
@@ -7,14 +7,15 @@ import {
|
||||
getSchemaPath,
|
||||
ApiExtraModels,
|
||||
} from '@nestjs/swagger';
|
||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import { BankingTransactionsApplication } from '../BankingTransactionsApplication.service';
|
||||
import { GetPendingTransactionsQueryDto } from '../dtos/GetPendingTransactionsQuery.dto';
|
||||
import { GetPendingTransactionResponseDto } from '../dtos/GetPendingTransactionResponse.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
|
||||
@Controller('banking/pending')
|
||||
@ApiTags('Banking Pending Transactions')
|
||||
@ApiExtraModels(GetPendingTransactionResponseDto)
|
||||
@ApiExtraModels(GetPendingTransactionResponseDto, PaginatedResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
export class BankingPendingTransactionsController {
|
||||
constructor(
|
||||
@@ -27,7 +28,17 @@ export class BankingPendingTransactionsController {
|
||||
status: 200,
|
||||
description: 'Returns a list of pending bank account transactions',
|
||||
schema: {
|
||||
$ref: getSchemaPath(GetPendingTransactionResponseDto),
|
||||
allOf: [
|
||||
{ $ref: getSchemaPath(PaginatedResponseDto) },
|
||||
{
|
||||
properties: {
|
||||
data: {
|
||||
type: 'array',
|
||||
items: { $ref: getSchemaPath(GetPendingTransactionResponseDto) },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@ApiQuery({
|
||||
|
||||
+18
-14
@@ -5,13 +5,17 @@ import {
|
||||
ApiResponse,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
ApiExtraModels,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
import { GetUncategorizedTransactionsQueryDto } from '../dtos/GetUncategorizedTransactionsQuery.dto';
|
||||
import { GetAutofillCategorizeTransactionResponseDto } from '../dtos/GetAutofillCategorizeTransactionResponse.dto';
|
||||
import { BankingTransactionsApplication } from '../BankingTransactionsApplication.service';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
|
||||
@Controller('banking/uncategorized')
|
||||
@ApiTags('Banking Uncategorized Transactions')
|
||||
@ApiExtraModels(GetAutofillCategorizeTransactionResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
export class BankingUncategorizedTransactionsController {
|
||||
constructor(
|
||||
@@ -20,29 +24,29 @@ export class BankingUncategorizedTransactionsController {
|
||||
|
||||
@Get('autofill')
|
||||
@ApiOperation({ summary: 'Get autofill values for categorize transactions' })
|
||||
@ApiQuery({
|
||||
name: 'uncategorizedTransactionIds',
|
||||
required: true,
|
||||
type: [Number],
|
||||
isArray: true,
|
||||
description: 'Uncategorized transaction IDs to get autofill for',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Returns autofill values for categorize transactions',
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'accountId',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'Bank account ID',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'uncategorizeTransactionsId',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'Uncategorize transactions ID',
|
||||
schema: { $ref: getSchemaPath(GetAutofillCategorizeTransactionResponseDto) },
|
||||
})
|
||||
async getAutofillCategorizeTransaction(
|
||||
@Query('uncategorizedTransactionIds')
|
||||
uncategorizedTransactionIds: Array<number> | number,
|
||||
) {
|
||||
console.log(uncategorizedTransactionIds);
|
||||
const ids = Array.isArray(uncategorizedTransactionIds)
|
||||
? uncategorizedTransactionIds
|
||||
: uncategorizedTransactionIds != null
|
||||
? [uncategorizedTransactionIds]
|
||||
: [];
|
||||
return this.bankingTransactionsApplication.getAutofillCategorizeTransaction(
|
||||
uncategorizedTransactionIds,
|
||||
ids,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class GetAutofillCategorizeTransactionResponseDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Assigned credit/debit account ID from recognition',
|
||||
example: 10,
|
||||
})
|
||||
creditAccountId?: number | null;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Bank account ID (debit)',
|
||||
example: 5,
|
||||
})
|
||||
debitAccountId?: number | null;
|
||||
|
||||
@ApiProperty({ description: 'Total amount of uncategorized transactions', example: -150.5 })
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({ description: 'Formatted amount', example: '$150.50' })
|
||||
formattedAmount: string;
|
||||
|
||||
@ApiProperty({ description: 'Transaction date', example: '2024-01-15' })
|
||||
date: string;
|
||||
|
||||
@ApiProperty({ description: 'Formatted date', example: 'Jan 15, 2024' })
|
||||
formattedDate: string;
|
||||
|
||||
@ApiProperty({ description: 'Whether the transaction is recognized by a rule', example: true })
|
||||
isRecognized: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Bank rule ID that recognized the transaction', example: 1 })
|
||||
recognizedByRuleId?: number | null;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Bank rule name that recognized the transaction', example: 'Salary Rule' })
|
||||
recognizedByRuleName?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Reference number', example: 'REF-001' })
|
||||
referenceNo?: string | null;
|
||||
|
||||
@ApiProperty({ description: 'Transaction type (category)', example: 'other_expense' })
|
||||
transactionType: string;
|
||||
|
||||
@ApiProperty({ description: 'Whether this is a deposit transaction', example: false })
|
||||
isDepositTransaction: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Whether this is a withdrawal transaction', example: true })
|
||||
isWithdrawalTransaction: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Assigned payee from recognition' })
|
||||
payee?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Assigned memo from recognition' })
|
||||
memo?: string | null;
|
||||
}
|
||||
+9
-2
@@ -4,12 +4,14 @@ import { GetBankAccountTransactionsRepository } from './GetBankAccountTransactio
|
||||
import { GetBankAccountTransactions } from './GetBankAccountTransactions';
|
||||
import { GetBankTransactionsQueryDto } from '../../dtos/GetBankTranasctionsQuery.dto';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
|
||||
@Injectable()
|
||||
export class GetBankAccountTransactionsService {
|
||||
constructor(
|
||||
private readonly getBankAccountTransactionsRepository: GetBankAccountTransactionsRepository,
|
||||
private readonly i18nService: I18nService
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -28,11 +30,16 @@ export class GetBankAccountTransactionsService {
|
||||
|
||||
await this.getBankAccountTransactionsRepository.asyncInit();
|
||||
|
||||
// Retrieve the tenant metadata to get the date format.
|
||||
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
|
||||
const dateFormat = tenantMetadata?.dateFormat;
|
||||
|
||||
// Retrieve the computed report.
|
||||
const report = new GetBankAccountTransactions(
|
||||
this.getBankAccountTransactionsRepository,
|
||||
parsedQuery,
|
||||
this.i18nService
|
||||
this.i18nService,
|
||||
dateFormat,
|
||||
);
|
||||
const transactions = report.reportData();
|
||||
const pagination = this.getBankAccountTransactionsRepository.pagination;
|
||||
|
||||
+4
-1
@@ -24,17 +24,20 @@ export class GetBankAccountTransactions extends FinancialSheet {
|
||||
* @param {IAccountTransaction[]} transactions -
|
||||
* @param {number} openingBalance -
|
||||
* @param {ICashflowAccountTransactionsQuery} query -
|
||||
* @param {string} dateFormat - The date format from organization settings.
|
||||
*/
|
||||
constructor(
|
||||
repo: GetBankAccountTransactionsRepository,
|
||||
query: ICashflowAccountTransactionsQuery,
|
||||
i18n: I18nService,
|
||||
dateFormat?: string,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.repo = repo;
|
||||
this.query = query;
|
||||
this.i18n = i18n;
|
||||
this.dateFormat = dateFormat || this.dateFormat;
|
||||
|
||||
this.runningBalance = runningBalance(this.repo.openingBalance);
|
||||
}
|
||||
@@ -98,7 +101,7 @@ export class GetBankAccountTransactions extends FinancialSheet {
|
||||
|
||||
return {
|
||||
date: transaction.date,
|
||||
formattedDate: moment(transaction.date).format('YYYY-MM-DD'),
|
||||
formattedDate: this.getDateFormatted(transaction.date),
|
||||
|
||||
withdrawal: transaction.credit,
|
||||
deposit: transaction.debit,
|
||||
|
||||
+26
-14
@@ -4,12 +4,10 @@ import {
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ExcludeBankTransactionsApplication } from './ExcludeBankTransactionsApplication';
|
||||
import { ExcludedBankTransactionsQuery } from './types/BankTransactionsExclude.types';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiOperation,
|
||||
@@ -17,12 +15,15 @@ import {
|
||||
ApiTags,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
import { GetExcludedBankTransactionResponseDto } from './dtos/GetExcludedBankTransactionResponse.dto';
|
||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import { GetExcludedBankTransactionResponseDto } from './dtos/GetExcludedBankTransactionResponse.dto';
|
||||
import { ExcludeBankTransactionsBulkDto } from './dtos/ExcludeBankTransactionsBulk.dto';
|
||||
import { GetExcludedBankTransactionsQueryDto } from './dtos/GetExcludedBankTransactionsQuery.dto';
|
||||
|
||||
@Controller('banking/exclude')
|
||||
@ApiTags('Banking Transactions')
|
||||
@ApiExtraModels(GetExcludedBankTransactionResponseDto)
|
||||
@ApiExtraModels(GetExcludedBankTransactionResponseDto, ExcludeBankTransactionsBulkDto, PaginatedResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
export class BankingTransactionsExcludeController {
|
||||
constructor(
|
||||
@@ -31,15 +32,19 @@ export class BankingTransactionsExcludeController {
|
||||
|
||||
@Put('bulk')
|
||||
@ApiOperation({ summary: 'Exclude the given bank transactions.' })
|
||||
public excludeBankTransactions(@Body('ids') ids: number[]) {
|
||||
return this.excludeBankTransactionsApplication.excludeBankTransactions(ids);
|
||||
@ApiResponse({ status: 200, description: 'Bank transactions excluded successfully.' })
|
||||
public excludeBankTransactions(@Body() body: ExcludeBankTransactionsBulkDto) {
|
||||
return this.excludeBankTransactionsApplication.excludeBankTransactions(
|
||||
body.ids,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('bulk')
|
||||
@ApiOperation({ summary: 'Unexclude the given bank transactions.' })
|
||||
public unexcludeBankTransactions(@Body('ids') ids: number[]) {
|
||||
@ApiResponse({ status: 200, description: 'Bank transactions unexcluded successfully.' })
|
||||
public unexcludeBankTransactions(@Body() body: ExcludeBankTransactionsBulkDto) {
|
||||
return this.excludeBankTransactionsApplication.unexcludeBankTransactions(
|
||||
ids,
|
||||
body.ids,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,17 +55,24 @@ export class BankingTransactionsExcludeController {
|
||||
description:
|
||||
'The excluded bank transactions has been retrieved successfully.',
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: getSchemaPath(GetExcludedBankTransactionResponseDto),
|
||||
},
|
||||
allOf: [
|
||||
{ $ref: getSchemaPath(PaginatedResponseDto) },
|
||||
{
|
||||
properties: {
|
||||
data: {
|
||||
type: 'array',
|
||||
items: { $ref: getSchemaPath(GetExcludedBankTransactionResponseDto) },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
public getExcludedBankTransactions(
|
||||
@Query() query: ExcludedBankTransactionsQuery,
|
||||
@Query() query: GetExcludedBankTransactionsQueryDto,
|
||||
) {
|
||||
return this.excludeBankTransactionsApplication.getExcludedBankTransactions(
|
||||
query,
|
||||
query as any,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsNumber, ArrayMinSize } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class ExcludeBankTransactionsBulkDto {
|
||||
@ApiProperty({
|
||||
description: 'IDs of uncategorized bank transactions to exclude or unexclude',
|
||||
type: [Number],
|
||||
example: [1, 2, 3],
|
||||
})
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@IsNumber({}, { each: true })
|
||||
@Type(() => Number)
|
||||
ids: number[];
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsOptional, IsNumber, IsDateString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class GetExcludedBankTransactionsQueryDto {
|
||||
@ApiPropertyOptional({ description: 'Page number', example: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Page size', example: 25 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
pageSize?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by bank account ID', example: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
accountId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Minimum date (ISO)', example: '2024-01-01' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
minDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Maximum date (ISO)', example: '2024-12-31' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
maxDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Minimum amount', example: 0 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
minAmount?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Maximum amount', example: 10000 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
maxAmount?: number;
|
||||
}
|
||||
@@ -13,6 +13,13 @@ export class BillLandedCostEntry extends BaseModel {
|
||||
return 'bill_located_cost_entries';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
|
||||
@@ -7,12 +7,14 @@ import {
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { BillPaymentsApplication } from './BillPaymentsApplication.service';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
getSchemaPath,
|
||||
@@ -26,12 +28,18 @@ import { BillPaymentsPages } from './commands/BillPaymentsPages.service';
|
||||
import { BillPaymentResponseDto } from './dtos/BillPaymentResponse.dto';
|
||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||
import { IPaymentMadeAction } from './types/BillPayments.types';
|
||||
|
||||
@Controller('bill-payments')
|
||||
@ApiTags('Bill Payments')
|
||||
@ApiExtraModels(BillPaymentResponseDto)
|
||||
@ApiExtraModels(PaginatedResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||
export class BillPaymentsController {
|
||||
constructor(
|
||||
private billPaymentsApplication: BillPaymentsApplication,
|
||||
@@ -39,12 +47,14 @@ export class BillPaymentsController {
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermission(IPaymentMadeAction.Create, AbilitySubject.PaymentMade)
|
||||
@ApiOperation({ summary: 'Create a new bill payment.' })
|
||||
public createBillPayment(@Body() billPaymentDTO: CreateBillPaymentDto) {
|
||||
return this.billPaymentsApplication.createBillPayment(billPaymentDTO);
|
||||
}
|
||||
|
||||
@Delete(':billPaymentId')
|
||||
@RequirePermission(IPaymentMadeAction.Delete, AbilitySubject.PaymentMade)
|
||||
@ApiOperation({ summary: 'Delete the given bill payment.' })
|
||||
@ApiParam({
|
||||
name: 'billPaymentId',
|
||||
@@ -59,6 +69,7 @@ export class BillPaymentsController {
|
||||
}
|
||||
|
||||
@Put(':billPaymentId')
|
||||
@RequirePermission(IPaymentMadeAction.Edit, AbilitySubject.PaymentMade)
|
||||
@ApiOperation({ summary: 'Edit the given bill payment.' })
|
||||
@ApiParam({
|
||||
name: 'billPaymentId',
|
||||
@@ -77,11 +88,12 @@ export class BillPaymentsController {
|
||||
}
|
||||
|
||||
@Get('/new-page/entries')
|
||||
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Retrieves the payable entries of the new page once vendor be selected.',
|
||||
})
|
||||
@ApiParam({
|
||||
@ApiQuery({
|
||||
name: 'vendorId',
|
||||
required: true,
|
||||
type: Number,
|
||||
@@ -95,6 +107,7 @@ export class BillPaymentsController {
|
||||
}
|
||||
|
||||
@Get(':billPaymentId/bills')
|
||||
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
|
||||
@ApiOperation({ summary: 'Retrieves the bills of the given bill payment.' })
|
||||
@ApiParam({
|
||||
name: 'billPaymentId',
|
||||
@@ -107,6 +120,7 @@ export class BillPaymentsController {
|
||||
}
|
||||
|
||||
@Get('/:billPaymentId/edit-page')
|
||||
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
|
||||
@ApiOperation({
|
||||
summary: 'Retrieves the edit page of the given bill payment.',
|
||||
})
|
||||
@@ -126,6 +140,7 @@ export class BillPaymentsController {
|
||||
}
|
||||
|
||||
@Get(':billPaymentId')
|
||||
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
|
||||
@ApiOperation({ summary: 'Retrieves the bill payment details.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -145,6 +160,7 @@ export class BillPaymentsController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
|
||||
@ApiOperation({ summary: 'Retrieves the bill payments list.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
|
||||
@@ -9,7 +9,9 @@ import { BillPaymentMeta } from './BillPayment.meta';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
|
||||
import { BillPaymentDefaultViews } from '../constants';
|
||||
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
|
||||
|
||||
@InjectAttachable()
|
||||
@ImportableModel()
|
||||
@ExportableModel()
|
||||
@InjectModelMeta(BillPaymentMeta)
|
||||
|
||||
@@ -2,7 +2,8 @@ import { CreateBill } from './commands/CreateBill.service';
|
||||
import { EditBillService } from './commands/EditBill.service';
|
||||
import { GetBill } from './queries/GetBill';
|
||||
import { DeleteBill } from './commands/DeleteBill.service';
|
||||
import { IBillDTO, IBillEditDTO, IBillsFilter } from './Bills.types';
|
||||
import { IBillDTO, IBillEditDTO } from './Bills.types';
|
||||
import { GetBillsQueryDto } from './dtos/GetBillsQuery.dto';
|
||||
import { GetDueBills } from './queries/GetDueBills.service';
|
||||
import { OpenBillService } from './commands/OpenBill.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
@@ -78,9 +79,9 @@ export class BillsApplication {
|
||||
|
||||
/**
|
||||
* Retrieve bills data table list.
|
||||
* @param {IBillsFilter} billsFilter -
|
||||
* @param {GetBillsQueryDto} filterDTO -
|
||||
*/
|
||||
public getBills(filterDTO: Partial<IBillsFilter>) {
|
||||
public getBills(filterDTO: GetBillsQueryDto) {
|
||||
return this.getBillsService.getBills(filterDTO);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
ApiExtraModels,
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
getSchemaPath,
|
||||
@@ -17,10 +18,11 @@ import {
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { BillsApplication } from './Bills.application';
|
||||
import { IBillsFilter } from './Bills.types';
|
||||
import { CreateBillDto, EditBillDto } from './dtos/Bill.dto';
|
||||
import { GetBillsQueryDto } from './dtos/GetBillsQuery.dto';
|
||||
import { BillResponseDto } from './dtos/BillResponse.dto';
|
||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
@@ -28,6 +30,11 @@ import {
|
||||
BulkDeleteDto,
|
||||
ValidateBulkDeleteResponseDto,
|
||||
} from '@/common/dtos/BulkDelete.dto';
|
||||
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||
import { BillAction } from './Bills.types';
|
||||
|
||||
@Controller('bills')
|
||||
@ApiTags('Bills')
|
||||
@@ -35,10 +42,12 @@ import {
|
||||
@ApiExtraModels(PaginatedResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||
export class BillsController {
|
||||
constructor(private billsApplication: BillsApplication) { }
|
||||
|
||||
@Post('validate-bulk-delete')
|
||||
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
|
||||
@ApiOperation({
|
||||
summary: 'Validate which bills can be deleted and return the results.',
|
||||
})
|
||||
@@ -58,6 +67,7 @@ export class BillsController {
|
||||
}
|
||||
|
||||
@Post('bulk-delete')
|
||||
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
|
||||
@ApiOperation({ summary: 'Deletes multiple bills.' })
|
||||
@HttpCode(200)
|
||||
@ApiResponse({
|
||||
@@ -73,12 +83,14 @@ export class BillsController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermission(BillAction.Create, AbilitySubject.Bill)
|
||||
@ApiOperation({ summary: 'Create a new bill.' })
|
||||
createBill(@Body() billDTO: CreateBillDto) {
|
||||
return this.billsApplication.createBill(billDTO);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@RequirePermission(BillAction.Edit, AbilitySubject.Bill)
|
||||
@ApiOperation({ summary: 'Edit the given bill.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
@@ -91,6 +103,7 @@ export class BillsController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
|
||||
@ApiOperation({ summary: 'Delete the given bill.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
@@ -103,6 +116,7 @@ export class BillsController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequirePermission(BillAction.View, AbilitySubject.Bill)
|
||||
@ApiOperation({ summary: 'Retrieves the bills.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -127,11 +141,12 @@ export class BillsController {
|
||||
type: Number,
|
||||
description: 'The bill id',
|
||||
})
|
||||
getBills(@Query() filterDTO: Partial<IBillsFilter>) {
|
||||
getBills(@Query() filterDTO: GetBillsQueryDto) {
|
||||
return this.billsApplication.getBills(filterDTO);
|
||||
}
|
||||
|
||||
@Get(':id/payment-transactions')
|
||||
@RequirePermission(BillAction.View, AbilitySubject.Bill)
|
||||
@ApiOperation({
|
||||
summary: 'Retrieve the specific bill associated payment transactions.',
|
||||
})
|
||||
@@ -141,11 +156,16 @@ export class BillsController {
|
||||
type: Number,
|
||||
description: 'The bill id',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'List of payment transactions for the bill.',
|
||||
})
|
||||
getBillPaymentTransactions(@Param('id') billId: number) {
|
||||
return this.billsApplication.getBillPaymentTransactions(billId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermission(BillAction.View, AbilitySubject.Bill)
|
||||
@ApiOperation({ summary: 'Retrieves the bill details.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -165,6 +185,7 @@ export class BillsController {
|
||||
}
|
||||
|
||||
@Patch(':id/open')
|
||||
@RequirePermission(BillAction.Edit, AbilitySubject.Bill)
|
||||
@ApiOperation({ summary: 'Open the given bill.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
@@ -177,8 +198,19 @@ export class BillsController {
|
||||
}
|
||||
|
||||
@Get('due')
|
||||
@RequirePermission(BillAction.View, AbilitySubject.Bill)
|
||||
@ApiOperation({ summary: 'Retrieves the due bills.' })
|
||||
getDueBills(@Body('vendorId') vendorId?: number) {
|
||||
@ApiQuery({
|
||||
name: 'vendor_id',
|
||||
required: false,
|
||||
type: Number,
|
||||
description: 'Filter due bills by vendor ID.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'List of due bills (optionally filtered by vendor).',
|
||||
})
|
||||
getDueBills(@Query('vendor_id') vendorId?: number) {
|
||||
return this.billsApplication.getDueBills(vendorId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
|
||||
import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto';
|
||||
import { BranchResponseDto } from '@/modules/Branches/dtos/BranchResponse.dto';
|
||||
import { DiscountType } from '@/common/types/Discount';
|
||||
|
||||
export class BillResponseDto {
|
||||
@@ -89,6 +91,14 @@ export class BillResponseDto {
|
||||
})
|
||||
branchId?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Branch details',
|
||||
type: () => BranchResponseDto,
|
||||
required: false,
|
||||
})
|
||||
@Type(() => BranchResponseDto)
|
||||
branch?: BranchResponseDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The ID of the project',
|
||||
example: 301,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { DynamicFilterQueryDto } from '@/modules/DynamicListing/dtos/DynamicFilterQuery.dto';
|
||||
|
||||
export class GetBillsQueryDto extends DynamicFilterQueryDto {}
|
||||
@@ -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 { sanitizeSortDirection } from '@/modules/DynamicListing/DynamicFilter/sanitizeSortDirection';
|
||||
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
|
||||
import { BillDefaultViews } from '../Bills.constants';
|
||||
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
|
||||
|
||||
@InjectAttachable()
|
||||
@ExportableModel()
|
||||
@InjectModelMeta(BillMeta)
|
||||
@InjectModelDefaultViews(BillDefaultViews)
|
||||
@@ -405,7 +408,8 @@ export class Bill extends TenantBaseModel {
|
||||
* Sort the bills by full-payment bills.
|
||||
*/
|
||||
sortByStatus(query, order) {
|
||||
query.orderByRaw(`PAYMENT_AMOUNT = AMOUNT ${order}`);
|
||||
const dir = sanitizeSortDirection(order);
|
||||
query.orderByRaw(`PAYMENT_AMOUNT = AMOUNT ${dir}`);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,6 +30,7 @@ export class BillTransformer extends Transformer {
|
||||
'taxes',
|
||||
'entries',
|
||||
'attachments',
|
||||
'branch',
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service
|
||||
import { Bill } from '../models/Bill';
|
||||
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
|
||||
import { BillTransformer } from './Bill.transformer';
|
||||
import { IBillsFilter } from '../Bills.types';
|
||||
import { GetBillsQueryDto } from '../dtos/GetBillsQuery.dto';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
@@ -19,10 +19,10 @@ export class GetBillsService {
|
||||
|
||||
/**
|
||||
* Retrieve bills data table list.
|
||||
* @param {IBillsFilter} billsFilter -
|
||||
* @param {GetBillsQueryDto} filterDTO -
|
||||
*/
|
||||
public async getBills(filterDTO: Partial<IBillsFilter>): Promise<{
|
||||
bills: Bill;
|
||||
public async getBills(filterDTO: GetBillsQueryDto): Promise<{
|
||||
bills: Bill[];
|
||||
pagination: IPaginationMeta;
|
||||
filterMeta: IFilterMeta;
|
||||
}> {
|
||||
|
||||
@@ -31,6 +31,12 @@ import { ValidateBranchExistance } from './integrations/ValidateBranchExistance'
|
||||
import { ManualJournalBranchesValidator } from './integrations/ManualJournals/ManualJournalsBranchesValidator';
|
||||
import { CashflowTransactionsActivateBranches } from './integrations/Cashflow/CashflowActivateBranches';
|
||||
import { ExpensesActivateBranches } from './integrations/Expense/ExpensesActivateBranches';
|
||||
import { BillActivateBranches } from './integrations/Purchases/BillBranchesActivate';
|
||||
import { VendorCreditActivateBranches } from './integrations/Purchases/VendorCreditBranchesActivate';
|
||||
import { BillPaymentsActivateBranches } from './integrations/Purchases/PaymentMadeBranchesActivate';
|
||||
import { BillBranchesActivateSubscriber } from './subscribers/Activate/BillBranchesActivateSubscriber';
|
||||
import { VendorCreditBranchesActivateSubscriber } from './subscribers/Activate/VendorCreditBranchesActivateSubscriber';
|
||||
import { PaymentMadeActivateBranchesSubscriber } from './subscribers/Activate/PaymentMadeBranchesActivateSubscriber';
|
||||
import { FeaturesModule } from '../Features/Features.module';
|
||||
|
||||
@Module({
|
||||
@@ -66,7 +72,13 @@ import { FeaturesModule } from '../Features/Features.module';
|
||||
ValidateBranchExistance,
|
||||
ManualJournalBranchesValidator,
|
||||
CashflowTransactionsActivateBranches,
|
||||
ExpensesActivateBranches
|
||||
ExpensesActivateBranches,
|
||||
BillActivateBranches,
|
||||
VendorCreditActivateBranches,
|
||||
BillPaymentsActivateBranches,
|
||||
BillBranchesActivateSubscriber,
|
||||
VendorCreditBranchesActivateSubscriber,
|
||||
PaymentMadeActivateBranchesSubscriber
|
||||
],
|
||||
exports: [
|
||||
BranchesSettingsService,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { Bill } from '@/modules/Bills/models/Bill';
|
||||
|
||||
@Injectable()
|
||||
export class BillActivateBranches {
|
||||
constructor(private readonly billModel: TenantModelProxy<typeof Bill>) {}
|
||||
constructor(
|
||||
@Inject(Bill.name)
|
||||
private readonly billModel: TenantModelProxy<typeof Bill>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Updates all bills transactions with the primary branch.
|
||||
@@ -17,7 +20,7 @@ export class BillActivateBranches {
|
||||
primaryBranchId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) => {
|
||||
// Updates the sale invoice with primary branch.
|
||||
await Bill.query(trx).update({ branchId: primaryBranchId });
|
||||
// Updates the bills with primary branch.
|
||||
await this.billModel().query(trx).update({ branchId: primaryBranchId });
|
||||
};
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,11 +1,12 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { BillPayment } from '@/modules/BillPayments/models/BillPayment';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class BillPaymentsActivateBranches {
|
||||
constructor(
|
||||
@Inject(BillPayment.name)
|
||||
private readonly billPaymentModel: TenantModelProxy<typeof BillPayment>,
|
||||
) {}
|
||||
|
||||
|
||||
+2
-1
@@ -1,11 +1,12 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { VendorCredit } from '@/modules/VendorCredit/models/VendorCredit';
|
||||
|
||||
@Injectable()
|
||||
export class VendorCreditActivateBranches {
|
||||
constructor(
|
||||
@Inject(VendorCredit.name)
|
||||
private readonly vendorCreditModel: TenantModelProxy<typeof VendorCredit>,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export class Branch extends BaseModel{
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['created_at', 'updated_at'];
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { IBranchesActivatedPayload } from '../../Branches.types';
|
||||
import { events } from '@/common/events/events';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BillActivateBranches } from '../../integrations/Purchases/BillBranchesActivate';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
@Injectable()
|
||||
export class BillBranchesActivateSubscriber {
|
||||
constructor(
|
||||
private readonly billActivateBranches: BillActivateBranches,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Updates bills transactions with the primary branch once
|
||||
* the multi-branches is activated.
|
||||
* @param {IBranchesActivatedPayload}
|
||||
*/
|
||||
@OnEvent(events.branch.onActivated)
|
||||
async updateBillsWithBranchOnActivated({
|
||||
primaryBranch,
|
||||
trx,
|
||||
}: IBranchesActivatedPayload) {
|
||||
await this.billActivateBranches.updateBillsWithBranch(
|
||||
primaryBranch.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { IBranchesActivatedPayload } from '../../Branches.types';
|
||||
import { events } from '@/common/events/events';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { VendorCreditActivateBranches } from '../../integrations/Purchases/VendorCreditBranchesActivate';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
@Injectable()
|
||||
export class VendorCreditBranchesActivateSubscriber {
|
||||
constructor(
|
||||
private readonly vendorCreditActivateBranches: VendorCreditActivateBranches,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Updates vendor credits transactions with the primary branch once
|
||||
* the multi-branches is activated.
|
||||
* @param {IBranchesActivatedPayload}
|
||||
*/
|
||||
@OnEvent(events.branch.onActivated)
|
||||
async updateVendorCreditsWithBranchOnActivated({
|
||||
primaryBranch,
|
||||
trx,
|
||||
}: IBranchesActivatedPayload) {
|
||||
await this.vendorCreditActivateBranches.updateVendorCreditsWithBranch(
|
||||
primaryBranch.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { TenantsMigrateMakeCommand } from './commands/TenantsMigrateMake.command
|
||||
import { TenantsListCommand } from './commands/TenantsList.command';
|
||||
import { SystemSeedLatestCommand } from './commands/SystemSeedLatest.command';
|
||||
import { TenantsSeedLatestCommand } from './commands/TenantsSeedLatest.command';
|
||||
import { OpenApiExportCommand } from './commands/OpenApiExport.command';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -30,6 +31,7 @@ import { TenantsSeedLatestCommand } from './commands/TenantsSeedLatest.command';
|
||||
TenantsListCommand,
|
||||
SystemSeedLatestCommand,
|
||||
TenantsSeedLatestCommand,
|
||||
OpenApiExportCommand,
|
||||
],
|
||||
})
|
||||
export class CLIModule { }
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { ClsMiddleware } from 'nestjs-cls';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import '@/utils/moment-mysql';
|
||||
import { AppModule } from '@/modules/App/App.module';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
|
||||
@Command({
|
||||
name: 'openapi:export',
|
||||
description: 'Export the OpenAPI document from the NestJS app to shared/sdk-ts/openapi.json',
|
||||
})
|
||||
export class OpenApiExportCommand extends CommandRunner {
|
||||
async run(): Promise<void> {
|
||||
const serverRoot = process.cwd();
|
||||
global.__public_dirname = path.join(serverRoot, 'public');
|
||||
global.__static_dirname = path.join(serverRoot, 'static');
|
||||
global.__views_dirname = path.join(global.__static_dirname, '/views');
|
||||
global.__images_dirname = path.join(global.__static_dirname, '/images');
|
||||
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
rawBody: true,
|
||||
});
|
||||
app.set('query parser', 'extended');
|
||||
app.setGlobalPrefix('/api');
|
||||
app.use(new ClsMiddleware({}).use);
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Bigcapital')
|
||||
.setDescription('Financial accounting software')
|
||||
.setVersion('1.0')
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
await app.close();
|
||||
|
||||
const outputPath = path.resolve(process.cwd(), '../../shared/sdk-ts/openapi.json');
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
fs.writeFileSync(outputPath, JSON.stringify(document, null, 2), 'utf-8');
|
||||
console.log(`OpenAPI spec written to ${outputPath}`);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export class DocumentLink extends BaseModel{
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Document = require('./Document');
|
||||
const { Document } = require('./Document');
|
||||
|
||||
return {
|
||||
/**
|
||||
@@ -36,7 +36,7 @@ export class DocumentLink extends BaseModel{
|
||||
*/
|
||||
document: {
|
||||
relation: Model.HasOneRelation,
|
||||
modelClass: Document.default,
|
||||
modelClass: Document,
|
||||
join: {
|
||||
from: 'document_links.documentId',
|
||||
to: 'documents.id',
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
Patch,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiParam } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
|
||||
import { GetContactsAutoCompleteQuery } from './dtos/GetContactsAutoCompleteQuery.dto';
|
||||
import { GetAutoCompleteContactsService } from './queries/GetAutoCompleteContacts.service';
|
||||
import { GetContactService } from './queries/GetContact.service';
|
||||
import { ActivateContactService } from './commands/ActivateContact.service';
|
||||
import { InactivateContactService } from './commands/InactivateContact.service';
|
||||
|
||||
@@ -17,6 +18,7 @@ import { InactivateContactService } from './commands/InactivateContact.service';
|
||||
export class ContactsController {
|
||||
constructor(
|
||||
private readonly getAutoCompleteService: GetAutoCompleteContactsService,
|
||||
private readonly getContactService: GetContactService,
|
||||
private readonly activateContactService: ActivateContactService,
|
||||
private readonly inactivateContactService: InactivateContactService,
|
||||
) {}
|
||||
@@ -27,6 +29,14 @@ export class ContactsController {
|
||||
return this.getAutoCompleteService.autocompleteContacts(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get contact by ID (customer or vendor)' })
|
||||
@ApiParam({ name: 'id', type: Number, description: 'Contact ID' })
|
||||
@ApiResponse({ status: 200, description: 'Contact details (under "customer" key for form/duplicate use)' })
|
||||
getContact(@Param('id', ParseIntPipe) contactId: number) {
|
||||
return this.getContactService.getContact(contactId);
|
||||
}
|
||||
|
||||
@Patch(':id/activate')
|
||||
@ApiOperation({ summary: 'Activate a contact' })
|
||||
@ApiParam({ name: 'id', type: 'number', description: 'Contact ID' })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GetAutoCompleteContactsService } from './queries/GetAutoCompleteContacts.service';
|
||||
import { GetContactService } from './queries/GetContact.service';
|
||||
import { ContactsController } from './Contacts.controller';
|
||||
import { ActivateContactService } from './commands/ActivateContact.service';
|
||||
import { InactivateContactService } from './commands/InactivateContact.service';
|
||||
@@ -7,6 +8,7 @@ import { InactivateContactService } from './commands/InactivateContact.service';
|
||||
@Module({
|
||||
providers: [
|
||||
GetAutoCompleteContactsService,
|
||||
GetContactService,
|
||||
ActivateContactService,
|
||||
InactivateContactService,
|
||||
],
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Contact } from '../models/Contact';
|
||||
import { ContactTransfromer } from '../Contact.transformer';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetContactService {
|
||||
constructor(
|
||||
private readonly transformer: TransformerInjectable,
|
||||
@Inject(Contact.name)
|
||||
private readonly contactModel: TenantModelProxy<typeof Contact>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve contact by id (customer or vendor).
|
||||
* Returns transformed contact for duplicate/form use.
|
||||
*/
|
||||
async getContact(contactId: number): Promise<Record<string, unknown>> {
|
||||
const contact = await this.contactModel()
|
||||
.query()
|
||||
.findById(contactId)
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(contact, new ContactTransfromer());
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export interface IContactAddress {
|
||||
billingAddressCity: string;
|
||||
billingAddressCountry: string;
|
||||
billingAddressEmail: string;
|
||||
billingAddressZipcode: string;
|
||||
billingAddressPostcode: string;
|
||||
billingAddressPhone: string;
|
||||
billingAddressState: string;
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface IContactAddress {
|
||||
shippingAddressCity: string;
|
||||
shippingAddressCountry: string;
|
||||
shippingAddressEmail: string;
|
||||
shippingAddressZipcode: string;
|
||||
shippingAddressPostcode: string;
|
||||
shippingAddressPhone: string;
|
||||
shippingAddressState: string;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export interface IContactAddressDTO {
|
||||
billingAddressCity?: string;
|
||||
billingAddressCountry?: string;
|
||||
billingAddressEmail?: string;
|
||||
billingAddressZipcode?: string;
|
||||
billingAddressPostcode?: string;
|
||||
billingAddressPhone?: string;
|
||||
billingAddressState?: string;
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface IContactAddressDTO {
|
||||
shippingAddressCity?: string;
|
||||
shippingAddressCountry?: string;
|
||||
shippingAddressEmail?: string;
|
||||
shippingAddressZipcode?: string;
|
||||
shippingAddressPostcode?: string;
|
||||
shippingAddressPhone?: string;
|
||||
shippingAddressState?: string;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,76 @@
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ICreditNoteRefundDTO } from '../CreditNotes/types/CreditNotes.types';
|
||||
import { CreditNotesRefundsApplication } from './CreditNotesRefundsApplication.service';
|
||||
import { RefundCreditNote } from './models/RefundCreditNote';
|
||||
import { CreditNoteRefundDto } from './dto/CreditNoteRefund.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||
import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
|
||||
import { RefundCreditNoteResponseDto } from './dto/RefundCreditNoteResponse.dto';
|
||||
|
||||
@Controller('credit-notes')
|
||||
@ApiTags('Credit Note Refunds')
|
||||
@ApiExtraModels(RefundCreditNoteResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||
export class CreditNoteRefundsController {
|
||||
constructor(
|
||||
private readonly creditNotesRefundsApplication: CreditNotesRefundsApplication,
|
||||
) {}
|
||||
|
||||
@Get(':creditNoteId/refunds')
|
||||
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
|
||||
@ApiOperation({ summary: 'Retrieve the credit note graph.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Credit note refunds retrieved successfully.',
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: { $ref: getSchemaPath(RefundCreditNoteResponseDto) },
|
||||
},
|
||||
})
|
||||
getCreditNoteRefunds(@Param('creditNoteId') creditNoteId: number) {
|
||||
return this.creditNotesRefundsApplication.getCreditNoteRefunds(
|
||||
creditNoteId,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('refunds/:refundCreditId')
|
||||
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
|
||||
@ApiOperation({ summary: 'Retrieve a refund transaction for the given credit note.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Refund credit note transaction retrieved successfully.',
|
||||
schema: {
|
||||
$ref: getSchemaPath(RefundCreditNoteResponseDto),
|
||||
},
|
||||
})
|
||||
getRefundCreditNoteTransaction(
|
||||
@Param('refundCreditId') refundCreditId: number,
|
||||
) {
|
||||
return this.creditNotesRefundsApplication.getRefundCreditNoteTransaction(
|
||||
refundCreditId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a refund credit note.
|
||||
* @param {number} creditNoteId - The credit note ID.
|
||||
@@ -29,6 +78,7 @@ export class CreditNoteRefundsController {
|
||||
* @returns {Promise<RefundCreditNote>}
|
||||
*/
|
||||
@Post(':creditNoteId/refunds')
|
||||
@RequirePermission(CreditNoteAction.Refund, AbilitySubject.CreditNote)
|
||||
@ApiOperation({ summary: 'Create a refund for the given credit note.' })
|
||||
createRefundCreditNote(
|
||||
@Param('creditNoteId') creditNoteId: number,
|
||||
@@ -46,6 +96,7 @@ export class CreditNoteRefundsController {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@Delete('refunds/:refundCreditId')
|
||||
@RequirePermission(CreditNoteAction.Refund, AbilitySubject.CreditNote)
|
||||
@ApiOperation({ summary: 'Delete a refund for the given credit note.' })
|
||||
deleteRefundCreditNote(
|
||||
@Param('refundCreditId') refundCreditId: number,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CreditNotesRefundsApplication } from './CreditNotesRefundsApplication.s
|
||||
import { CreditNoteRefundsController } from './CreditNoteRefunds.controller';
|
||||
import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
|
||||
import { GetCreditNoteRefundsService } from './queries/GetCreditNoteRefunds.service';
|
||||
import { GetRefundCreditNoteTransaction } from './queries/GetRefundCreditNoteTransaction.service';
|
||||
import { RefundCreditNoteGLEntries } from './commands/RefundCreditNoteGLEntries';
|
||||
import { RefundCreditNoteGLEntriesSubscriber } from '../CreditNotes/subscribers/RefundCreditNoteGLEntriesSubscriber';
|
||||
import { LedgerModule } from '../Ledger/Ledger.module';
|
||||
@@ -21,6 +22,7 @@ import { AccountsModule } from '../Accounts/Accounts.module';
|
||||
RefundSyncCreditNoteBalanceService,
|
||||
CreditNotesRefundsApplication,
|
||||
GetCreditNoteRefundsService,
|
||||
GetRefundCreditNoteTransaction,
|
||||
RefundCreditNoteGLEntries,
|
||||
RefundCreditNoteGLEntriesSubscriber,
|
||||
],
|
||||
|
||||
@@ -6,6 +6,7 @@ import { RefundCreditNoteService } from './commands/RefundCreditNote.service';
|
||||
import { RefundSyncCreditNoteBalanceService } from './commands/RefundSyncCreditNoteBalance';
|
||||
import { CreditNoteRefundDto } from './dto/CreditNoteRefund.dto';
|
||||
import { GetCreditNoteRefundsService } from './queries/GetCreditNoteRefunds.service';
|
||||
import { GetRefundCreditNoteTransaction } from './queries/GetRefundCreditNoteTransaction.service';
|
||||
|
||||
@Injectable()
|
||||
export class CreditNotesRefundsApplication {
|
||||
@@ -13,6 +14,7 @@ export class CreditNotesRefundsApplication {
|
||||
private readonly createRefundCreditNoteService: CreateRefundCreditNoteService,
|
||||
private readonly deleteRefundCreditNoteService: DeleteRefundCreditNoteService,
|
||||
private readonly getCreditNoteRefundsService: GetCreditNoteRefundsService,
|
||||
private readonly getRefundCreditNoteTransactionService: GetRefundCreditNoteTransaction,
|
||||
private readonly refundCreditNoteService: RefundCreditNoteService,
|
||||
private readonly refundSyncCreditNoteBalanceService: RefundSyncCreditNoteBalanceService,
|
||||
) {}
|
||||
@@ -26,6 +28,12 @@ export class CreditNotesRefundsApplication {
|
||||
return this.getCreditNoteRefundsService.getCreditNoteRefunds(creditNoteId);
|
||||
}
|
||||
|
||||
public getRefundCreditNoteTransaction(refundCreditId: number) {
|
||||
return this.getRefundCreditNoteTransactionService.getRefundCreditTransaction(
|
||||
refundCreditId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a refund credit note.
|
||||
* @param {number} creditNoteId - The credit note ID.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
class RefundCreditNoteSummaryDto {
|
||||
@ApiProperty({ example: 1 })
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ example: 'CN-0001' })
|
||||
creditNoteNumber: string;
|
||||
}
|
||||
|
||||
class RefundCreditAccountDto {
|
||||
@ApiProperty({ example: 10 })
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ example: 'Cash on Hand' })
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class RefundCreditNoteResponseDto {
|
||||
@ApiProperty({ example: 100 })
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ example: '2024-01-15' })
|
||||
date: string;
|
||||
|
||||
@ApiProperty({ example: '2024-01-15' })
|
||||
formattedDate: string;
|
||||
|
||||
@ApiProperty({ example: 250 })
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({ example: '$250.00' })
|
||||
formttedAmount: string;
|
||||
|
||||
@ApiProperty({ example: 'REF-001', required: false, nullable: true })
|
||||
referenceNo?: string | null;
|
||||
|
||||
@ApiProperty({ example: 'Refund issued to customer', required: false, nullable: true })
|
||||
description?: string | null;
|
||||
|
||||
@ApiProperty({ type: RefundCreditAccountDto })
|
||||
fromAccount: RefundCreditAccountDto;
|
||||
|
||||
@ApiProperty({ type: RefundCreditNoteSummaryDto })
|
||||
creditNote: RefundCreditNoteSummaryDto;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export class RefundCreditNote extends BaseModel {
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['created_at', 'updated_at'];
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user