const fs = require('fs'); const path = require('path'); const vm = require('vm'); const ROOT = path.resolve(__dirname, '..'); const read = (relativePath) => fs.readFileSync(path.join(ROOT, relativePath), 'utf8'); const precisionConfigSrc = read('App_Data/Config/swappriceprecision.js'); const precisionHelperSrc = read('wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js'); function loadConfiguredPrecision() { const context = { window: {} }; vm.runInNewContext(precisionConfigSrc, context); return context.window.main.swapPricePrecision; } function loadHelper(config) { const context = { window: { main: { swapPricePrecision: config } } }; vm.runInNewContext(precisionHelperSrc, context); return context.swapPricePrecision; } function activeViewSource(source) { return source.replace(/@\*[\s\S]*?\*@/g, ''); } describe('swap price precision common wiring', () => { test('configured and fallback common precision expose 2/2/4', () => { const configured = loadConfiguredPrecision(); expect(configured.common).toEqual({ amount: { precision: 2, grouping: true }, quantity: { integerDigits: 8, precision: 2, grouping: true }, rate: { precision: 4 } }); const helper = loadHelper(configured); expect(helper.getCommonPrecision('amount')).toBe(2); expect(helper.getCommonPrecision('quantity')).toBe(2); expect(helper.getCommonPrecision('rate')).toBe(4); const expectedQuantityPrecisions = { Stock: 2, StockIndex: 2, StockIF: 2, CommodityFutures: 2, CommoditySpot: 2, NewOtcStock: 2, HKStock: 2, HKStockIndex: 2, Fund: 4, Bond: 0, TBonds: 0, CreditBonds: 0, OtherBonds: 0, TBFutures: 2, OtherFutures: 2, GoldSpot: 2, OtherSpot: 2, AbroadFutures: 2, AbroadSpot: 2, AbroadStock: 2, AbroadStockIndex: 2, ExRate: 4, Shibor: 2, FixingRepoRate: 2, RateYield: 2, BondIndex: 2 }; Object.entries(expectedQuantityPrecisions).forEach(([instrumentType, precision]) => { expect(configured[instrumentType].quantityPrecision).toBe(precision); expect(helper.getCommonPrecision('quantity', instrumentType)).toBe(precision); expect(helper.getCommonInputFormat('quantity', { append: '' }, instrumentType).precision).toBe(precision); }); const expectedQuantityIntegerDigits = { Stock: 8, StockIndex: 8, StockIF: 8, CommodityFutures: 8, CommoditySpot: 8, NewOtcStock: 8, HKStock: 8, HKStockIndex: 8, Fund: 8, TBFutures: 8, OtherFutures: 8, GoldFutures: 8, GoldSpot: 8, OtherSpot: 8, AbroadFutures: 8, AbroadSpot: 8, AbroadStock: 8, AbroadStockIndex: 8, Shibor: 8, FixingRepoRate: 8, RateYield: 8, BondIndex: 8, Bond: 12, TBonds: 12, CreditBonds: 12, OtherBonds: 12, ExRate: 8 }; Object.entries(expectedQuantityIntegerDigits).forEach(([instrumentType, integerDigits]) => { expect(configured[instrumentType].quantityIntegerDigits).toBe(integerDigits); expect(helper.getCommonInputFormat('quantity', { append: '' }, instrumentType).integerDigits).toBe(integerDigits); }); expect(helper.getCommonPrecision('quantity', 'OtherRate')).toBe(2); expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Fund').precision).toBe(4); expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Fund').stringMode).toBe(true); expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Bond').precision).toBe(0); expect(helper.getCommonInputFormat('quantity', { append: '' }, 'OtherRate').integerDigits).toBe(8); const input = helper.getCommonInputFormat('amount', { append: '', trimTailZeros: true }); expect(input).toEqual(expect.objectContaining({ precision: 2, grouping: true, trimTailZeros: false })); expect(helper.getCommonInputFormat('rate', { append: '%' })).toEqual( expect.objectContaining({ precision: 4, grouping: false })); }); test('invalid common precision falls back and formatted values keep trailing zeros', () => { const helper = loadHelper({ common: { amount: { precision: -1 }, quantity: { precision: 14 }, rate: { precision: 'invalid' } } }); expect(helper.getCommonPrecision('amount')).toBe(2); expect(helper.getCommonPrecision('quantity')).toBe(2); expect(helper.getCommonPrecision('rate')).toBe(4); const configured = loadHelper({ common: { amount: { precision: 3 }, quantity: { precision: 1 }, rate: { precision: 2 } } }); expect(configured.formatCommon('amount', '12.5')).toBe('12.500'); expect(configured.formatCommon('quantity', 1)).toBe('1.0'); expect(configured.formatCommon('rate', '0.0123')).toBe('1.23%'); expect(configured.formatCommon('amount', '1234.5')).toBe('1,234.500'); expect(configured.normalizeCommon('amount', '1234.5')).toBe('1234.500'); expect(configured.formatCommon('amount', NaN)).toBe(''); expect(configured.formatCommon('amount', Infinity)).toBe(''); }); test('common grouping can be disabled independently from precision', () => { const helper = loadHelper({ common: { amount: { precision: 2, grouping: false }, quantity: { precision: 2, grouping: false }, rate: { precision: 4, grouping: false } } }); expect(helper.formatCommon('amount', '1234.5')).toBe('1234.50'); expect(helper.getCommonInputFormat('quantity', { append: '' }).grouping).toBe(false); }); test('quantity display can trim trailing zeros without changing configured precision', () => { const helper = loadHelper({ common: { quantity: { precision: 8, grouping: true } } }); expect(helper.formatCommon('quantity', '10000.12345600', { trimTailZeros: true })) .toBe('10,000.123456'); expect(helper.formatCommon('quantity', '10000.12345600')).toBe('10,000.12345600'); }); test('missing or invalid asset quantity precision falls back to common quantity precision', () => { const helper = loadHelper({ common: { quantity: { precision: 3, grouping: true } }, Stock: { quantityPrecision: 'invalid' }, Bond: {} }); expect(helper.getCommonPrecision('quantity', 'Stock')).toBe(3); expect(helper.getCommonPrecision('quantity', 'Bond')).toBe(3); expect(helper.getCommonPrecision('quantity', 'OtherRate')).toBe(3); expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Stock').integerDigits).toBe(8); expect(loadHelper({ common: { quantity: { integerDigits: 0, precision: 3 } }, Stock: { quantityIntegerDigits: 'invalid' } }).getCommonInputFormat('quantity', { append: '' }, 'Stock').integerDigits).toBe(8); }); test('SwapTrade2 common fields use the price helper without changing price responsibilities', () => { const scripts = { edit: read('wwwroot/Scripts/app/swaptrade/swapTradeEdit.js'), income: read('wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js'), unwind: read('wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js'), longShort: read('wwwroot/Scripts/app/swaptrade/swapLongShort.js'), unwindLongShort: read('wwwroot/Scripts/app/swaptrade/unwindLongShort.js'), view: read('wwwroot/Scripts/app/swaptrade/swapTradeView.js'), flow: read('wwwroot/Scripts/app/swaptrade/SwapflowList.js'), helper: precisionHelperSrc }; const view = read('Views/SwapTrade2/TradeView.cshtml'); const activeView = activeViewSource(view); for (const script of Object.values(scripts)) { expect(script).not.toMatch(/otcformat\.trading\.swap(?:Amount|Quantity|RateP)/); } expect(scripts.edit).toMatch(/getCommonInputFormat\('amount'/); expect(scripts.edit).toMatch(/getCommonInputFormat\(\s*['"]quantity['"]/); expect(scripts.edit).toMatch(/getCommonInputFormat\('rate'/); expect(scripts.view).toContain("swapPricePrecision.formatCommon('rate', item.Rate)"); expect(scripts.flow).toMatch(/name:\s*'ytm'[\s\S]*formatter:\s*otcformat\.trading\.marginRateP/); expect(scripts.flow).not.toMatch(/name:\s*'ytm'[\s\S]*formatCommon\('rate'/); expect(scripts.unwind).toContain('ClosePercent = otcformat.fixed6'); expect(scripts.unwind).toContain('_.round(tradingFee, 2)'); expect(scripts.unwind).toContain('.toFixed(2)'); expect(scripts.edit).toContain('observationRatePrecision = 12'); expect(scripts.edit).toMatch(/roundObservationRate[\s\S]*swapPricePrecision\.roundDecimal/); expect(scripts.helper).toContain('getRule: getRule'); expect(scripts.helper).toContain('getInputFormat: function'); expect(scripts.helper).toContain('roundForSubmit: function'); expect(scripts.helper).toContain('normalizeCommon: function'); expect(activeView).not.toContain('OtcFormatHelper'); expect(activeView).not.toMatch(/OtcFormatMoney|OtcFormatNotional/); expect(activeView).toContain('SwapCommonData'); expect(activeView).toMatch(/js-swap-common[\s\S]*data-kind="amount"/); expect(activeView).toMatch(/js-swap-common[\s\S]*data-kind="quantity"/); expect(activeView).toMatch(/js-swap-common[\s\S]*data-kind="rate"/); expect(scripts.view).toContain('formatSwapCommonElements'); expect(scripts.edit).toContain('getQuantityInputFormat'); expect(scripts.unwind).toContain('getQuantityInputFormat'); expect(scripts.flow).toContain('getQuantityInputFormat'); expect(scripts.flow).toMatch(/formatSwapQuantity = function \(value, options, rowObject\)/); expect(scripts.flow).toMatch(/row\.UnderlyingInstrumentType[\s\S]*row\.InstrumentType[\s\S]*row\.position[\s\S]*UnderlyingInstrumentType/); expect(scripts.flow).toContain("formatCommon('quantity', value, instrumentType)"); expect(activeView).toMatch(/data-kind="quantity"[^>]*data-instrument-type=/); }); });