feat: wip clickhouse reports
This commit is contained in:
@@ -106,3 +106,11 @@ STRIPE_PAYMENT_CLIENT_ID=
|
|||||||
STRIPE_PAYMENT_WEBHOOKS_SECRET=
|
STRIPE_PAYMENT_WEBHOOKS_SECRET=
|
||||||
# Replace example.com with the correct domain
|
# Replace example.com with the correct domain
|
||||||
STRIPE_PAYMENT_REDIRECT_URL=https://example.com/preferences/payment-methods/stripe/callback
|
STRIPE_PAYMENT_REDIRECT_URL=https://example.com/preferences/payment-methods/stripe/callback
|
||||||
|
|
||||||
|
# ClickHouse
|
||||||
|
CLICKHOUSE_ENABLED=false
|
||||||
|
CLICKHOUSE_HOST=localhost
|
||||||
|
CLICKHOUSE_PORT=8123
|
||||||
|
CLICKHOUSE_USER=default
|
||||||
|
CLICKHOUSE_PASSWORD=
|
||||||
|
CLICKHOUSE_DATABASE=bigcapital_analytics
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ services:
|
|||||||
links:
|
links:
|
||||||
- mysql
|
- mysql
|
||||||
- redis
|
- redis
|
||||||
|
- clickhouse
|
||||||
depends_on:
|
depends_on:
|
||||||
- mysql
|
- mysql
|
||||||
- redis
|
- redis
|
||||||
|
- clickhouse
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
networks:
|
networks:
|
||||||
- bigcapital_network
|
- bigcapital_network
|
||||||
@@ -127,6 +129,14 @@ services:
|
|||||||
- STRIPE_PAYMENT_WEBHOOKS_SECRET=${STRIPE_PAYMENT_WEBHOOKS_SECRET}
|
- STRIPE_PAYMENT_WEBHOOKS_SECRET=${STRIPE_PAYMENT_WEBHOOKS_SECRET}
|
||||||
- STRIPE_PAYMENT_REDIRECT_URL=${STRIPE_PAYMENT_REDIRECT_URL}
|
- STRIPE_PAYMENT_REDIRECT_URL=${STRIPE_PAYMENT_REDIRECT_URL}
|
||||||
|
|
||||||
|
# ClickHouse
|
||||||
|
- CLICKHOUSE_ENABLED=${CLICKHOUSE_ENABLED:-true}
|
||||||
|
- CLICKHOUSE_HOST=${CLICKHOUSE_HOST:-clickhouse}
|
||||||
|
- CLICKHOUSE_PORT=${CLICKHOUSE_PORT:-8123}
|
||||||
|
- CLICKHOUSE_USER=${CLICKHOUSE_USER:-default}
|
||||||
|
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
|
||||||
|
- CLICKHOUSE_DATABASE=${CLICKHOUSE_DATABASE:-bigcapital_analytics}
|
||||||
|
|
||||||
database_migration:
|
database_migration:
|
||||||
container_name: bigcapital-database-migration
|
container_name: bigcapital-database-migration
|
||||||
build:
|
build:
|
||||||
@@ -175,6 +185,18 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- bigcapital_network
|
- bigcapital_network
|
||||||
|
|
||||||
|
clickhouse:
|
||||||
|
container_name: bigcapital-clickhouse
|
||||||
|
image: clickhouse/clickhouse-server:24.8
|
||||||
|
restart: on-failure
|
||||||
|
expose:
|
||||||
|
- '8123'
|
||||||
|
- '9000'
|
||||||
|
volumes:
|
||||||
|
- clickhouse:/var/lib/clickhouse
|
||||||
|
networks:
|
||||||
|
- bigcapital_network
|
||||||
|
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: gotenberg/gotenberg:7
|
image: gotenberg/gotenberg:7
|
||||||
expose:
|
expose:
|
||||||
@@ -192,6 +214,10 @@ volumes:
|
|||||||
name: bigcapital_prod_redis
|
name: bigcapital_prod_redis
|
||||||
driver: local
|
driver: local
|
||||||
|
|
||||||
|
clickhouse:
|
||||||
|
name: bigcapital_prod_clickhouse
|
||||||
|
driver: local
|
||||||
|
|
||||||
# Networks
|
# Networks
|
||||||
networks:
|
networks:
|
||||||
bigcapital_network:
|
bigcapital_network:
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- '9000:3000'
|
- '9000:3000'
|
||||||
|
|
||||||
|
clickhouse:
|
||||||
|
image: clickhouse/clickhouse-server:24.8
|
||||||
|
container_name: bigcapital-clickhouse
|
||||||
|
ports:
|
||||||
|
- '8123:8123'
|
||||||
|
- '9001:9000'
|
||||||
|
volumes:
|
||||||
|
- clickhouse:/var/lib/clickhouse
|
||||||
|
environment:
|
||||||
|
CLICKHOUSE_DB: bigcapital_analytics
|
||||||
|
deploy:
|
||||||
|
restart_policy:
|
||||||
|
condition: unless-stopped
|
||||||
|
|
||||||
# Volumes
|
# Volumes
|
||||||
volumes:
|
volumes:
|
||||||
mysql:
|
mysql:
|
||||||
@@ -51,3 +65,7 @@ volumes:
|
|||||||
redis:
|
redis:
|
||||||
name: bigcapital_dev_redis
|
name: bigcapital_dev_redis
|
||||||
driver: local
|
driver: local
|
||||||
|
|
||||||
|
clickhouse:
|
||||||
|
name: bigcapital_dev_clickhouse
|
||||||
|
driver: local
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM mariadb:10.2
|
FROM mariadb:11.4
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
ADD my.cnf /etc/mysql/conf.d/my.cnf
|
ADD my.cnf /etc/mysql/conf.d/my.cnf
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
GRANT ALL PRIVILEGES ON *.* TO '{MYSQL_USER}'@'%' IDENTIFIED BY '{MYSQL_PASSWORD}' WITH GRANT OPTION;
|
GRANT ALL PRIVILEGES ON *.* TO '{MYSQL_USER}'@'%' IDENTIFIED BY '{MYSQL_PASSWORD}' WITH GRANT OPTION;
|
||||||
|
|
||||||
FLUSH PRIVILEGES;
|
-- Create ClickHouse CDC replication user
|
||||||
|
CREATE USER IF NOT EXISTS 'clickpipes_user'@'%' IDENTIFIED BY 'clickpipes_password';
|
||||||
|
GRANT SELECT ON *.* TO 'clickpipes_user'@'%';
|
||||||
|
GRANT REPLICATION CLIENT ON *.* TO 'clickpipes_user'@'%';
|
||||||
|
GRANT REPLICATION SLAVE ON *.* TO 'clickpipes_user'@'%';
|
||||||
|
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
|||||||
+16
-1
@@ -1,2 +1,17 @@
|
|||||||
[mysqld]
|
[mysqld]
|
||||||
bind-address = 0.0.0.0
|
bind-address = 0.0.0.0
|
||||||
|
|
||||||
|
# Binary logging for ClickHouse CDC (MaterializedMySQL)
|
||||||
|
server_id = 1
|
||||||
|
log_bin = ON
|
||||||
|
binlog_format = ROW
|
||||||
|
binlog_row_image = FULL
|
||||||
|
binlog_row_metadata = FULL
|
||||||
|
binlog_expire_logs_seconds = 86400
|
||||||
|
|
||||||
|
# Required for replication from replica
|
||||||
|
log_slave_updates = ON
|
||||||
|
|
||||||
|
# Character set
|
||||||
|
character-set-server = utf8mb4
|
||||||
|
collation-server = utf8mb4_unicode_ci
|
||||||
|
|||||||
@@ -95,4 +95,12 @@ 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=
|
||||||
|
|
||||||
|
# ClickHouse
|
||||||
|
CLICKHOUSE_ENABLED=false
|
||||||
|
CLICKHOUSE_HOST=localhost
|
||||||
|
CLICKHOUSE_PORT=8123
|
||||||
|
CLICKHOUSE_USER=default
|
||||||
|
CLICKHOUSE_PASSWORD=
|
||||||
|
CLICKHOUSE_DATABASE=bigcapital_analytics
|
||||||
Generated
+17449
File diff suppressed because it is too large
Load Diff
@@ -38,13 +38,14 @@
|
|||||||
"@bigcapital/pdf-templates": "workspace:*",
|
"@bigcapital/pdf-templates": "workspace:*",
|
||||||
"@bigcapital/sdk-ts": "workspace:*",
|
"@bigcapital/sdk-ts": "workspace:*",
|
||||||
"@bigcapital/utils": "workspace:*",
|
"@bigcapital/utils": "workspace:*",
|
||||||
"@casl/ability": "^5.4.3",
|
|
||||||
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
|
||||||
"@liaoliaots/nestjs-redis": "^10.0.0",
|
|
||||||
"@nest-lab/throttler-storage-redis": "^1.1.0",
|
|
||||||
"@bull-board/api": "^5.22.0",
|
"@bull-board/api": "^5.22.0",
|
||||||
"@bull-board/express": "^5.22.0",
|
"@bull-board/express": "^5.22.0",
|
||||||
"@bull-board/nestjs": "^5.22.0",
|
"@bull-board/nestjs": "^5.22.0",
|
||||||
|
"@casl/ability": "^5.4.3",
|
||||||
|
"@clickhouse/client": "^1.18.3",
|
||||||
|
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||||
|
"@liaoliaots/nestjs-redis": "^10.0.0",
|
||||||
|
"@nest-lab/throttler-storage-redis": "^1.1.0",
|
||||||
"@nestjs/bull": "^10.2.1",
|
"@nestjs/bull": "^10.2.1",
|
||||||
"@nestjs/bullmq": "^10.2.2",
|
"@nestjs/bullmq": "^10.2.2",
|
||||||
"@nestjs/cache-manager": "^2.2.2",
|
"@nestjs/cache-manager": "^2.2.2",
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('clickhouse', () => ({
|
||||||
|
enabled: process.env.CLICKHOUSE_ENABLED === 'true',
|
||||||
|
host: process.env.CLICKHOUSE_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.CLICKHOUSE_PORT || '8123', 10),
|
||||||
|
user: process.env.CLICKHOUSE_USER || 'default',
|
||||||
|
password: process.env.CLICKHOUSE_PASSWORD || '',
|
||||||
|
database: process.env.CLICKHOUSE_DATABASE || 'bigcapital_analytics',
|
||||||
|
}));
|
||||||
@@ -20,6 +20,7 @@ import cloud from './cloud';
|
|||||||
import redis from './redis';
|
import redis from './redis';
|
||||||
import queue from './queue';
|
import queue from './queue';
|
||||||
import bullBoard from './bull-board';
|
import bullBoard from './bull-board';
|
||||||
|
import clickhouse from './clickhouse-database';
|
||||||
|
|
||||||
export const config = [
|
export const config = [
|
||||||
app,
|
app,
|
||||||
@@ -44,4 +45,5 @@ export const config = [
|
|||||||
redis,
|
redis,
|
||||||
queue,
|
queue,
|
||||||
bullBoard,
|
bullBoard,
|
||||||
|
clickhouse,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module
|
|||||||
import { PaymentServicesModule } from '../PaymentServices/PaymentServices.module';
|
import { PaymentServicesModule } from '../PaymentServices/PaymentServices.module';
|
||||||
import { AuthModule } from '../Auth/Auth.module';
|
import { AuthModule } from '../Auth/Auth.module';
|
||||||
import { TenancyModule } from '../Tenancy/Tenancy.module';
|
import { TenancyModule } from '../Tenancy/Tenancy.module';
|
||||||
|
import { ClickHouseModule } from '../ClickHouse/ClickHouse.module';
|
||||||
import { LoopsModule } from '../Loops/Loops.module';
|
import { LoopsModule } from '../Loops/Loops.module';
|
||||||
import { AttachmentsModule } from '../Attachments/Attachment.module';
|
import { AttachmentsModule } from '../Attachments/Attachment.module';
|
||||||
import { S3Module } from '../S3/S3.module';
|
import { S3Module } from '../S3/S3.module';
|
||||||
@@ -119,6 +120,7 @@ import { AppThrottleModule } from './AppThrottle.module';
|
|||||||
}),
|
}),
|
||||||
SystemDatabaseModule,
|
SystemDatabaseModule,
|
||||||
SystemModelsModule,
|
SystemModelsModule,
|
||||||
|
ClickHouseModule,
|
||||||
EventEmitterModule.forRoot(),
|
EventEmitterModule.forRoot(),
|
||||||
I18nModule.forRootAsync({
|
I18nModule.forRootAsync({
|
||||||
useFactory: () => ({
|
useFactory: () => ({
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const CLICKHOUSE_CLIENT = Symbol('CLICKHOUSE_CLIENT');
|
||||||
|
export const CLICKHOUSE_CONNECTION_OPTIONS = Symbol('CLICKHOUSE_CONNECTION_OPTIONS');
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { ClickHouseService } from './ClickHouse.service';
|
||||||
|
import { ClickHouseMigrationService } from './ClickHouseMigration.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [ClickHouseService, ClickHouseMigrationService],
|
||||||
|
exports: [ClickHouseService, ClickHouseMigrationService],
|
||||||
|
})
|
||||||
|
export class ClickHouseModule {}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { createClient, ClickHouseClient } from '@clickhouse/client';
|
||||||
|
import * as LRUCache from 'lru-cache';
|
||||||
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
|
||||||
|
@Injectable({ scope: Scope.DEFAULT })
|
||||||
|
export class ClickHouseService {
|
||||||
|
private readonly cache: LRUCache<string, ClickHouseClient>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly cls: ClsService,
|
||||||
|
) {
|
||||||
|
this.cache = new LRUCache({ max: 100 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves or creates a ClickHouse client.
|
||||||
|
* If database is not specified, uses the tenant-specific database.
|
||||||
|
*/
|
||||||
|
getClient(database?: string): ClickHouseClient {
|
||||||
|
const organizationId = this.cls.get('organizationId');
|
||||||
|
const tenantId = organizationId ? String(organizationId) : 'system';
|
||||||
|
const dbName = database || this.getTenantDatabaseName(tenantId);
|
||||||
|
const cacheKey = `${tenantId}:${dbName}`;
|
||||||
|
const cachedClient = this.cache.get(cacheKey);
|
||||||
|
|
||||||
|
if (cachedClient) {
|
||||||
|
return cachedClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = this.configService.get<string>('clickhouse.host', 'localhost');
|
||||||
|
const port = this.configService.get<number>('clickhouse.port', 8123);
|
||||||
|
const user = this.configService.get<string>('clickhouse.user', 'default');
|
||||||
|
const password = this.configService.get<string>('clickhouse.password', '');
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
host: `http://${host}:${port}`,
|
||||||
|
username: user,
|
||||||
|
password,
|
||||||
|
database: dbName,
|
||||||
|
request_timeout: 30000,
|
||||||
|
max_open_connections: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cache.set(cacheKey, client);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default ClickHouse database name for a tenant.
|
||||||
|
*/
|
||||||
|
getTenantDatabaseName(tenantId: string): string {
|
||||||
|
const prefix = this.configService.get<string>(
|
||||||
|
'tenantDatabase.dbNamePrefix',
|
||||||
|
'bigcapital_tenant_',
|
||||||
|
);
|
||||||
|
return `${prefix}${tenantId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a query and returns the result.
|
||||||
|
* Uses the tenant database by default.
|
||||||
|
*/
|
||||||
|
async query<T = any>(
|
||||||
|
query: string,
|
||||||
|
params?: Record<string, unknown>,
|
||||||
|
database?: string,
|
||||||
|
): Promise<T[]> {
|
||||||
|
const client = this.getClient(database);
|
||||||
|
const resultSet = await client.query({
|
||||||
|
query,
|
||||||
|
query_params: params,
|
||||||
|
format: 'JSONEachRow',
|
||||||
|
});
|
||||||
|
return await resultSet.json<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a command (DDL, INSERT, etc.).
|
||||||
|
*/
|
||||||
|
async command(query: string, database?: string): Promise<void> {
|
||||||
|
const client = this.getClient(database);
|
||||||
|
await client.command({ query });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts data into a table.
|
||||||
|
*/
|
||||||
|
async insert<T = any>(table: string, values: T[], database?: string): Promise<void> {
|
||||||
|
const client = this.getClient(database);
|
||||||
|
await client.insert({
|
||||||
|
table,
|
||||||
|
values,
|
||||||
|
format: 'JSONEachRow',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { ClickHouseService } from './ClickHouse.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ClickHouseMigrationService {
|
||||||
|
constructor(
|
||||||
|
private readonly clickHouse: ClickHouseService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstraps MaterializedMySQL replication for a tenant database.
|
||||||
|
*/
|
||||||
|
async bootstrapTenantReplication(tenantDbName: string): Promise<void> {
|
||||||
|
const enabled = this.configService.get<boolean>('clickhouse.enabled', false);
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mariadbHost = this.configService.get<string>('tenantDatabase.host', 'mariadb');
|
||||||
|
const mariadbPort = this.configService.get<number>('tenantDatabase.port', 3306);
|
||||||
|
const mariadbUser = this.configService.get<string>('tenantDatabase.user', 'clickpipes_user');
|
||||||
|
const mariadbPassword = this.configService.get<string>('tenantDatabase.password', 'clickpipes_password');
|
||||||
|
|
||||||
|
// MaterializedMySQL requires the database to not exist before creation
|
||||||
|
const checkQuery = `SELECT count() FROM system.databases WHERE name = {tenantDb:String}`;
|
||||||
|
const exists = await this.clickHouse.query<{ count: number }>(checkQuery, {
|
||||||
|
tenantDb: tenantDbName,
|
||||||
|
});
|
||||||
|
if (exists[0]?.count > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const createQuery = `
|
||||||
|
CREATE DATABASE IF NOT EXISTS ${tenantDbName}
|
||||||
|
ENGINE = MaterializedMySQL('${mariadbHost}:${mariadbPort}', '${tenantDbName}', '${mariadbUser}', '${mariadbPassword}')
|
||||||
|
SETTINGS
|
||||||
|
materialized_mysql_tables_list = 'accounts_transactions',
|
||||||
|
materialized_mysql_wait_for replication,
|
||||||
|
materialized_mysql_snapshot_mode = 'standard'
|
||||||
|
`;
|
||||||
|
|
||||||
|
await this.clickHouse.command(createQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates pre-aggregated balance tables for a tenant.
|
||||||
|
* These tables are populated from MaterializedMySQL replicated data.
|
||||||
|
*/
|
||||||
|
async createPreAggregatedTables(tenantDbName: string): Promise<void> {
|
||||||
|
const enabled = this.configService.get<boolean>('clickhouse.enabled', false);
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SummingMergeTree for daily account balances
|
||||||
|
const createBalancesTable = `
|
||||||
|
CREATE TABLE IF NOT EXISTS ${tenantDbName}.account_balances_daily (
|
||||||
|
account_id UInt32,
|
||||||
|
date Date,
|
||||||
|
credit_sum Decimal(15, 5),
|
||||||
|
debit_sum Decimal(15, 5),
|
||||||
|
branch_id Nullable(UInt32)
|
||||||
|
) ENGINE = SummingMergeTree()
|
||||||
|
ORDER BY (account_id, date, branch_id)
|
||||||
|
`;
|
||||||
|
|
||||||
|
await this.clickHouse.command(createBalancesTable);
|
||||||
|
|
||||||
|
// Materialized view to auto-populate from replicated transactions
|
||||||
|
const createMv = `
|
||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS ${tenantDbName}.mv_account_balances_daily
|
||||||
|
TO ${tenantDbName}.account_balances_daily
|
||||||
|
AS SELECT
|
||||||
|
account_id,
|
||||||
|
date,
|
||||||
|
sum(credit) AS credit_sum,
|
||||||
|
sum(debit) AS debit_sum,
|
||||||
|
branch_id
|
||||||
|
FROM ${tenantDbName}.accounts_transactions
|
||||||
|
GROUP BY account_id, date, branch_id
|
||||||
|
`;
|
||||||
|
await this.clickHouse.command(createMv);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-1
@@ -9,13 +9,18 @@ import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
|||||||
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
||||||
import { BalanceSheetStatementController } from './BalanceSheet.controller';
|
import { BalanceSheetStatementController } from './BalanceSheet.controller';
|
||||||
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||||
|
import { BalanceSheetClickHouseRepository } from './BalanceSheetClickHouseRepository';
|
||||||
|
import { BalanceSheetRepositoryFactory } from './BalanceSheetRepositoryFactory';
|
||||||
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
|
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
|
||||||
|
import { ClickHouseModule } from '@/modules/ClickHouse/ClickHouse.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [FinancialSheetCommonModule, AccountsModule],
|
imports: [FinancialSheetCommonModule, AccountsModule, ClickHouseModule],
|
||||||
controllers: [BalanceSheetStatementController],
|
controllers: [BalanceSheetStatementController],
|
||||||
providers: [
|
providers: [
|
||||||
BalanceSheetRepository,
|
BalanceSheetRepository,
|
||||||
|
BalanceSheetClickHouseRepository,
|
||||||
|
BalanceSheetRepositoryFactory,
|
||||||
BalanceSheetInjectable,
|
BalanceSheetInjectable,
|
||||||
BalanceSheetTableInjectable,
|
BalanceSheetTableInjectable,
|
||||||
BalanceSheetExportInjectable,
|
BalanceSheetExportInjectable,
|
||||||
|
|||||||
+438
@@ -0,0 +1,438 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { ModelObject } from 'objection';
|
||||||
|
import { IBalanceSheetQuery, IAccountTransactionsGroupBy } from './BalanceSheet.types';
|
||||||
|
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||||
|
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
|
||||||
|
import { Ledger } from '@/modules/Ledger/Ledger';
|
||||||
|
import { transformToMapBy } from '@/utils/transform-to-map-by';
|
||||||
|
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { ClickHouseService } from '@/modules/ClickHouse/ClickHouse.service';
|
||||||
|
import { ACCOUNT_PARENT_TYPE } from '@/constants/accounts';
|
||||||
|
import { IBalanceSheetRepository } from './IBalanceSheetRepository';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps ClickHouse query result row to a ledger-compatible transaction object.
|
||||||
|
*/
|
||||||
|
interface CHTransactionRow {
|
||||||
|
account_id: number;
|
||||||
|
credit: string | number;
|
||||||
|
debit: string | number;
|
||||||
|
date?: string;
|
||||||
|
account_normal?: string;
|
||||||
|
account_type?: string;
|
||||||
|
account_parent_type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
export class BalanceSheetClickHouseRepository implements IBalanceSheetRepository {
|
||||||
|
@Inject(Account.name)
|
||||||
|
public readonly accountModel: TenantModelProxy<typeof Account>;
|
||||||
|
|
||||||
|
@Inject(ClickHouseService)
|
||||||
|
private readonly clickHouse: ClickHouseService;
|
||||||
|
|
||||||
|
public query: BalanceSheetQuery;
|
||||||
|
public accounts: ModelObject<Account>[] = [];
|
||||||
|
public accountsGraph: any;
|
||||||
|
public accountsByType: Map<string, ModelObject<Account>[]> = new Map();
|
||||||
|
public accountsByParentType: Map<string, ModelObject<Account>[]> = new Map();
|
||||||
|
|
||||||
|
public totalAccountsLedger: ILedger;
|
||||||
|
public incomeLedger: ILedger;
|
||||||
|
public expensesLedger: ILedger;
|
||||||
|
|
||||||
|
public periodsAccountsLedger: ILedger;
|
||||||
|
public periodsOpeningAccountLedger: ILedger;
|
||||||
|
|
||||||
|
public PYTotalAccountsLedger: ILedger;
|
||||||
|
public PYPeriodsAccountsLedger: ILedger;
|
||||||
|
public PYPeriodsOpeningAccountLedger: ILedger;
|
||||||
|
|
||||||
|
public PPTotalAccountsLedger: ILedger;
|
||||||
|
public PPPeriodsAccountsLedger: ILedger;
|
||||||
|
public PPPeriodsOpeningAccountLedger: ILedger;
|
||||||
|
|
||||||
|
public incomePeriodsAccountsLedger: ILedger;
|
||||||
|
public incomePeriodsOpeningAccountsLedger: ILedger;
|
||||||
|
public expensesPeriodsAccountsLedger: ILedger;
|
||||||
|
public expensesOpeningAccountLedger: ILedger;
|
||||||
|
|
||||||
|
public incomePPAccountsLedger: ILedger;
|
||||||
|
public expensePPAccountsLedger: ILedger;
|
||||||
|
public incomePPPeriodsAccountsLedger: ILedger;
|
||||||
|
public incomePPPeriodsOpeningAccountLedger: ILedger;
|
||||||
|
public expensePPPeriodsAccountsLedger: ILedger;
|
||||||
|
public expensePPPeriodsOpeningAccountLedger: ILedger;
|
||||||
|
|
||||||
|
public incomePYTotalAccountsLedger: ILedger;
|
||||||
|
public expensePYTotalAccountsLedger: ILedger;
|
||||||
|
public incomePYPeriodsAccountsLedger: ILedger;
|
||||||
|
public incomePYPeriodsOpeningAccountLedger: ILedger;
|
||||||
|
public expensePYPeriodsAccountsLedger: ILedger;
|
||||||
|
public expensePYPeriodsOpeningAccountLedger: ILedger;
|
||||||
|
|
||||||
|
public transactionsGroupType: IAccountTransactionsGroupBy = IAccountTransactionsGroupBy.Month;
|
||||||
|
|
||||||
|
// Internal account normal map built from MySQL accounts
|
||||||
|
private accountNormalMap: Map<number, string> = new Map();
|
||||||
|
|
||||||
|
public setQuery(query: IBalanceSheetQuery) {
|
||||||
|
this.query = new BalanceSheetQuery(query);
|
||||||
|
this.transactionsGroupType = this.getGroupByFromDisplayColumnsBy(
|
||||||
|
this.query.displayColumnsBy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public asyncInitialize = async (query: IBalanceSheetQuery) => {
|
||||||
|
this.setQuery(query);
|
||||||
|
|
||||||
|
await this.initAccounts();
|
||||||
|
await this.initAccountsGraph();
|
||||||
|
await this.initAccountsTotalLedger();
|
||||||
|
|
||||||
|
if (this.query.isDatePeriodsColumnsType()) {
|
||||||
|
await this.initTotalDatePeriods();
|
||||||
|
}
|
||||||
|
if (this.query.isPreviousYearActive()) {
|
||||||
|
await this.initTotalPreviousYear();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.query.isPreviousYearActive() &&
|
||||||
|
this.query.isDatePeriodsColumnsType()
|
||||||
|
) {
|
||||||
|
await this.initPeriodsPreviousYear();
|
||||||
|
}
|
||||||
|
if (this.query.isPreviousPeriodActive()) {
|
||||||
|
await this.initTotalPreviousPeriod();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.query.isPreviousPeriodActive() &&
|
||||||
|
this.query.isDatePeriodsColumnsType()
|
||||||
|
) {
|
||||||
|
await this.initPeriodsPreviousPeriod();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.asyncInitializeNetIncome();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// # Accounts
|
||||||
|
// ----------------------------
|
||||||
|
public initAccounts = async () => {
|
||||||
|
const accounts = await this.getAccounts();
|
||||||
|
this.accounts = accounts;
|
||||||
|
this.accountsByType = transformToMapBy(accounts, 'accountType');
|
||||||
|
this.accountsByParentType = transformToMapBy(accounts, 'accountParentType');
|
||||||
|
|
||||||
|
// Build account normal map for ledger entries
|
||||||
|
accounts.forEach((acc) => {
|
||||||
|
this.accountNormalMap.set(acc.id, acc.accountNormal);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public initAccountsGraph = async () => {
|
||||||
|
this.accountsGraph = this.accountModel().toDependencyGraph(this.accounts);
|
||||||
|
};
|
||||||
|
|
||||||
|
public getAccounts = () => {
|
||||||
|
return this.accountModel().query();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// # Closing Total
|
||||||
|
// ----------------------------
|
||||||
|
public initAccountsTotalLedger = async (): Promise<void> => {
|
||||||
|
const totalByAccount = await this.closingAccountsTotal(this.query.toDate);
|
||||||
|
this.totalAccountsLedger = Ledger.fromTransactions(totalByAccount);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// # Date periods.
|
||||||
|
// ----------------------------
|
||||||
|
public initTotalDatePeriods = async (): Promise<void> => {
|
||||||
|
const [periodsByAccount, periodsOpeningByAccount] = await Promise.all([
|
||||||
|
this.accountsDatePeriods(
|
||||||
|
this.query.fromDate,
|
||||||
|
this.query.toDate,
|
||||||
|
this.transactionsGroupType,
|
||||||
|
),
|
||||||
|
this.closingAccountsTotal(this.query.fromDate),
|
||||||
|
]);
|
||||||
|
this.periodsAccountsLedger = Ledger.fromTransactions(periodsByAccount);
|
||||||
|
this.periodsOpeningAccountLedger = Ledger.fromTransactions(periodsOpeningByAccount);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// # Previous Year (PY).
|
||||||
|
// ----------------------------
|
||||||
|
public initTotalPreviousYear = async (): Promise<void> => {
|
||||||
|
const PYTotalsByAccounts = await this.closingAccountsTotal(this.query.PYToDate);
|
||||||
|
this.PYTotalAccountsLedger = Ledger.fromTransactions(PYTotalsByAccounts);
|
||||||
|
};
|
||||||
|
|
||||||
|
public initPeriodsPreviousYear = async (): Promise<void> => {
|
||||||
|
const [PYPeriodsBYAccounts, periodsOpeningByAccount] = await Promise.all([
|
||||||
|
this.accountsDatePeriods(
|
||||||
|
this.query.PYFromDate,
|
||||||
|
this.query.PYToDate,
|
||||||
|
this.transactionsGroupType,
|
||||||
|
),
|
||||||
|
this.closingAccountsTotal(this.query.PYFromDate),
|
||||||
|
]);
|
||||||
|
this.PYPeriodsAccountsLedger = Ledger.fromTransactions(PYPeriodsBYAccounts);
|
||||||
|
this.PYPeriodsOpeningAccountLedger = Ledger.fromTransactions(periodsOpeningByAccount);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// # Previous Year (PP).
|
||||||
|
// ----------------------------
|
||||||
|
public initTotalPreviousPeriod = async (): Promise<void> => {
|
||||||
|
const PPTotalsByAccounts = await this.closingAccountsTotal(this.query.PPToDate);
|
||||||
|
this.PPTotalAccountsLedger = Ledger.fromTransactions(PPTotalsByAccounts);
|
||||||
|
};
|
||||||
|
|
||||||
|
public initPeriodsPreviousPeriod = async (): Promise<void> => {
|
||||||
|
const [PPPeriodsBYAccounts, periodsOpeningByAccount] = await Promise.all([
|
||||||
|
this.accountsDatePeriods(
|
||||||
|
this.query.PPFromDate,
|
||||||
|
this.query.PPToDate,
|
||||||
|
this.transactionsGroupType,
|
||||||
|
),
|
||||||
|
this.closingAccountsTotal(this.query.PPFromDate),
|
||||||
|
]);
|
||||||
|
this.PPPeriodsAccountsLedger = Ledger.fromTransactions(PPPeriodsBYAccounts);
|
||||||
|
this.PPPeriodsOpeningAccountLedger = Ledger.fromTransactions(periodsOpeningByAccount);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// # ClickHouse Queries
|
||||||
|
// ----------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve closing accounts total up to a date from ClickHouse.
|
||||||
|
*/
|
||||||
|
public closingAccountsTotal = async (toDate: Date | string) => {
|
||||||
|
const dateStr = this.formatDate(toDate);
|
||||||
|
const branchFilter = this.buildBranchFilter();
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
account_id,
|
||||||
|
sum(credit) AS credit,
|
||||||
|
sum(debit) AS debit
|
||||||
|
FROM accounts_transactions
|
||||||
|
WHERE date <= {toDate:Date}
|
||||||
|
${branchFilter}
|
||||||
|
GROUP BY account_id
|
||||||
|
`;
|
||||||
|
|
||||||
|
const rows = await this.clickHouse.query<CHTransactionRow>(query, { toDate: dateStr });
|
||||||
|
return this.mapToLedgerTransactions(rows);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve account transactions grouped by date periods from ClickHouse.
|
||||||
|
*/
|
||||||
|
public accountsDatePeriods = async (
|
||||||
|
fromDate: Date,
|
||||||
|
toDate: Date,
|
||||||
|
datePeriodsType: string,
|
||||||
|
) => {
|
||||||
|
const fromStr = this.formatDate(fromDate);
|
||||||
|
const toStr = this.formatDate(toDate);
|
||||||
|
const groupFormat = this.getClickHouseDateFormat(datePeriodsType);
|
||||||
|
const branchFilter = this.buildBranchFilter();
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
account_id,
|
||||||
|
formatDateTime(date, '${groupFormat}') AS date,
|
||||||
|
sum(credit) AS credit,
|
||||||
|
sum(debit) AS debit
|
||||||
|
FROM accounts_transactions
|
||||||
|
WHERE date >= {fromDate:Date}
|
||||||
|
AND date <= {toDate:Date}
|
||||||
|
${branchFilter}
|
||||||
|
GROUP BY account_id, formatDateTime(date, '${groupFormat}')
|
||||||
|
ORDER BY account_id, date
|
||||||
|
`;
|
||||||
|
|
||||||
|
const rows = await this.clickHouse.query<CHTransactionRow>(query, {
|
||||||
|
fromDate: fromStr,
|
||||||
|
toDate: toStr,
|
||||||
|
});
|
||||||
|
return this.mapToLedgerTransactions(rows);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// # Net Income (mirrored from BalanceSheetRepositoryNetIncome)
|
||||||
|
// ----------------------------
|
||||||
|
public asyncInitializeNetIncome = async () => {
|
||||||
|
this.initIncomeAccounts();
|
||||||
|
this.initExpenseAccounts();
|
||||||
|
this.initIncomeTotalLedger();
|
||||||
|
this.initExpensesTotalLedger();
|
||||||
|
|
||||||
|
if (this.query.isDatePeriodsColumnsType()) {
|
||||||
|
this.initNetIncomeDatePeriods();
|
||||||
|
}
|
||||||
|
if (this.query.isPreviousYearActive()) {
|
||||||
|
this.initNetIncomePreviousYear();
|
||||||
|
}
|
||||||
|
if (this.query.isPreviousPeriodActive()) {
|
||||||
|
this.initNetIncomePreviousPeriod();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.query.isPreviousYearActive() &&
|
||||||
|
this.query.isDatePeriodsColumnsType()
|
||||||
|
) {
|
||||||
|
this.initNetIncomePeriodsPreviewYear();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.query.isPreviousPeriodActive() &&
|
||||||
|
this.query.isDatePeriodsColumnsType()
|
||||||
|
) {
|
||||||
|
this.initNetIncomePeriodsPreviousPeriod();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public incomeAccounts: ModelObject<Account>[] = [];
|
||||||
|
public incomeAccountsIds: number[] = [];
|
||||||
|
public expenseAccounts: ModelObject<Account>[] = [];
|
||||||
|
public expenseAccountsIds: number[] = [];
|
||||||
|
|
||||||
|
public initIncomeAccounts = () => {
|
||||||
|
const incomeAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.INCOME) || [];
|
||||||
|
const incomeAccountsIds = incomeAccounts.map((a) => a.id);
|
||||||
|
this.incomeAccounts = incomeAccounts;
|
||||||
|
this.incomeAccountsIds = incomeAccountsIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
public initExpenseAccounts = () => {
|
||||||
|
const expenseAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.EXPENSE) || [];
|
||||||
|
const expenseAccountsIds = expenseAccounts.map((a) => a.id);
|
||||||
|
this.expenseAccounts = expenseAccounts;
|
||||||
|
this.expenseAccountsIds = expenseAccountsIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
public initIncomeTotalLedger = (): void => {
|
||||||
|
const incomeAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.INCOME) || [];
|
||||||
|
const incomeAccountsIds = incomeAccounts.map((a) => a.id);
|
||||||
|
this.incomeLedger = this.totalAccountsLedger.whereAccountsIds(incomeAccountsIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
public initExpensesTotalLedger = (): void => {
|
||||||
|
const expenseAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.EXPENSE) || [];
|
||||||
|
const expenseAccountsIds = expenseAccounts.map((a) => a.id);
|
||||||
|
this.expensesLedger = this.totalAccountsLedger.whereAccountsIds(expenseAccountsIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
public initNetIncomeDatePeriods = () => {
|
||||||
|
const incomeAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.INCOME) || [];
|
||||||
|
const incomeAccountsIds = incomeAccounts.map((a) => a.id);
|
||||||
|
const expenseAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.EXPENSE) || [];
|
||||||
|
const expenseAccountsIds = expenseAccounts.map((a) => a.id);
|
||||||
|
|
||||||
|
this.incomePeriodsAccountsLedger = this.periodsAccountsLedger.whereAccountsIds(incomeAccountsIds);
|
||||||
|
this.incomePeriodsOpeningAccountsLedger = this.periodsOpeningAccountLedger.whereAccountsIds(incomeAccountsIds);
|
||||||
|
this.expensesPeriodsAccountsLedger = this.periodsAccountsLedger.whereAccountsIds(expenseAccountsIds);
|
||||||
|
this.expensesOpeningAccountLedger = this.periodsOpeningAccountLedger.whereAccountsIds(expenseAccountsIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
public initNetIncomePreviousPeriod = () => {
|
||||||
|
const incomeAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.INCOME) || [];
|
||||||
|
const incomeAccountsIds = incomeAccounts.map((a) => a.id);
|
||||||
|
const expenseAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.EXPENSE) || [];
|
||||||
|
const expenseAccountsIds = expenseAccounts.map((a) => a.id);
|
||||||
|
|
||||||
|
this.incomePPAccountsLedger = this.PPTotalAccountsLedger.whereAccountsIds(incomeAccountsIds);
|
||||||
|
this.expensePPAccountsLedger = this.PPTotalAccountsLedger.whereAccountsIds(expenseAccountsIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
public initNetIncomePeriodsPreviousPeriod = () => {
|
||||||
|
const incomeAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.INCOME) || [];
|
||||||
|
const incomeAccountsIds = incomeAccounts.map((a) => a.id);
|
||||||
|
const expenseAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.EXPENSE) || [];
|
||||||
|
const expenseAccountsIds = expenseAccounts.map((a) => a.id);
|
||||||
|
|
||||||
|
this.incomePPPeriodsAccountsLedger = this.PPPeriodsAccountsLedger.whereAccountsIds(incomeAccountsIds);
|
||||||
|
this.incomePPPeriodsOpeningAccountLedger = this.PPPeriodsOpeningAccountLedger.whereAccountsIds(incomeAccountsIds);
|
||||||
|
this.expensePPPeriodsAccountsLedger = this.PPPeriodsAccountsLedger.whereAccountsIds(expenseAccountsIds);
|
||||||
|
this.expensePPPeriodsOpeningAccountLedger = this.PPPeriodsOpeningAccountLedger.whereAccountsIds(expenseAccountsIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
public initNetIncomePreviousYear = () => {
|
||||||
|
const incomeAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.INCOME) || [];
|
||||||
|
const incomeAccountsIds = incomeAccounts.map((a) => a.id);
|
||||||
|
const expenseAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.EXPENSE) || [];
|
||||||
|
const expenseAccountsIds = expenseAccounts.map((a) => a.id);
|
||||||
|
|
||||||
|
this.incomePYTotalAccountsLedger = this.PYTotalAccountsLedger.whereAccountsIds(incomeAccountsIds);
|
||||||
|
this.expensePYTotalAccountsLedger = this.PYTotalAccountsLedger.whereAccountsIds(expenseAccountsIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
public initNetIncomePeriodsPreviewYear = () => {
|
||||||
|
const incomeAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.INCOME) || [];
|
||||||
|
const incomeAccountsIds = incomeAccounts.map((a) => a.id);
|
||||||
|
const expenseAccounts = this.accountsByParentType.get(ACCOUNT_PARENT_TYPE.EXPENSE) || [];
|
||||||
|
const expenseAccountsIds = expenseAccounts.map((a) => a.id);
|
||||||
|
|
||||||
|
this.incomePYPeriodsAccountsLedger = this.PYPeriodsAccountsLedger.whereAccountsIds(incomeAccountsIds);
|
||||||
|
this.incomePYPeriodsOpeningAccountLedger = this.PYPeriodsOpeningAccountLedger.whereAccountsIds(incomeAccountsIds);
|
||||||
|
this.expensePYPeriodsAccountsLedger = this.PYPeriodsAccountsLedger.whereAccountsIds(expenseAccountsIds);
|
||||||
|
this.expensePYPeriodsOpeningAccountLedger = this.PYPeriodsOpeningAccountLedger.whereAccountsIds(expenseAccountsIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// # Helpers
|
||||||
|
// ----------------------------
|
||||||
|
|
||||||
|
private mapToLedgerTransactions(rows: CHTransactionRow[]): any[] {
|
||||||
|
return rows.map((row) => ({
|
||||||
|
accountId: row.account_id,
|
||||||
|
credit: Number(row.credit) || 0,
|
||||||
|
debit: Number(row.debit) || 0,
|
||||||
|
date: row.date,
|
||||||
|
account: {
|
||||||
|
accountNormal: this.accountNormalMap.get(row.account_id) || 'debit',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDate(date: Date | string): string {
|
||||||
|
const d = new Date(date);
|
||||||
|
return d.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getClickHouseDateFormat(groupType: string): string {
|
||||||
|
const formats: Record<string, string> = {
|
||||||
|
day: '%Y-%m-%d',
|
||||||
|
month: '%Y-%m',
|
||||||
|
year: '%Y',
|
||||||
|
};
|
||||||
|
return formats[groupType] || '%Y-%m';
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildBranchFilter(): string {
|
||||||
|
if (!isEmpty(this.query?.branchesIds)) {
|
||||||
|
const ids = this.query.branchesIds.map((id: number) => String(id)).join(',');
|
||||||
|
return `AND branch_id IN (${ids})`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getGroupByFromDisplayColumnsBy(columnsBy: string): IAccountTransactionsGroupBy {
|
||||||
|
const mapping: Record<string, IAccountTransactionsGroupBy> = {
|
||||||
|
week: IAccountTransactionsGroupBy.Day,
|
||||||
|
quarter: IAccountTransactionsGroupBy.Month,
|
||||||
|
year: IAccountTransactionsGroupBy.Year,
|
||||||
|
month: IAccountTransactionsGroupBy.Month,
|
||||||
|
day: IAccountTransactionsGroupBy.Day,
|
||||||
|
};
|
||||||
|
return mapping[columnsBy] || IAccountTransactionsGroupBy.Month;
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-4
@@ -3,7 +3,7 @@ import {
|
|||||||
IBalanceSheetDOO,
|
IBalanceSheetDOO,
|
||||||
IBalanceSheetQuery,
|
IBalanceSheetQuery,
|
||||||
} from './BalanceSheet.types';
|
} from './BalanceSheet.types';
|
||||||
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
import { BalanceSheetRepositoryFactory } from './BalanceSheetRepositoryFactory';
|
||||||
import { BalanceSheetMetaInjectable } from './BalanceSheetMeta';
|
import { BalanceSheetMetaInjectable } from './BalanceSheetMeta';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
@@ -20,7 +20,8 @@ export class BalanceSheetInjectable {
|
|||||||
private readonly eventPublisher: EventEmitter2,
|
private readonly eventPublisher: EventEmitter2,
|
||||||
private readonly tenancyContext: TenancyContext,
|
private readonly tenancyContext: TenancyContext,
|
||||||
private readonly i18n: I18nService,
|
private readonly i18n: I18nService,
|
||||||
private readonly balanceSheetRepository: BalanceSheetRepository,
|
@Inject(BalanceSheetRepositoryFactory)
|
||||||
|
private readonly repositoryFactory: BalanceSheetRepositoryFactory,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,7 +39,8 @@ export class BalanceSheetInjectable {
|
|||||||
const tenantMetadata = await this.tenancyContext.getTenantMetadata(true);
|
const tenantMetadata = await this.tenancyContext.getTenantMetadata(true);
|
||||||
|
|
||||||
// Loads all resources.
|
// Loads all resources.
|
||||||
await this.balanceSheetRepository.asyncInitialize(filter);
|
const repository = this.repositoryFactory.getRepository();
|
||||||
|
await repository.asyncInitialize(filter);
|
||||||
|
|
||||||
// Balance sheet meta first to get date format.
|
// Balance sheet meta first to get date format.
|
||||||
const meta = await this.balanceSheetMeta.meta(filter);
|
const meta = await this.balanceSheetMeta.meta(filter);
|
||||||
@@ -46,7 +48,7 @@ export class BalanceSheetInjectable {
|
|||||||
// Balance sheet report instance.
|
// Balance sheet report instance.
|
||||||
const balanceSheetInstanace = new BalanceSheet(
|
const balanceSheetInstanace = new BalanceSheet(
|
||||||
filter,
|
filter,
|
||||||
this.balanceSheetRepository,
|
repository,
|
||||||
this.i18n,
|
this.i18n,
|
||||||
{ baseCurrency: tenantMetadata.baseCurrency, dateFormat: meta.dateFormat },
|
{ baseCurrency: tenantMetadata.baseCurrency, dateFormat: meta.dateFormat },
|
||||||
);
|
);
|
||||||
|
|||||||
+17
@@ -57,6 +57,11 @@ export class BalanceSheetRepository extends R.compose(
|
|||||||
*/
|
*/
|
||||||
public accountsByType: any;
|
public accountsByType: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public accountsByParentType: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PY from date.
|
* PY from date.
|
||||||
* @param {Date}
|
* @param {Date}
|
||||||
@@ -97,6 +102,18 @@ export class BalanceSheetRepository extends R.compose(
|
|||||||
*/
|
*/
|
||||||
public expensesLedger: Ledger;
|
public expensesLedger: Ledger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Income accounts.
|
||||||
|
*/
|
||||||
|
public incomeAccounts: any;
|
||||||
|
public incomeAccountsIds: number[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expense accounts.
|
||||||
|
*/
|
||||||
|
public expenseAccounts: any;
|
||||||
|
public expenseAccountsIds: number[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transactions group type.
|
* Transactions group type.
|
||||||
* @param {IAccountTransactionsGroupBy}
|
* @param {IAccountTransactionsGroupBy}
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { IBalanceSheetRepository } from './IBalanceSheetRepository';
|
||||||
|
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||||
|
import { BalanceSheetClickHouseRepository } from './BalanceSheetClickHouseRepository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BalanceSheetRepositoryFactory {
|
||||||
|
constructor(
|
||||||
|
@Inject(BalanceSheetRepository)
|
||||||
|
private readonly mysqlRepository: BalanceSheetRepository,
|
||||||
|
@Inject(BalanceSheetClickHouseRepository)
|
||||||
|
private readonly clickHouseRepository: BalanceSheetClickHouseRepository,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the appropriate balance sheet repository based on configuration.
|
||||||
|
*/
|
||||||
|
getRepository(): IBalanceSheetRepository {
|
||||||
|
const enabled = this.configService.get<boolean>('clickhouse.enabled', false);
|
||||||
|
if (enabled) {
|
||||||
|
return this.clickHouseRepository;
|
||||||
|
}
|
||||||
|
return this.mysqlRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
|
||||||
|
import { ModelObject } from 'objection';
|
||||||
|
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||||
|
import { IBalanceSheetQuery } from './BalanceSheet.types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Balance sheet repository interface.
|
||||||
|
* Both MySQL and ClickHouse implementations must conform to this interface.
|
||||||
|
*/
|
||||||
|
export interface IBalanceSheetRepository {
|
||||||
|
// Query
|
||||||
|
query: any;
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
accounts: ModelObject<Account>[];
|
||||||
|
accountsGraph: any;
|
||||||
|
accountsByType: Map<string, ModelObject<Account>[]>;
|
||||||
|
accountsByParentType: Map<string, ModelObject<Account>[]>;
|
||||||
|
|
||||||
|
// Total closing ledger
|
||||||
|
totalAccountsLedger: ILedger;
|
||||||
|
|
||||||
|
// Income / Expense ledgers
|
||||||
|
incomeLedger: ILedger;
|
||||||
|
expensesLedger: ILedger;
|
||||||
|
|
||||||
|
// Date periods
|
||||||
|
periodsAccountsLedger: ILedger;
|
||||||
|
periodsOpeningAccountLedger: ILedger;
|
||||||
|
|
||||||
|
// Previous Year (PY)
|
||||||
|
PYTotalAccountsLedger: ILedger;
|
||||||
|
PYPeriodsAccountsLedger: ILedger;
|
||||||
|
PYPeriodsOpeningAccountLedger: ILedger;
|
||||||
|
|
||||||
|
// Previous Period (PP)
|
||||||
|
PPTotalAccountsLedger: ILedger;
|
||||||
|
PPPeriodsAccountsLedger: ILedger;
|
||||||
|
PPPeriodsOpeningAccountLedger: ILedger;
|
||||||
|
|
||||||
|
// Net Income - Date Periods
|
||||||
|
incomePeriodsAccountsLedger: ILedger;
|
||||||
|
incomePeriodsOpeningAccountsLedger: ILedger;
|
||||||
|
expensesPeriodsAccountsLedger: ILedger;
|
||||||
|
expensesOpeningAccountLedger: ILedger;
|
||||||
|
|
||||||
|
// Net Income - Previous Period
|
||||||
|
incomePPAccountsLedger: ILedger;
|
||||||
|
expensePPAccountsLedger: ILedger;
|
||||||
|
incomePPPeriodsAccountsLedger: ILedger;
|
||||||
|
incomePPPeriodsOpeningAccountLedger: ILedger;
|
||||||
|
expensePPPeriodsAccountsLedger: ILedger;
|
||||||
|
expensePPPeriodsOpeningAccountLedger: ILedger;
|
||||||
|
|
||||||
|
// Net Income - Previous Year
|
||||||
|
incomePYTotalAccountsLedger: ILedger;
|
||||||
|
expensePYTotalAccountsLedger: ILedger;
|
||||||
|
incomePYPeriodsAccountsLedger: ILedger;
|
||||||
|
incomePYPeriodsOpeningAccountLedger: ILedger;
|
||||||
|
expensePYPeriodsAccountsLedger: ILedger;
|
||||||
|
expensePYPeriodsOpeningAccountLedger: ILedger;
|
||||||
|
|
||||||
|
// Income / Expense account lists
|
||||||
|
incomeAccounts: ModelObject<Account>[];
|
||||||
|
incomeAccountsIds: number[];
|
||||||
|
expenseAccounts: ModelObject<Account>[];
|
||||||
|
expenseAccountsIds: number[];
|
||||||
|
|
||||||
|
// Transactions group type
|
||||||
|
transactionsGroupType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async initialize the repository with the given query.
|
||||||
|
*/
|
||||||
|
asyncInitialize(query: IBalanceSheetQuery): Promise<void>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user