Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { createMock } from '@golevelup/ts-jest';
import { NotificationService } from 'src/subdomains/supporting/notification/services/notification.service';
import { LiquidityManagementOrder } from '../entities/liquidity-management-order.entity';
import { LiquidityManagementPipeline } from '../entities/liquidity-management-pipeline.entity';
import { LiquidityManagementRule } from '../entities/liquidity-management-rule.entity';
import { LiquidityManagementPipelineStatus, LiquidityManagementRuleStatus, LiquidityOptimizationType } from '../enums';
import { LiquidityActionIntegrationFactory } from '../factories/liquidity-action-integration.factory';
import { LiquidityManagementOrderRepository } from '../repositories/liquidity-management-order.repository';
import { LiquidityManagementPipelineRepository } from '../repositories/liquidity-management-pipeline.repository';
import { LiquidityManagementRuleRepository } from '../repositories/liquidity-management-rule.repository';
import { LiquidityManagementPipelineService } from './liquidity-management-pipeline.service';
import { LiquidityManagementService } from './liquidity-management.service';

describe('LiquidityManagementPipelineService', () => {
let service: LiquidityManagementPipelineService;
let ruleRepo: LiquidityManagementRuleRepository;
let orderRepo: LiquidityManagementOrderRepository;
let pipelineRepo: LiquidityManagementPipelineRepository;
let actionIntegrationFactory: LiquidityActionIntegrationFactory;
let notificationService: NotificationService;
let liquidityManagementService: LiquidityManagementService;

beforeEach(() => {
ruleRepo = createMock<LiquidityManagementRuleRepository>();
orderRepo = createMock<LiquidityManagementOrderRepository>();
pipelineRepo = createMock<LiquidityManagementPipelineRepository>();
actionIntegrationFactory = createMock<LiquidityActionIntegrationFactory>();
notificationService = createMock<NotificationService>();
liquidityManagementService = createMock<LiquidityManagementService>();

service = new LiquidityManagementPipelineService(
ruleRepo,
orderRepo,
pipelineRepo,
actionIntegrationFactory,
notificationService,
liquidityManagementService,
);
});

describe('handlePipelineFail', () => {
it('resets the activation debounce timer when a rule is paused', async () => {
const rule = Object.assign(new LiquidityManagementRule(), {
id: 42,
status: LiquidityManagementRuleStatus.PROCESSING,
sendNotifications: false,
targetFiat: { name: 'EUR' },
});
const pipeline = Object.assign(new LiquidityManagementPipeline(), {
id: 1,
type: LiquidityOptimizationType.DEFICIT,
maxAmount: 100,
status: LiquidityManagementPipelineStatus.FAILED,
rule,
});
const order = Object.assign(new LiquidityManagementOrder(), { errorMessage: 'order failed' });

await service['handlePipelineFail'](pipeline, order);

expect(liquidityManagementService.resetActivation).toHaveBeenCalledWith(42);
expect(rule.status).toBe(LiquidityManagementRuleStatus.PAUSED);
expect(ruleRepo.save).toHaveBeenCalledWith(rule);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { LiquidityActionIntegrationFactory } from '../factories/liquidity-action
import { LiquidityManagementOrderRepository } from '../repositories/liquidity-management-order.repository';
import { LiquidityManagementPipelineRepository } from '../repositories/liquidity-management-pipeline.repository';
import { LiquidityManagementRuleRepository } from '../repositories/liquidity-management-rule.repository';
import { LiquidityManagementService } from './liquidity-management.service';

@Injectable()
export class LiquidityManagementPipelineService {
Expand All @@ -28,6 +29,7 @@ export class LiquidityManagementPipelineService {
private readonly pipelineRepo: LiquidityManagementPipelineRepository,
private readonly actionIntegrationFactory: LiquidityActionIntegrationFactory,
private readonly notificationService: NotificationService,
private readonly liquidityManagementService: LiquidityManagementService,
) {}

//*** JOBS ***//
Expand Down Expand Up @@ -264,6 +266,9 @@ export class LiquidityManagementPipelineService {
): Promise<void> {
const rule = pipeline.rule.pause();

// The rule is now paused; clear its activation-debounce timer so a later reactivation re-debounces.
this.liquidityManagementService.resetActivation(rule.id);

await this.ruleRepo.save(rule);

const [errorMessage, mailRequest] = this.generateFailMessage(pipeline, order);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { createMock } from '@golevelup/ts-jest';
import { ConfigService } from 'src/config/config';
import { SettingService } from 'src/shared/models/setting/setting.service';
import { Util } from 'src/shared/utils/util';
import { Price } from 'src/subdomains/supporting/pricing/domain/entities/price';
import { PricingService } from 'src/subdomains/supporting/pricing/services/pricing.service';
import { createDefaultLiquidityBalance } from '../__mocks__/liquidity-balance.entity.mock';
import { LiquidityManagementRule } from '../entities/liquidity-management-rule.entity';
import { LiquidityManagementRuleStatus, LiquidityOptimizationType } from '../enums';
import { LiquidityManagementPipelineRepository } from '../repositories/liquidity-management-pipeline.repository';
import { LiquidityManagementRuleRepository } from '../repositories/liquidity-management-rule.repository';
import { LiquidityManagementBalanceService } from './liquidity-management-balance.service';
import { LiquidityManagementService } from './liquidity-management.service';

describe('LiquidityManagementService', () => {
let service: LiquidityManagementService;
let ruleRepo: LiquidityManagementRuleRepository;
let pipelineRepo: LiquidityManagementPipelineRepository;
let balanceService: LiquidityManagementBalanceService;
let settingService: SettingService;
let pricingService: PricingService;
let executeRuleSpy: jest.SpyInstance;

beforeAll(() => {
new ConfigService(); // sets module-level Config (verifyRule reads Config.liquidityManagement)
});

beforeEach(() => {
ruleRepo = createMock<LiquidityManagementRuleRepository>();
pipelineRepo = createMock<LiquidityManagementPipelineRepository>();
balanceService = createMock<LiquidityManagementBalanceService>();
settingService = createMock<SettingService>();
pricingService = createMock<PricingService>();

service = new LiquidityManagementService(ruleRepo, pipelineRepo, balanceService, settingService, pricingService);

executeRuleSpy = jest.spyOn(service as any, 'executeRule').mockResolvedValue(undefined as any);
});

function createRule(partial: Partial<LiquidityManagementRule>): LiquidityManagementRule {
return Object.assign(new LiquidityManagementRule(), {
delayActivation: true,
optimal: 0,
...partial,
});
}

describe('verifyRule ruleActivations debounce invariant', () => {
it('keeps the activation timer across a drain chunk', async () => {
const rule = createRule({
id: 1,
status: LiquidityManagementRuleStatus.ACTIVE,
delayActivation: true,
});
const balance = createDefaultLiquidityBalance();

jest.spyOn(balanceService, 'findRelevantBalance').mockReturnValue(balance);
jest.spyOn(pricingService, 'getPrice').mockResolvedValue(Price.create('EUR', 'ASSET', 1));
jest.spyOn(rule, 'verify').mockReturnValue({
action: LiquidityOptimizationType.REDUNDANCY,
minAmount: 0,
maxAmount: 100,
});
jest.spyOn(balanceService, 'hasPendingOrders').mockResolvedValue(false);
jest.spyOn(settingService, 'get').mockResolvedValue('15');

service['ruleActivations'].set(rule.id, Util.minutesBefore(60));

await service['verifyRule'](rule, [balance]);

expect(executeRuleSpy).toHaveBeenCalledTimes(1);
expect(service['ruleActivations'].has(rule.id)).toBe(true);
});

it('keeps the activation timer while a rule is processing between chunks', async () => {
const rule = createRule({
id: 3,
status: LiquidityManagementRuleStatus.PROCESSING,
});

service['ruleActivations'].set(rule.id, new Date());

await service['verifyRule'](rule, []);

expect(service['ruleActivations'].has(rule.id)).toBe(true);
expect(executeRuleSpy).not.toHaveBeenCalled();
});
});

describe('resetActivation', () => {
it('clears the activation timer for the given rule id', () => {
const ruleId = 7;

service['ruleActivations'].set(ruleId, new Date());

service.resetActivation(ruleId);

expect(service['ruleActivations'].has(ruleId)).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ export class LiquidityManagementService {
return this.executeRule(rule, liquidityState, LiquidityOptimizationType.REDUNDANCY);
}

// Clears the activation-debounce timer for a rule. Called when a rule leaves the drain lifecycle
// (e.g. paused after a failed pipeline) so that a subsequent reactivation re-debounces from scratch.
resetActivation(ruleId: number): void {
this.ruleActivations.delete(ruleId);
}

//*** HELPER METHODS ***//

private async findRuleByAssetOrThrow(assetId: number): Promise<LiquidityManagementRule> {
Expand Down Expand Up @@ -144,8 +150,6 @@ export class LiquidityManagementService {
const requiredActivationTime = Util.minutesBefore(+delay);

if (!rule.delayActivation || this.ruleActivations.get(rule.id) < requiredActivationTime) {
this.ruleActivations.delete(rule.id);

await this.executeRule(rule, result);
}
} else {
Expand Down
Loading