Skip to content

Instantly share code, notes, and snippets.

@Dozorengel
Created March 13, 2022 03:32
Show Gist options
  • Save Dozorengel/ed704f29dab16bdfbfd0b62391025fcd to your computer and use it in GitHub Desktop.
Save Dozorengel/ed704f29dab16bdfbfd0b62391025fcd to your computer and use it in GitHub Desktop.
Payment integration with YooKassa API, including single and recurrent payments (NestJS)
import {HttpService, Injectable} from '@nestjs/common';
import {InjectRepository} from '@nestjs/typeorm';
import {ConfigService} from '@nestjs/config';
import {Repository} from 'typeorm';
import {map} from 'rxjs/operators';
import * as hmacSHA512 from 'crypto-js/hmac-sha512';
import * as hex from 'crypto-js/enc-hex';
import {
PaymentAmount,
PaymentData,
PaymentDataRecurrent,
PaymentHookRequest,
PaymentReceipt,
PaymentReceiptItem,
PaymentResponse,
PaymentStatus,
PaymentStatusDto,
} from '@lib/data-transfer-objects';
import {Payment, PaymentItem} from './entities';
import {CreateAutoPaymentRequest, CreatePaymentRequest, CreatePaymentResponse, OrderProduct} from './payment.interface';
const StatusMap = {
[PaymentStatus.PENDING]: PaymentStatusDto.PENDING,
[PaymentStatus.CANCELED]: PaymentStatusDto.FAILED,
[PaymentStatus.SUCCEEDED]: PaymentStatusDto.PAID,
[PaymentStatus.WAITING_FOR_CAPTURE]: PaymentStatusDto.PENDING,
};
@Injectable()
export class PaymentService {
private config: {
HOST?: string;
PORT?: string;
NOTIFICATION_URL?: string;
RETURN_URL?: string;
TOKEN?: string;
HMAC_KEY?: string;
} = {};
constructor(
private httpService: HttpService,
@InjectRepository(Payment)
private paymentRepository: Repository<Payment>,
@InjectRepository(PaymentItem)
private paymentItemRepository: Repository<PaymentItem>,
private configService: ConfigService
) {
this.config.HOST = this.configService.get<string>('payment.host');
this.config.PORT = this.configService.get<string>('payment.port');
this.config.NOTIFICATION_URL = this.configService.get<string>('payment.notification_url');
this.config.RETURN_URL = this.configService.get<string>('payment.return_url');
this.config.TOKEN = this.configService.get<string>('payment.token');
this.config.HMAC_KEY = this.configService.get<string>('payment.hmac_key');
}
async createPayment(createPaymentRequest: CreatePaymentRequest): Promise<CreatePaymentResponse> {
const {orderProducts, email, shopAccountId, isAutoPay = false} = createPaymentRequest;
const payment = await this.makePayment(email, orderProducts);
const paymentData: PaymentData = {
acquiring_bank_account_id: shopAccountId,
amount: this.makeAmount(payment.amount),
confirmation: {
type: 'redirect',
return_url: this.generateReturnUrl(payment),
},
description: 'Заказ на сайте',
notification_url: this.config.NOTIFICATION_URL,
receipt: await this.makeReceipt(payment.email, payment.paymentItems, orderProducts),
};
if (isAutoPay) {
paymentData.auto = true;
}
const response = await this.sendPaymentRequest(paymentData);
payment.payment_external_id = response.id;
payment.status = PaymentStatusDto.PENDING;
await this.paymentRepository.save(payment);
return {
id: payment.id,
email: payment.email,
status: payment.status,
confirmation: response.confirmation,
};
}
async createAutoPayment(createAutoPaymentRequest: CreateAutoPaymentRequest): Promise<void> {
const {orderProducts, email, paymentSubscription, shopAccountId} = createAutoPaymentRequest;
const payment = await this.makePayment(email, orderProducts);
const paymentData: PaymentDataRecurrent = {
acquiring_bank_account_id: shopAccountId,
notification_url: this.config.NOTIFICATION_URL,
auto_payment_id: paymentSubscription.recurrentPaymentExternalId,
};
const response = await this.sendPaymentRequest(paymentData);
payment.payment_external_id = response.id;
payment.status = PaymentStatusDto.PENDING;
await this.paymentRepository.save(payment);
}
async confirmPayment(request: PaymentHookRequest): Promise<Payment> {
const payment = await this.paymentRepository.findOneOrFail(
{payment_external_id: request.order_id},
{relations: ['paymentItems']}
);
payment.status = StatusMap[request.status];
return this.paymentRepository.save(payment);
}
async failedProcessPayment(payment: Payment): Promise<Payment> {
payment.status = PaymentStatusDto.FAILED_IN_PROCESSING;
return payment.save();
}
async processedPayment(payment: Payment): Promise<Payment> {
payment.status = PaymentStatusDto.PROCESSED;
return payment.save();
}
private generateReturnUrl(payment: Payment) {
const type = payment.paymentItems[0].product_type;
return `${this.config.RETURN_URL}?orderId=${payment.id}&type=${type}`;
}
private async makePayment(email: string, orderProducts: OrderProduct[]): Promise<Payment> {
return await this.paymentRepository.manager.connection.transaction(async (manager) => {
let payment = this.paymentRepository.create({email, amount: 0, status: PaymentStatusDto.IN_PAY});
payment = await manager.save(payment);
const paymentItems: PaymentItem[] = [];
for (const orderProduct of orderProducts) {
const paymentItem = this.paymentItemRepository.create({
payment_id: payment.id,
product_type: orderProduct.productType,
product_id: orderProduct.product.id,
amount: orderProduct.product.price * orderProduct.count * 100,
quantity: orderProduct.count,
});
paymentItems.push(paymentItem);
payment.amount += paymentItem.amount;
await manager.save(paymentItem);
}
payment.paymentItems = paymentItems;
return await manager.save(payment);
});
}
private makeAmount(amount: number): PaymentAmount {
return {
value: (amount / 100).toFixed(2),
currency: 'RUB',
};
}
private async makeReceipt(
email: string,
paymentItems: PaymentItem[],
orderProducts: OrderProduct[]
): Promise<PaymentReceipt> {
const receiptItems: PaymentReceiptItem[] = [];
for (const paymentItem of paymentItems) {
const orderProduct = orderProducts.find((orderProduct) => orderProduct.product.id === paymentItem.product_id);
receiptItems.push({
description: orderProduct.product.title,
quantity: String(paymentItem.quantity),
amount: this.makeAmount(paymentItem.amount),
vat_code: 1,
payment_subject: 'commodity',
});
}
return {
customer: {email},
tax_system_code: 2,
items: receiptItems,
};
}
private sendPaymentRequest(data: PaymentData | PaymentDataRecurrent): Promise<PaymentResponse> {
let paymentServiceUrl = `${this.config.HOST}:${this.config.PORT}/payments`;
if ('auto_payment_id' in data && data.auto_payment_id) {
paymentServiceUrl += '/auto';
}
const config = {headers: {Authorization: 'Bearer ' + this.config.TOKEN}};
return this.httpService
.post(paymentServiceUrl, data, config)
.pipe(map((res) => res.data))
.toPromise();
}
validateHook(request: PaymentHookRequest) {
const data = Object.keys(request)
.sort()
.reduce((acc, item) => ({...acc, [item]: request[item]}), {} as PaymentHookRequest);
const hash = data.hash;
delete data.hash;
return hash === hmacSHA512(JSON.stringify(data), this.config.HMAC_KEY).toString(hex);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment