Compare commits
5
Commits
b728c34357
...
c7ac66a5cd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7ac66a5cd | ||
|
|
500fe6cf94 | ||
|
|
917e86b3de | ||
|
|
cf92212226 | ||
|
|
ced92ead37 |
@@ -36,6 +36,7 @@ export class OrderCreationService {
|
|||||||
.leftJoinAndSelect('session.discounts', 'discount')
|
.leftJoinAndSelect('session.discounts', 'discount')
|
||||||
.leftJoinAndSelect('session.invoice', 'invoice')
|
.leftJoinAndSelect('session.invoice', 'invoice')
|
||||||
.leftJoinAndSelect('invoice.moneroDetails', 'moneroDetails')
|
.leftJoinAndSelect('invoice.moneroDetails', 'moneroDetails')
|
||||||
|
.leftJoinAndSelect('invoice.btcDetails', 'btcDetails')
|
||||||
.leftJoinAndSelect('invoice.payments', 'payment')
|
.leftJoinAndSelect('invoice.payments', 'payment')
|
||||||
.leftJoin('session.order', 'order')
|
.leftJoin('session.order', 'order')
|
||||||
.where('session.id = :sessionId', { sessionId })
|
.where('session.id = :sessionId', { sessionId })
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ describe('OrderService', () => {
|
|||||||
({
|
({
|
||||||
id: 'order-1',
|
id: 'order-1',
|
||||||
lines: [{ deliveryMode: DeliveryMode.Manual }],
|
lines: [{ deliveryMode: DeliveryMode.Manual }],
|
||||||
checkoutInvoice: { id: 'checkout-invoice-1', fiatCurrency: 'USD' }
|
checkoutInvoice: { id: 'checkout-invoice-1', fiatCurrency: 'USD', paymentMethod: PaymentMethod.Xmr }
|
||||||
}) as Order;
|
}) as Order;
|
||||||
|
|
||||||
it('throws when the order cannot be quoted', async () => {
|
it('throws when the order cannot be quoted', async () => {
|
||||||
@@ -300,6 +300,26 @@ describe('OrderService', () => {
|
|||||||
});
|
});
|
||||||
expect(result).toBe(orderExtended);
|
expect(result).toBe(orderExtended);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the checkout invoice payment method for shipping invoices', async () => {
|
||||||
|
orderRepo.findOne.mockResolvedValue({
|
||||||
|
...buildQuotableOrder(),
|
||||||
|
checkoutInvoice: {
|
||||||
|
id: 'checkout-invoice-1',
|
||||||
|
fiatCurrency: 'USD',
|
||||||
|
paymentMethod: PaymentMethod.Btc
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.setDeliveryCost('order-1', { deliveryCost: 12.5 });
|
||||||
|
|
||||||
|
expect(invoiceService.issueInvoice).toHaveBeenCalledWith({
|
||||||
|
paymentMethod: PaymentMethod.Btc,
|
||||||
|
reason: InvoiceReason.Shipping,
|
||||||
|
contextId: 'order-1',
|
||||||
|
amountFiat: 12.5
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fulfillManualLine', () => {
|
describe('fulfillManualLine', () => {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import type { InvoiceExtended } from '../../payment/types/InvoiceExtended';
|
|||||||
import type { InvoicePaymentExtended } from '../../payment/types/InvoicePaymentExtended';
|
import type { InvoicePaymentExtended } from '../../payment/types/InvoicePaymentExtended';
|
||||||
import { InvoiceReason } from '../../payment/types/InvoiceReason';
|
import { InvoiceReason } from '../../payment/types/InvoiceReason';
|
||||||
import { InvoiceService } from '../../payment/services/InvoiceService';
|
import { InvoiceService } from '../../payment/services/InvoiceService';
|
||||||
import { PaymentMethod } from '../../payment/types/PaymentMethod';
|
|
||||||
import { DeliveryMode } from '../../product/types/DeliveryMode';
|
import { DeliveryMode } from '../../product/types/DeliveryMode';
|
||||||
import { SetDeliveryCostDto } from '../dto/SetDeliveryCostDto';
|
import { SetDeliveryCostDto } from '../dto/SetDeliveryCostDto';
|
||||||
import type { ListOrdersQueryDto } from '../dto/ListOrdersQueryDto';
|
import type { ListOrdersQueryDto } from '../dto/ListOrdersQueryDto';
|
||||||
@@ -99,9 +98,11 @@ export class OrderService {
|
|||||||
'checkoutInvoice',
|
'checkoutInvoice',
|
||||||
'checkoutInvoice.payments',
|
'checkoutInvoice.payments',
|
||||||
'checkoutInvoice.moneroDetails',
|
'checkoutInvoice.moneroDetails',
|
||||||
|
'checkoutInvoice.btcDetails',
|
||||||
'shippingInvoice',
|
'shippingInvoice',
|
||||||
'shippingInvoice.payments',
|
'shippingInvoice.payments',
|
||||||
'shippingInvoice.moneroDetails',
|
'shippingInvoice.moneroDetails',
|
||||||
|
'shippingInvoice.btcDetails',
|
||||||
'lines',
|
'lines',
|
||||||
'lines.manualFulfillment',
|
'lines.manualFulfillment',
|
||||||
'discounts',
|
'discounts',
|
||||||
@@ -244,7 +245,7 @@ export class OrderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shippingInvoice = await this.invoiceService.issueInvoice({
|
const shippingInvoice = await this.invoiceService.issueInvoice({
|
||||||
paymentMethod: PaymentMethod.Xmr,
|
paymentMethod: order.checkoutInvoice.paymentMethod,
|
||||||
reason: InvoiceReason.Shipping,
|
reason: InvoiceReason.Shipping,
|
||||||
contextId: orderId,
|
contextId: orderId,
|
||||||
amountFiat: deliveryCost
|
amountFiat: deliveryCost
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export class CheckoutPaymentPollerService {
|
|||||||
.createQueryBuilder('session')
|
.createQueryBuilder('session')
|
||||||
.innerJoinAndSelect('session.invoice', 'invoice')
|
.innerJoinAndSelect('session.invoice', 'invoice')
|
||||||
.leftJoinAndSelect('invoice.moneroDetails', 'moneroDetails')
|
.leftJoinAndSelect('invoice.moneroDetails', 'moneroDetails')
|
||||||
|
.leftJoinAndSelect('invoice.btcDetails', 'btcDetails')
|
||||||
.leftJoinAndSelect('invoice.payments', 'payment')
|
.leftJoinAndSelect('invoice.payments', 'payment')
|
||||||
.leftJoin('session.order', 'order')
|
.leftJoin('session.order', 'order')
|
||||||
.where('session.cancelledAt IS NULL')
|
.where('session.cancelledAt IS NULL')
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { CookieCartSummary } from '../../storefrontCart/types/CookieCartSum
|
|||||||
import { deriveCheckoutSessionState } from '../../../utils/checkout/deriveCheckoutSessionState';
|
import { deriveCheckoutSessionState } from '../../../utils/checkout/deriveCheckoutSessionState';
|
||||||
import { InvoiceReason } from '../../payment/types/InvoiceReason';
|
import { InvoiceReason } from '../../payment/types/InvoiceReason';
|
||||||
import { InvoiceService } from '../../payment/services/InvoiceService';
|
import { InvoiceService } from '../../payment/services/InvoiceService';
|
||||||
import { PaymentMethod } from '../../payment/types/PaymentMethod';
|
import type { PaymentMethod } from '../../payment/types/PaymentMethod';
|
||||||
import { CheckoutSessionDiscount } from '../entities/CheckoutSessionDiscount';
|
import { CheckoutSessionDiscount } from '../entities/CheckoutSessionDiscount';
|
||||||
import { CheckoutSessionLine } from '../entities/CheckoutSessionLine';
|
import { CheckoutSessionLine } from '../entities/CheckoutSessionLine';
|
||||||
import { CheckoutSession } from '../entities/CheckoutSession';
|
import { CheckoutSession } from '../entities/CheckoutSession';
|
||||||
@@ -26,7 +26,14 @@ export class CheckoutSessionService {
|
|||||||
async findById(id: string): Promise<CheckoutSession | null> {
|
async findById(id: string): Promise<CheckoutSession | null> {
|
||||||
return this.sessionRepo.findOne({
|
return this.sessionRepo.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
relations: ['lines', 'discounts', 'invoice', 'invoice.moneroDetails', 'invoice.payments']
|
relations: [
|
||||||
|
'lines',
|
||||||
|
'discounts',
|
||||||
|
'invoice',
|
||||||
|
'invoice.moneroDetails',
|
||||||
|
'invoice.btcDetails',
|
||||||
|
'invoice.payments'
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +94,7 @@ export class CheckoutSessionService {
|
|||||||
async cancelSession(sessionId: string): Promise<void> {
|
async cancelSession(sessionId: string): Promise<void> {
|
||||||
const session = await this.sessionRepo.findOne({
|
const session = await this.sessionRepo.findOne({
|
||||||
where: { id: sessionId },
|
where: { id: sessionId },
|
||||||
relations: ['invoice', 'invoice.moneroDetails', 'invoice.payments']
|
relations: ['invoice', 'invoice.moneroDetails', 'invoice.btcDetails', 'invoice.payments']
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
:root {
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
--sf-font-sans:
|
--sf-font-sans:
|
||||||
'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
|
'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
|
||||||
--sf-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
--sf-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root:not([data-theme='light']) {
|
:root:not([data-theme='light']) {
|
||||||
|
color-scheme: dark;
|
||||||
--sf-bg: #0a0a0a;
|
--sf-bg: #0a0a0a;
|
||||||
--sf-surface: #141414;
|
--sf-surface: #141414;
|
||||||
--sf-surface-muted: #262727;
|
--sf-surface-muted: #262727;
|
||||||
@@ -85,6 +87,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme='dark'] {
|
:root[data-theme='dark'] {
|
||||||
|
color-scheme: dark;
|
||||||
--sf-bg: #0a0a0a;
|
--sf-bg: #0a0a0a;
|
||||||
--sf-surface: #141414;
|
--sf-surface: #141414;
|
||||||
--sf-surface-muted: #262727;
|
--sf-surface-muted: #262727;
|
||||||
@@ -117,6 +120,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme='light'] {
|
:root[data-theme='light'] {
|
||||||
|
color-scheme: light;
|
||||||
--sf-bg: #ffffff;
|
--sf-bg: #ffffff;
|
||||||
--sf-surface: #ffffff;
|
--sf-surface: #ffffff;
|
||||||
--sf-surface-muted: #f5f7fa;
|
--sf-surface-muted: #f5f7fa;
|
||||||
@@ -154,10 +158,6 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
|
||||||
color-scheme: light dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.sf-body {
|
body.sf-body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export class StorefrontOrderService {
|
|||||||
.createQueryBuilder('order')
|
.createQueryBuilder('order')
|
||||||
.leftJoinAndSelect('order.checkoutInvoice', 'checkoutInvoice')
|
.leftJoinAndSelect('order.checkoutInvoice', 'checkoutInvoice')
|
||||||
.leftJoinAndSelect('checkoutInvoice.moneroDetails', 'moneroDetails')
|
.leftJoinAndSelect('checkoutInvoice.moneroDetails', 'moneroDetails')
|
||||||
|
.leftJoinAndSelect('checkoutInvoice.btcDetails', 'btcDetails')
|
||||||
.leftJoinAndSelect('checkoutInvoice.payments', 'payment')
|
.leftJoinAndSelect('checkoutInvoice.payments', 'payment')
|
||||||
.where('order.id = :orderId', { orderId })
|
.where('order.id = :orderId', { orderId })
|
||||||
.getOne();
|
.getOne();
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import type Decimal from 'decimal.js';
|
||||||
|
|
||||||
|
export type CryptoAtomicConverter = (amountAtomic: string, rounding?: Decimal.Rounding) => string;
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
import { InternalServerErrorException } from '@nestjs/common';
|
import { InternalServerErrorException } from '@nestjs/common';
|
||||||
import { XMR_ATOMIC_PER_XMR } from '../../consts/xmrAtomicPerXmr';
|
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 { Invoice } from '../../modules/payment/entities/Invoice';
|
||||||
|
import type { InvoiceBtcDetails } from '../../modules/payment/entities/InvoiceBtcDetails';
|
||||||
import type { InvoiceMoneroDetails } from '../../modules/payment/entities/InvoiceMoneroDetails';
|
import type { InvoiceMoneroDetails } from '../../modules/payment/entities/InvoiceMoneroDetails';
|
||||||
import type { InvoicePayment } from '../../modules/payment/entities/InvoicePayment';
|
import type { InvoicePayment } from '../../modules/payment/entities/InvoicePayment';
|
||||||
import { PaymentMethod } from '../../modules/payment/types/PaymentMethod';
|
import { PaymentMethod } from '../../modules/payment/types/PaymentMethod';
|
||||||
import { InvoiceReason } from '../../modules/payment/types/InvoiceReason';
|
import { InvoiceReason } from '../../modules/payment/types/InvoiceReason';
|
||||||
|
import type { BtcDetailsOverrides, MoneroDetailsOverrides } from './types/ToStorefrontInvoiceViewTestTypes';
|
||||||
import * as formatRelativeTimeAgoModule from '../formatRelativeTimeAgo';
|
import * as formatRelativeTimeAgoModule from '../formatRelativeTimeAgo';
|
||||||
import * as generateQrCodeDataUrlModule from '../generateQrCodeDataUrl';
|
import * as generateQrCodeDataUrlModule from '../generateQrCodeDataUrl';
|
||||||
import { toStorefrontInvoiceView } from './toStorefrontInvoiceView';
|
import { toStorefrontInvoiceView } from './toStorefrontInvoiceView';
|
||||||
|
|
||||||
const oneXmrAtomic = XMR_ATOMIC_PER_XMR.toString();
|
const oneXmrAtomic = XMR_ATOMIC_PER_XMR.toString();
|
||||||
|
const oneBtcAtomic = BTC_ATOMIC_PER_BTC.toString();
|
||||||
const paymentAddress = '4StorefrontInvoiceViewTestAddress';
|
const paymentAddress = '4StorefrontInvoiceViewTestAddress';
|
||||||
|
|
||||||
const buildPayment = (overrides: Partial<InvoicePayment> = {}): InvoicePayment =>
|
const buildPayment = (overrides: Partial<InvoicePayment> = {}): InvoicePayment =>
|
||||||
@@ -21,10 +25,6 @@ const buildPayment = (overrides: Partial<InvoicePayment> = {}): InvoicePayment =
|
|||||||
...overrides
|
...overrides
|
||||||
}) as InvoicePayment;
|
}) as InvoicePayment;
|
||||||
|
|
||||||
type MoneroDetailsOverrides = Partial<
|
|
||||||
Pick<InvoiceMoneroDetails, 'paymentAddressIndex' | 'fiatPerXmrAtCreation' | 'requiredConfirmations'>
|
|
||||||
>;
|
|
||||||
|
|
||||||
const buildMoneroDetails = (overrides: MoneroDetailsOverrides = {}): InvoiceMoneroDetails =>
|
const buildMoneroDetails = (overrides: MoneroDetailsOverrides = {}): InvoiceMoneroDetails =>
|
||||||
({
|
({
|
||||||
paymentAddressIndex: 1,
|
paymentAddressIndex: 1,
|
||||||
@@ -33,6 +33,13 @@ const buildMoneroDetails = (overrides: MoneroDetailsOverrides = {}): InvoiceMone
|
|||||||
...overrides
|
...overrides
|
||||||
}) as InvoiceMoneroDetails;
|
}) as InvoiceMoneroDetails;
|
||||||
|
|
||||||
|
const buildBtcDetails = (overrides: BtcDetailsOverrides = {}): InvoiceBtcDetails =>
|
||||||
|
({
|
||||||
|
fiatPerBtcAtCreation: 60_000,
|
||||||
|
requiredConfirmations: 1,
|
||||||
|
...overrides
|
||||||
|
}) as InvoiceBtcDetails;
|
||||||
|
|
||||||
const buildInvoice = (overrides: Partial<Invoice> = {}): Invoice =>
|
const buildInvoice = (overrides: Partial<Invoice> = {}): Invoice =>
|
||||||
({
|
({
|
||||||
reason: InvoiceReason.Checkout,
|
reason: InvoiceReason.Checkout,
|
||||||
@@ -77,10 +84,21 @@ describe('toStorefrontInvoiceView', () => {
|
|||||||
await expect(toStorefrontInvoiceView(invoice)).rejects.toBeInstanceOf(InternalServerErrorException);
|
await expect(toStorefrontInvoiceView(invoice)).rejects.toBeInstanceOf(InternalServerErrorException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws for unsupported payment methods', async () => {
|
it('throws when bitcoin details are missing', async () => {
|
||||||
const invoice = buildInvoice({ paymentMethod: 'btc' as PaymentMethod });
|
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`);
|
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 () => {
|
it('formats expiry as seconds only when under one minute remains', async () => {
|
||||||
const invoice = buildInvoice({
|
const invoice = buildInvoice({
|
||||||
expiresAt: new Date('2026-06-01T12:00:45.000Z')
|
expiresAt: new Date('2026-06-01T12:00:45.000Z')
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import Decimal from 'decimal.js';
|
|||||||
import { InternalServerErrorException } from '@nestjs/common';
|
import { InternalServerErrorException } from '@nestjs/common';
|
||||||
import type { StorefrontInvoicePaymentView } from '../../modules/storefrontCore/types/StorefrontInvoicePaymentView';
|
import type { StorefrontInvoicePaymentView } from '../../modules/storefrontCore/types/StorefrontInvoicePaymentView';
|
||||||
import type { StorefrontInvoiceView } from '../../modules/storefrontCore/types/StorefrontInvoiceView';
|
import type { StorefrontInvoiceView } from '../../modules/storefrontCore/types/StorefrontInvoiceView';
|
||||||
|
import type { CryptoAtomicConverter } from '../../types/CryptoAtomicConverter';
|
||||||
import type { Invoice } from '../../modules/payment/entities/Invoice';
|
import type { Invoice } from '../../modules/payment/entities/Invoice';
|
||||||
import type { InvoicePayment } from '../../modules/payment/entities/InvoicePayment';
|
import type { InvoicePayment } from '../../modules/payment/entities/InvoicePayment';
|
||||||
import { PaymentMethod } from '../../modules/payment/types/PaymentMethod';
|
import { PaymentMethod } from '../../modules/payment/types/PaymentMethod';
|
||||||
import dayjs from '../../plugins/dayjs';
|
import dayjs from '../../plugins/dayjs';
|
||||||
import { generateQrCodeDataUrl } from '../generateQrCodeDataUrl';
|
import { generateQrCodeDataUrl } from '../generateQrCodeDataUrl';
|
||||||
|
import { convertBtcAtomicToBtc } from '../bitcoin/convertBtcAtomicToBtc';
|
||||||
import { convertXmrAtomicToXmr } from '../monero/convertXmrAtomicToXmr';
|
import { convertXmrAtomicToXmr } from '../monero/convertXmrAtomicToXmr';
|
||||||
import { subtractAtomic } from '../atomic/subtractAtomic';
|
import { subtractAtomic } from '../atomic/subtractAtomic';
|
||||||
|
import { paymentMethodLabel } from '../../consts/paymentMethodLabel';
|
||||||
import { deriveInvoiceState } from './deriveInvoiceState';
|
import { deriveInvoiceState } from './deriveInvoiceState';
|
||||||
import { formatInvoicePaymentConfirmationStatus } from './formatInvoicePaymentConfirmationStatus';
|
import { formatInvoicePaymentConfirmationStatus } from './formatInvoicePaymentConfirmationStatus';
|
||||||
import { resolveInvoiceStatusMessage } from './resolveInvoiceStatusMessage';
|
import { resolveInvoiceStatusMessage } from './resolveInvoiceStatusMessage';
|
||||||
@@ -19,17 +22,44 @@ import { sumInvoicePaymentAmountsAtomic } from './sumInvoicePaymentAmountsAtomic
|
|||||||
export const toStorefrontInvoiceView = async (invoice: Invoice): Promise<StorefrontInvoiceView> => {
|
export const toStorefrontInvoiceView = async (invoice: Invoice): Promise<StorefrontInvoiceView> => {
|
||||||
switch (invoice.paymentMethod) {
|
switch (invoice.paymentMethod) {
|
||||||
case PaymentMethod.Xmr:
|
case PaymentMethod.Xmr:
|
||||||
return toXmrInvoiceView(invoice);
|
if (!invoice.moneroDetails) {
|
||||||
|
throw new InternalServerErrorException('Invoice is missing Monero payment details');
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildStorefrontInvoiceView({
|
||||||
|
invoice,
|
||||||
|
paymentLabel: paymentMethodLabel[PaymentMethod.Xmr],
|
||||||
|
convertAtomicToCrypto: convertXmrAtomicToXmr,
|
||||||
|
buildQrCodePayload: (paymentAddress, amountCrypto) =>
|
||||||
|
`monero:${paymentAddress}?tx_amount=${amountCrypto}`
|
||||||
|
});
|
||||||
|
case PaymentMethod.Btc:
|
||||||
|
if (!invoice.btcDetails) {
|
||||||
|
throw new InternalServerErrorException('Invoice is missing Bitcoin payment details');
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildStorefrontInvoiceView({
|
||||||
|
invoice,
|
||||||
|
paymentLabel: paymentMethodLabel[PaymentMethod.Btc],
|
||||||
|
convertAtomicToCrypto: convertBtcAtomicToBtc,
|
||||||
|
buildQrCodePayload: (paymentAddress, amountCrypto) => `bitcoin:${paymentAddress}?amount=${amountCrypto}`
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
throw new InternalServerErrorException(`Unsupported payment method: ${String(invoice.paymentMethod)}`);
|
throw new InternalServerErrorException(`Unsupported payment method: ${String(invoice.paymentMethod)}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toXmrInvoiceView = async (invoice: Invoice): Promise<StorefrontInvoiceView> => {
|
const buildStorefrontInvoiceView = async ({
|
||||||
if (!invoice.moneroDetails) {
|
invoice,
|
||||||
throw new InternalServerErrorException('Invoice is missing Monero payment details');
|
paymentLabel,
|
||||||
}
|
convertAtomicToCrypto,
|
||||||
|
buildQrCodePayload
|
||||||
|
}: {
|
||||||
|
invoice: Invoice;
|
||||||
|
paymentLabel: string;
|
||||||
|
convertAtomicToCrypto: CryptoAtomicConverter;
|
||||||
|
buildQrCodePayload: (paymentAddress: string, amountCrypto: string) => string;
|
||||||
|
}): Promise<StorefrontInvoiceView> => {
|
||||||
const invoiceState = deriveInvoiceState(invoice);
|
const invoiceState = deriveInvoiceState(invoice);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -42,22 +72,20 @@ const toXmrInvoiceView = async (invoice: Invoice): Promise<StorefrontInvoiceView
|
|||||||
hasPendingConfirmations
|
hasPendingConfirmations
|
||||||
} = invoiceState;
|
} = invoiceState;
|
||||||
|
|
||||||
const cryptoCurrency = 'XMR';
|
const expectedTotalCrypto = convertAtomicToCrypto(invoice.expectedTotalAtomic);
|
||||||
const expectedTotalCrypto = convertXmrAtomicToXmr(invoice.expectedTotalAtomic);
|
|
||||||
const receivedAtomic = sumInvoicePaymentAmountsAtomic(invoice.payments);
|
const receivedAtomic = sumInvoicePaymentAmountsAtomic(invoice.payments);
|
||||||
const receivedTotalCrypto = receivedAtomic === '0' ? null : convertXmrAtomicToXmr(receivedAtomic);
|
const receivedTotalCrypto = receivedAtomic === '0' ? null : convertAtomicToCrypto(receivedAtomic);
|
||||||
|
|
||||||
const requiredConfirmations = resolveInvoiceRequiredConfirmations(invoice);
|
const requiredConfirmations = resolveInvoiceRequiredConfirmations(invoice);
|
||||||
|
|
||||||
const payments = [...(invoice.payments ?? [])]
|
const payments = [...(invoice.payments ?? [])]
|
||||||
.sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime())
|
.sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime())
|
||||||
.map(payment => toPaymentView(payment, requiredConfirmations));
|
.map(payment => toPaymentView(payment, requiredConfirmations, convertAtomicToCrypto));
|
||||||
|
|
||||||
const showPaymentCapture = (isAwaitingPayment || isUnderpaid) && !isExpired;
|
const showPaymentCapture = (isAwaitingPayment || isUnderpaid) && !isExpired;
|
||||||
|
|
||||||
const expiresInDuration = showPaymentCapture ? formatInvoiceExpiresInDuration(invoice.expiresAt) : null;
|
const expiresInDuration = showPaymentCapture ? formatInvoiceExpiresInDuration(invoice.expiresAt) : null;
|
||||||
|
|
||||||
let remainingTotalCrypto: string | null = null;
|
|
||||||
let instructionPrefix: string | null = null;
|
let instructionPrefix: string | null = null;
|
||||||
let instructionAmountCrypto: string | null = null;
|
let instructionAmountCrypto: string | null = null;
|
||||||
let instructionSuffix: string | null = null;
|
let instructionSuffix: string | null = null;
|
||||||
@@ -65,22 +93,20 @@ const toXmrInvoiceView = async (invoice: Invoice): Promise<StorefrontInvoiceView
|
|||||||
|
|
||||||
if (showPaymentCapture) {
|
if (showPaymentCapture) {
|
||||||
if (isAwaitingPayment) {
|
if (isAwaitingPayment) {
|
||||||
remainingTotalCrypto = expectedTotalCrypto;
|
|
||||||
instructionAmountCrypto = expectedTotalCrypto;
|
instructionAmountCrypto = expectedTotalCrypto;
|
||||||
instructionPrefix = 'Send exactly';
|
instructionPrefix = 'Send exactly';
|
||||||
instructionSuffix = 'to the address below.';
|
instructionSuffix = 'to the address below.';
|
||||||
} else {
|
} else {
|
||||||
const remainingTotalCryptoAtomic = subtractAtomic(invoice.expectedTotalAtomic, receivedAtomic);
|
const remainingTotalCryptoAtomic = subtractAtomic(invoice.expectedTotalAtomic, receivedAtomic);
|
||||||
|
|
||||||
remainingTotalCrypto = convertXmrAtomicToXmr(remainingTotalCryptoAtomic, Decimal.ROUND_CEIL);
|
instructionAmountCrypto = convertAtomicToCrypto(remainingTotalCryptoAtomic, Decimal.ROUND_CEIL);
|
||||||
instructionAmountCrypto = remainingTotalCrypto;
|
|
||||||
instructionPrefix = 'Send';
|
instructionPrefix = 'Send';
|
||||||
instructionSuffix = 'more to the same address below.';
|
instructionSuffix = 'more to the same address below.';
|
||||||
}
|
}
|
||||||
|
|
||||||
qrCodeUrl = await generateQrCodeDataUrl(
|
const qrCodePayload = buildQrCodePayload(invoice.paymentAddress, instructionAmountCrypto);
|
||||||
`monero:${invoice.paymentAddress}?tx_amount=${instructionAmountCrypto}`
|
|
||||||
);
|
qrCodeUrl = await generateQrCodeDataUrl(qrCodePayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusMessage = resolveInvoiceStatusMessage(invoiceState);
|
const statusMessage = resolveInvoiceStatusMessage(invoiceState);
|
||||||
@@ -88,7 +114,7 @@ const toXmrInvoiceView = async (invoice: Invoice): Promise<StorefrontInvoiceView
|
|||||||
const statusVariant = resolveInvoiceStatusVariant(invoiceState);
|
const statusVariant = resolveInvoiceStatusVariant(invoiceState);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cryptoCurrency,
|
cryptoCurrency: paymentLabel,
|
||||||
expectedTotalCrypto,
|
expectedTotalCrypto,
|
||||||
receivedTotalCrypto,
|
receivedTotalCrypto,
|
||||||
paymentAddress: invoice.paymentAddress,
|
paymentAddress: invoice.paymentAddress,
|
||||||
@@ -122,12 +148,16 @@ const toXmrInvoiceView = async (invoice: Invoice): Promise<StorefrontInvoiceView
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const toPaymentView = (payment: InvoicePayment, requiredConfirmations: number): StorefrontInvoicePaymentView => {
|
const toPaymentView = (
|
||||||
|
payment: InvoicePayment,
|
||||||
|
requiredConfirmations: number,
|
||||||
|
convertAtomicToCrypto: CryptoAtomicConverter
|
||||||
|
): StorefrontInvoicePaymentView => {
|
||||||
const isConfirmed = payment.confirmations >= requiredConfirmations;
|
const isConfirmed = payment.confirmations >= requiredConfirmations;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
txHash: payment.txHash,
|
txHash: payment.txHash,
|
||||||
amountCrypto: convertXmrAtomicToXmr(payment.amountAtomic),
|
amountCrypto: convertAtomicToCrypto(payment.amountAtomic),
|
||||||
confirmationStatus: formatInvoicePaymentConfirmationStatus({
|
confirmationStatus: formatInvoicePaymentConfirmationStatus({
|
||||||
confirmations: payment.confirmations,
|
confirmations: payment.confirmations,
|
||||||
requiredConfirmations,
|
requiredConfirmations,
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { InvoiceBtcDetails } from '../../../modules/payment/entities/InvoiceBtcDetails';
|
||||||
|
import type { InvoiceMoneroDetails } from '../../../modules/payment/entities/InvoiceMoneroDetails';
|
||||||
|
|
||||||
|
export type MoneroDetailsOverrides = Partial<
|
||||||
|
Pick<InvoiceMoneroDetails, 'paymentAddressIndex' | 'fiatPerXmrAtCreation' | 'requiredConfirmations'>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type BtcDetailsOverrides = Partial<Pick<InvoiceBtcDetails, 'fiatPerBtcAtCreation' | 'requiredConfirmations'>>;
|
||||||
@@ -11,9 +11,11 @@ export const createOrderDetailQuery = (orderRepo: Repository<Order>, orderId: st
|
|||||||
.leftJoinAndSelect('order.discounts', 'orderDiscount')
|
.leftJoinAndSelect('order.discounts', 'orderDiscount')
|
||||||
.leftJoinAndSelect('order.checkoutInvoice', 'checkoutInvoice')
|
.leftJoinAndSelect('order.checkoutInvoice', 'checkoutInvoice')
|
||||||
.leftJoinAndSelect('checkoutInvoice.moneroDetails', 'checkoutMoneroDetails')
|
.leftJoinAndSelect('checkoutInvoice.moneroDetails', 'checkoutMoneroDetails')
|
||||||
|
.leftJoinAndSelect('checkoutInvoice.btcDetails', 'checkoutBtcDetails')
|
||||||
.leftJoinAndSelect('checkoutInvoice.payments', 'checkoutPayment')
|
.leftJoinAndSelect('checkoutInvoice.payments', 'checkoutPayment')
|
||||||
.leftJoinAndSelect('order.shippingInvoice', 'shippingInvoice')
|
.leftJoinAndSelect('order.shippingInvoice', 'shippingInvoice')
|
||||||
.leftJoinAndSelect('shippingInvoice.moneroDetails', 'shippingMoneroDetails')
|
.leftJoinAndSelect('shippingInvoice.moneroDetails', 'shippingMoneroDetails')
|
||||||
|
.leftJoinAndSelect('shippingInvoice.btcDetails', 'shippingBtcDetails')
|
||||||
.leftJoinAndSelect('shippingInvoice.payments', 'shippingPayment')
|
.leftJoinAndSelect('shippingInvoice.payments', 'shippingPayment')
|
||||||
.leftJoinAndSelect('order.messages', 'message')
|
.leftJoinAndSelect('order.messages', 'message')
|
||||||
.addSelect('order.accessToken')
|
.addSelect('order.accessToken')
|
||||||
|
|||||||
Reference in New Issue
Block a user