diff --git a/backend/src/modules/storefrontCheckout/controllers/StorefrontCheckoutController.ts b/backend/src/modules/storefrontCheckout/controllers/StorefrontCheckoutController.ts index 0056244..f59d748 100644 --- a/backend/src/modules/storefrontCheckout/controllers/StorefrontCheckoutController.ts +++ b/backend/src/modules/storefrontCheckout/controllers/StorefrontCheckoutController.ts @@ -41,7 +41,7 @@ export class StorefrontCheckoutController { @Post('shop/checkout/pay') @Throttle(throttleProfiles.checkoutPay) - async pay(@Req() req: Request, @Res() res: Response, @Body() { captcha }: PayCheckoutDto): Promise { + async pay(@Req() req: Request, @Res() res: Response, @Body() { captcha, paymentMethod }: PayCheckoutDto): Promise { const sessionId = this.checkoutSessionCookieService.getSessionId(req, res); if (sessionId) { @@ -64,7 +64,7 @@ export class StorefrontCheckoutController { 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); diff --git a/backend/src/modules/storefrontCheckout/dto/PayCheckoutDto.ts b/backend/src/modules/storefrontCheckout/dto/PayCheckoutDto.ts index 265ce0a..0e612c5 100644 --- a/backend/src/modules/storefrontCheckout/dto/PayCheckoutDto.ts +++ b/backend/src/modules/storefrontCheckout/dto/PayCheckoutDto.ts @@ -1,10 +1,13 @@ -import { IsNotEmpty, IsString, Length } from 'class-validator'; -import { getAppConfig } from '../../../config'; +import { IsEnum, IsIn, IsNotEmpty, IsString, Length } from 'class-validator'; +import { getAppConfig, getShopSettingsConfig } from '../../../config'; +import { PaymentMethod } from '../../payment/types/PaymentMethod'; const { captcha: { length: captchaLength } } = getAppConfig(); +const { enabledPaymentMethods } = getShopSettingsConfig(); + export class PayCheckoutDto { @IsNotEmpty() @IsString() @@ -12,4 +15,9 @@ export class PayCheckoutDto { message: `Captcha should be ${captchaLength} characters long` }) captcha: string; + + @IsNotEmpty() + @IsEnum(PaymentMethod) + @IsIn(enabledPaymentMethods, { message: 'Select a valid payment method' }) + paymentMethod: PaymentMethod; } diff --git a/backend/src/modules/storefrontCheckout/services/CheckoutSessionService.spec.ts b/backend/src/modules/storefrontCheckout/services/CheckoutSessionService.spec.ts index 9c21e21..6093b07 100644 --- a/backend/src/modules/storefrontCheckout/services/CheckoutSessionService.spec.ts +++ b/backend/src/modules/storefrontCheckout/services/CheckoutSessionService.spec.ts @@ -38,8 +38,6 @@ const buildSummary = (overrides: Partial = {}): CookieCartSum discounts: [{ code: 'SAVE1', amount: 1, issueMessage: null }], cartDiscountTotal: 1, cartTotalPrice: 9, - cartTotalXmr: '0.06000000', - fiatPerXmr: 150, hasManualLines: false, hasAutoLines: true, cartTotalIssueMessage: null, @@ -115,15 +113,15 @@ describe('CheckoutSessionService', () => { describe('createFromCartSummary', () => { it('rejects an empty cart', async () => { - await expect(service.createFromCartSummary(buildSummary({ cartExtended: [] }))).rejects.toThrow( - new BadRequestException('Your cart is empty') - ); + await expect( + service.createFromCartSummary(buildSummary({ cartExtended: [] }), PaymentMethod.Xmr) + ).rejects.toThrow(new BadRequestException('Your cart is empty')); }); it('rejects carts that still have unresolved issues', async () => { - await expect(service.createFromCartSummary(buildSummary({ hasIssues: true }))).rejects.toThrow( - new BadRequestException('Resolve cart issues before paying') - ); + await expect( + 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 () => { @@ -135,7 +133,7 @@ describe('CheckoutSessionService', () => { ] }); - const session = await service.createFromCartSummary(summary); + const session = await service.createFromCartSummary(summary, PaymentMethod.Xmr); expect(invoiceService.issueInvoice).toHaveBeenCalledWith({ paymentMethod: PaymentMethod.Xmr, @@ -180,7 +178,8 @@ describe('CheckoutSessionService', () => { await service.createFromCartSummary( buildSummary({ discounts: [{ code: 'BAD', amount: null, issueMessage: 'Invalid discount code' }] - }) + }), + PaymentMethod.Xmr ); 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', () => { @@ -200,7 +210,14 @@ describe('CheckoutSessionService', () => { await expect(service.findById('session-1')).resolves.toBe(session); expect(sessionRepo.findOne).toHaveBeenCalledWith({ where: { id: 'session-1' }, - relations: ['lines', 'discounts', 'invoice', 'invoice.moneroDetails', 'invoice.payments'] + relations: [ + 'lines', + 'discounts', + 'invoice', + 'invoice.moneroDetails', + 'invoice.btcDetails', + 'invoice.payments' + ] }); }); }); diff --git a/backend/src/modules/storefrontCheckout/services/CheckoutSessionService.ts b/backend/src/modules/storefrontCheckout/services/CheckoutSessionService.ts index 2a4f983..72d47f5 100644 --- a/backend/src/modules/storefrontCheckout/services/CheckoutSessionService.ts +++ b/backend/src/modules/storefrontCheckout/services/CheckoutSessionService.ts @@ -30,7 +30,10 @@ export class CheckoutSessionService { }); } - async createFromCartSummary(summary: CookieCartSummary): Promise { + async createFromCartSummary( + summary: CookieCartSummary, + requestedPaymentMethod: PaymentMethod + ): Promise { if (summary.cartExtended.length === 0) { throw new BadRequestException('Your cart is empty'); } @@ -42,7 +45,7 @@ export class CheckoutSessionService { const sessionId = randomUUID(); const invoice = await this.invoiceService.issueInvoice({ - paymentMethod: PaymentMethod.Xmr, + paymentMethod: requestedPaymentMethod, reason: InvoiceReason.Checkout, contextId: sessionId, amountFiat: summary.cartTotalPrice