Slave/btc integration #4

Merged
nobswebdev merged 47 commits from slave/btc-integration into master 2026-09-07 11:11:10 +00:00
4 changed files with 45 additions and 17 deletions
Showing only changes of commit b728c34357 - Show all commits
@@ -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<void> {
async pay(@Req() req: Request, @Res() res: Response, @Body() { captcha, paymentMethod }: PayCheckoutDto): Promise<void> {
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);
@@ -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;
}
@@ -38,8 +38,6 @@ const buildSummary = (overrides: Partial<CookieCartSummary> = {}): 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'
]
});
});
});
@@ -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) {
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