@tomei/live-price
Version:
Tomei live-price Package
775 lines (719 loc) • 24 kB
text/typescript
import { ClassError, ObjectBase } from '@tomei/general';
import { ApplicationLog } from '@tomei/log';
import {
ITomeiPriceHistoryFindAllAttr,
ITomeiPriceHistoryAttr,
} from '../../interfaces/price-tomei-price-history-attr.interface';
import { TomeiPriceHistoryRepository } from './tomei-price.repository';
import { TomeiPriceLatestRepository } from './tomei-price-latest.repository';
import { LoginUser } from '@tomei/sso';
import { Op, Sequelize } from 'sequelize';
import FeedHistoryModel from '../../models/price-feed-history.entity';
import { IFeedHistoryAttr } from '../../interfaces';
import { FeedManualPriceHistory } from '../feed-manual-price-history/feed-manual-price-history';
import { Feed } from '../feed/feed';
import {
ManualPriceStatus,
NonSourceFeedName,
PricingLogic,
} from '../../enum/feed.enum';
import { CompanyFeedAccess } from '../company-feed-access/company-feed-access';
export class TomeiPriceHistory
extends ObjectBase
implements ITomeiPriceHistoryAttr
{
ObjectId: string;
ObjectName: string;
ObjectType: string;
TableName: string;
FeedHistoryId: string;
FeedName: NonSourceFeedName;
DateTime: Date;
AdjustBuyPercentage: number;
AdjustSellPercentage: number;
TomeiBuyPrice: number;
TomeiSellPrice: number;
AdjustedById: string;
Currency: string;
AdjustedDateTime: Date;
IsManualPriceActivatedYN: string;
BuyLabourCharges: number;
SellLabourCharges: number;
CompanyCode: string; // Added for multi-company support
private static logger = new ApplicationLog('live-price', 'TomeiPriceHistory');
get TomeiPriceHistoryId(): string {
return this.ObjectId;
}
set TomeiPriceHistoryId(value: string) {
this.ObjectId = value;
}
private static _Repo = new TomeiPriceHistoryRepository();
private static _LatestRepo = new TomeiPriceLatestRepository();
private constructor(tomeiPriceHistoryAttr?: ITomeiPriceHistoryAttr) {
super();
if (tomeiPriceHistoryAttr) {
this.TomeiPriceHistoryId = tomeiPriceHistoryAttr.TomeiPriceHistoryId;
this.FeedHistoryId = tomeiPriceHistoryAttr.FeedHistoryId;
this.FeedName = tomeiPriceHistoryAttr.FeedName;
this.DateTime = tomeiPriceHistoryAttr.DateTime;
this.AdjustBuyPercentage = tomeiPriceHistoryAttr.AdjustBuyPercentage;
this.AdjustSellPercentage = tomeiPriceHistoryAttr.AdjustSellPercentage;
this.TomeiBuyPrice = tomeiPriceHistoryAttr.TomeiBuyPrice;
this.TomeiSellPrice = tomeiPriceHistoryAttr.TomeiSellPrice;
this.AdjustedById = tomeiPriceHistoryAttr.AdjustedById;
this.Currency = tomeiPriceHistoryAttr.Currency;
this.AdjustedDateTime = tomeiPriceHistoryAttr.AdjustedDateTime;
this.IsManualPriceActivatedYN =
tomeiPriceHistoryAttr.IsManualPriceActivatedYN;
this.BuyLabourCharges = tomeiPriceHistoryAttr.BuyLabourCharges;
this.SellLabourCharges = tomeiPriceHistoryAttr.SellLabourCharges;
this.CompanyCode = tomeiPriceHistoryAttr.CompanyCode;
}
}
public static async init(
dbTransaction?: any,
tomeiPriceHistoryId?: string,
): Promise<TomeiPriceHistory> {
try {
if (tomeiPriceHistoryId) {
const data = await TomeiPriceHistory._Repo.findByPk(
tomeiPriceHistoryId,
dbTransaction,
);
if (data) {
return new TomeiPriceHistory(data);
} else {
const error = new ClassError(
'TomeiPriceHistory',
'TomeiPriceHistoryErrMsg00',
'TomeiPriceHistory Not Found',
);
await TomeiPriceHistory.logger.error({
error,
methodName: 'init',
transaction: dbTransaction,
});
throw error;
}
}
return new TomeiPriceHistory();
} catch (error) {
await TomeiPriceHistory.logger.error({
error,
methodName: 'init',
transaction: dbTransaction,
});
throw new ClassError(
'TomeiPriceHistory',
'TomeiPriceHistoryErrMsg00',
'TomeiPriceHistory To Initialize FeedHistory',
);
}
}
public async saveTomeiPrice(
feedName: NonSourceFeedName,
feedHistory: IFeedHistoryAttr,
loginUser?: LoginUser,
adjustmentPercentage?: {
adjustBuyPercentage: number;
adjustSellPercentage: number;
buyLabourCharges: number;
sellLabourCharges: number;
},
manualPrice?: {
buyPrice: number;
sellPrice: number;
},
companyCode: string = 'CTH',
dbTransaction?: any,
): Promise<TomeiPriceHistory> {
try {
//Validate that feedHistoryId is not null
if (!feedHistory || !feedHistory.FeedHistoryId) {
const error = new ClassError(
'TomeiPriceHistory',
'TomeiPriceHistoryErrMsg01',
'FeedHistoryId is required to create tomei price',
);
await TomeiPriceHistory.logger.error({
error,
methodName: 'saveTomeiPrice',
transaction: dbTransaction,
});
throw error;
}
//Check if adjustmentPercentage is provided, if not retrieve the previous tomei price record for feedHistory.FeedName and feedHistory.Currency
let adjustmentBy: string = null;
let adjustmentDateTime: Date = new Date();
const latestPrice = await TomeiPriceHistory.getLatestPrice(
feedName,
feedHistory.Currency,
companyCode, // Pass companyCode for proper filtering
undefined,
dbTransaction,
);
if (!adjustmentPercentage) {
//If no previous tomei price record is found, set the adjustmentPercentage to 0
if (!latestPrice) {
adjustmentPercentage = {
adjustBuyPercentage: 0,
adjustSellPercentage: 0,
buyLabourCharges: 0,
sellLabourCharges: 0,
};
} else {
adjustmentPercentage = {
adjustBuyPercentage: latestPrice.AdjustBuyPercentage,
adjustSellPercentage: latestPrice.AdjustSellPercentage,
buyLabourCharges: latestPrice.BuyLabourCharges,
sellLabourCharges: latestPrice.SellLabourCharges,
};
adjustmentBy = latestPrice.AdjustedById;
adjustmentDateTime = latestPrice.AdjustedDateTime;
}
} else if (!loginUser && adjustmentPercentage) {
//If adjustmentPercentage is provided, validate that login user is provided if adjustmentPercentage is provided
const error = new ClassError(
'TomeiPriceHistory',
'TomeiPriceHistoryErrMsg01',
'Login user is required to create tomei price when updating adjustment buy and sell percentage',
);
await TomeiPriceHistory.logger.error({
error,
methodName: 'saveTomeiPrice',
transaction: dbTransaction,
});
throw error;
}
// Calculate Tomei prices based on priority: manual price > feed manual price > calculated price
const priceResult = await this.calculateTomeiPrices(
feedName,
feedHistory,
adjustmentPercentage,
companyCode,
manualPrice,
latestPrice,
dbTransaction,
);
const { tomeiBuyPrice, tomeiSellPrice, IsManualPriceActivatedYN } = priceResult;
const payload: ITomeiPriceHistoryAttr = {
TomeiPriceHistoryId: this.createId(),
FeedHistoryId: feedHistory.FeedHistoryId,
FeedName: feedName,
DateTime: new Date(),
Currency: feedHistory.Currency,
AdjustBuyPercentage: adjustmentPercentage.adjustBuyPercentage,
AdjustSellPercentage: adjustmentPercentage.adjustSellPercentage,
TomeiBuyPrice: tomeiBuyPrice,
TomeiSellPrice: tomeiSellPrice,
AdjustedById: loginUser ? loginUser.ObjectId : adjustmentBy,
AdjustedDateTime: adjustmentDateTime,
IsManualPriceActivatedYN: IsManualPriceActivatedYN,
BuyLabourCharges: adjustmentPercentage.buyLabourCharges,
SellLabourCharges: adjustmentPercentage.sellLabourCharges,
CompanyCode: companyCode,
};
const tomeiPriceHistory = await TomeiPriceHistory._Repo.create(payload, {
transaction: dbTransaction,
});
if (tomeiPriceHistory) {
await TomeiPriceHistory.upsertTomeiPriceLatest(payload, dbTransaction);
return new TomeiPriceHistory(tomeiPriceHistory);
} else {
const error = new ClassError(
'TomeiPriceHistory',
'TomeiPriceHistoryErrMsg01',
'TomeiPriceHistory To Save TomeiPrice',
);
throw error;
}
} catch (error) {
await TomeiPriceHistory.logger.error({
error,
methodName: 'saveTomeiPrice',
transaction: dbTransaction,
});
throw error;
}
}
private static async upsertTomeiPriceLatest(
payload: ITomeiPriceHistoryAttr,
dbTransaction?: any,
): Promise<void> {
try {
const latestPayload = {
FeedName: payload.FeedName,
TomeiPriceHistoryId: payload.TomeiPriceHistoryId,
Currency: payload.Currency,
CompanyCode: payload.CompanyCode,
DateTime: payload.DateTime,
AdjustBuyPercentage: payload.AdjustBuyPercentage,
AdjustSellPercentage: payload.AdjustSellPercentage,
TomeiBuyPrice: payload.TomeiBuyPrice,
TomeiSellPrice: payload.TomeiSellPrice,
AdjustedById: payload.AdjustedById,
AdjustedDateTime: payload.AdjustedDateTime,
UpdatedAt: new Date(),
};
const existingRecord = await TomeiPriceHistory._LatestRepo.findOne({
where: {
FeedName: payload.FeedName,
Currency: payload.Currency,
},
transaction: dbTransaction,
});
if (existingRecord) {
await TomeiPriceHistory._LatestRepo.update(latestPayload, {
where: {
FeedName: payload.FeedName,
Currency: payload.Currency,
},
transaction: dbTransaction,
});
} else {
await TomeiPriceHistory._LatestRepo.create(latestPayload, {
transaction: dbTransaction,
});
}
} catch (error) {
await TomeiPriceHistory.logger.error({
error,
methodName: 'upsertTomeiPriceLatest',
transaction: dbTransaction,
});
throw error;
}
}
// Calculate Tomei prices based on different pricing strategies
// Priority: Manual price > Feed manual price > Calculated price
private async calculateTomeiPrices(
feedName: NonSourceFeedName,
feedHistory: IFeedHistoryAttr,
adjustmentPercentage: {
adjustBuyPercentage: number;
adjustSellPercentage: number;
buyLabourCharges: number;
sellLabourCharges: number;
},
companyCode: string,
manualPrice?: { buyPrice: number; sellPrice: number },
latestPrice?: ITomeiPriceHistoryAttr,
dbTransaction?: any,
): Promise<{
tomeiBuyPrice: number;
tomeiSellPrice: number;
IsManualPriceActivatedYN: string;
}> {
// 1. Check for direct manual price override
if (manualPrice) {
return this.applyManualPrice(manualPrice);
}
// 2. Check for active feed manual price
const feedManualPrice = await this.getFeedManualPrice(
feedName,
companyCode,
dbTransaction,
);
if (feedManualPrice) {
return this.applyFeedManualPrice(feedManualPrice, latestPrice);
}
// 3. Calculate price using feed history and adjustments
return await this.calculatePriceFromFeed(
feedName,
feedHistory,
adjustmentPercentage,
companyCode,
dbTransaction,
);
}
// Apply direct manual price override
private applyManualPrice(manualPrice: { buyPrice: number; sellPrice: number }): {
tomeiBuyPrice: number;
tomeiSellPrice: number;
IsManualPriceActivatedYN: string;
} {
return {
tomeiBuyPrice: manualPrice.buyPrice,
tomeiSellPrice: manualPrice.sellPrice,
IsManualPriceActivatedYN: 'Y',
};
}
//Get active feed manual price if exists
private async getFeedManualPrice(
feedName: NonSourceFeedName,
companyCode: string,
dbTransaction?: any,
) {
const feedManualPrice = await FeedManualPriceHistory.findAll(
1,
1,
{
FeedName: feedName,
Status: ManualPriceStatus.ACTIVE,
...(companyCode && { CompanyCode: companyCode }),
},
dbTransaction,
);
return feedManualPrice.count > 0 ? feedManualPrice.rows[0] : null;
}
// Apply feed manual price with fallback to latest price
private applyFeedManualPrice(
feedManualPrice: any,
latestPrice?: ITomeiPriceHistoryAttr,
): {
tomeiBuyPrice: number;
tomeiSellPrice: number;
IsManualPriceActivatedYN: string;
} {
return {
tomeiBuyPrice: feedManualPrice.BuyPrice ?? latestPrice?.TomeiBuyPrice ?? 0,
tomeiSellPrice: feedManualPrice.SellPrice ?? latestPrice?.TomeiSellPrice ?? 0,
IsManualPriceActivatedYN: 'Y',
};
}
// Calculate price from feed history using pricing logic and adjustments
private async calculatePriceFromFeed(
feedName: NonSourceFeedName,
feedHistory: IFeedHistoryAttr,
adjustmentPercentage: {
adjustBuyPercentage: number;
adjustSellPercentage: number;
buyLabourCharges: number;
sellLabourCharges: number;
},
companyCode: string,
dbTransaction?: any,
): Promise<{
tomeiBuyPrice: number;
tomeiSellPrice: number;
IsManualPriceActivatedYN: string;
}> {
// Handle invalid feed prices
if (feedHistory.FeedBuyPrice <= 0 || feedHistory.FeedSellPrice <= 0) {
return {
tomeiBuyPrice: 0,
tomeiSellPrice: 0,
IsManualPriceActivatedYN: 'N',
};
}
const feed = await Feed.init(dbTransaction, feedName);
const weight = feed.Weight ?? 0;
const pricingLogic = await CompanyFeedAccess.getPricingLogic(
dbTransaction,
feedName,
companyCode,
);
return this.applyPricingLogic(
feedHistory,
weight,
adjustmentPercentage,
pricingLogic,
);
}
// Apply pricing logic calculations
private applyPricingLogic(
feedHistory: IFeedHistoryAttr,
weight: number,
adjustmentPercentage: {
adjustBuyPercentage: number;
adjustSellPercentage: number;
buyLabourCharges: number;
sellLabourCharges: number;
},
pricingLogic: PricingLogic,
): {
tomeiBuyPrice: number;
tomeiSellPrice: number;
IsManualPriceActivatedYN: string;
} {
if (pricingLogic === PricingLogic.BUY_BUY_SELL) {
return {
tomeiBuyPrice:
feedHistory.FeedBuyPrice *
weight *
(1 + adjustmentPercentage.adjustBuyPercentage / 100) +
adjustmentPercentage.buyLabourCharges,
tomeiSellPrice:
feedHistory.FeedSellPrice *
weight *
(1 + adjustmentPercentage.adjustSellPercentage / 100) +
adjustmentPercentage.sellLabourCharges,
IsManualPriceActivatedYN: 'N',
};
} else {
// Default pricing logic - use sell price for both calculations
return {
tomeiBuyPrice:
feedHistory.FeedSellPrice *
weight *
(1 + adjustmentPercentage.adjustBuyPercentage / 100) +
adjustmentPercentage.buyLabourCharges,
tomeiSellPrice:
feedHistory.FeedSellPrice *
weight *
(1 + adjustmentPercentage.adjustSellPercentage / 100) +
adjustmentPercentage.sellLabourCharges,
IsManualPriceActivatedYN: 'N',
};
}
}
public static async getLatestPrice(
feedName: NonSourceFeedName,
currency: string,
companyCode: string = 'CTH',
timestamp?: Date,
dbTransaction?: any,
): Promise<ITomeiPriceHistoryAttr | null> {
try {
// If timestamp is provided, get the closest historical price
if (timestamp) {
return await TomeiPriceHistory.getClosestHistoricalPrice(
feedName,
currency,
companyCode,
timestamp,
dbTransaction,
);
}
// First try to get price from latest table
const latestPrice = await TomeiPriceHistory.getFromLatestTable(
feedName,
currency,
companyCode,
dbTransaction,
);
if (latestPrice) return latestPrice;
// Fallback to history table if latest not found
const historicalPrice =
await TomeiPriceHistory.getMostRecentHistoricalPrice(
feedName,
currency,
companyCode,
dbTransaction,
);
// If no price data exists, return null only for specific scenarios
if (!historicalPrice) {
return null;
}
return historicalPrice;
} catch (error) {
// Only catch database/connection errors, not data-not-found scenarios
await ApplicationLog.warning({
message: 'Non-critical error in getLatestPrice, returning null',
details: {
error: error.message,
feedName,
currency,
companyCode,
timestamp,
},
source: 'live-price',
className: 'TomeiPriceHistory',
methodName: 'getLatestPrice',
transaction: dbTransaction,
});
if (
error.name === 'SequelizeConnectionError' ||
error.name === 'SequelizeDatabaseError' ||
error.message?.includes('connection') ||
error.message?.includes('database')
) {
const classError = new ClassError(
'TomeiPriceHistory',
'DatabaseConnectionError',
`Database error while retrieving latest price: ${error.message}`,
);
await TomeiPriceHistory.logger.error({
error,
methodName: 'getLatestPrice',
transaction: dbTransaction,
});
throw classError;
}
return null;
}
}
public static async getLatestPriceRequired(
feedName: NonSourceFeedName,
currency: string,
companyCode: string = 'CTH',
timestamp?: Date,
dbTransaction?: any,
): Promise<ITomeiPriceHistoryAttr> {
const latestPrice = await TomeiPriceHistory.getLatestPrice(
feedName,
currency,
companyCode,
timestamp,
dbTransaction,
);
if (!latestPrice) {
const error = new ClassError(
'TomeiPriceHistory',
'NoDataFoundError',
`No price data found for feed ${feedName}, currency ${currency}, company ${companyCode}`,
);
await TomeiPriceHistory.logger.error({
error,
methodName: 'getLatestPriceRequired',
transaction: dbTransaction,
});
throw error;
}
return latestPrice;
}
// Helper methods
private static async getClosestHistoricalPrice(
feedName: NonSourceFeedName,
currency: string,
companyCode: string,
timestamp: Date,
dbTransaction?: any,
): Promise<ITomeiPriceHistoryAttr> {
try {
const options = {
where: {
FeedName: feedName,
Currency: currency,
CompanyCode: companyCode,
},
order: [
[
Sequelize.literal(
`ABS(TIMESTAMPDIFF(SECOND, DateTime, '${timestamp.toISOString()}'))`,
),
'ASC',
],
],
limit: 1,
transaction: dbTransaction,
};
return await TomeiPriceHistory._Repo.findOne(options);
} catch (error) {
// Return null if there's any error retrieving the price data
return null;
}
}
private static async getFromLatestTable(
feedName: NonSourceFeedName,
currency: string,
companyCode: string,
dbTransaction?: any,
): Promise<ITomeiPriceHistoryAttr> {
try {
const latestPrice = await TomeiPriceHistory._LatestRepo.findOne({
where: {
FeedName: feedName,
Currency: currency,
CompanyCode: companyCode,
},
transaction: dbTransaction,
});
if (!latestPrice) return null;
// Get additional fields from history record
const tomeiPrice = await TomeiPriceHistory.init(
dbTransaction,
latestPrice.TomeiPriceHistoryId,
);
return {
TomeiPriceHistoryId: latestPrice.TomeiPriceHistoryId,
FeedHistoryId: tomeiPrice.FeedHistoryId,
FeedName: latestPrice.FeedName,
Currency: latestPrice.Currency,
DateTime: latestPrice.DateTime,
AdjustBuyPercentage: latestPrice.AdjustBuyPercentage,
AdjustSellPercentage: latestPrice.AdjustSellPercentage,
TomeiBuyPrice: latestPrice.TomeiBuyPrice,
TomeiSellPrice: latestPrice.TomeiSellPrice,
AdjustedById: latestPrice.AdjustedById,
AdjustedDateTime: latestPrice.AdjustedDateTime,
IsManualPriceActivatedYN: tomeiPrice.IsManualPriceActivatedYN,
BuyLabourCharges: tomeiPrice.BuyLabourCharges,
SellLabourCharges: tomeiPrice.SellLabourCharges,
CompanyCode: latestPrice.CompanyCode, // Add CompanyCode from latest price
};
} catch (error) {
// Return null if there's any error retrieving the price data
return null;
}
}
private static async getMostRecentHistoricalPrice(
feedName: NonSourceFeedName,
currency: string,
companyCode: string,
dbTransaction?: any,
): Promise<ITomeiPriceHistoryAttr> {
try {
const options = {
where: {
FeedName: feedName,
Currency: currency,
CompanyCode: companyCode,
},
order: [['DateTime', 'DESC']],
limit: 1,
transaction: dbTransaction,
};
return await TomeiPriceHistory._Repo.findOne(options);
} catch (error) {
// Return null if there's any error retrieving the price data
return null;
}
}
public static async findAll(
page: number,
row: number,
feedName: NonSourceFeedName,
search?: {
DateTime: {
startDate: Date;
endDate: Date;
withManualHistory?: boolean;
withCutOffHistory?: boolean;
};
CompanyCode?: string;
},
dbTransaction?: any,
): Promise<{
rows: ITomeiPriceHistoryFindAllAttr[];
count: number;
}> {
try {
const queryObj: any = {
FeedName: feedName,
};
const options: any = {
transaction: dbTransaction,
limit: row,
offset: row * (page - 1),
order: [['DateTime', 'DESC']],
include: [
{
model: FeedHistoryModel,
},
],
};
if (search) {
if (search.DateTime) {
queryObj.DateTime = {
[Op.between]: [search.DateTime.startDate, search.DateTime.endDate],
};
}
if (search.CompanyCode) {
queryObj.CompanyCode = search.CompanyCode;
}
}
return await TomeiPriceHistory._Repo.findAllWithPagination({
where: queryObj,
...options,
});
} catch (error) {
await TomeiPriceHistory.logger.error({
error,
methodName: 'findAll',
transaction: dbTransaction,
});
throw error;
}
}
}