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, @InjectRepository(CheckoutSessionLine) private readonly lineRepo: Repository, @InjectRepository(CheckoutSessionDiscount) private readonly discountRepo: Repository, private readonly invoiceService: InvoiceService ) {} async findById(id: string): Promise { return this.sessionRepo.findOne({ where: { id }, relations: [ 'lines', 'discounts', 'invoice', 'invoice.moneroDetails', 'invoice.btcDetails', 'invoice.payments' ] }); } async createFromCartSummary( summary: CookieCartSummary, requestedPaymentMethod: PaymentMethod ): Promise { 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 { 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() }); } }