UNPKG

@tomei/live-price

Version:

Tomei live-price Package

775 lines (719 loc) 24 kB
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; } } }