add bitcoin support to storefront invoice view
Refactor invoice view building to share logic across XMR and BTC, including BIP21 QR codes and payment detail validation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
import { InternalServerErrorException } from '@nestjs/common';
|
||||
import { XMR_ATOMIC_PER_XMR } from '../../consts/xmrAtomicPerXmr';
|
||||
import { BTC_ATOMIC_PER_BTC } from '../../consts/btcAtomicPerBtc';
|
||||
import type { Invoice } from '../../modules/payment/entities/Invoice';
|
||||
import type { InvoiceBtcDetails } from '../../modules/payment/entities/InvoiceBtcDetails';
|
||||
import type { InvoiceMoneroDetails } from '../../modules/payment/entities/InvoiceMoneroDetails';
|
||||
import type { InvoicePayment } from '../../modules/payment/entities/InvoicePayment';
|
||||
import { PaymentMethod } from '../../modules/payment/types/PaymentMethod';
|
||||
import { InvoiceReason } from '../../modules/payment/types/InvoiceReason';
|
||||
import type { BtcDetailsOverrides, MoneroDetailsOverrides } from './types/ToStorefrontInvoiceViewTestTypes';
|
||||
import * as formatRelativeTimeAgoModule from '../formatRelativeTimeAgo';
|
||||
import * as generateQrCodeDataUrlModule from '../generateQrCodeDataUrl';
|
||||
import { toStorefrontInvoiceView } from './toStorefrontInvoiceView';
|
||||
|
||||
const oneXmrAtomic = XMR_ATOMIC_PER_XMR.toString();
|
||||
const oneBtcAtomic = BTC_ATOMIC_PER_BTC.toString();
|
||||
const paymentAddress = '4StorefrontInvoiceViewTestAddress';
|
||||
|
||||
const buildPayment = (overrides: Partial<InvoicePayment> = {}): InvoicePayment =>
|
||||
@@ -21,10 +25,6 @@ const buildPayment = (overrides: Partial<InvoicePayment> = {}): InvoicePayment =
|
||||
...overrides
|
||||
}) as InvoicePayment;
|
||||
|
||||
type MoneroDetailsOverrides = Partial<
|
||||
Pick<InvoiceMoneroDetails, 'paymentAddressIndex' | 'fiatPerXmrAtCreation' | 'requiredConfirmations'>
|
||||
>;
|
||||
|
||||
const buildMoneroDetails = (overrides: MoneroDetailsOverrides = {}): InvoiceMoneroDetails =>
|
||||
({
|
||||
paymentAddressIndex: 1,
|
||||
@@ -33,6 +33,13 @@ const buildMoneroDetails = (overrides: MoneroDetailsOverrides = {}): InvoiceMone
|
||||
...overrides
|
||||
}) as InvoiceMoneroDetails;
|
||||
|
||||
const buildBtcDetails = (overrides: BtcDetailsOverrides = {}): InvoiceBtcDetails =>
|
||||
({
|
||||
fiatPerBtcAtCreation: 60_000,
|
||||
requiredConfirmations: 1,
|
||||
...overrides
|
||||
}) as InvoiceBtcDetails;
|
||||
|
||||
const buildInvoice = (overrides: Partial<Invoice> = {}): Invoice =>
|
||||
({
|
||||
reason: InvoiceReason.Checkout,
|
||||
@@ -77,10 +84,21 @@ describe('toStorefrontInvoiceView', () => {
|
||||
await expect(toStorefrontInvoiceView(invoice)).rejects.toBeInstanceOf(InternalServerErrorException);
|
||||
});
|
||||
|
||||
it('throws for unsupported payment methods', async () => {
|
||||
const invoice = buildInvoice({ paymentMethod: 'btc' as PaymentMethod });
|
||||
it('throws when bitcoin details are missing', async () => {
|
||||
const invoice = buildInvoice({
|
||||
paymentMethod: PaymentMethod.Btc,
|
||||
moneroDetails: null,
|
||||
btcDetails: null,
|
||||
expectedTotalAtomic: oneBtcAtomic
|
||||
});
|
||||
|
||||
await expect(toStorefrontInvoiceView(invoice)).rejects.toThrow('Unsupported payment method: btc');
|
||||
await expect(toStorefrontInvoiceView(invoice)).rejects.toThrow('Invoice is missing Bitcoin payment details');
|
||||
});
|
||||
|
||||
it('throws for unsupported payment methods', async () => {
|
||||
const invoice = buildInvoice({ paymentMethod: 'eth' as PaymentMethod, moneroDetails: null });
|
||||
|
||||
await expect(toStorefrontInvoiceView(invoice)).rejects.toThrow('Unsupported payment method: eth');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,6 +142,27 @@ describe('toStorefrontInvoiceView', () => {
|
||||
expect(generateQrCodeDataUrlSpy).toHaveBeenCalledWith(`monero:${paymentAddress}?tx_amount=1.00000000`);
|
||||
});
|
||||
|
||||
it('maps bitcoin invoices and builds a bip21 qr code', async () => {
|
||||
const invoice = buildInvoice({
|
||||
paymentMethod: PaymentMethod.Btc,
|
||||
moneroDetails: null,
|
||||
btcDetails: buildBtcDetails(),
|
||||
expectedTotalAtomic: oneBtcAtomic,
|
||||
paymentAddress: 'bc1qstorefronttest'
|
||||
});
|
||||
|
||||
const view = await toStorefrontInvoiceView(invoice);
|
||||
|
||||
expect(view).toMatchObject({
|
||||
cryptoCurrency: 'BTC',
|
||||
expectedTotalCrypto: '1.00000000',
|
||||
paymentAddress: 'bc1qstorefronttest'
|
||||
});
|
||||
expect(generateQrCodeDataUrlSpy).toHaveBeenCalledWith(
|
||||
'bitcoin:bc1qstorefronttest?amount=1.00000000'
|
||||
);
|
||||
});
|
||||
|
||||
it('formats expiry as seconds only when under one minute remains', async () => {
|
||||
const invoice = buildInvoice({
|
||||
expiresAt: new Date('2026-06-01T12:00:45.000Z')
|
||||
|
||||
Reference in New Issue
Block a user