init
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
import { Logger, ServiceUnavailableException } from '@nestjs/common';
|
||||
import type { ConfigService } from '@nestjs/config';
|
||||
import type { Repository } from 'typeorm';
|
||||
import type { MoneroWalletRpcClient } from '../../moneroWallet/services/MoneroWalletRpcClient';
|
||||
import type { XmrRateService } from '../../xmrRate/services/XmrRateService';
|
||||
import { Invoice } from '../entities/Invoice';
|
||||
import { InvoiceReason } from '../types/InvoiceReason';
|
||||
import { PaymentMethod } from '../types/PaymentMethod';
|
||||
import { InvoiceService } from './InvoiceService';
|
||||
|
||||
const confirmationTiers = [{ upToTotalFiat: 1000, minConfirmations: 1 }];
|
||||
|
||||
describe('InvoiceService', () => {
|
||||
let service: InvoiceService;
|
||||
let invoiceRepo: {
|
||||
create: jest.Mock;
|
||||
save: jest.Mock;
|
||||
};
|
||||
let configService: {
|
||||
get: jest.Mock;
|
||||
};
|
||||
let walletRpcClient: {
|
||||
createAddress: jest.Mock;
|
||||
};
|
||||
let xmrRateService: {
|
||||
getLiveFiatPerXmr: jest.Mock;
|
||||
};
|
||||
let errorLogSpy: jest.SpiedFunction<typeof Logger.prototype.error>;
|
||||
|
||||
beforeEach(() => {
|
||||
errorLogSpy = jest.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined);
|
||||
|
||||
invoiceRepo = {
|
||||
create: jest.fn(data => ({ id: 'invoice-1', ...data })),
|
||||
save: jest.fn(async (invoice: Invoice) => invoice)
|
||||
};
|
||||
|
||||
configService = {
|
||||
get: jest.fn((key: string) => {
|
||||
if (key === 'shopSettings') {
|
||||
return { shopFiatCurrency: 'USD' };
|
||||
}
|
||||
|
||||
if (key === 'shopSettings.monero') {
|
||||
return { confirmationTiers };
|
||||
}
|
||||
|
||||
if (key === 'order') {
|
||||
return { checkoutValidityMs: 3_600_000, shippingPaymentValidityMs: 7_200_000 };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})
|
||||
};
|
||||
|
||||
walletRpcClient = {
|
||||
createAddress: jest.fn().mockResolvedValue({
|
||||
address: '4MoneroPaymentAddressExample',
|
||||
address_index: 12
|
||||
})
|
||||
};
|
||||
|
||||
xmrRateService = {
|
||||
getLiveFiatPerXmr: jest.fn().mockReturnValue(150)
|
||||
};
|
||||
|
||||
service = new InvoiceService(
|
||||
invoiceRepo as unknown as Repository<Invoice>,
|
||||
configService as unknown as ConfigService,
|
||||
walletRpcClient as unknown as MoneroWalletRpcClient,
|
||||
xmrRateService as unknown as XmrRateService
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
errorLogSpy.mockRestore();
|
||||
});
|
||||
|
||||
const issueCheckoutInvoice = () =>
|
||||
service.issueInvoice({
|
||||
paymentMethod: PaymentMethod.Xmr,
|
||||
reason: InvoiceReason.Checkout,
|
||||
contextId: 'session-uuid',
|
||||
amountFiat: 15
|
||||
});
|
||||
|
||||
it('throws when the live XMR rate is unavailable for checkout invoices', async () => {
|
||||
xmrRateService.getLiveFiatPerXmr.mockReturnValue(null);
|
||||
|
||||
await expect(issueCheckoutInvoice()).rejects.toThrow(
|
||||
new ServiceUnavailableException("We can't show a price right now. Please try again in a few minutes.")
|
||||
);
|
||||
|
||||
expect(walletRpcClient.createAddress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws and logs when wallet address allocation fails for checkout invoices', async () => {
|
||||
walletRpcClient.createAddress.mockRejectedValue(new Error('rpc down'));
|
||||
|
||||
await expect(issueCheckoutInvoice()).rejects.toThrow(
|
||||
new ServiceUnavailableException("We can't take payments right now. Please try again in a few minutes.")
|
||||
);
|
||||
|
||||
expect(errorLogSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to allocate Monero payment address'));
|
||||
expect(invoiceRepo.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a checkout invoice with converted totals and monero details', async () => {
|
||||
const invoice = await issueCheckoutInvoice();
|
||||
|
||||
expect(walletRpcClient.createAddress).toHaveBeenCalledWith('checkout - session-uuid');
|
||||
expect(invoiceRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reason: InvoiceReason.Checkout,
|
||||
paymentMethod: PaymentMethod.Xmr,
|
||||
amountFiat: 15,
|
||||
fiatCurrency: 'USD',
|
||||
paymentAddress: '4MoneroPaymentAddressExample',
|
||||
expectedTotalAtomic: '100000000000',
|
||||
expiresAt: expect.any(Date),
|
||||
moneroDetails: {
|
||||
paymentAddressIndex: 12,
|
||||
fiatPerXmrAtCreation: 150,
|
||||
requiredConfirmations: 1
|
||||
}
|
||||
})
|
||||
);
|
||||
expect(invoiceRepo.save).toHaveBeenCalled();
|
||||
expect(invoice).toEqual(expect.objectContaining({ id: 'invoice-1', amountFiat: 15 }));
|
||||
});
|
||||
|
||||
it('uses a higher confirmation tier for larger checkout amounts', async () => {
|
||||
configService.get.mockImplementation((key: string) => {
|
||||
if (key === 'shopSettings') {
|
||||
return { shopFiatCurrency: 'USD' };
|
||||
}
|
||||
|
||||
if (key === 'shopSettings.monero') {
|
||||
return {
|
||||
confirmationTiers: [
|
||||
{ upToTotalFiat: 10, minConfirmations: 1 },
|
||||
{ upToTotalFiat: 100, minConfirmations: 5 },
|
||||
{ minConfirmations: 10 }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'order') {
|
||||
return { checkoutValidityMs: 3_600_000, shippingPaymentValidityMs: 7_200_000 };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await service.issueInvoice({
|
||||
paymentMethod: PaymentMethod.Xmr,
|
||||
reason: InvoiceReason.Checkout,
|
||||
contextId: 'session-large',
|
||||
amountFiat: 75
|
||||
});
|
||||
|
||||
expect(invoiceRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
moneroDetails: expect.objectContaining({
|
||||
requiredConfirmations: 5
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('uses shipping-specific messages and address labels for shipping invoices', async () => {
|
||||
xmrRateService.getLiveFiatPerXmr.mockReturnValue(null);
|
||||
|
||||
await expect(
|
||||
service.issueInvoice({
|
||||
paymentMethod: PaymentMethod.Xmr,
|
||||
reason: InvoiceReason.Shipping,
|
||||
contextId: 'order-1',
|
||||
amountFiat: 5
|
||||
})
|
||||
).rejects.toThrow(
|
||||
new ServiceUnavailableException(
|
||||
"We can't quote shipping in XMR right now. Please try again in a few minutes."
|
||||
)
|
||||
);
|
||||
|
||||
xmrRateService.getLiveFiatPerXmr.mockReturnValue(150);
|
||||
walletRpcClient.createAddress.mockRejectedValue(new Error('rpc down'));
|
||||
|
||||
await expect(
|
||||
service.issueInvoice({
|
||||
paymentMethod: PaymentMethod.Xmr,
|
||||
reason: InvoiceReason.Shipping,
|
||||
contextId: 'order-1',
|
||||
amountFiat: 5
|
||||
})
|
||||
).rejects.toThrow(
|
||||
new ServiceUnavailableException(
|
||||
"We can't take shipping payments right now. Please try again in a few minutes."
|
||||
)
|
||||
);
|
||||
|
||||
walletRpcClient.createAddress.mockResolvedValue({
|
||||
address: '4ShippingPaymentAddressExample',
|
||||
address_index: 3
|
||||
});
|
||||
|
||||
await service.issueInvoice({
|
||||
paymentMethod: PaymentMethod.Xmr,
|
||||
reason: InvoiceReason.Shipping,
|
||||
contextId: 'order-1',
|
||||
amountFiat: 5
|
||||
});
|
||||
|
||||
expect(walletRpcClient.createAddress).toHaveBeenCalledWith('order-shipping - order-1');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user