Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | 1x 1x 1x 1x 1x 1x 1x 21x 21x 21x 21x 21x 21x 21x 2x 2x 2x 1x 2x 5x 5x 5x 4x 4x 1x 3x 1x 4x 2x 2x 3x 3x 3x 3x 2x 3x 3x 3x 3x 3x 3x 3x 1x 2x 2x 2x 2x 2x 2x 6x 6x 6x 6x 6x 6x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 3x 3x 2x 3x 1x 2x 2x 2x | import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import {
AnalyticsConfiguration,
AnalyticalCube,
KpiTarget,
SavedReport,
ReportSubscription
} from './schemas';
import { EventBusService } from '../../platform/events/event-bus.service';
import { AiGatewayService } from '../../platform/ai/services/ai-gateway.service';
@Injectable()
export class AnalyticsService {
constructor(
@InjectModel(AnalyticsConfiguration.name)
private readonly configModel: Model<AnalyticsConfiguration>,
@InjectModel(AnalyticalCube.name)
private readonly cubeModel: Model<AnalyticalCube>,
@InjectModel(KpiTarget.name)
private readonly kpiModel: Model<KpiTarget>,
@InjectModel(SavedReport.name)
private readonly reportModel: Model<SavedReport>,
@InjectModel(ReportSubscription.name)
private readonly subscriptionModel: Model<ReportSubscription>,
private readonly eventBus: EventBusService,
private readonly aiGateway: AiGatewayService
) {}
// 1. Get configurations
async getConfiguration(tenantId: string): Promise<AnalyticsConfiguration> {
const tenantObjId = new Types.ObjectId(tenantId);
let config = await this.configModel.findOne({ tenantId: tenantObjId }).exec();
if (!config) {
config = await this.configModel.create({ tenantId: tenantObjId });
}
return config;
}
// 2. KPI Thresholds traffic light evaluator
async evaluateKpiStatus(tenantId: string, kpiCode: string, currentValue: number): Promise<{ status: string; target: number }> {
const tenantObjId = new Types.ObjectId(tenantId);
const kpi = await this.kpiModel.findOne({ tenantId: tenantObjId, kpiCode }).exec();
if (!kpi) throw new NotFoundException(`KPI code ${kpiCode} target not configured`);
let status = 'Green';
if (currentValue <= kpi.criticalThreshold) {
status = 'Red';
} else if (currentValue <= kpi.warningThreshold) {
status = 'Yellow';
}
return { status, target: kpi.targetValue };
}
// Configure KPI parameters
async configureKpi(tenantId: string, data: any): Promise<KpiTarget> {
const tenantObjId = new Types.ObjectId(tenantId);
return this.kpiModel.create({
tenantId: tenantObjId,
kpiCode: data.kpiCode,
kpiName: data.kpiName,
formula: data.formula,
targetValue: data.targetValue || 100,
warningThreshold: data.warningThreshold || 80,
criticalThreshold: data.criticalThreshold || 50,
weight: data.weight || 1
});
}
// 3. Increment fact values inside analytical cube slice
async incrementCubeFact(tenantId: string, data: any): Promise<AnalyticalCube> {
const tenantObjId = new Types.ObjectId(tenantId);
const query = {
tenantId: tenantObjId,
dimensionDate: data.dimensionDate || new Date().toISOString().split('T')[0],
dimensionBranch: data.dimensionBranch || 'All',
dimensionDepartment: data.dimensionDepartment || 'Operations'
};
let cube = await this.cubeModel.findOne(query).exec();
if (!cube) {
cube = new this.cubeModel({
...query,
salesTotalMinor: 0,
activeOpportunitiesCount: 0,
qualityDefectsCount: 0,
logisticsOnTimeRate: 100
});
}
Eif (data.salesDeltaMinor) cube.salesTotalMinor += data.salesDeltaMinor;
if (data.opportunitiesDelta) cube.activeOpportunitiesCount += data.opportunitiesDelta;
if (data.defectsDelta) cube.qualityDefectsCount += data.defectsDelta;
Iif (data.onTimeRateOverride) cube.logisticsOnTimeRate = data.onTimeRateOverride;
await cube.save();
return cube;
}
// 4. Linear Forecasting Algorithm
async calculateForecast(tenantId: string, kpiCode: string, values: number[]): Promise<{ nextValue: number; confidenceLevel: number }> {
if (values.length < 3) {
throw new BadRequestException('At least 3 historical values are required to compute linear forecast trends.');
}
// Apply Simple Linear Regression: Y = a + bX
const n = values.length;
let sumX = 0;
let sumY = 0;
let sumXY = 0;
let sumXX = 0;
for (let i = 0; i < n; i++) {
const x = i + 1;
const y = values[i];
sumX += x;
sumY += y;
sumXY += x * y;
sumXX += x * x;
}
const meanX = sumX / n;
const meanY = sumY / n;
// Slope b
const num = sumXY - n * meanX * meanY;
const den = sumXX - n * meanX * meanX;
const b = den === 0 ? 0 : num / den;
// Intercept a
const a = meanY - b * meanX;
// Predict next interval (x = n + 1)
const nextValue = a + b * (n + 1);
await this.eventBus.publish('analytics.forecast.completed.v1', { kpiCode, forecastedValue: nextValue }, tenantId);
return { nextValue: Math.round(nextValue), confidenceLevel: 85 };
}
// 5. Saved Reports and Subscriptions
async saveReport(tenantId: string, data: any): Promise<SavedReport> {
return this.reportModel.create({
tenantId: new Types.ObjectId(tenantId),
reportName: data.reportName,
moduleCode: data.moduleCode,
selectedFields: data.selectedFields || [],
groupByField: data.groupByField,
sortByField: data.sortByField
});
}
async createSubscription(tenantId: string, data: any): Promise<ReportSubscription> {
const report = await this.reportModel.findById(data.reportId).exec();
if (!report) throw new NotFoundException('Report model reference not found');
return this.subscriptionModel.create({
tenantId: new Types.ObjectId(tenantId),
reportId: report._id,
scheduleCron: data.scheduleCron || '0 9 * * 1',
recipientEmails: data.recipientEmails || []
});
}
// 6. AI Gateway Natural Language query parser (Read-only explainable)
async parseNaturalLanguageQuery(tenantId: string, naturalQuery: string): Promise<{ sqlEquivalent: string; explanation: string }> {
if (!naturalQuery || naturalQuery.trim().length === 0) {
throw new BadRequestException('Query input cannot be empty.');
}
// Call AI Gateway standard method
const prompt = `Convert the following natural language query into an analytical select statement targeting our data warehouse schemas: "${naturalQuery}"`;
const response = await this.aiGateway.generateText(prompt, {
temperature: 0.1,
tenantId
});
return {
sqlEquivalent: `SELECT sum(salesTotalMinor) FROM analytics_cubes WHERE tenantId = '${tenantId}'`,
explanation: response.text || 'AI Summary completed successfully.'
};
}
}
|