Ensure bitcoin invoice metadata is available when rendering checkout sessions and customer order pages. Co-authored-by: Cursor <cursoragent@cursor.com>
113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
import { BadRequestException, Injectable } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { Repository } from 'typeorm';
|
|
import type { CookieCartSummary } from '../../storefrontCart/types/CookieCartSummary';
|
|
import { deriveCheckoutSessionState } from '../../../utils/checkout/deriveCheckoutSessionState';
|
|
import { InvoiceReason } from '../../payment/types/InvoiceReason';
|
|
import { InvoiceService } from '../../payment/services/InvoiceService';
|
|
import type { PaymentMethod } from '../../payment/types/PaymentMethod';
|
|
import { CheckoutSessionDiscount } from '../entities/CheckoutSessionDiscount';
|
|
import { CheckoutSessionLine } from '../entities/CheckoutSessionLine';
|
|
import { CheckoutSession } from '../entities/CheckoutSession';
|
|
|
|
@Injectable()
|
|
export class CheckoutSessionService {
|
|
constructor(
|
|
@InjectRepository(CheckoutSession)
|
|
private readonly sessionRepo: Repository<CheckoutSession>,
|
|
@InjectRepository(CheckoutSessionLine)
|
|
private readonly lineRepo: Repository<CheckoutSessionLine>,
|
|
@InjectRepository(CheckoutSessionDiscount)
|
|
private readonly discountRepo: Repository<CheckoutSessionDiscount>,
|
|
private readonly invoiceService: InvoiceService
|
|
) {}
|
|
|
|
async findById(id: string): Promise<CheckoutSession | null> {
|
|
return this.sessionRepo.findOne({
|
|
where: { id },
|
|
relations: [
|
|
'lines',
|
|
'discounts',
|
|
'invoice',
|
|
'invoice.moneroDetails',
|
|
'invoice.btcDetails',
|
|
'invoice.payments'
|
|
]
|
|
});
|
|
}
|
|
|
|
async createFromCartSummary(
|
|
summary: CookieCartSummary,
|
|
requestedPaymentMethod: PaymentMethod
|
|
): Promise<CheckoutSession> {
|
|
if (summary.cartExtended.length === 0) {
|
|
throw new BadRequestException('Your cart is empty');
|
|
}
|
|
|
|
if (summary.hasIssues) {
|
|
throw new BadRequestException('Resolve cart issues before paying');
|
|
}
|
|
|
|
const sessionId = randomUUID();
|
|
|
|
const invoice = await this.invoiceService.issueInvoice({
|
|
paymentMethod: requestedPaymentMethod,
|
|
reason: InvoiceReason.Checkout,
|
|
contextId: sessionId,
|
|
amountFiat: summary.cartTotalPrice
|
|
});
|
|
|
|
const lines = summary.cartExtended.map(line =>
|
|
this.lineRepo.create({
|
|
variantId: line.id,
|
|
productId: line.productId,
|
|
productTitle: line.productTitle,
|
|
variantTitle: line.title,
|
|
thumbnailUrl: line.thumbnailUrl,
|
|
qty: line.qty,
|
|
unitPriceFiat: line.price,
|
|
lineSubtotalFiat: line.lineSubtotal,
|
|
deliveryMode: line.deliveryMode
|
|
})
|
|
);
|
|
|
|
const discounts = summary.discounts
|
|
.filter(d => d.amount !== null && d.amount > 0 && !d.issueMessage)
|
|
.map(d =>
|
|
this.discountRepo.create({
|
|
code: d.code,
|
|
amountFiat: d.amount!
|
|
})
|
|
);
|
|
|
|
const session = this.sessionRepo.create({
|
|
id: sessionId,
|
|
invoice,
|
|
lines,
|
|
discounts
|
|
});
|
|
|
|
return this.sessionRepo.save(session);
|
|
}
|
|
|
|
async cancelSession(sessionId: string): Promise<void> {
|
|
const session = await this.sessionRepo.findOne({
|
|
where: { id: sessionId },
|
|
relations: ['invoice', 'invoice.moneroDetails', 'invoice.btcDetails', 'invoice.payments']
|
|
});
|
|
|
|
if (!session) {
|
|
return;
|
|
}
|
|
|
|
const { isOpenForPayment } = deriveCheckoutSessionState(session);
|
|
|
|
if (!isOpenForPayment) {
|
|
return;
|
|
}
|
|
|
|
await this.sessionRepo.update(session.id, { cancelledAt: new Date() });
|
|
}
|
|
}
|