wire checkout pay to selected payment method

This commit is contained in:
2026-09-05 22:07:02 +02:00
parent 7222d0c9be
commit b728c34357
4 changed files with 45 additions and 17 deletions
@@ -41,7 +41,7 @@ export class StorefrontCheckoutController {
@Post('shop/checkout/pay') @Post('shop/checkout/pay')
@Throttle(throttleProfiles.checkoutPay) @Throttle(throttleProfiles.checkoutPay)
async pay(@Req() req: Request, @Res() res: Response, @Body() { captcha }: PayCheckoutDto): Promise<void> { async pay(@Req() req: Request, @Res() res: Response, @Body() { captcha, paymentMethod }: PayCheckoutDto): Promise<void> {
const sessionId = this.checkoutSessionCookieService.getSessionId(req, res); const sessionId = this.checkoutSessionCookieService.getSessionId(req, res);
if (sessionId) { if (sessionId) {
@@ -64,7 +64,7 @@ export class StorefrontCheckoutController {
const summary = await this.cartService.getCartSummary(cart, discountCodes); const summary = await this.cartService.getCartSummary(cart, discountCodes);
const session = await this.checkoutSessionService.createFromCartSummary(summary); const session = await this.checkoutSessionService.createFromCartSummary(summary, paymentMethod);
this.checkoutSessionCookieService.setSessionId(req, res, session.id); this.checkoutSessionCookieService.setSessionId(req, res, session.id);
@@ -1,10 +1,13 @@
import { IsNotEmpty, IsString, Length } from 'class-validator'; import { IsEnum, IsIn, IsNotEmpty, IsString, Length } from 'class-validator';
import { getAppConfig } from '../../../config'; import { getAppConfig, getShopSettingsConfig } from '../../../config';
import { PaymentMethod } from '../../payment/types/PaymentMethod';
const { const {
captcha: { length: captchaLength } captcha: { length: captchaLength }
} = getAppConfig(); } = getAppConfig();
const { enabledPaymentMethods } = getShopSettingsConfig();
export class PayCheckoutDto { export class PayCheckoutDto {
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
@@ -12,4 +15,9 @@ export class PayCheckoutDto {
message: `Captcha should be ${captchaLength} characters long` message: `Captcha should be ${captchaLength} characters long`
}) })
captcha: string; captcha: string;
@IsNotEmpty()
@IsEnum(PaymentMethod)
@IsIn(enabledPaymentMethods, { message: 'Select a valid payment method' })
paymentMethod: PaymentMethod;
} }
@@ -38,8 +38,6 @@ const buildSummary = (overrides: Partial<CookieCartSummary> = {}): CookieCartSum
discounts: [{ code: 'SAVE1', amount: 1, issueMessage: null }], discounts: [{ code: 'SAVE1', amount: 1, issueMessage: null }],
cartDiscountTotal: 1, cartDiscountTotal: 1,
cartTotalPrice: 9, cartTotalPrice: 9,
cartTotalXmr: '0.06000000',
fiatPerXmr: 150,
hasManualLines: false, hasManualLines: false,
hasAutoLines: true, hasAutoLines: true,
cartTotalIssueMessage: null, cartTotalIssueMessage: null,
@@ -115,15 +113,15 @@ describe('CheckoutSessionService', () => {
describe('createFromCartSummary', () => { describe('createFromCartSummary', () => {
it('rejects an empty cart', async () => { it('rejects an empty cart', async () => {
await expect(service.createFromCartSummary(buildSummary({ cartExtended: [] }))).rejects.toThrow( await expect(
new BadRequestException('Your cart is empty') service.createFromCartSummary(buildSummary({ cartExtended: [] }), PaymentMethod.Xmr)
); ).rejects.toThrow(new BadRequestException('Your cart is empty'));
}); });
it('rejects carts that still have unresolved issues', async () => { it('rejects carts that still have unresolved issues', async () => {
await expect(service.createFromCartSummary(buildSummary({ hasIssues: true }))).rejects.toThrow( await expect(
new BadRequestException('Resolve cart issues before paying') service.createFromCartSummary(buildSummary({ hasIssues: true }), PaymentMethod.Xmr)
); ).rejects.toThrow(new BadRequestException('Resolve cart issues before paying'));
}); });
it('creates a session, invoice, lines, and valid discounts from the cart summary', async () => { it('creates a session, invoice, lines, and valid discounts from the cart summary', async () => {
@@ -135,7 +133,7 @@ describe('CheckoutSessionService', () => {
] ]
}); });
const session = await service.createFromCartSummary(summary); const session = await service.createFromCartSummary(summary, PaymentMethod.Xmr);
expect(invoiceService.issueInvoice).toHaveBeenCalledWith({ expect(invoiceService.issueInvoice).toHaveBeenCalledWith({
paymentMethod: PaymentMethod.Xmr, paymentMethod: PaymentMethod.Xmr,
@@ -180,7 +178,8 @@ describe('CheckoutSessionService', () => {
await service.createFromCartSummary( await service.createFromCartSummary(
buildSummary({ buildSummary({
discounts: [{ code: 'BAD', amount: null, issueMessage: 'Invalid discount code' }] discounts: [{ code: 'BAD', amount: null, issueMessage: 'Invalid discount code' }]
}) }),
PaymentMethod.Xmr
); );
expect(discountRepo.create).not.toHaveBeenCalled(); expect(discountRepo.create).not.toHaveBeenCalled();
@@ -190,6 +189,17 @@ describe('CheckoutSessionService', () => {
}) })
); );
}); });
it('issues a bitcoin checkout invoice when that payment method is selected', async () => {
await service.createFromCartSummary(buildSummary(), PaymentMethod.Btc);
expect(invoiceService.issueInvoice).toHaveBeenCalledWith({
paymentMethod: PaymentMethod.Btc,
reason: InvoiceReason.Checkout,
contextId: 'session-uuid',
amountFiat: 9
});
});
}); });
describe('findById', () => { describe('findById', () => {
@@ -200,7 +210,14 @@ describe('CheckoutSessionService', () => {
await expect(service.findById('session-1')).resolves.toBe(session); await expect(service.findById('session-1')).resolves.toBe(session);
expect(sessionRepo.findOne).toHaveBeenCalledWith({ expect(sessionRepo.findOne).toHaveBeenCalledWith({
where: { id: 'session-1' }, where: { id: 'session-1' },
relations: ['lines', 'discounts', 'invoice', 'invoice.moneroDetails', 'invoice.payments'] relations: [
'lines',
'discounts',
'invoice',
'invoice.moneroDetails',
'invoice.btcDetails',
'invoice.payments'
]
}); });
}); });
}); });
@@ -30,7 +30,10 @@ export class CheckoutSessionService {
}); });
} }
async createFromCartSummary(summary: CookieCartSummary): Promise<CheckoutSession> { async createFromCartSummary(
summary: CookieCartSummary,
requestedPaymentMethod: PaymentMethod
): Promise<CheckoutSession> {
if (summary.cartExtended.length === 0) { if (summary.cartExtended.length === 0) {
throw new BadRequestException('Your cart is empty'); throw new BadRequestException('Your cart is empty');
} }
@@ -42,7 +45,7 @@ export class CheckoutSessionService {
const sessionId = randomUUID(); const sessionId = randomUUID();
const invoice = await this.invoiceService.issueInvoice({ const invoice = await this.invoiceService.issueInvoice({
paymentMethod: PaymentMethod.Xmr, paymentMethod: requestedPaymentMethod,
reason: InvoiceReason.Checkout, reason: InvoiceReason.Checkout,
contextId: sessionId, contextId: sessionId,
amountFiat: summary.cartTotalPrice amountFiat: summary.cartTotalPrice