Compare commits
3 Commits
bugs-bashi
...
copilot/su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d035bcc9a3 | ||
|
|
d9e07fdd99 | ||
|
|
8c5a359610 |
@@ -168,16 +168,6 @@
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Daniel15",
|
||||
"name": "Daniel Lo Nigro",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/91933?v=4",
|
||||
"profile": "https://d.sb/",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
12
.env.example
12
.env.example
@@ -98,18 +98,8 @@ POSTHOG_API_KEY=
|
||||
POSTHOG_HOST=
|
||||
|
||||
# Stripe Payment
|
||||
# Get the keys from the Stripe dashboard
|
||||
# Starts with "sk_"
|
||||
STRIPE_PAYMENT_SECRET_KEY=
|
||||
# Starts with "pk_"
|
||||
STRIPE_PAYMENT_PUBLISHABLE_KEY=
|
||||
# Get the client ID from https://dashboard.stripe.com/settings/connect/onboarding-options/oauth
|
||||
# Starts with "ca_"
|
||||
STRIPE_PAYMENT_CLIENT_ID=
|
||||
# Configure the webhook here: https://dashboard.stripe.com/workbench/webhooks/
|
||||
# Endpoint URL is https://example.com/api/webhooks/stripe (replace "example.com" with the correct domain)
|
||||
# Select the "checkout.session.completed" and "account.updated" events
|
||||
# Starts with "whsec_"
|
||||
STRIPE_PAYMENT_WEBHOOKS_SECRET=
|
||||
# Replace example.com with the correct domain
|
||||
STRIPE_PAYMENT_REDIRECT_URL=https://example.com/preferences/payment-methods/stripe/callback
|
||||
STRIPE_PAYMENT_REDIRECT_URL=
|
||||
@@ -54,7 +54,7 @@ pnpm install
|
||||
- Run all required docker containers in the development, we already configured all containers under `docker-compose.yml`.
|
||||
|
||||
```
|
||||
docker compose up -d
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Wait some seconds, and hit `docker-compose ps` and you should see the same result below.
|
||||
@@ -75,7 +75,7 @@ pnpm run build:server
|
||||
- Run the database migration for system database.
|
||||
|
||||
```
|
||||
pnpm run system:migrate:latest
|
||||
node packages/server/build/commands.js system:migrate:latest
|
||||
```
|
||||
|
||||
And you should get something like that.
|
||||
@@ -84,10 +84,10 @@ And you should get something like that.
|
||||
Batch 1 run: 6 migrations
|
||||
```
|
||||
|
||||
- Next, start the server.
|
||||
- Next, start the webapp application.
|
||||
|
||||
```
|
||||
pnpm run server:start
|
||||
pnpm run dev:server
|
||||
```
|
||||
|
||||
**[`^top^`](#)**
|
||||
@@ -96,6 +96,12 @@ pnpm run server:start
|
||||
|
||||
## Contribute to Frontend
|
||||
|
||||
- Clone the `bigcapital` repository and cd into `bigcapital` directory.
|
||||
|
||||
```
|
||||
git clone https://github.com/bigcapital/bigcapital.git && cd bigcaptial
|
||||
```
|
||||
|
||||
- Install all npm dependencies of the monorepo, you don't have to change directory to the `frontend` package. just hit that command and will install all packages across all application.
|
||||
|
||||
```
|
||||
@@ -132,4 +138,4 @@ There are many other ways to get involved with the community and to participate
|
||||
|
||||
Again, Feel free to ping us on [`#contributing`](https://discord.com/invite/c8nPBJafeb) on our Discord community if you need any help on this :)
|
||||
|
||||
Thank You!
|
||||
Thank You!
|
||||
@@ -135,7 +135,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://myself.vercel.app/"><img src="https://avatars.githubusercontent.com/u/42431274?v=4?s=100" width="100px;" alt="Sachin Mittal"/><br /><sub><b>Sachin Mittal</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Amittalsam98" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.camilooviedo.com/"><img src="https://avatars.githubusercontent.com/u/64604272?v=4?s=100" width="100px;" alt="Camilo Oviedo"/><br /><sub><b>Camilo Oviedo</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=Champetaman" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://nklmantey.com/"><img src="https://avatars.githubusercontent.com/u/90279429?v=4?s=100" width="100px;" alt="Mantey"/><br /><sub><b>Mantey</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Anklmantey" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://d.sb/"><img src="https://avatars.githubusercontent.com/u/91933?v=4?s=100" width="100px;" alt="Daniel Lo Nigro"/><br /><sub><b>Daniel Lo Nigro</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3ADaniel15" title="Bug reports">🐛</a> <a href="https://github.com/bigcapitalhq/bigcapital/commits?author=Daniel15" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -93,7 +93,7 @@ services:
|
||||
- OPEN_EXCHANGE_RATE_APP_ID-${OPEN_EXCHANGE_RATE_APP_ID}
|
||||
|
||||
# Bank Sync
|
||||
- BANK_FEED_ENABLED=${BANK_FEED_ENABLED}
|
||||
- BANKING_CONNECT=${BANKING_CONNECT}
|
||||
|
||||
# Plaid
|
||||
- PLAID_ENV=${PLAID_ENV}
|
||||
@@ -122,13 +122,6 @@ services:
|
||||
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
|
||||
- S3_ENDPOINT=${S3_ENDPOINT}
|
||||
- S3_BUCKET=${S3_BUCKET}
|
||||
|
||||
# Stripe
|
||||
- STRIPE_PAYMENT_SECRET_KEY=${STRIPE_PAYMENT_SECRET_KEY}
|
||||
- STRIPE_PAYMENT_PUBLISHABLE_KEY=${STRIPE_PAYMENT_PUBLISHABLE_KEY}
|
||||
- STRIPE_PAYMENT_CLIENT_ID=${STRIPE_PAYMENT_CLIENT_ID}
|
||||
- STRIPE_PAYMENT_WEBHOOKS_SECRET=${STRIPE_PAYMENT_WEBHOOKS_SECRET}
|
||||
- STRIPE_PAYMENT_REDIRECT_URL=${STRIPE_PAYMENT_REDIRECT_URL}
|
||||
|
||||
database_migration:
|
||||
container_name: bigcapital-database-migration
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM redis:6.2.21
|
||||
FROM redis:6.2.0
|
||||
|
||||
COPY redis.conf /usr/local/etc/redis/redis.conf
|
||||
|
||||
|
||||
@@ -167,9 +167,6 @@
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node",
|
||||
"moduleNameMapper": {
|
||||
"^@/(.*)$": "<rootDir>/$1"
|
||||
}
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('app', () => ({
|
||||
baseUrl: process.env.BASE_URL,
|
||||
}));
|
||||
@@ -1,5 +0,0 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('cloud', () => ({
|
||||
hostedOnCloud: process.env.HOSTED_ON_BIGCAPITAL_CLOUD === 'true',
|
||||
}));
|
||||
@@ -1,4 +1,3 @@
|
||||
import app from './app';
|
||||
import systemDatabase from './system-database';
|
||||
import tenantDatabase from './tenant-database';
|
||||
import signup from './signup';
|
||||
@@ -16,12 +15,9 @@ import mail from './mail';
|
||||
import loops from './loops';
|
||||
import bankfeed from './bankfeed';
|
||||
import throttle from './throttle';
|
||||
import cloud from './cloud';
|
||||
|
||||
export const config = [
|
||||
app,
|
||||
systemDatabase,
|
||||
cloud,
|
||||
tenantDatabase,
|
||||
signup,
|
||||
gotenberg,
|
||||
|
||||
@@ -2,10 +2,12 @@ import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('throttle', () => ({
|
||||
global: {
|
||||
// TTL in milliseconds (NestJS throttler v6+)
|
||||
ttl: parseInt(process.env.THROTTLE_GLOBAL_TTL ?? '60000', 10),
|
||||
limit: parseInt(process.env.THROTTLE_GLOBAL_LIMIT ?? '100', 10),
|
||||
},
|
||||
auth: {
|
||||
// TTL in milliseconds (NestJS throttler v6+)
|
||||
ttl: parseInt(process.env.THROTTLE_AUTH_TTL ?? '60000', 10),
|
||||
limit: parseInt(process.env.THROTTLE_AUTH_LIMIT ?? '10', 10),
|
||||
},
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { IsArray, IsInt, ArrayNotEmpty, IsBoolean, IsOptional } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { parseBoolean } from '@/utils/parse-boolean';
|
||||
|
||||
export class BulkDeleteDto {
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@IsInt({ each: true })
|
||||
@ApiProperty({
|
||||
description: 'Array of IDs to delete',
|
||||
type: [Number],
|
||||
example: [1, 2, 3],
|
||||
})
|
||||
ids: number[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value, obj }) => parseBoolean(value ?? obj?.skip_undeletable, false))
|
||||
@ApiPropertyOptional({
|
||||
description: 'When true, undeletable items will be skipped and only deletable ones will be removed.',
|
||||
type: Boolean,
|
||||
default: false,
|
||||
})
|
||||
skipUndeletable?: boolean;
|
||||
}
|
||||
|
||||
export class ValidateBulkDeleteResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Number of items that can be deleted',
|
||||
example: 2,
|
||||
})
|
||||
deletableCount: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Number of items that cannot be deleted',
|
||||
example: 1,
|
||||
})
|
||||
nonDeletableCount: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'IDs of items that can be deleted',
|
||||
type: [Number],
|
||||
example: [1, 2],
|
||||
})
|
||||
deletableIds: number[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'IDs of items that cannot be deleted',
|
||||
type: [Number],
|
||||
example: [3],
|
||||
})
|
||||
nonDeletableIds: number[];
|
||||
}
|
||||
|
||||
@@ -7,46 +7,53 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { type Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { mapKeysDeep } from '@/utils/deepdash';
|
||||
|
||||
export function camelToSnake<T = any>(value: T): T {
|
||||
export function camelToSnake<T = any>(value: T) {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
return mapKeysDeep(
|
||||
value,
|
||||
(_value: string, key: any, parent: any, context: any) => {
|
||||
if (Array.isArray(parent)) {
|
||||
// tell mapKeysDeep to skip mapping inside this branch
|
||||
context.skipChildren = true;
|
||||
return key;
|
||||
}
|
||||
return key
|
||||
.split(/(?=[A-Z])/)
|
||||
.join('_')
|
||||
.toLowerCase();
|
||||
},
|
||||
) as T;
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(camelToSnake);
|
||||
}
|
||||
if (typeof value === 'object' && !(value instanceof Date)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, value]) => [
|
||||
key
|
||||
.split(/(?=[A-Z])/)
|
||||
.join('_')
|
||||
.toLowerCase(),
|
||||
camelToSnake(value),
|
||||
]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function snakeToCamel<T = any>(value: T): T {
|
||||
export function snakeToCamel<T = any>(value: T) {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
return mapKeysDeep(
|
||||
value,
|
||||
(_value: string, key: any, parent: any, context: any) => {
|
||||
if (Array.isArray(parent)) {
|
||||
// tell mapKeysDeep to skip mapping inside this branch
|
||||
context.skipChildren = true;
|
||||
return key;
|
||||
}
|
||||
const converted = key.replace(/([-_]\w)/g, (group) =>
|
||||
group[1].toUpperCase(),
|
||||
);
|
||||
return converted[0].toLowerCase() + converted.slice(1);
|
||||
},
|
||||
) as T;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(snakeToCamel);
|
||||
}
|
||||
|
||||
const impl = (str: string) => {
|
||||
const converted = str.replace(/([-_]\w)/g, (group) =>
|
||||
group[1].toUpperCase(),
|
||||
);
|
||||
return converted[0].toLowerCase() + converted.slice(1);
|
||||
};
|
||||
|
||||
if (typeof value === 'object' && !(value instanceof Date)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, value]) => [
|
||||
impl(key),
|
||||
snakeToCamel(value),
|
||||
]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const DEFAULT_STRATEGY = {
|
||||
@@ -56,7 +63,7 @@ export const DEFAULT_STRATEGY = {
|
||||
|
||||
@Injectable()
|
||||
export class SerializeInterceptor implements NestInterceptor<any, any> {
|
||||
constructor(@Optional() readonly strategy = DEFAULT_STRATEGY) { }
|
||||
constructor(@Optional() readonly strategy = DEFAULT_STRATEGY) {}
|
||||
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
|
||||
@@ -1,27 +1,4 @@
|
||||
import { chain, mapKeys } from 'lodash';
|
||||
|
||||
const getTransactionsLockingSettingsSchema = (modules: string[]) => {
|
||||
const moduleSchema = {
|
||||
active: { type: 'boolean' },
|
||||
lock_to_date: { type: 'date' },
|
||||
unlock_from_date: { type: 'date' },
|
||||
unlock_to_date: { type: 'date' },
|
||||
lock_reason: { type: 'string' },
|
||||
unlock_reason: { type: 'string' },
|
||||
};
|
||||
return chain(modules)
|
||||
.map((module: string) => {
|
||||
return mapKeys(moduleSchema, (value, key: string) => `${module}.${key}`);
|
||||
})
|
||||
.flattenDeep()
|
||||
.reduce((result, value) => {
|
||||
return {
|
||||
...result,
|
||||
...value,
|
||||
};
|
||||
}, {})
|
||||
.value();
|
||||
};
|
||||
// import { getTransactionsLockingSettingsSchema } from '@/api/controllers/TransactionsLocking/utils';
|
||||
|
||||
export const SettingsOptions = {
|
||||
organization: {
|
||||
@@ -246,12 +223,12 @@ export const SettingsOptions = {
|
||||
'locking-type': {
|
||||
type: 'string',
|
||||
},
|
||||
...getTransactionsLockingSettingsSchema([
|
||||
'all',
|
||||
'sales',
|
||||
'purchases',
|
||||
'financial',
|
||||
]),
|
||||
// ...getTransactionsLockingSettingsSchema([
|
||||
// 'all',
|
||||
// 'sales',
|
||||
// 'purchases',
|
||||
// 'financial',
|
||||
// ]),
|
||||
},
|
||||
features: {
|
||||
'multi-warehouses': {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('api_keys', (table) => {
|
||||
table.increments();
|
||||
table.string('key').notNullable().unique().index();
|
||||
table.string('name');
|
||||
table
|
||||
.integer('user_id')
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.index()
|
||||
.references('id')
|
||||
.inTable('users');
|
||||
table
|
||||
.bigInteger('tenant_id')
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.index()
|
||||
.references('id')
|
||||
.inTable('tenants');
|
||||
table.dateTime('expires_at').nullable().index();
|
||||
table.dateTime('revoked_at').nullable().index();
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.dropTableIfExists('api_keys');
|
||||
};
|
||||
@@ -3,7 +3,6 @@
|
||||
"field.description": "Description",
|
||||
"field.slug": "Account slug",
|
||||
"field.code": "Account code",
|
||||
"field.code_hint": "Unique number to identify the account.",
|
||||
"field.root_type": "Root type",
|
||||
"field.normal": "Account normal",
|
||||
"field.normal.credit": "Credit",
|
||||
@@ -14,6 +13,5 @@
|
||||
"field.balance": "Balance",
|
||||
"field.bank_balance": "Bank Balance",
|
||||
"field.parent_account": "Parent Account",
|
||||
"field.created_at": "Created at",
|
||||
"field.account_hint": "Matches the account name or code."
|
||||
"field.created_at": "Created at"
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"field.vendor": "Vendor",
|
||||
"field.bill_number": "Bill No.",
|
||||
"field.bill_date": "Date",
|
||||
"field.due_date": "Due Date",
|
||||
"field.reference_no": "Reference No.",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.note": "Note",
|
||||
"field.open": "Open",
|
||||
"field.entries": "Entries",
|
||||
"field.item": "Item",
|
||||
"field.item_hint": "Matches the item name or code.",
|
||||
"field.rate": "Rate",
|
||||
"field.quantity": "Quantity",
|
||||
"field.description": "Line Description",
|
||||
"field.amount": "Amount",
|
||||
"field.payment_amount": "Payment Amount",
|
||||
"field.status": "Status",
|
||||
"field.status.paid": "Paid",
|
||||
"field.status.partially-paid": "Partially Paid",
|
||||
"field.status.overdue": "Overdue",
|
||||
"field.status.unpaid": "Unpaid",
|
||||
"field.status.opened": "Opened",
|
||||
"field.status.draft": "Draft",
|
||||
"field.created_at": "Created At"
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"field.vendor": "Vendor",
|
||||
"field.payment_date": "Payment Date",
|
||||
"field.payment_number": "Payment No.",
|
||||
"field.payment_account": "Payment Account",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.note": "Note",
|
||||
"field.reference": "Reference",
|
||||
"field.entries": "Entries",
|
||||
"field.entries.bill": "Bill",
|
||||
"field.entries.payment_amount": "Payment Amount",
|
||||
"field.payment_number_hint": "The payment number should be unique.",
|
||||
"field.bill_hint": "Matches the bill number."
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"field.customer": "Customer",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.credit_note_date": "Credit Note Date",
|
||||
"field.reference_no": "Reference No.",
|
||||
"field.note": "Note",
|
||||
"field.terms_conditions": "Terms & Conditions",
|
||||
"field.credit_note_number": "Credit Note Number",
|
||||
"field.open": "Open",
|
||||
"field.entries": "Entries",
|
||||
"field.item": "Item",
|
||||
"field.rate": "Rate",
|
||||
"field.quantity": "Quantity",
|
||||
"field.description": "Description"
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"field.customer": "Customer",
|
||||
"field.estimate_date": "Estimate Date",
|
||||
"field.expiration_date": "Expiration Date",
|
||||
"field.estimate_number": "Estimate No.",
|
||||
"field.reference_no": "Reference No.",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.currency": "Currency",
|
||||
"field.note": "Note",
|
||||
"field.terms_conditions": "Terms & Conditions",
|
||||
"field.delivered": "Delivered",
|
||||
"field.entries": "Entries",
|
||||
"field.amount": "Amount",
|
||||
"field.status": "Status",
|
||||
"field.status.draft": "Draft",
|
||||
"field.status.delivered": "Delivered",
|
||||
"field.status.rejected": "Rejected",
|
||||
"field.status.approved": "Approved",
|
||||
"field.created_at": "Created At"
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"field.due_amount": "Due amount",
|
||||
"field.delivered": "Delivered",
|
||||
"field.item_name": "Item Name",
|
||||
"field.item_hint": "Matches the item name or code.",
|
||||
"field.rate": "Rate",
|
||||
"field.quantity": "Quantity",
|
||||
"field.description": "Description",
|
||||
@@ -39,7 +38,5 @@
|
||||
"field.status.draft": "Draft",
|
||||
"field.created_at": "Created at",
|
||||
"field.currency": "Currency",
|
||||
"field.entries": "Entries",
|
||||
"field.branch": "Branch",
|
||||
"field.warehouse": "Warehouse"
|
||||
"field.entries": "Entries"
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"field.quantity_on_hand": "Quantity on Hand",
|
||||
"field.note": "Note",
|
||||
"field.category": "Category",
|
||||
"field.category_hint": "Matches the category name.",
|
||||
"field.active": "Active",
|
||||
"field.created_at": "Created At"
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"field.customer": "Customer",
|
||||
"field.payment_date": "Payment Date",
|
||||
"field.amount": "Amount",
|
||||
"field.reference_no": "Reference No.",
|
||||
"field.deposit_account": "Deposit Account",
|
||||
"field.payment_receive_no": "Payment No.",
|
||||
"field.statement": "Statement",
|
||||
"field.entries": "Entries",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.invoice": "Invoice",
|
||||
"field.entries.payment_amount": "Payment Amount",
|
||||
"field.created_at": "Created At",
|
||||
"field.payment_no_hint": "The payment number should be unique.",
|
||||
"field.invoice_hint": "Matches the invoice number."
|
||||
}
|
||||
|
||||
@@ -10,21 +10,5 @@
|
||||
"paper.receipt_amount": "Receipt amount",
|
||||
"paper.total": "Total",
|
||||
"paper.balance_due": "Balance Due",
|
||||
"paper.payment_amount": "Payment Amount",
|
||||
|
||||
"field.receipt_date": "Receipt Date",
|
||||
"field.customer": "Customer",
|
||||
"field.deposit_account": "Deposit Account",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.receipt_number": "Receipt Number",
|
||||
"field.reference_no": "Reference No.",
|
||||
"field.closed": "Closed",
|
||||
"field.entries": "Entries",
|
||||
"field.statement": "Statement",
|
||||
"field.receipt_message": "Receipt Message",
|
||||
"field.amount": "Amount",
|
||||
"field.status": "Status",
|
||||
"field.status.draft": "Draft",
|
||||
"field.status.closed": "Closed",
|
||||
"field.created_at": "Created At"
|
||||
"paper.payment_amount": "Payment Amount"
|
||||
}
|
||||
@@ -2,18 +2,5 @@
|
||||
"view.draft": "Draft",
|
||||
"view.published": "Published",
|
||||
"view.open": "Open",
|
||||
"view.closed": "Closed",
|
||||
|
||||
"field.vendor": "Vendor",
|
||||
"field.vendor_credit_number": "Vendor Credit No.",
|
||||
"field.vendor_credit_date": "Vendor Credit Date",
|
||||
"field.reference_no": "Reference No.",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.note": "Note",
|
||||
"field.open": "Open",
|
||||
"field.entries": "Entries",
|
||||
"field.item": "Item",
|
||||
"field.rate": "Rate",
|
||||
"field.quantity": "Quantity",
|
||||
"field.description": "Description"
|
||||
"view.closed": "Closed"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ class FsMigrations {
|
||||
private sortDirsSeparately: boolean;
|
||||
private migrationsPaths: string[];
|
||||
private loadExtensions: string[];
|
||||
private seedsDirectory: string;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
@@ -31,8 +30,6 @@ class FsMigrations {
|
||||
}
|
||||
this.migrationsPaths = migrationDirectories;
|
||||
this.loadExtensions = loadExtensions || DEFAULT_LOAD_EXTENSIONS;
|
||||
// Store the seeds directory (first path is the seeds directory)
|
||||
this.seedsDirectory = migrationDirectories[0] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,10 +93,7 @@ class FsMigrations {
|
||||
* @returns {string}
|
||||
*/
|
||||
public getMigration(migration: MigrateItem): string {
|
||||
return importWebpackSeedModule(
|
||||
migration.file.replace('.ts', ''),
|
||||
this.seedsDirectory,
|
||||
);
|
||||
return importWebpackSeedModule(migration.file.replace('.ts', ''));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* Detarmines the module type of the given file path.
|
||||
@@ -36,32 +35,8 @@ export async function importFile(filepath: string): any {
|
||||
/**
|
||||
*
|
||||
* @param {string} moduleName
|
||||
* @param {string} seedsDirectory - The seeds directory path from config
|
||||
* @returns
|
||||
*/
|
||||
export async function importWebpackSeedModule(
|
||||
moduleName: string,
|
||||
seedsDirectory: string,
|
||||
): any {
|
||||
// Convert the seeds directory to a relative path from this file's location
|
||||
const utilsDir = __dirname;
|
||||
const seedsDirAbsolute = path.isAbsolute(seedsDirectory)
|
||||
? seedsDirectory
|
||||
: path.resolve(process.cwd(), seedsDirectory);
|
||||
|
||||
// Get relative path from Utils.js location to seeds directory
|
||||
const relativePath = path.relative(utilsDir, seedsDirAbsolute);
|
||||
|
||||
// Convert to forward slashes for import (works on all platforms)
|
||||
const importPath = relativePath.split(path.sep).join('/');
|
||||
|
||||
// Construct the import path (add ./ prefix if not already present, or handle empty/current dir)
|
||||
let finalPath = importPath;
|
||||
if (!finalPath || finalPath === '.') {
|
||||
finalPath = './';
|
||||
} else if (!finalPath.startsWith('.')) {
|
||||
finalPath = `./${finalPath}`;
|
||||
}
|
||||
|
||||
return import(`${finalPath}/${moduleName}`);
|
||||
export async function importWebpackSeedModule(moduleName: string): any {
|
||||
return import(`../../database/seeds/core/${moduleName}`);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ServiceErrorFilter } from './common/filters/service-error.filter';
|
||||
import { ModelHasRelationsFilter } from './common/filters/model-has-relations.filter';
|
||||
import { ValidationPipe } from './common/pipes/ClassValidation.pipe';
|
||||
import { ToJsonInterceptor } from './common/interceptors/to-json.interceptor';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
|
||||
global.__public_dirname = path.join(__dirname, '..', 'public');
|
||||
global.__static_dirname = path.join(__dirname, '../static');
|
||||
@@ -16,10 +15,7 @@ global.__views_dirname = path.join(global.__static_dirname, '/views');
|
||||
global.__images_dirname = path.join(global.__static_dirname, '/images');
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
rawBody: true,
|
||||
});
|
||||
app.set('query parser', 'extended');
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.setGlobalPrefix('/api');
|
||||
|
||||
// create and mount the middleware manually here
|
||||
|
||||
@@ -7,16 +7,15 @@ import {
|
||||
Get,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
Put,
|
||||
} from '@nestjs/common';
|
||||
import { AccountsApplication } from './AccountsApplication.service';
|
||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||
import { EditAccountDTO } from './EditAccount.dto';
|
||||
import { IAccountsFilter } from './Accounts.types';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
getSchemaPath,
|
||||
@@ -25,57 +24,16 @@ import { AccountResponseDto } from './dtos/AccountResponse.dto';
|
||||
import { AccountTypeResponseDto } from './dtos/AccountTypeResponse.dto';
|
||||
import { GetAccountTransactionResponseDto } from './dtos/GetAccountTransactionResponse.dto';
|
||||
import { GetAccountTransactionsQueryDto } from './dtos/GetAccountTransactionsQuery.dto';
|
||||
import { GetAccountsQueryDto } from './dtos/GetAccountsQuery.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import {
|
||||
BulkDeleteDto,
|
||||
ValidateBulkDeleteResponseDto,
|
||||
} from '@/common/dtos/BulkDelete.dto';
|
||||
|
||||
@Controller('accounts')
|
||||
@ApiTags('Accounts')
|
||||
@ApiExtraModels(AccountResponseDto)
|
||||
@ApiExtraModels(AccountTypeResponseDto)
|
||||
@ApiExtraModels(GetAccountTransactionResponseDto)
|
||||
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
export class AccountsController {
|
||||
constructor(private readonly accountsApplication: AccountsApplication) { }
|
||||
|
||||
@Post('validate-bulk-delete')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'Validation completed. Returns counts and IDs of deletable and non-deletable accounts.',
|
||||
schema: {
|
||||
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||
},
|
||||
})
|
||||
async validateBulkDeleteAccounts(
|
||||
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||
): Promise<ValidateBulkDeleteResponseDto> {
|
||||
return this.accountsApplication.validateBulkDeleteAccounts(
|
||||
bulkDeleteDto.ids,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('bulk-delete')
|
||||
@ApiOperation({ summary: 'Deletes multiple accounts in bulk.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The accounts have been successfully deleted.',
|
||||
})
|
||||
async bulkDeleteAccounts(
|
||||
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||
): Promise<void> {
|
||||
return this.accountsApplication.bulkDeleteAccounts(bulkDeleteDto.ids, {
|
||||
skipUndeletable: bulkDeleteDto.skipUndeletable ?? false,
|
||||
});
|
||||
}
|
||||
constructor(private readonly accountsApplication: AccountsApplication) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create an account' })
|
||||
@@ -87,7 +45,7 @@ export class AccountsController {
|
||||
return this.accountsApplication.createAccount(accountDTO);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Post(':id')
|
||||
@ApiOperation({ summary: 'Edit the given account.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -220,19 +178,7 @@ export class AccountsController {
|
||||
items: { $ref: getSchemaPath(AccountResponseDto) },
|
||||
},
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'onlyInactive',
|
||||
required: false,
|
||||
type: Boolean,
|
||||
description: 'Filter to show only inactive accounts',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'structure',
|
||||
required: false,
|
||||
type: String,
|
||||
description: 'Structure type for the accounts list',
|
||||
})
|
||||
async getAccounts(@Query() filter: GetAccountsQueryDto) {
|
||||
async getAccounts(@Query() filter: Partial<IAccountsFilter>) {
|
||||
return this.accountsApplication.getAccounts(filter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ import { GetAccountsService } from './GetAccounts.service';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
import { AccountsExportable } from './AccountsExportable.service';
|
||||
import { AccountsImportable } from './AccountsImportable.service';
|
||||
import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service';
|
||||
import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service';
|
||||
|
||||
const models = [RegisterTenancyModel(BankAccount)];
|
||||
|
||||
@@ -42,9 +40,7 @@ const models = [RegisterTenancyModel(BankAccount)];
|
||||
GetAccountTransactionsService,
|
||||
GetAccountsService,
|
||||
AccountsExportable,
|
||||
AccountsImportable,
|
||||
BulkDeleteAccountsService,
|
||||
ValidateBulkDeleteAccountsService,
|
||||
AccountsImportable
|
||||
],
|
||||
exports: [
|
||||
AccountRepository,
|
||||
|
||||
@@ -10,14 +10,10 @@ import { GetAccount } from './GetAccount.service';
|
||||
import { ActivateAccount } from './ActivateAccount.service';
|
||||
import { GetAccountTypesService } from './GetAccountTypes.service';
|
||||
import { GetAccountTransactionsService } from './GetAccountTransactions.service';
|
||||
import { IAccountsTransactionsFilter } from './Accounts.types';
|
||||
import { IAccountsFilter, IAccountsTransactionsFilter } from './Accounts.types';
|
||||
import { GetAccountsService } from './GetAccounts.service';
|
||||
import { IFilterMeta } from '@/interfaces/Model';
|
||||
import { GetAccountTransactionResponseDto } from './dtos/GetAccountTransactionResponse.dto';
|
||||
import { GetAccountsQueryDto } from './dtos/GetAccountsQuery.dto';
|
||||
import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service';
|
||||
import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service';
|
||||
import { ValidateBulkDeleteResponseDto } from '@/common/dtos/BulkDelete.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AccountsApplication {
|
||||
@@ -40,9 +36,7 @@ export class AccountsApplication {
|
||||
private readonly getAccountService: GetAccount,
|
||||
private readonly getAccountTransactionsService: GetAccountTransactionsService,
|
||||
private readonly getAccountsService: GetAccountsService,
|
||||
private readonly bulkDeleteAccountsService: BulkDeleteAccountsService,
|
||||
private readonly validateBulkDeleteAccountsService: ValidateBulkDeleteAccountsService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new account.
|
||||
@@ -114,11 +108,11 @@ export class AccountsApplication {
|
||||
|
||||
/**
|
||||
* Retrieves the accounts list.
|
||||
* @param {GetAccountsQueryDto} filterDTO - Filter DTO.
|
||||
* @param {IAccountsFilter} filterDTO - Filter DTO.
|
||||
* @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>}
|
||||
*/
|
||||
public getAccounts = (
|
||||
filterDTO: GetAccountsQueryDto,
|
||||
filterDTO: Partial<IAccountsFilter>,
|
||||
): Promise<{ accounts: Account[]; filterMeta: IFilterMeta }> => {
|
||||
return this.getAccountsService.getAccountsList(filterDTO);
|
||||
};
|
||||
@@ -133,28 +127,4 @@ export class AccountsApplication {
|
||||
): Promise<Array<GetAccountTransactionResponseDto>> => {
|
||||
return this.getAccountTransactionsService.getAccountsTransactions(filter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates which accounts can be deleted in bulk.
|
||||
*/
|
||||
public validateBulkDeleteAccounts = (
|
||||
accountIds: number[],
|
||||
): Promise<ValidateBulkDeleteResponseDto> => {
|
||||
return this.validateBulkDeleteAccountsService.validateBulkDeleteAccounts(
|
||||
accountIds,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes multiple accounts in bulk.
|
||||
*/
|
||||
public bulkDeleteAccounts = (
|
||||
accountIds: number[],
|
||||
options?: { skipUndeletable?: boolean },
|
||||
): Promise<void> => {
|
||||
return this.bulkDeleteAccountsService.bulkDeleteAccounts(
|
||||
accountIds,
|
||||
options,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { castArray, uniq } from 'lodash';
|
||||
import { DeleteAccount } from './DeleteAccount.service';
|
||||
|
||||
@Injectable()
|
||||
export class BulkDeleteAccountsService {
|
||||
constructor(private readonly deleteAccountService: DeleteAccount) { }
|
||||
|
||||
/**
|
||||
* Deletes multiple accounts.
|
||||
* @param {number | Array<number>} accountIds - The account id or ids.
|
||||
* @param {Knex.Transaction} trx - The transaction.
|
||||
*/
|
||||
async bulkDeleteAccounts(
|
||||
accountIds: number | Array<number>,
|
||||
options?: { skipUndeletable?: boolean },
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const { skipUndeletable = false } = options ?? {};
|
||||
const accountsIds = uniq(castArray(accountIds));
|
||||
|
||||
const results = await PromisePool.withConcurrency(1)
|
||||
.for(accountsIds)
|
||||
.process(async (accountId: number) => {
|
||||
try {
|
||||
await this.deleteAccountService.deleteAccount(accountId);
|
||||
} catch (error) {
|
||||
if (!skipUndeletable) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||
throw results.errors[0].raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export class CommandAccountValidators {
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
private readonly accountRepository: AccountRepository,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Throws error if the account was prefined.
|
||||
@@ -115,7 +115,7 @@ export class CommandAccountValidators {
|
||||
accountName: string,
|
||||
notAccountId?: number,
|
||||
) {
|
||||
const foundAccount = await this.accountModel()
|
||||
const foundAccount = await this.accountModel
|
||||
.query()
|
||||
.findOne('name', accountName)
|
||||
.onBuild((query) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsInt,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
@@ -65,7 +65,7 @@ export class CreateAccountDTO {
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
@ApiProperty({
|
||||
description: 'ID of the parent account',
|
||||
example: 1,
|
||||
|
||||
@@ -50,12 +50,8 @@ export class DeleteAccount {
|
||||
/**
|
||||
* Deletes the account from the storage.
|
||||
* @param {number} accountId
|
||||
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||
*/
|
||||
public deleteAccount = async (
|
||||
accountId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> => {
|
||||
public deleteAccount = async (accountId: number): Promise<void> => {
|
||||
// Retrieve account or not found service error.
|
||||
const oldAccount = await this.accountModel().query().findById(accountId);
|
||||
|
||||
@@ -86,6 +82,6 @@ export class DeleteAccount {
|
||||
oldAccount,
|
||||
trx,
|
||||
} as IAccountEventDeletedPayload);
|
||||
}, trx);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { IsString, MinLength, MaxLength } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsInt,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class EditAccountDTO {
|
||||
@IsString()
|
||||
@@ -40,7 +45,7 @@ export class EditAccountDTO {
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
@ApiProperty({
|
||||
description: 'The parent account ID of the account',
|
||||
example: 1,
|
||||
|
||||
@@ -17,7 +17,7 @@ export class EditAccount {
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize the account editing.
|
||||
@@ -85,7 +85,8 @@ export class EditAccount {
|
||||
// Update the account on the storage.
|
||||
const account = await this.accountModel()
|
||||
.query(trx)
|
||||
.updateAndFetchById(accountId, { ...accountDTO });
|
||||
.findById(accountId)
|
||||
.updateAndFetch({ ...accountDTO });
|
||||
|
||||
// Triggers `onAccountEdited` event.
|
||||
// await this.eventEmitter.emitAsync(events.accounts.onEdited, {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Account } from './models/Account.model';
|
||||
import { AccountRepository } from './repositories/Account.repository';
|
||||
import { IFilterMeta } from '@/interfaces/Model';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { GetAccountsQueryDto } from './dtos/GetAccountsQuery.dto';
|
||||
|
||||
@Injectable()
|
||||
export class GetAccountsService {
|
||||
@@ -19,7 +18,7 @@ export class GetAccountsService {
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve accounts datatable list.
|
||||
@@ -27,12 +26,12 @@ export class GetAccountsService {
|
||||
* @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>}
|
||||
*/
|
||||
public async getAccountsList(
|
||||
filterDto: Partial<GetAccountsQueryDto>,
|
||||
filterDto: Partial<IAccountsFilter>,
|
||||
): Promise<{ accounts: Account[]; filterMeta: IFilterMeta }> {
|
||||
const parsedFilterDto = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
onlyInactive: false,
|
||||
inactiveMode: false,
|
||||
structure: IAccountsStructureType.Tree,
|
||||
...filterDto,
|
||||
};
|
||||
@@ -49,7 +48,7 @@ export class GetAccountsService {
|
||||
.query()
|
||||
.onBuild((builder) => {
|
||||
dynamicList.buildQuery()(builder);
|
||||
builder.modify('inactiveMode', filterDto.onlyInactive);
|
||||
builder.modify('inactiveMode', filter.inactiveMode);
|
||||
});
|
||||
const accountsGraph = await this.accountRepository.getDependencyGraph();
|
||||
|
||||
@@ -59,6 +58,7 @@ export class GetAccountsService {
|
||||
new AccountTransformer(),
|
||||
{ accountsGraph, structure: parsedFilterDto.structure },
|
||||
);
|
||||
|
||||
return {
|
||||
accounts: transformedAccounts,
|
||||
filterMeta: dynamicList.getResponseMeta(),
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||
import { DeleteAccount } from './DeleteAccount.service';
|
||||
import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateBulkDeleteAccountsService {
|
||||
constructor(
|
||||
private readonly deleteAccountService: DeleteAccount,
|
||||
@Inject(TENANCY_DB_CONNECTION)
|
||||
private readonly tenantKnex: () => Knex,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates which accounts from the provided IDs can be deleted.
|
||||
* Uses the actual deleteAccount service to validate, ensuring the same validation logic.
|
||||
* Uses a transaction that is always rolled back to ensure no database changes.
|
||||
* @param {number[]} accountIds - Array of account IDs to validate
|
||||
* @returns {Promise<{deletableCount: number, nonDeletableCount: number, deletableIds: number[], nonDeletableIds: number[]}>}
|
||||
*/
|
||||
public async validateBulkDeleteAccounts(accountIds: number[]): Promise<{
|
||||
deletableCount: number;
|
||||
nonDeletableCount: number;
|
||||
deletableIds: number[];
|
||||
nonDeletableIds: number[];
|
||||
}> {
|
||||
const trx = await this.tenantKnex().transaction({
|
||||
isolationLevel: 'read uncommitted',
|
||||
});
|
||||
|
||||
try {
|
||||
const deletableIds: number[] = [];
|
||||
const nonDeletableIds: number[] = [];
|
||||
|
||||
for (const accountId of accountIds) {
|
||||
try {
|
||||
await this.deleteAccountService.deleteAccount(accountId, trx);
|
||||
deletableIds.push(accountId);
|
||||
} catch (error) {
|
||||
if (error instanceof ModelHasRelationsError) {
|
||||
nonDeletableIds.push(accountId);
|
||||
} else {
|
||||
nonDeletableIds.push(accountId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await trx.rollback();
|
||||
|
||||
return {
|
||||
deletableCount: deletableIds.length,
|
||||
nonDeletableCount: nonDeletableIds.length,
|
||||
deletableIds,
|
||||
nonDeletableIds,
|
||||
};
|
||||
} catch (error) {
|
||||
await trx.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { IsBoolean, IsEnum, IsOptional } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { parseBoolean } from '@/utils/parse-boolean';
|
||||
import { IAccountsStructureType } from '../Accounts.types';
|
||||
|
||||
export class GetAccountsQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
type: Boolean,
|
||||
description: 'Filter to show only inactive accounts',
|
||||
default: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
onlyInactive?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: IAccountsStructureType,
|
||||
description: 'Structure type for the accounts list',
|
||||
default: IAccountsStructureType.Tree,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(IAccountsStructureType)
|
||||
structure?: IAccountsStructureType;
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ export const AccountMeta = {
|
||||
minLength: 3,
|
||||
maxLength: 6,
|
||||
unique: true,
|
||||
importHint: 'account.field.code_hint',
|
||||
importHint: 'Unique number to identify the account.',
|
||||
},
|
||||
accountType: {
|
||||
name: 'account.field.type',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Post, Param, Get, Put, Body } from '@nestjs/common';
|
||||
import { Controller, Post, Param, Get, Put } from '@nestjs/common';
|
||||
import { GenerateApiKey } from './commands/GenerateApiKey.service';
|
||||
import { GetApiKeysService } from './queries/GetApiKeys.service';
|
||||
import {
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
ApiParam,
|
||||
ApiExtraModels,
|
||||
getSchemaPath,
|
||||
ApiBody,
|
||||
ApiProperty,
|
||||
} from '@nestjs/swagger';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import {
|
||||
@@ -18,20 +16,6 @@ import {
|
||||
ApiKeyListResponseDto,
|
||||
ApiKeyListItemDto,
|
||||
} from './dtos/ApiKey.dto';
|
||||
import { IsString, MaxLength } from 'class-validator';
|
||||
import { IsOptional } from '@/common/decorators/Validators';
|
||||
|
||||
class GenerateApiKeyDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
@ApiProperty({
|
||||
description: 'Optional name for the API key',
|
||||
required: false,
|
||||
example: 'My API Key',
|
||||
})
|
||||
name?: string;
|
||||
}
|
||||
|
||||
@Controller('api-keys')
|
||||
@ApiTags('Api keys')
|
||||
@@ -45,18 +29,17 @@ export class AuthApiKeysController {
|
||||
constructor(
|
||||
private readonly getApiKeysService: GetApiKeysService,
|
||||
private readonly generateApiKeyService: GenerateApiKey,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
@Post('generate')
|
||||
@ApiOperation({ summary: 'Generate a new API key' })
|
||||
@ApiBody({ type: GenerateApiKeyDto })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'The generated API key',
|
||||
type: ApiKeyResponseDto,
|
||||
})
|
||||
async generate(@Body() body: GenerateApiKeyDto) {
|
||||
return this.generateApiKeyService.generate(body.name);
|
||||
async generate() {
|
||||
return this.generateApiKeyService.generate();
|
||||
}
|
||||
|
||||
@Put(':id/revoke')
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { defaultTo } from 'ramda';
|
||||
import { ERRORS } from '../Auth.constants';
|
||||
import { hashPassword } from '../Auth.utils';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
|
||||
@Injectable()
|
||||
export class AuthSignupService {
|
||||
@@ -30,7 +29,6 @@ export class AuthSignupService {
|
||||
private readonly configService: ConfigService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly tenantsManager: TenantsManagerService,
|
||||
private readonly clsService: ClsService,
|
||||
|
||||
@Inject(SystemUser.name)
|
||||
private readonly systemUserModel: typeof SystemUser,
|
||||
@@ -72,11 +70,6 @@ export class AuthSignupService {
|
||||
tenantId: tenant.id,
|
||||
inviteAcceptedAt,
|
||||
});
|
||||
// Set the user in the cls service.
|
||||
this.clsService.set('tenantId', user.tenantId);
|
||||
this.clsService.set('userId', user.id);
|
||||
this.clsService.set('organizationId', tenant.organizationId);
|
||||
|
||||
// Triggers signed up event.
|
||||
await this.eventEmitter.emitAsync(events.auth.signUp, {
|
||||
signupDTO,
|
||||
|
||||
@@ -10,15 +10,14 @@ export class GenerateApiKey {
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
@Inject(ApiKeyModel.name)
|
||||
private readonly apiKeyModel: typeof ApiKeyModel,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generates a new secure API key for the current tenant and system user.
|
||||
* The key is saved in the database and returned (only the key and id for security).
|
||||
* @param {string} name - Optional name for the API key.
|
||||
* @returns {Promise<{ key: string; id: number }>} The generated API key and its database id.
|
||||
*/
|
||||
async generate(name?: string) {
|
||||
async generate() {
|
||||
const tenant = await this.tenancyContext.getTenant();
|
||||
const user = await this.tenancyContext.getSystemUser();
|
||||
|
||||
@@ -27,7 +26,6 @@ export class GenerateApiKey {
|
||||
// Save the API key to the database
|
||||
const apiKeyRecord = await this.apiKeyModel.query().insert({
|
||||
key,
|
||||
name,
|
||||
tenantId: tenant.id,
|
||||
userId: user.id,
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -29,12 +29,6 @@ export class ApiKeyListItemDto {
|
||||
@ApiProperty({ example: 'My API Key', description: 'API key name' })
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'bc_1234...',
|
||||
description: 'First 8 characters of the API key token',
|
||||
})
|
||||
token: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2024-01-01T00:00:00.000Z',
|
||||
description: 'Creation date',
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
|
||||
export class GetApiKeysTransformer extends Transformer {
|
||||
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['token'];
|
||||
};
|
||||
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['tenantId'];
|
||||
};
|
||||
|
||||
public token(apiKey) {
|
||||
return apiKey.key ? `${apiKey.key.substring(0, 8)}...` : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import * as yup from 'yup';
|
||||
import * as uniqid from 'uniqid';
|
||||
import uniqid from 'uniqid';
|
||||
import { Importable } from '../../Import/Importable';
|
||||
import { CreateUncategorizedTransactionService } from './CreateUncategorizedTransaction.service';
|
||||
import { ImportableContext } from '../../Import/interfaces';
|
||||
@@ -9,10 +9,8 @@ import { BankTransactionsSampleData } from '../../BankingTransactions/constants'
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { CreateUncategorizedTransactionDTO } from '../types/BankingCategorize.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { ImportableService } from '../../Import/decorators/Import.decorator';
|
||||
import { UncategorizedBankTransaction } from '../../BankingTransactions/models/UncategorizedBankTransaction';
|
||||
|
||||
@Injectable()
|
||||
@ImportableService({ name: UncategorizedBankTransaction.name })
|
||||
export class UncategorizedTransactionsImportable extends Importable {
|
||||
constructor(
|
||||
private readonly createUncategorizedTransaction: CreateUncategorizedTransactionService,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { isEmpty, round, sumBy } from 'lodash';
|
||||
import { isEmpty, sumBy } from 'lodash';
|
||||
import { ERRORS, MatchedTransactionPOJO } from './types';
|
||||
import { ServiceError } from '../Items/ServiceError';
|
||||
|
||||
@@ -22,24 +22,18 @@ export const sortClosestMatchTransactions = (
|
||||
};
|
||||
|
||||
export const sumMatchTranasctions = (transactions: Array<any>) => {
|
||||
const total = transactions.reduce(
|
||||
(sum, item) => {
|
||||
const amount = parseFloat(item.amount) || 0;
|
||||
const multiplier = item.transactionNormal === 'debit' ? 1 : -1;
|
||||
return sum + multiplier * amount;
|
||||
},
|
||||
return transactions.reduce(
|
||||
(total, item) =>
|
||||
total +
|
||||
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
||||
0
|
||||
);
|
||||
// Round to 2 decimal places to avoid floating-point precision issues
|
||||
return round(total, 2);
|
||||
};
|
||||
|
||||
export const sumUncategorizedTransactions = (
|
||||
uncategorizedTransactions: Array<any>
|
||||
) => {
|
||||
const total = sumBy(uncategorizedTransactions, 'amount');
|
||||
// Round to 2 decimal places to avoid floating-point precision issues
|
||||
return round(total, 2);
|
||||
return sumBy(uncategorizedTransactions, 'amount');
|
||||
};
|
||||
|
||||
export const validateUncategorizedTransactionsNotMatched = (
|
||||
|
||||
@@ -34,7 +34,7 @@ export class MatchBankTransactions {
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the match bank transactions DTO.
|
||||
@@ -100,10 +100,7 @@ export class MatchBankTransactions {
|
||||
);
|
||||
// Validates the total given matching transcations whether is not equal
|
||||
// uncategorized transaction amount.
|
||||
// Use tolerance-based comparison to handle floating-point precision issues
|
||||
const tolerance = 0.01; // Allow 0.01 difference for floating-point precision
|
||||
const difference = Math.abs(totalUncategorizedTransactions - totalMatchedTranasctions);
|
||||
if (difference > tolerance) {
|
||||
if (totalUncategorizedTransactions !== totalMatchedTranasctions) {
|
||||
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,30 +12,24 @@ export class MatchTransactionsTypes {
|
||||
private static registry: MatchTransactionsTypesRegistry;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* Consttuctor method.
|
||||
*/
|
||||
constructor(
|
||||
private readonly getMatchedInvoicesService: GetMatchedTransactionsByInvoices,
|
||||
private readonly getMatchedBillsService: GetMatchedTransactionsByBills,
|
||||
private readonly getMatchedExpensesService: GetMatchedTransactionsByExpenses,
|
||||
private readonly getMatchedManualJournalsService: GetMatchedTransactionsByManualJournals,
|
||||
private readonly getMatchedCashflowService: GetMatchedTransactionsByCashflow,
|
||||
) {
|
||||
constructor() {
|
||||
this.boot();
|
||||
}
|
||||
|
||||
get registered() {
|
||||
return [
|
||||
{ type: 'SaleInvoice', service: this.getMatchedInvoicesService },
|
||||
{ type: 'Bill', service: this.getMatchedBillsService },
|
||||
{ type: 'Expense', service: this.getMatchedExpensesService },
|
||||
{ type: 'SaleInvoice', service: GetMatchedTransactionsByInvoices },
|
||||
{ type: 'Bill', service: GetMatchedTransactionsByBills },
|
||||
{ type: 'Expense', service: GetMatchedTransactionsByExpenses },
|
||||
{
|
||||
type: 'ManualJournal',
|
||||
service: this.getMatchedManualJournalsService,
|
||||
service: GetMatchedTransactionsByManualJournals,
|
||||
},
|
||||
{
|
||||
type: 'CashflowTransaction',
|
||||
service: this.getMatchedCashflowService,
|
||||
service: GetMatchedTransactionsByCashflow,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -56,13 +50,14 @@ export class MatchTransactionsTypes {
|
||||
* Boots all the registered importables.
|
||||
*/
|
||||
public boot() {
|
||||
const instance = MatchTransactionsTypesRegistry.getInstance();
|
||||
if (!MatchTransactionsTypes.registry) {
|
||||
const instance = MatchTransactionsTypesRegistry.getInstance();
|
||||
|
||||
// Always register services to ensure they're available
|
||||
this.registered.forEach((registered) => {
|
||||
instance.register(registered.type, registered.service);
|
||||
});
|
||||
|
||||
MatchTransactionsTypes.registry = instance;
|
||||
this.registered.forEach((registered) => {
|
||||
// const serviceInstanace = Container.get(registered.service);
|
||||
// instance.register(registered.type, serviceInstanace);
|
||||
});
|
||||
MatchTransactionsTypes.registry = instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,4 +83,4 @@ const models = [
|
||||
CreateBankTransactionService
|
||||
],
|
||||
})
|
||||
export class BankingTransactionsModule { }
|
||||
export class BankingTransactionsModule {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
IsPositive,
|
||||
} from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { parseBoolean } from '@/utils/parse-boolean';
|
||||
|
||||
export class NumberFormatQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
@@ -25,7 +24,6 @@ export class NumberFormatQueryDto {
|
||||
example: false,
|
||||
})
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
@IsOptional()
|
||||
readonly divideOn1000: boolean;
|
||||
|
||||
@@ -34,7 +32,6 @@ export class NumberFormatQueryDto {
|
||||
example: true,
|
||||
})
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
@IsOptional()
|
||||
readonly showZero: boolean;
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
export const UncategorizedBankTransactionMeta = {
|
||||
defaultFilterField: 'createdAt',
|
||||
defaultSort: {
|
||||
sortOrder: 'DESC',
|
||||
sortField: 'created_at',
|
||||
},
|
||||
importable: true,
|
||||
fields: {
|
||||
date: {
|
||||
name: 'Date',
|
||||
column: 'date',
|
||||
fieldType: 'date',
|
||||
},
|
||||
payee: {
|
||||
name: 'Payee',
|
||||
column: 'payee',
|
||||
fieldType: 'text',
|
||||
},
|
||||
description: {
|
||||
name: 'Description',
|
||||
column: 'description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'Reference No.',
|
||||
column: 'reference_no',
|
||||
fieldType: 'text',
|
||||
},
|
||||
amount: {
|
||||
name: 'Amount',
|
||||
column: 'Amount',
|
||||
fieldType: 'numeric',
|
||||
required: true,
|
||||
},
|
||||
account: {
|
||||
name: 'Account',
|
||||
column: 'account_id',
|
||||
fieldType: 'relation',
|
||||
to: { model: 'Account', to: 'id' },
|
||||
},
|
||||
createdAt: {
|
||||
name: 'Created At',
|
||||
column: 'createdAt',
|
||||
fieldType: 'date',
|
||||
importable: false,
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
date: {
|
||||
name: 'Date',
|
||||
fieldType: 'date',
|
||||
required: true,
|
||||
},
|
||||
payee: {
|
||||
name: 'Payee',
|
||||
fieldType: 'text',
|
||||
},
|
||||
description: {
|
||||
name: 'Description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'Reference No.',
|
||||
fieldType: 'text',
|
||||
},
|
||||
amount: {
|
||||
name: 'Amount',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -2,10 +2,7 @@
|
||||
import * as moment from 'moment';
|
||||
import { Model } from 'objection';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
import { UncategorizedBankTransactionMeta } from './UncategorizedBankTransaction.meta';
|
||||
import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator';
|
||||
|
||||
@InjectModelMeta(UncategorizedBankTransactionMeta)
|
||||
export class UncategorizedBankTransaction extends TenantBaseModel {
|
||||
readonly amount!: number;
|
||||
readonly date!: Date | string;
|
||||
|
||||
@@ -104,12 +104,6 @@ export class BillPaymentResponseDto {
|
||||
@ApiProperty({ description: 'The formatted amount', example: '100.00 USD' })
|
||||
formattedAmount: string;
|
||||
|
||||
@ApiProperty({ description: 'The formatted total', example: '100.00 USD' })
|
||||
formattedTotal: string;
|
||||
|
||||
@ApiProperty({ description: 'The formatted subtotal', example: '100.00 USD' })
|
||||
formattedSubtotal: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The date when the payment was created',
|
||||
example: '2024-01-01T12:00:00Z',
|
||||
|
||||
@@ -167,7 +167,7 @@ export const BillPaymentMeta = {
|
||||
name: 'bill_payment.field.payment_number',
|
||||
fieldType: 'text',
|
||||
unique: true,
|
||||
importHint: 'bill_payment.field.payment_number_hint',
|
||||
importHint: 'The payment number should be unique.',
|
||||
},
|
||||
paymentAccountId: {
|
||||
name: 'bill_payment.field.payment_account',
|
||||
@@ -175,7 +175,7 @@ export const BillPaymentMeta = {
|
||||
relationModel: 'Account',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
required: true,
|
||||
importHint: 'account.field.account_hint',
|
||||
importHint: 'Matches the account name or code.',
|
||||
},
|
||||
exchangeRate: {
|
||||
name: 'bill_payment.field.exchange_rate',
|
||||
@@ -203,7 +203,7 @@ export const BillPaymentMeta = {
|
||||
relationModel: 'Bill',
|
||||
relationImportMatch: 'billNumber',
|
||||
required: true,
|
||||
importHint: 'bill_payment.field.bill_hint',
|
||||
importHint: 'Matches the bill number.',
|
||||
},
|
||||
paymentAmount: {
|
||||
name: 'bill_payment.field.entries.payment_amount',
|
||||
@@ -213,7 +213,7 @@ export const BillPaymentMeta = {
|
||||
},
|
||||
},
|
||||
branchId: {
|
||||
name: 'invoice.field.branch',
|
||||
name: 'Branch',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Branch',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
|
||||
@@ -13,8 +13,6 @@ export class BillPaymentTransformer extends Transformer {
|
||||
'formattedPaymentDate',
|
||||
'formattedCreatedAt',
|
||||
'formattedAmount',
|
||||
'formattedTotal',
|
||||
'formattedSubtotal',
|
||||
'entries',
|
||||
'attachments',
|
||||
];
|
||||
@@ -49,29 +47,6 @@ export class BillPaymentTransformer extends Transformer {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted total.
|
||||
* @param {IBillPayment} billPayment
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedTotal = (billPayment: BillPayment): string => {
|
||||
return this.formatNumber(billPayment.amount, {
|
||||
currencyCode: billPayment.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted subtotal.
|
||||
* @param {IBillPayment} billPayment
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedSubtotal = (billPayment: BillPayment): string => {
|
||||
return this.formatNumber(billPayment.amount, {
|
||||
currencyCode: billPayment.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retreives the bill payment entries.
|
||||
*/
|
||||
|
||||
@@ -9,8 +9,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { GetBillsService } from './queries/GetBills.service';
|
||||
import { CreateBillDto, EditBillDto } from './dtos/Bill.dto';
|
||||
import { GetBillPaymentTransactionsService } from './queries/GetBillPayments';
|
||||
import { BulkDeleteBillsService } from './BulkDeleteBills.service';
|
||||
import { ValidateBulkDeleteBillsService } from './ValidateBulkDeleteBills.service';
|
||||
// import { GetBillPayments } from './queries/GetBillPayments';
|
||||
// import { GetBills } from './queries/GetBills';
|
||||
|
||||
@@ -25,9 +23,7 @@ export class BillsApplication {
|
||||
private openBillService: OpenBillService,
|
||||
private getBillsService: GetBillsService,
|
||||
private getBillPaymentTransactionsService: GetBillPaymentTransactionsService,
|
||||
private bulkDeleteBillsService: BulkDeleteBillsService,
|
||||
private validateBulkDeleteBillsService: ValidateBulkDeleteBillsService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new bill with associated GL entries.
|
||||
@@ -57,25 +53,6 @@ export class BillsApplication {
|
||||
return this.deleteBillService.deleteBill(billId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes multiple bills.
|
||||
* @param {number[]} billIds
|
||||
*/
|
||||
public bulkDeleteBills(
|
||||
billIds: number[],
|
||||
options?: { skipUndeletable?: boolean },
|
||||
) {
|
||||
return this.bulkDeleteBillsService.bulkDeleteBills(billIds, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates which bills can be deleted.
|
||||
* @param {number[]} billIds
|
||||
*/
|
||||
public validateBulkDeleteBills(billIds: number[]) {
|
||||
return this.validateBulkDeleteBillsService.validateBulkDeleteBills(billIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve bills data table list.
|
||||
* @param {IBillsFilter} billsFilter -
|
||||
|
||||
@@ -22,51 +22,14 @@ import { CreateBillDto, EditBillDto } from './dtos/Bill.dto';
|
||||
import { BillResponseDto } from './dtos/BillResponse.dto';
|
||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import {
|
||||
BulkDeleteDto,
|
||||
ValidateBulkDeleteResponseDto,
|
||||
} from '@/common/dtos/BulkDelete.dto';
|
||||
|
||||
@Controller('bills')
|
||||
@ApiTags('Bills')
|
||||
@ApiExtraModels(BillResponseDto)
|
||||
@ApiExtraModels(PaginatedResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||
export class BillsController {
|
||||
constructor(private billsApplication: BillsApplication) { }
|
||||
|
||||
@Post('validate-bulk-delete')
|
||||
@ApiOperation({
|
||||
summary: 'Validate which bills can be deleted and return the results.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'Validation completed with counts and IDs of deletable and non-deletable bills.',
|
||||
schema: {
|
||||
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||
},
|
||||
})
|
||||
validateBulkDeleteBills(
|
||||
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||
): Promise<ValidateBulkDeleteResponseDto> {
|
||||
return this.billsApplication.validateBulkDeleteBills(bulkDeleteDto.ids);
|
||||
}
|
||||
|
||||
@Post('bulk-delete')
|
||||
@ApiOperation({ summary: 'Deletes multiple bills.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Bills deleted successfully',
|
||||
})
|
||||
bulkDeleteBills(
|
||||
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||
): Promise<void> {
|
||||
return this.billsApplication.bulkDeleteBills(bulkDeleteDto.ids, {
|
||||
skipUndeletable: bulkDeleteDto.skipUndeletable ?? false,
|
||||
});
|
||||
}
|
||||
constructor(private billsApplication: BillsApplication) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new bill.' })
|
||||
|
||||
@@ -29,8 +29,6 @@ import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
|
||||
import { BillsExportable } from './commands/BillsExportable';
|
||||
import { BillsImportable } from './commands/BillsImportable';
|
||||
import { GetBillPaymentTransactionsService } from './queries/GetBillPayments';
|
||||
import { BulkDeleteBillsService } from './BulkDeleteBills.service';
|
||||
import { ValidateBulkDeleteBillsService } from './ValidateBulkDeleteBills.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -65,10 +63,8 @@ import { ValidateBulkDeleteBillsService } from './ValidateBulkDeleteBills.servic
|
||||
BillsExportable,
|
||||
BillsImportable,
|
||||
GetBillPaymentTransactionsService,
|
||||
BulkDeleteBillsService,
|
||||
ValidateBulkDeleteBillsService,
|
||||
],
|
||||
controllers: [BillsController],
|
||||
exports: [BillsExportable, BillsImportable],
|
||||
})
|
||||
export class BillsModule { }
|
||||
export class BillsModule {}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { castArray, uniq } from 'lodash';
|
||||
import { DeleteBill } from './commands/DeleteBill.service';
|
||||
|
||||
@Injectable()
|
||||
export class BulkDeleteBillsService {
|
||||
constructor(private readonly deleteBillService: DeleteBill) { }
|
||||
|
||||
async bulkDeleteBills(
|
||||
billIds: number | Array<number>,
|
||||
options?: { skipUndeletable?: boolean },
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const { skipUndeletable = false } = options ?? {};
|
||||
const billsIds = uniq(castArray(billIds));
|
||||
|
||||
const results = await PromisePool.withConcurrency(1)
|
||||
.for(billsIds)
|
||||
.process(async (billId: number) => {
|
||||
try {
|
||||
await this.deleteBillService.deleteBill(billId);
|
||||
} catch (error) {
|
||||
if (!skipUndeletable) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||
throw results.errors[0].raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||
import { DeleteBill } from './commands/DeleteBill.service';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateBulkDeleteBillsService {
|
||||
constructor(
|
||||
private readonly deleteBillService: DeleteBill,
|
||||
@Inject(TENANCY_DB_CONNECTION)
|
||||
private readonly tenantKnex: () => Knex,
|
||||
) { }
|
||||
|
||||
public async validateBulkDeleteBills(billIds: number[]): Promise<{
|
||||
deletableCount: number;
|
||||
nonDeletableCount: number;
|
||||
deletableIds: number[];
|
||||
nonDeletableIds: number[];
|
||||
}> {
|
||||
const trx = await this.tenantKnex().transaction({
|
||||
isolationLevel: 'read uncommitted',
|
||||
});
|
||||
|
||||
try {
|
||||
const deletableIds: number[] = [];
|
||||
const nonDeletableIds: number[] = [];
|
||||
|
||||
for (const billId of billIds) {
|
||||
try {
|
||||
await this.deleteBillService.deleteBill(billId, trx);
|
||||
deletableIds.push(billId);
|
||||
} catch (error) {
|
||||
nonDeletableIds.push(billId);
|
||||
}
|
||||
}
|
||||
|
||||
await trx.rollback();
|
||||
|
||||
return {
|
||||
deletableCount: deletableIds.length,
|
||||
nonDeletableCount: nonDeletableIds.length,
|
||||
deletableIds,
|
||||
nonDeletableIds,
|
||||
};
|
||||
} catch (error) {
|
||||
await trx.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,9 @@ export class DeleteBill {
|
||||
/**
|
||||
* Deletes the bill with associated entries.
|
||||
* @param {number} billId
|
||||
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||
* @return {void}
|
||||
*/
|
||||
public async deleteBill(billId: number, trx?: Knex.Transaction) {
|
||||
public async deleteBill(billId: number) {
|
||||
// Retrieve the given bill or throw not found error.
|
||||
const oldBill = await this.billModel()
|
||||
.query()
|
||||
@@ -76,6 +75,6 @@ export class DeleteBill {
|
||||
oldBill,
|
||||
trx,
|
||||
} as IBIllEventDeletedPayload);
|
||||
}, trx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,76 +184,76 @@ export const BillMeta = {
|
||||
},
|
||||
fields2: {
|
||||
billNumber: {
|
||||
name: 'bill.field.bill_number',
|
||||
name: 'Bill No.',
|
||||
fieldType: 'text',
|
||||
required: true,
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'bill.field.reference_no',
|
||||
name: 'Reference No.',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billDate: {
|
||||
name: 'bill.field.bill_date',
|
||||
name: 'Date',
|
||||
fieldType: 'date',
|
||||
required: true,
|
||||
},
|
||||
dueDate: {
|
||||
name: 'bill.field.due_date',
|
||||
name: 'Due Date',
|
||||
fieldType: 'date',
|
||||
required: true,
|
||||
},
|
||||
vendorId: {
|
||||
name: 'bill.field.vendor',
|
||||
name: 'Vendor',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Contact',
|
||||
relationImportMatch: 'displayName',
|
||||
required: true,
|
||||
},
|
||||
exchangeRate: {
|
||||
name: 'bill.field.exchange_rate',
|
||||
name: 'Exchange Rate',
|
||||
fieldType: 'number',
|
||||
},
|
||||
note: {
|
||||
name: 'bill.field.note',
|
||||
name: 'Note',
|
||||
fieldType: 'text',
|
||||
},
|
||||
open: {
|
||||
name: 'bill.field.open',
|
||||
name: 'Open',
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
entries: {
|
||||
name: 'bill.field.entries',
|
||||
name: 'Entries',
|
||||
fieldType: 'collection',
|
||||
collectionOf: 'object',
|
||||
collectionMinLength: 1,
|
||||
required: true,
|
||||
fields: {
|
||||
itemId: {
|
||||
name: 'bill.field.item',
|
||||
name: 'Item',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Item',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
required: true,
|
||||
importHint: 'bill.field.item_hint',
|
||||
importHint: 'Matches the item name or code.',
|
||||
},
|
||||
rate: {
|
||||
name: 'bill.field.rate',
|
||||
name: 'Rate',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
quantity: {
|
||||
name: 'bill.field.quantity',
|
||||
name: 'Quantity',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
name: 'bill.field.description',
|
||||
name: 'Line Description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
branchId: {
|
||||
name: 'invoice.field.branch',
|
||||
name: 'Branch',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Branch',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
@@ -261,7 +261,7 @@ export const BillMeta = {
|
||||
required: true,
|
||||
},
|
||||
warehouseId: {
|
||||
name: 'invoice.field.warehouse',
|
||||
name: 'Warehouse',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Warehouse',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
|
||||
@@ -94,7 +94,6 @@ export class BillTransformer extends Transformer {
|
||||
protected formattedDueAmount = (bill: Bill): string => {
|
||||
return this.formatNumber(bill.dueAmount, {
|
||||
currencyCode: bill.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -170,7 +169,6 @@ export class BillTransformer extends Transformer {
|
||||
protected totalFormatted = (bill: Bill): string => {
|
||||
return this.formatNumber(bill.total, {
|
||||
currencyCode: bill.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { castArray, uniq } from 'lodash';
|
||||
import { DeleteCreditNoteService } from './commands/DeleteCreditNote.service';
|
||||
|
||||
@Injectable()
|
||||
export class BulkDeleteCreditNotesService {
|
||||
constructor(
|
||||
private readonly deleteCreditNoteService: DeleteCreditNoteService,
|
||||
) { }
|
||||
|
||||
async bulkDeleteCreditNotes(
|
||||
creditNoteIds: number | Array<number>,
|
||||
options?: { skipUndeletable?: boolean },
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const { skipUndeletable = false } = options ?? {};
|
||||
const notesIds = uniq(castArray(creditNoteIds));
|
||||
|
||||
const results = await PromisePool.withConcurrency(1)
|
||||
.for(notesIds)
|
||||
.process(async (creditNoteId: number) => {
|
||||
try {
|
||||
await this.deleteCreditNoteService.deleteCreditNote(creditNoteId);
|
||||
} catch (error) {
|
||||
if (!skipUndeletable) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||
throw results.errors[0].raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import { GetCreditNotesService } from './queries/GetCreditNotes.service';
|
||||
import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
|
||||
import { GetCreditNoteState } from './queries/GetCreditNoteState.service';
|
||||
import { GetCreditNoteService } from './queries/GetCreditNote.service';
|
||||
import { BulkDeleteCreditNotesService } from './BulkDeleteCreditNotes.service';
|
||||
import { ValidateBulkDeleteCreditNotesService } from './ValidateBulkDeleteCreditNotes.service';
|
||||
|
||||
@Injectable()
|
||||
export class CreditNoteApplication {
|
||||
@@ -22,10 +20,8 @@ export class CreditNoteApplication {
|
||||
private readonly getCreditNotePdfService: GetCreditNotePdf,
|
||||
private readonly getCreditNotesService: GetCreditNotesService,
|
||||
private readonly getCreditNoteStateService: GetCreditNoteState,
|
||||
private readonly getCreditNoteService: GetCreditNoteService,
|
||||
private readonly bulkDeleteCreditNotesService: BulkDeleteCreditNotesService,
|
||||
private readonly validateBulkDeleteCreditNotesService: ValidateBulkDeleteCreditNotesService,
|
||||
) { }
|
||||
private readonly getCreditNoteService: GetCreditNoteService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new credit note.
|
||||
@@ -101,30 +97,4 @@ export class CreditNoteApplication {
|
||||
getCreditNote(creditNoteId: number) {
|
||||
return this.getCreditNoteService.getCreditNote(creditNoteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes multiple credit notes.
|
||||
* @param {number[]} creditNoteIds
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
bulkDeleteCreditNotes(
|
||||
creditNoteIds: number[],
|
||||
options?: { skipUndeletable?: boolean },
|
||||
) {
|
||||
return this.bulkDeleteCreditNotesService.bulkDeleteCreditNotes(
|
||||
creditNoteIds,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates which credit notes can be deleted.
|
||||
* @param {number[]} creditNoteIds
|
||||
* @returns {Promise<{deletableCount: number, nonDeletableCount: number, deletableIds: number[], nonDeletableIds: number[]}>}
|
||||
*/
|
||||
validateBulkDeleteCreditNotes(creditNoteIds: number[]) {
|
||||
return this.validateBulkDeleteCreditNotesService.validateBulkDeleteCreditNotes(
|
||||
creditNoteIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Response } from 'express';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
@@ -12,12 +11,10 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Headers,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { CreditNoteApplication } from './CreditNoteApplication.service';
|
||||
import { ICreditNotesQueryDTO } from './types/CreditNotes.types';
|
||||
@@ -25,23 +22,17 @@ import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
|
||||
import { CreditNoteResponseDto } from './dtos/CreditNoteResponse.dto';
|
||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import {
|
||||
BulkDeleteDto,
|
||||
ValidateBulkDeleteResponseDto,
|
||||
} from '@/common/dtos/BulkDelete.dto';
|
||||
import { AcceptType } from '@/constants/accept-type';
|
||||
|
||||
@Controller('credit-notes')
|
||||
@ApiTags('Credit Notes')
|
||||
@ApiExtraModels(CreditNoteResponseDto)
|
||||
@ApiExtraModels(PaginatedResponseDto)
|
||||
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
export class CreditNotesController {
|
||||
/**
|
||||
* @param {CreditNoteApplication} creditNoteApplication - The credit note application service.
|
||||
*/
|
||||
constructor(private creditNoteApplication: CreditNoteApplication) { }
|
||||
constructor(private creditNoteApplication: CreditNoteApplication) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new credit note' })
|
||||
@@ -69,26 +60,8 @@ export class CreditNotesController {
|
||||
},
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Credit note not found' })
|
||||
async getCreditNote(
|
||||
@Param('id') creditNoteId: number,
|
||||
@Headers('accept') acceptHeader: string,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
|
||||
const [pdfContent, filename] =
|
||||
await this.creditNoteApplication.getCreditNotePdf(creditNoteId);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
});
|
||||
res.status(200).send(pdfContent);
|
||||
} else {
|
||||
const creditNote =
|
||||
await this.creditNoteApplication.getCreditNote(creditNoteId);
|
||||
return creditNote;
|
||||
}
|
||||
getCreditNote(@Param('id') creditNoteId: number) {
|
||||
return this.creditNoteApplication.getCreditNote(creditNoteId);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@@ -139,39 +112,6 @@ export class CreditNotesController {
|
||||
return this.creditNoteApplication.openCreditNote(creditNoteId);
|
||||
}
|
||||
|
||||
@Post('validate-bulk-delete')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Validates which credit notes can be deleted and returns the results.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'Validation completed with counts and IDs of deletable and non-deletable credit notes.',
|
||||
schema: {
|
||||
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||
},
|
||||
})
|
||||
validateBulkDeleteCreditNotes(
|
||||
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||
): Promise<ValidateBulkDeleteResponseDto> {
|
||||
return this.creditNoteApplication.validateBulkDeleteCreditNotes(
|
||||
bulkDeleteDto.ids,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('bulk-delete')
|
||||
@ApiOperation({ summary: 'Deletes multiple credit notes.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Credit notes deleted successfully',
|
||||
})
|
||||
bulkDeleteCreditNotes(@Body() bulkDeleteDto: BulkDeleteDto): Promise<void> {
|
||||
return this.creditNoteApplication.bulkDeleteCreditNotes(bulkDeleteDto.ids, {
|
||||
skipUndeletable: bulkDeleteDto.skipUndeletable ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete a credit note' })
|
||||
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
|
||||
|
||||
@@ -29,13 +29,10 @@ import { CreditNotesImportable } from './commands/CreditNotesImportable';
|
||||
import { CreditNoteInventoryTransactionsSubscriber } from './subscribers/CreditNoteInventoryTransactionsSubscriber';
|
||||
import { RefundSyncCreditNoteBalanceSubscriber } from './subscribers/RefundSyncCreditNoteBalanceSubscriber';
|
||||
import { DeleteCustomerLinkedCreditSubscriber } from './subscribers/DeleteCustomerLinkedCreditSubscriber';
|
||||
import { CreditNoteAutoSerialSubscriber } from './subscribers/CreditNoteAutoSerialSubscriber';
|
||||
import { CreditNoteInventoryTransactions } from './commands/CreditNotesInventoryTransactions';
|
||||
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
|
||||
import { CreditNoteRefundsModule } from '../CreditNoteRefunds/CreditNoteRefunds.module';
|
||||
import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/CreditNotesApplyInvoice.module';
|
||||
import { BulkDeleteCreditNotesService } from './BulkDeleteCreditNotes.service';
|
||||
import { ValidateBulkDeleteCreditNotesService } from './ValidateBulkDeleteCreditNotes.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -74,9 +71,6 @@ import { ValidateBulkDeleteCreditNotesService } from './ValidateBulkDeleteCredit
|
||||
CreditNoteInventoryTransactionsSubscriber,
|
||||
RefundSyncCreditNoteBalanceSubscriber,
|
||||
DeleteCustomerLinkedCreditSubscriber,
|
||||
CreditNoteAutoSerialSubscriber,
|
||||
BulkDeleteCreditNotesService,
|
||||
ValidateBulkDeleteCreditNotesService,
|
||||
],
|
||||
exports: [
|
||||
CreateCreditNoteService,
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||
import { DeleteCreditNoteService } from './commands/DeleteCreditNote.service';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateBulkDeleteCreditNotesService {
|
||||
constructor(
|
||||
private readonly deleteCreditNoteService: DeleteCreditNoteService,
|
||||
@Inject(TENANCY_DB_CONNECTION)
|
||||
private readonly tenantKnex: () => Knex,
|
||||
) { }
|
||||
|
||||
public async validateBulkDeleteCreditNotes(creditNoteIds: number[]): Promise<{
|
||||
deletableCount: number;
|
||||
nonDeletableCount: number;
|
||||
deletableIds: number[];
|
||||
nonDeletableIds: number[];
|
||||
}> {
|
||||
const trx = await this.tenantKnex().transaction({
|
||||
isolationLevel: 'read uncommitted',
|
||||
});
|
||||
|
||||
try {
|
||||
const deletableIds: number[] = [];
|
||||
const nonDeletableIds: number[] = [];
|
||||
|
||||
for (const creditNoteId of creditNoteIds) {
|
||||
try {
|
||||
await this.deleteCreditNoteService.deleteCreditNote(
|
||||
creditNoteId,
|
||||
trx,
|
||||
);
|
||||
deletableIds.push(creditNoteId);
|
||||
} catch (error) {
|
||||
nonDeletableIds.push(creditNoteId);
|
||||
}
|
||||
}
|
||||
|
||||
await trx.rollback();
|
||||
|
||||
return {
|
||||
deletableCount: deletableIds.length,
|
||||
nonDeletableCount: nonDeletableIds.length,
|
||||
deletableIds,
|
||||
nonDeletableIds,
|
||||
};
|
||||
} catch (error) {
|
||||
await trx.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,13 +49,9 @@ export class DeleteCreditNoteService {
|
||||
/**
|
||||
* Deletes the given credit note transactions.
|
||||
* @param {number} creditNoteId
|
||||
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async deleteCreditNote(
|
||||
creditNoteId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
public async deleteCreditNote(creditNoteId: number): Promise<void> {
|
||||
// Retrieve the credit note or throw not found service error.
|
||||
const oldCreditNote = await this.creditNoteModel()
|
||||
.query()
|
||||
@@ -92,7 +88,7 @@ export class DeleteCreditNoteService {
|
||||
creditNoteId,
|
||||
trx,
|
||||
} as ICreditNoteDeletedPayload);
|
||||
}, trx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -164,73 +164,73 @@ export const CreditNoteMeta = {
|
||||
},
|
||||
fields2: {
|
||||
customerId: {
|
||||
name: 'credit_note.field.customer',
|
||||
name: 'Customer',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Contact',
|
||||
relationImportMatch: 'displayName',
|
||||
required: true,
|
||||
},
|
||||
exchangeRate: {
|
||||
name: 'credit_note.field.exchange_rate',
|
||||
name: 'Exchange Rate',
|
||||
fieldType: 'number',
|
||||
},
|
||||
creditNoteDate: {
|
||||
name: 'credit_note.field.credit_note_date',
|
||||
name: 'Credit Note Date',
|
||||
fieldType: 'date',
|
||||
required: true,
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'credit_note.field.reference_no',
|
||||
name: 'Reference No.',
|
||||
fieldType: 'text',
|
||||
},
|
||||
note: {
|
||||
name: 'credit_note.field.note',
|
||||
name: 'Note',
|
||||
fieldType: 'text',
|
||||
},
|
||||
termsConditions: {
|
||||
name: 'credit_note.field.terms_conditions',
|
||||
name: 'Terms & Conditions',
|
||||
fieldType: 'text',
|
||||
},
|
||||
creditNoteNumber: {
|
||||
name: 'credit_note.field.credit_note_number',
|
||||
name: 'Credit Note Number',
|
||||
fieldType: 'text',
|
||||
},
|
||||
open: {
|
||||
name: 'credit_note.field.open',
|
||||
name: 'Open',
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
entries: {
|
||||
name: 'credit_note.field.entries',
|
||||
name: 'Entries',
|
||||
fieldType: 'collection',
|
||||
collectionOf: 'object',
|
||||
collectionMinLength: 1,
|
||||
fields: {
|
||||
itemId: {
|
||||
name: 'credit_note.field.item',
|
||||
name: 'Item',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Item',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
required: true,
|
||||
importHint: 'invoice.field.item_hint',
|
||||
importHint: 'Matches the item name or code.',
|
||||
},
|
||||
rate: {
|
||||
name: 'credit_note.field.rate',
|
||||
name: 'Rate',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
quantity: {
|
||||
name: 'credit_note.field.quantity',
|
||||
name: 'Quantity',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
name: 'credit_note.field.description',
|
||||
name: 'Description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
branchId: {
|
||||
name: 'invoice.field.branch',
|
||||
name: 'Branch',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Branch',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
@@ -238,7 +238,7 @@ export const CreditNoteMeta = {
|
||||
required: true,
|
||||
},
|
||||
warehouseId: {
|
||||
name: 'invoice.field.warehouse',
|
||||
name: 'Warehouse',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Warehouse',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
|
||||
@@ -90,7 +90,7 @@ export class CreditNoteTransformer extends Transformer {
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedSubtotal = (credit): string => {
|
||||
return this.formatNumber(credit.amount);
|
||||
return this.formatNumber(credit.amount, { money: false });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -130,7 +130,7 @@ export class CreditNoteTransformer extends Transformer {
|
||||
* @returns {string}
|
||||
*/
|
||||
protected adjustmentFormatted = (credit): string => {
|
||||
return this.formatNumber(credit.adjustment, {
|
||||
return this.formatMoney(credit.adjustment, {
|
||||
currencyCode: credit.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
@@ -156,7 +156,6 @@ export class CreditNoteTransformer extends Transformer {
|
||||
protected totalFormatted = (credit): string => {
|
||||
return this.formatNumber(credit.total, {
|
||||
currencyCode: credit.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -168,7 +167,6 @@ export class CreditNoteTransformer extends Transformer {
|
||||
protected totalLocalFormatted = (credit): string => {
|
||||
return this.formatNumber(credit.totalLocal, {
|
||||
currencyCode: credit.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -5,17 +5,17 @@ import { ICreditNoteCreatedPayload } from '../types/CreditNotes.types';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class CreditNoteAutoSerialSubscriber {
|
||||
export default class CreditNoteAutoSerialSubscriber {
|
||||
constructor(
|
||||
private readonly creditNoteIncrementService: CreditNoteAutoIncrementService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Auto serial increment once credit note created.
|
||||
* @param {ICreditNoteCreatedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.creditNote.onCreated)
|
||||
async autoSerialIncrementOnceCreated({ }: ICreditNoteCreatedPayload) {
|
||||
async autoSerialIncrementOnceCreated({}: ICreditNoteCreatedPayload) {
|
||||
await this.creditNoteIncrementService.incrementSerialNumber();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ export const InitialCurrencies = [
|
||||
];
|
||||
|
||||
export const ERRORS = {
|
||||
CURRENCY_NOT_FOUND: 'CURRENCY_NOT_FOUND',
|
||||
CURRENCY_CODE_EXISTS: 'CURRENCY_CODE_EXISTS',
|
||||
CURRENCY_NOT_FOUND: 'currency_not_found',
|
||||
CURRENCY_CODE_EXISTS: 'currency_code_exists',
|
||||
BASE_CURRENCY_INVALID: 'BASE_CURRENCY_INVALID',
|
||||
CANNOT_DELETE_BASE_CURRENCY: 'CANNOT_DELETE_BASE_CURRENCY',
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { castArray, uniq } from 'lodash';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { DeleteCustomer } from './commands/DeleteCustomer.service';
|
||||
|
||||
@Injectable()
|
||||
export class BulkDeleteCustomersService {
|
||||
constructor(private readonly deleteCustomerService: DeleteCustomer) {}
|
||||
|
||||
public async bulkDeleteCustomers(
|
||||
customerIds: number[],
|
||||
options?: { skipUndeletable?: boolean },
|
||||
): Promise<void> {
|
||||
const { skipUndeletable = false } = options ?? {};
|
||||
const ids = uniq(castArray(customerIds));
|
||||
|
||||
const results = await PromisePool.withConcurrency(1)
|
||||
.for(ids)
|
||||
.process(async (customerId: number) => {
|
||||
try {
|
||||
await this.deleteCustomerService.deleteCustomer(customerId);
|
||||
} catch (error) {
|
||||
if (!skipUndeletable) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||
throw results.errors[0].raw ?? results.errors[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,6 @@ import { CreateCustomerDto } from './dtos/CreateCustomer.dto';
|
||||
import { EditCustomerDto } from './dtos/EditCustomer.dto';
|
||||
import { CustomerResponseDto } from './dtos/CustomerResponse.dto';
|
||||
import { GetCustomersQueryDto } from './dtos/GetCustomersQuery.dto';
|
||||
import {
|
||||
BulkDeleteCustomersDto,
|
||||
ValidateBulkDeleteCustomersResponseDto,
|
||||
} from './dtos/BulkDeleteCustomers.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
|
||||
@Controller('customers')
|
||||
@@ -35,7 +31,7 @@ import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
@ApiExtraModels(CustomerResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
export class CustomersController {
|
||||
constructor(private customersApplication: CustomersApplication) { }
|
||||
constructor(private customersApplication: CustomersApplication) {}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Retrieves the customer details.' })
|
||||
@@ -113,37 +109,4 @@ export class CustomersController {
|
||||
openingBalanceDTO,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('validate-bulk-delete')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Validates which customers can be deleted and returns counts of deletable and non-deletable customers.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'Validation completed. Returns counts and IDs of deletable and non-deletable customers.',
|
||||
schema: { $ref: getSchemaPath(ValidateBulkDeleteCustomersResponseDto) },
|
||||
})
|
||||
validateBulkDeleteCustomers(
|
||||
@Body() bulkDeleteDto: BulkDeleteCustomersDto,
|
||||
): Promise<ValidateBulkDeleteCustomersResponseDto> {
|
||||
return this.customersApplication.validateBulkDeleteCustomers(
|
||||
bulkDeleteDto.ids,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('bulk-delete')
|
||||
@ApiOperation({ summary: 'Deletes multiple customers in bulk.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The customers have been successfully deleted.',
|
||||
})
|
||||
async bulkDeleteCustomers(
|
||||
@Body() bulkDeleteDto: BulkDeleteCustomersDto,
|
||||
): Promise<void> {
|
||||
return this.customersApplication.bulkDeleteCustomers(bulkDeleteDto.ids, {
|
||||
skipUndeletable: bulkDeleteDto.skipUndeletable ?? false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ import { CustomersExportable } from './CustomersExportable';
|
||||
import { CustomersImportable } from './CustomersImportable';
|
||||
import { GetCustomers } from './queries/GetCustomers.service';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
import { BulkDeleteCustomersService } from './BulkDeleteCustomers.service';
|
||||
import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomers.service';
|
||||
|
||||
@Module({
|
||||
imports: [TenancyDatabaseModule, DynamicListModule],
|
||||
@@ -39,8 +37,6 @@ import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomer
|
||||
CustomersExportable,
|
||||
CustomersImportable,
|
||||
GetCustomers,
|
||||
BulkDeleteCustomersService,
|
||||
ValidateBulkDeleteCustomersService,
|
||||
],
|
||||
})
|
||||
export class CustomersModule {}
|
||||
|
||||
@@ -12,8 +12,6 @@ import { CreateCustomerDto } from './dtos/CreateCustomer.dto';
|
||||
import { EditCustomerDto } from './dtos/EditCustomer.dto';
|
||||
import { GetCustomers } from './queries/GetCustomers.service';
|
||||
import { GetCustomersQueryDto } from './dtos/GetCustomersQuery.dto';
|
||||
import { BulkDeleteCustomersService } from './BulkDeleteCustomers.service';
|
||||
import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomers.service';
|
||||
|
||||
@Injectable()
|
||||
export class CustomersApplication {
|
||||
@@ -24,8 +22,6 @@ export class CustomersApplication {
|
||||
private deleteCustomerService: DeleteCustomer,
|
||||
private editOpeningBalanceService: EditOpeningBalanceCustomer,
|
||||
private getCustomersService: GetCustomers,
|
||||
private readonly bulkDeleteCustomersService: BulkDeleteCustomersService,
|
||||
private readonly validateBulkDeleteCustomersService: ValidateBulkDeleteCustomersService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -87,20 +83,4 @@ export class CustomersApplication {
|
||||
public getCustomers = (filterDTO: GetCustomersQueryDto) => {
|
||||
return this.getCustomersService.getCustomersList(filterDTO);
|
||||
};
|
||||
|
||||
public bulkDeleteCustomers = (
|
||||
customerIds: number[],
|
||||
options?: { skipUndeletable?: boolean },
|
||||
) => {
|
||||
return this.bulkDeleteCustomersService.bulkDeleteCustomers(
|
||||
customerIds,
|
||||
options,
|
||||
);
|
||||
};
|
||||
|
||||
public validateBulkDeleteCustomers = (customerIds: number[]) => {
|
||||
return this.validateBulkDeleteCustomersService.validateBulkDeleteCustomers(
|
||||
customerIds,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||
import { DeleteCustomer } from './commands/DeleteCustomer.service';
|
||||
import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateBulkDeleteCustomersService {
|
||||
constructor(
|
||||
private readonly deleteCustomerService: DeleteCustomer,
|
||||
@Inject(TENANCY_DB_CONNECTION)
|
||||
private readonly tenantKnex: () => Knex,
|
||||
) {}
|
||||
|
||||
public async validateBulkDeleteCustomers(customerIds: number[]): Promise<{
|
||||
deletableCount: number;
|
||||
nonDeletableCount: number;
|
||||
deletableIds: number[];
|
||||
nonDeletableIds: number[];
|
||||
}> {
|
||||
const trx = await this.tenantKnex().transaction({
|
||||
isolationLevel: 'read uncommitted',
|
||||
});
|
||||
|
||||
try {
|
||||
const deletableIds: number[] = [];
|
||||
const nonDeletableIds: number[] = [];
|
||||
|
||||
for (const customerId of customerIds) {
|
||||
try {
|
||||
await this.deleteCustomerService.deleteCustomer(customerId, trx);
|
||||
deletableIds.push(customerId);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ModelHasRelationsError ||
|
||||
(error instanceof ServiceError &&
|
||||
error.errorType === 'CUSTOMER_HAS_TRANSACTIONS')
|
||||
) {
|
||||
nonDeletableIds.push(customerId);
|
||||
} else {
|
||||
nonDeletableIds.push(customerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await trx.rollback();
|
||||
|
||||
return {
|
||||
deletableCount: deletableIds.length,
|
||||
nonDeletableCount: nonDeletableIds.length,
|
||||
deletableIds,
|
||||
nonDeletableIds,
|
||||
};
|
||||
} catch (error) {
|
||||
await trx.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,13 +31,12 @@ export class DeleteCustomer {
|
||||
* @param {number} customerId - Customer ID.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteCustomer(
|
||||
customerId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
public async deleteCustomer(customerId: number): Promise<void> {
|
||||
// Retrieve the customer or throw not found service error.
|
||||
const query = this.customerModel().query(trx);
|
||||
const oldCustomer = await query.findById(customerId).throwIfNotFound();
|
||||
const oldCustomer = await this.customerModel()
|
||||
.query()
|
||||
.findById(customerId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Triggers `onCustomerDeleting` event.
|
||||
await this.eventPublisher.emitAsync(events.customers.onDeleting, {
|
||||
@@ -46,10 +45,10 @@ export class DeleteCustomer {
|
||||
} as ICustomerDeletingPayload);
|
||||
|
||||
// Deletes the customer and associated entities under UOW transaction.
|
||||
return this.uow.withTransaction(async (transaction: Knex.Transaction) => {
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Delete the customer from the storage.
|
||||
await this.customerModel()
|
||||
.query(transaction)
|
||||
.query(trx)
|
||||
.findById(customerId)
|
||||
.deleteIfNoRelations({
|
||||
type: ERRORS.CUSTOMER_HAS_TRANSACTIONS,
|
||||
@@ -59,8 +58,8 @@ export class DeleteCustomer {
|
||||
await this.eventPublisher.emitAsync(events.customers.onDeleted, {
|
||||
customerId,
|
||||
oldCustomer,
|
||||
trx: transaction,
|
||||
trx,
|
||||
} as ICustomerEventDeletedPayload);
|
||||
}, trx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import {
|
||||
ArrayNotEmpty,
|
||||
IsArray,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { parseBoolean } from '@/utils/parse-boolean';
|
||||
|
||||
export class BulkDeleteCustomersDto {
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@IsInt({ each: true })
|
||||
@ApiProperty({
|
||||
description: 'Array of customer IDs to delete',
|
||||
type: [Number],
|
||||
example: [1, 2, 3],
|
||||
})
|
||||
ids: number[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value, obj }) => parseBoolean(value ?? obj?.skip_undeletable, false))
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'When true, undeletable customers will be skipped and only deletable ones removed.',
|
||||
type: Boolean,
|
||||
default: false,
|
||||
})
|
||||
skipUndeletable?: boolean;
|
||||
}
|
||||
|
||||
export class ValidateBulkDeleteCustomersResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Number of customers that can be deleted',
|
||||
example: 2,
|
||||
})
|
||||
deletableCount: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Number of customers that cannot be deleted',
|
||||
example: 1,
|
||||
})
|
||||
nonDeletableCount: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'IDs of customers that can be deleted',
|
||||
type: [Number],
|
||||
example: [1, 2],
|
||||
})
|
||||
deletableIds: number[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'IDs of customers that cannot be deleted',
|
||||
type: [Number],
|
||||
example: [3],
|
||||
})
|
||||
nonDeletableIds: number[];
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { castArray, uniq } from 'lodash';
|
||||
import { DeleteExpense } from './commands/DeleteExpense.service';
|
||||
|
||||
@Injectable()
|
||||
export class BulkDeleteExpensesService {
|
||||
constructor(private readonly deleteExpenseService: DeleteExpense) { }
|
||||
|
||||
async bulkDeleteExpenses(
|
||||
expenseIds: number | Array<number>,
|
||||
options?: { skipUndeletable?: boolean },
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const { skipUndeletable = false } = options ?? {};
|
||||
const expensesIds = uniq(castArray(expenseIds));
|
||||
|
||||
const results = await PromisePool.withConcurrency(1)
|
||||
.for(expensesIds)
|
||||
.process(async (expenseId: number) => {
|
||||
try {
|
||||
await this.deleteExpenseService.deleteExpense(expenseId);
|
||||
} catch (error) {
|
||||
if (!skipUndeletable) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||
throw results.errors[0].raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,55 +21,13 @@ import { CreateExpenseDto, EditExpenseDto } from './dtos/Expense.dto';
|
||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||
import { ExpenseResponseDto } from './dtos/ExpenseResponse.dto';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import {
|
||||
BulkDeleteDto,
|
||||
ValidateBulkDeleteResponseDto,
|
||||
} from '@/common/dtos/BulkDelete.dto';
|
||||
|
||||
@Controller('expenses')
|
||||
@ApiTags('Expenses')
|
||||
@ApiExtraModels(
|
||||
PaginatedResponseDto,
|
||||
ExpenseResponseDto,
|
||||
ValidateBulkDeleteResponseDto,
|
||||
)
|
||||
@ApiExtraModels(PaginatedResponseDto, ExpenseResponseDto)
|
||||
@ApiCommonHeaders()
|
||||
export class ExpensesController {
|
||||
constructor(private readonly expensesApplication: ExpensesApplication) { }
|
||||
|
||||
@Post('validate-bulk-delete')
|
||||
@ApiOperation({
|
||||
summary: 'Validate which expenses can be deleted and return the results.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'Validation completed with counts and IDs of deletable and non-deletable expenses.',
|
||||
schema: {
|
||||
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||
},
|
||||
})
|
||||
public validateBulkDeleteExpenses(
|
||||
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||
): Promise<ValidateBulkDeleteResponseDto> {
|
||||
return this.expensesApplication.validateBulkDeleteExpenses(
|
||||
bulkDeleteDto.ids,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('bulk-delete')
|
||||
@ApiOperation({ summary: 'Deletes multiple expenses.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Expenses deleted successfully',
|
||||
})
|
||||
public bulkDeleteExpenses(
|
||||
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||
) {
|
||||
return this.expensesApplication.bulkDeleteExpenses(bulkDeleteDto.ids, {
|
||||
skipUndeletable: bulkDeleteDto.skipUndeletable ?? false,
|
||||
});
|
||||
}
|
||||
constructor(private readonly expensesApplication: ExpensesApplication) {}
|
||||
|
||||
/**
|
||||
* Create a new expense transaction.
|
||||
|
||||
@@ -19,8 +19,6 @@ import { GetExpensesService } from './queries/GetExpenses.service';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
import { ExpensesExportable } from './ExpensesExportable';
|
||||
import { ExpensesImportable } from './ExpensesImportable';
|
||||
import { BulkDeleteExpensesService } from './BulkDeleteExpenses.service';
|
||||
import { ValidateBulkDeleteExpensesService } from './ValidateBulkDeleteExpenses.service';
|
||||
|
||||
@Module({
|
||||
imports: [LedgerModule, BranchesModule, DynamicListModule],
|
||||
@@ -43,8 +41,6 @@ import { ValidateBulkDeleteExpensesService } from './ValidateBulkDeleteExpenses.
|
||||
GetExpensesService,
|
||||
ExpensesExportable,
|
||||
ExpensesImportable,
|
||||
BulkDeleteExpensesService,
|
||||
ValidateBulkDeleteExpensesService,
|
||||
],
|
||||
})
|
||||
export class ExpensesModule {}
|
||||
|
||||
@@ -7,8 +7,6 @@ import { GetExpenseService } from './queries/GetExpense.service';
|
||||
import { IExpensesFilter } from './interfaces/Expenses.interface';
|
||||
import { GetExpensesService } from './queries/GetExpenses.service';
|
||||
import { CreateExpenseDto, EditExpenseDto } from './dtos/Expense.dto';
|
||||
import { BulkDeleteExpensesService } from './BulkDeleteExpenses.service';
|
||||
import { ValidateBulkDeleteExpensesService } from './ValidateBulkDeleteExpenses.service';
|
||||
|
||||
@Injectable()
|
||||
export class ExpensesApplication {
|
||||
@@ -19,9 +17,7 @@ export class ExpensesApplication {
|
||||
private readonly publishExpenseService: PublishExpense,
|
||||
private readonly getExpenseService: GetExpenseService,
|
||||
private readonly getExpensesService: GetExpensesService,
|
||||
private readonly bulkDeleteExpensesService: BulkDeleteExpensesService,
|
||||
private readonly validateBulkDeleteExpensesService: ValidateBulkDeleteExpensesService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new expense transaction.
|
||||
@@ -51,30 +47,6 @@ export class ExpensesApplication {
|
||||
return this.deleteExpenseService.deleteExpense(expenseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes expenses in bulk.
|
||||
* @param {number[]} expenseIds - Expense ids.
|
||||
*/
|
||||
public bulkDeleteExpenses(
|
||||
expenseIds: number[],
|
||||
options?: { skipUndeletable?: boolean },
|
||||
) {
|
||||
return this.bulkDeleteExpensesService.bulkDeleteExpenses(
|
||||
expenseIds,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates which expenses can be deleted.
|
||||
* @param {number[]} expenseIds - Expense ids.
|
||||
*/
|
||||
public validateBulkDeleteExpenses(expenseIds: number[]) {
|
||||
return this.validateBulkDeleteExpensesService.validateBulkDeleteExpenses(
|
||||
expenseIds,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes the given expense.
|
||||
* @param {number} expenseId - Expense id.
|
||||
|
||||
@@ -5,10 +5,10 @@ import { ExpensesSampleData } from './constants';
|
||||
import { CreateExpense } from './commands/CreateExpense.service';
|
||||
import { CreateExpenseDto } from './dtos/Expense.dto';
|
||||
import { ImportableService } from '../Import/decorators/Import.decorator';
|
||||
import { Expense } from './models/Expense.model';
|
||||
import { ManualJournal } from '../ManualJournals/models/ManualJournal';
|
||||
|
||||
@Injectable()
|
||||
@ImportableService({ name: Expense.name })
|
||||
@ImportableService({ name: ManualJournal.name })
|
||||
export class ExpensesImportable extends Importable {
|
||||
constructor(private readonly createExpenseService: CreateExpense) {
|
||||
super();
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||
import { DeleteExpense } from './commands/DeleteExpense.service';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateBulkDeleteExpensesService {
|
||||
constructor(
|
||||
private readonly deleteExpenseService: DeleteExpense,
|
||||
@Inject(TENANCY_DB_CONNECTION)
|
||||
private readonly tenantKnex: () => Knex,
|
||||
) { }
|
||||
|
||||
public async validateBulkDeleteExpenses(expenseIds: number[]): Promise<{
|
||||
deletableCount: number;
|
||||
nonDeletableCount: number;
|
||||
deletableIds: number[];
|
||||
nonDeletableIds: number[];
|
||||
}> {
|
||||
const trx = await this.tenantKnex().transaction({
|
||||
isolationLevel: 'read uncommitted',
|
||||
});
|
||||
|
||||
try {
|
||||
const deletableIds: number[] = [];
|
||||
const nonDeletableIds: number[] = [];
|
||||
|
||||
for (const expenseId of expenseIds) {
|
||||
try {
|
||||
await this.deleteExpenseService.deleteExpense(expenseId, trx);
|
||||
deletableIds.push(expenseId);
|
||||
} catch (error) {
|
||||
nonDeletableIds.push(expenseId);
|
||||
}
|
||||
}
|
||||
|
||||
await trx.rollback();
|
||||
|
||||
return {
|
||||
deletableCount: deletableIds.length,
|
||||
nonDeletableCount: nonDeletableIds.length,
|
||||
deletableIds,
|
||||
nonDeletableIds,
|
||||
};
|
||||
} catch (error) {
|
||||
await trx.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,12 +36,9 @@ export class DeleteExpense {
|
||||
/**
|
||||
* Deletes the given expense.
|
||||
* @param {number} expenseId
|
||||
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
public async deleteExpense(
|
||||
expenseId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
public async deleteExpense(expenseId: number): Promise<void> {
|
||||
// Retrieves the expense transaction with associated entries or
|
||||
// throw not found error.
|
||||
const oldExpense = await this.expenseModel()
|
||||
@@ -77,6 +74,6 @@ export class DeleteExpense {
|
||||
oldExpense,
|
||||
trx,
|
||||
} as IExpenseEventDeletePayload);
|
||||
}, trx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ export const ExpenseMeta = {
|
||||
relationModel: 'Account',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
required: true,
|
||||
importHint: 'account.field.account_hint',
|
||||
importHint: 'Matches the account name or code.',
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'expense.field.reference_no',
|
||||
@@ -169,7 +169,7 @@ export const ExpenseMeta = {
|
||||
relationModel: 'Account',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
required: true,
|
||||
importHint: 'account.field.account_hint',
|
||||
importHint: 'Matches the account name or code.',
|
||||
},
|
||||
amount: {
|
||||
name: 'expense.field.amount',
|
||||
@@ -187,7 +187,7 @@ export const ExpenseMeta = {
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
branchId: {
|
||||
name: 'invoice.field.branch',
|
||||
name: 'Branch',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Branch',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy.service';
|
||||
import { renderExportResourceTableTemplateHtml } from '@bigcapital/pdf-templates';
|
||||
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable.service';
|
||||
import { mapPdfRows } from './utils';
|
||||
|
||||
@Injectable()
|
||||
export class ExportPdf {
|
||||
constructor(
|
||||
private readonly templateInjectable: TemplateInjectable,
|
||||
private readonly chromiumlyTenancy: ChromiumlyTenancy,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generates the pdf table sheet for the given data and columns.
|
||||
@@ -18,18 +19,21 @@ export class ExportPdf {
|
||||
* @returns
|
||||
*/
|
||||
public async pdf(
|
||||
columns: { accessor: string; name?: string; style?: string; group?: string }[],
|
||||
columns: { accessor: string },
|
||||
data: Record<string, any>,
|
||||
sheetTitle: string = '',
|
||||
sheetDescription: string = ''
|
||||
) {
|
||||
const rows = mapPdfRows(columns, data);
|
||||
|
||||
const htmlContent = renderExportResourceTableTemplateHtml({
|
||||
table: { rows, columns },
|
||||
sheetTitle,
|
||||
sheetDescription,
|
||||
});
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
'modules/export-resource-table',
|
||||
{
|
||||
table: { rows, columns },
|
||||
sheetTitle,
|
||||
sheetDescription,
|
||||
}
|
||||
);
|
||||
// Convert the HTML content to PDF
|
||||
return this.chromiumlyTenancy.convertHtmlContent(htmlContent, {
|
||||
margins: { top: 0.2, bottom: 0.2, left: 0.2, right: 0.2 },
|
||||
|
||||
@@ -3,30 +3,29 @@ import { ITableColumn, ITableData, ITableRow } from '../types/Table.types';
|
||||
import { FinancialTableStructure } from './FinancialTableStructure';
|
||||
import { tableClassNames } from '../utils';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TemplateInjectable } from '../../TemplateInjectable/TemplateInjectable.service';
|
||||
import { ChromiumlyTenancy } from '../../ChromiumlyTenancy/ChromiumlyTenancy.service';
|
||||
import { renderFinancialSheetTemplateHtml } from '@bigcapital/pdf-templates';
|
||||
|
||||
@Injectable()
|
||||
export class TableSheetPdf {
|
||||
/**
|
||||
* @param {TemplateInjectable} templateInjectable - The template injectable service.
|
||||
* @param {ChromiumlyTenancy} chromiumlyTenancy - The chromiumly tenancy service.
|
||||
*/
|
||||
constructor(
|
||||
private readonly templateInjectable: TemplateInjectable,
|
||||
private readonly chromiumlyTenancy: ChromiumlyTenancy,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Converts the table data into a PDF format.
|
||||
* @param {ITableData} table - The table data to be converted.
|
||||
* @param {string} organizationName - The organization name.
|
||||
* @param {string} sheetName - The name of the sheet.
|
||||
* @param {string} sheetDate - The date of the sheet.
|
||||
* @param {string} customCSS - Optional custom CSS to inject.
|
||||
* @returns A promise that resolves with the PDF conversion result.
|
||||
*/
|
||||
public async convertToPdf(
|
||||
table: ITableData,
|
||||
organizationName: string,
|
||||
sheetName: string,
|
||||
sheetDate: string,
|
||||
customCSS?: string,
|
||||
@@ -34,26 +33,19 @@ export class TableSheetPdf {
|
||||
// Prepare columns and rows for PDF conversion
|
||||
const columns = this.tablePdfColumns(table.columns);
|
||||
const rows = this.tablePdfRows(table.rows);
|
||||
|
||||
const landscape = columns.length > 4;
|
||||
|
||||
// Generate HTML content from the React template
|
||||
const htmlContent = renderFinancialSheetTemplateHtml({
|
||||
organizationName,
|
||||
sheetName,
|
||||
sheetDate,
|
||||
table: {
|
||||
columns: columns.map((col) => ({
|
||||
key: col.key,
|
||||
label: col.label,
|
||||
style: (col as any).style, // style may be added during transformation
|
||||
})),
|
||||
rows: rows.map((row) => ({
|
||||
cells: row.cells,
|
||||
classNames: (row as any).classNames,
|
||||
})),
|
||||
// Generate HTML content from the template
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
'financial-sheet',
|
||||
{
|
||||
table: { rows, columns },
|
||||
sheetName,
|
||||
sheetDate,
|
||||
customCSS,
|
||||
},
|
||||
customCSS,
|
||||
});
|
||||
);
|
||||
// Convert the HTML content to PDF
|
||||
return this.chromiumlyTenancy.convertHtmlContent(htmlContent, {
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
@@ -82,6 +74,7 @@ export class TableSheetPdf {
|
||||
const flatNestedTree = curriedFlatNestedTree(R.__, {
|
||||
nestedPrefix: '<span style="padding-left: 15px;"></span>',
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return R.compose(tableClassNames, flatNestedTree)(rows);
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@ export class APAgingSummaryPdfInjectable {
|
||||
|
||||
return this.tableSheetPdf.convertToPdf(
|
||||
table.table,
|
||||
table.meta.organizationName,
|
||||
table.meta.sheetName,
|
||||
table.meta.formattedAsDate,
|
||||
HtmlTableCss,
|
||||
|
||||
@@ -21,7 +21,6 @@ export class ARAgingSummaryPdfInjectable {
|
||||
|
||||
return this.tableSheetPdf.convertToPdf(
|
||||
table.table,
|
||||
table.meta.organizationName,
|
||||
table.meta.sheetName,
|
||||
table.meta.formattedDateRange,
|
||||
HtmlTableCss,
|
||||
|
||||
@@ -9,7 +9,7 @@ export class BalanceSheetPdfInjectable {
|
||||
constructor(
|
||||
private readonly balanceSheetTable: BalanceSheetTableInjectable,
|
||||
private readonly tableSheetPdf: TableSheetPdf,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Converts the given balance sheet table to pdf.
|
||||
@@ -21,7 +21,6 @@ export class BalanceSheetPdfInjectable {
|
||||
|
||||
return this.tableSheetPdf.convertToPdf(
|
||||
table.table,
|
||||
table.meta.organizationName,
|
||||
table.meta.sheetName,
|
||||
table.meta.formattedDateRange,
|
||||
HtmlTableCustomCss,
|
||||
|
||||
@@ -81,7 +81,6 @@ export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto {
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => NumberFormatQueryDto)
|
||||
@IsOptional()
|
||||
numberFormat: NumberFormatQueryDto;
|
||||
|
||||
@ApiProperty({
|
||||
|
||||
@@ -22,7 +22,6 @@ export class CashflowTablePdfInjectable {
|
||||
|
||||
return this.tableSheetPdf.convertToPdf(
|
||||
table.table,
|
||||
table.meta.organizationName,
|
||||
table.meta.sheetName,
|
||||
table.meta.formattedDateRange,
|
||||
HtmlTableCustomCss,
|
||||
|
||||
@@ -21,7 +21,6 @@ export class CustomerBalanceSummaryPdf {
|
||||
|
||||
return this.tableSheetPdf.convertToPdf(
|
||||
table.table,
|
||||
table.meta.organizationName,
|
||||
table.meta.sheetName,
|
||||
table.meta.formattedDateRange,
|
||||
HtmlTableCustomCss,
|
||||
|
||||
@@ -21,7 +21,6 @@ export class GeneralLedgerPdf {
|
||||
|
||||
return this.tableSheetPdf.convertToPdf(
|
||||
table.table,
|
||||
table.meta.organizationName,
|
||||
table.meta.sheetName,
|
||||
table.meta.formattedDateRange,
|
||||
HtmlTableCustomCss,
|
||||
|
||||
@@ -32,7 +32,7 @@ export class InventoryItemDetailsQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Number format for the inventory item details',
|
||||
})
|
||||
numberFormat: NumberFormatQueryDto;
|
||||
numberFormat: INumberFormatQuery;
|
||||
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
@IsBoolean()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user