mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Merge pull request #108 from Tria-plc/freight/feature/payment
providers
This commit is contained in:
@@ -3,45 +3,57 @@ import { BookingsRepository } from './bookings.repository';
|
|||||||
import { Booking } from './entities/booking.entity';
|
import { Booking } from './entities/booking.entity';
|
||||||
import { assertBookingStatus } from './booking-status.util';
|
import { assertBookingStatus } from './booking-status.util';
|
||||||
import { InAppPaymentReceiptDto } from './dto/pay-booking.dto';
|
import { InAppPaymentReceiptDto } from './dto/pay-booking.dto';
|
||||||
|
import { PaymentService } from '../payment/payment.service';
|
||||||
|
|
||||||
export interface InAppPaymentReceipt extends InAppPaymentReceiptDto {}
|
export interface InAppPaymentReceipt extends InAppPaymentReceiptDto { }
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BookingPaymentService {
|
export class BookingPaymentService {
|
||||||
constructor(private readonly bookingsRepository: BookingsRepository) {}
|
constructor(private readonly bookingsRepository: BookingsRepository, private readonly paymentService: PaymentService) { }
|
||||||
|
|
||||||
async pay(
|
async pay(
|
||||||
bookingId: string,
|
bookingId: string,
|
||||||
): Promise<{ booking: Booking; receipt: InAppPaymentReceipt }> {
|
): Promise<{ redirectUrl: string }> {
|
||||||
const booking = await this.requireBooking(bookingId);
|
const booking = await this.requireBooking(bookingId);
|
||||||
assertBookingStatus(booking, ['FULLY_EXECUTED']);
|
assertBookingStatus(booking, ['FULLY_EXECUTED']);
|
||||||
|
|
||||||
const receipt = this.buildMockReceipt(booking);
|
// const receipt = this.buildMockReceipt(booking);
|
||||||
|
|
||||||
const updated = await this.bookingsRepository.update(bookingId, {
|
// const updated = await this.bookingsRepository.update(bookingId, {
|
||||||
status: 'PAID',
|
// status: 'PAID',
|
||||||
paymentStatus: 'PAID',
|
// paymentStatus: 'PAID',
|
||||||
} as never);
|
// } as never);
|
||||||
|
const resp = await this.paymentService.pay(booking.totalAmount, "ETB", "telebirr", "payment for booking", 'booking', (_) => {
|
||||||
return { booking: updated!, receipt };
|
return new Promise((resp, _) => {
|
||||||
}
|
resp({
|
||||||
|
id: booking.id,
|
||||||
private buildMockReceipt(booking: Booking): InAppPaymentReceipt {
|
type: "booking"
|
||||||
const timestamp = Date.now();
|
})
|
||||||
const isEtb = booking.paymentCurrency === 'ETB';
|
});
|
||||||
const prefix = isEtb ? 'TB' : 'CARD';
|
})
|
||||||
const provider = isEtb ? 'TELEBIRR' : 'CARD';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
redirectUrl: resp.clientAction.type == "REDIRECT" ? `http://localhost:3001/api/payments/telebirr/${booking.id}` : ""
|
||||||
provider,
|
}
|
||||||
providerRef: `${prefix}-${booking.reference}-${timestamp}`,
|
|
||||||
amount: booking.totalAmount,
|
|
||||||
currency: booking.paymentCurrency,
|
|
||||||
paidAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// private buildMockReceipt(booking: Booking): InAppPaymentReceipt {
|
||||||
|
// const timestamp = Date.now();
|
||||||
|
// const isEtb = booking.paymentCurrency === 'ETB';
|
||||||
|
// const prefix = isEtb ? 'TB' : 'CARD';
|
||||||
|
// const provider = isEtb ? 'TELEBIRR' : 'CARD';
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// success: true,
|
||||||
|
// provider,
|
||||||
|
// providerRef: `${prefix}-${booking.reference}-${timestamp}`,
|
||||||
|
// amount: booking.totalAmount,
|
||||||
|
// currency: booking.paymentCurrency,
|
||||||
|
// paidAt: new Date().toISOString(),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
private async requireBooking(id: string): Promise<Booking> {
|
private async requireBooking(id: string): Promise<Booking> {
|
||||||
const booking = await this.bookingsRepository.findById(id);
|
const booking = await this.bookingsRepository.findById(id);
|
||||||
if (!booking) throw new NotFoundException(`Booking ${id} not found`);
|
if (!booking) throw new NotFoundException(`Booking ${id} not found`);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { ContractPricingScheduleBuilder } from '../../contracts/contract-pricing
|
|||||||
import { ContractRendererService } from '../../contracts/contract-renderer.service';
|
import { ContractRendererService } from '../../contracts/contract-renderer.service';
|
||||||
import { ContractTemplateResolver } from '../../contracts/contract-template.resolver';
|
import { ContractTemplateResolver } from '../../contracts/contract-template.resolver';
|
||||||
import { ContractViewModelBuilder } from '../../contracts/contract-view-model.builder';
|
import { ContractViewModelBuilder } from '../../contracts/contract-view-model.builder';
|
||||||
|
import { PaymentModule } from '../payment/payment.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -40,6 +41,7 @@ import { ContractViewModelBuilder } from '../../contracts/contract-view-model.bu
|
|||||||
BookingReviewNote,
|
BookingReviewNote,
|
||||||
BookingContractSignature,
|
BookingContractSignature,
|
||||||
]),
|
]),
|
||||||
|
PaymentModule,
|
||||||
FilesModule,
|
FilesModule,
|
||||||
MinioModule,
|
MinioModule,
|
||||||
CompaniesModule,
|
CompaniesModule,
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { Controller, Param, ParseUUIDPipe, Post } from '@nestjs/common';
|
|||||||
import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { BookingPaymentService } from './booking-payment.service';
|
import { BookingPaymentService } from './booking-payment.service';
|
||||||
import { BookingTransitionService } from './booking-transition.service';
|
// import { BookingTransitionService } from './booking-transition.service';
|
||||||
import { InAppPaymentReceiptDto } from './dto/pay-booking.dto';
|
// import { InAppPaymentReceiptDto } from './dto/pay-booking.dto';
|
||||||
import { Booking } from './entities/booking.entity';
|
// import { Booking } from './entities/booking.entity';
|
||||||
import { BookingNextStep } from './booking-next-step.util';
|
// import { BookingNextStep } from './booking-next-step.util';
|
||||||
|
|
||||||
@ApiTags('payments')
|
@ApiTags('payments')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@@ -13,22 +13,15 @@ import { BookingNextStep } from './booking-next-step.util';
|
|||||||
export class PayController {
|
export class PayController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly paymentService: BookingPaymentService,
|
private readonly paymentService: BookingPaymentService,
|
||||||
private readonly transitionService: BookingTransitionService,
|
// private readonly transitionService: BookingTransitionService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@Post(':id/payment/pay')
|
@Post(':id/payment/pay')
|
||||||
@ApiOperation({ summary: 'Complete in-app payment (mock)' })
|
@ApiOperation({ summary: 'Complete in-app payment (mock)' })
|
||||||
@ApiOkResponse({ description: 'Enriched booking with ephemeral payment receipt' })
|
@ApiOkResponse({ description: 'Enriched booking with ephemeral payment receipt' })
|
||||||
async pay(@Param('id', ParseUUIDPipe) id: string): Promise<
|
async pay(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
Booking & {
|
return await this.paymentService.pay(id);
|
||||||
latestChangeRequestNote?: string | null;
|
// const abstract = await this.transitionService.enrichBookingResponse(booking);
|
||||||
contractSummary?: string | null;
|
// return { ...abstract, paymentReceipt: receipt };
|
||||||
nextStep: BookingNextStep | null;
|
|
||||||
paymentReceipt: InAppPaymentReceiptDto;
|
|
||||||
}
|
|
||||||
> {
|
|
||||||
const { booking, receipt } = await this.paymentService.pay(id);
|
|
||||||
const abstract = await this.transitionService.enrichBookingResponse(booking);
|
|
||||||
return { ...abstract, paymentReceipt: receipt };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,42 @@
|
|||||||
import { Controller, Get, NotFoundException, Param, ParseUUIDPipe, Post, Res } from "@nestjs/common";
|
import { Controller, Get, NotFoundException, Param, ParseUUIDPipe, Post, Res } from "@nestjs/common";
|
||||||
import { PaymentService } from "./payment.service";
|
import { PaymentService } from "./payment.service";
|
||||||
import { Public } from "@edr/api-common";
|
import { Public } from "@edr/api-common";
|
||||||
import { randomUUID } from "crypto";
|
// import { randomUUID } from "crypto";
|
||||||
import { Response } from "express"
|
import { Response } from "express"
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@Controller("payments")
|
@Controller("payments")
|
||||||
export class PaymentController {
|
export class PaymentController {
|
||||||
constructor(private readonly paymentService: PaymentService) { }
|
constructor(private readonly paymentService: PaymentService,) { }
|
||||||
|
|
||||||
|
|
||||||
@Get("/receipts/:orderId/html")
|
// @Get("/receipts/:orderId/html")
|
||||||
async genReceipt(@Param("orderId") orderId: string, @Res() res: Response) {
|
// async genReceipt(@Param("orderId") orderId: string, @Res() res: Response) {
|
||||||
const filled = await this.paymentService.genReceiptHtml(orderId);
|
// const filled = await this.paymentService.genReceiptHtml(orderId);
|
||||||
return res.send(filled)
|
// return res.send(filled)
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Post("/initiate/booking")
|
// @Post("/initiate/booking")
|
||||||
async initiatePayment() {
|
// async initiatePayment() {
|
||||||
|
|
||||||
//Only for testing..
|
// //Only for testing..
|
||||||
const description = "booking"
|
// const description = "Booking for contact"
|
||||||
const data = await this.paymentService.pay(20, "ETB", "telebirr", description, "booking", (_) => {
|
// const price = 2000
|
||||||
return new Promise((resp, _) => {
|
// const data = await this.paymentService.pay(price, "ETB", "telebirr", description, "booking", (_) => {
|
||||||
resp({
|
// return new Promise((resp, _) => {
|
||||||
id: randomUUID(),
|
// resp({
|
||||||
type: "booking"
|
// id: randomUUID(),
|
||||||
})
|
// type: "booking"
|
||||||
});
|
// })
|
||||||
})
|
// });
|
||||||
|
// })
|
||||||
|
|
||||||
return data
|
// return data
|
||||||
|
// }
|
||||||
|
|
||||||
|
@Post("/bookings/check-payment/:orderId")
|
||||||
|
checkPayment(@Param("orderId", ParseUUIDPipe) orderId: string) {
|
||||||
|
return this.paymentService.checkStatusAndUpdate(orderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import { ConfigModule } from "@nestjs/config";
|
|||||||
import { PaymentRepository } from "./payment.repository";
|
import { PaymentRepository } from "./payment.repository";
|
||||||
import { WebhookController } from "./webhooks/webhook.controller";
|
import { WebhookController } from "./webhooks/webhook.controller";
|
||||||
import { TelebirrWebhookService } from "./webhooks/providers/telebirr.service";
|
import { TelebirrWebhookService } from "./webhooks/providers/telebirr.service";
|
||||||
import { BookingsModule } from "../bookings/bookings.module";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [HttpModule, ConfigModule, BookingsModule],
|
imports: [HttpModule, ConfigModule],
|
||||||
providers: [PaymentRepository, PaymentTelebirrStrategy, PaymentService, TelebirrWebhookService],
|
providers: [PaymentRepository, PaymentTelebirrStrategy, PaymentService, TelebirrWebhookService],
|
||||||
controllers: [PaymentController, WebhookController]
|
controllers: [PaymentController, WebhookController],
|
||||||
|
exports: [PaymentService]
|
||||||
})
|
})
|
||||||
export class PaymentModule { }
|
export class PaymentModule { }
|
||||||
@@ -10,6 +10,8 @@ import * as crypto from 'crypto';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as Handlebars from 'handlebars';
|
import * as Handlebars from 'handlebars';
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { Booking } from "../bookings/entities/booking.entity";
|
||||||
|
|
||||||
|
|
||||||
type PaymentMethod = PaymentEntity["method"]
|
type PaymentMethod = PaymentEntity["method"]
|
||||||
@@ -20,6 +22,7 @@ export class PaymentService {
|
|||||||
private strategies: Map<PaymentMethod, PaymentStrategy>;
|
private strategies: Map<PaymentMethod, PaymentStrategy>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
private readonly datasource: DataSource,
|
private readonly datasource: DataSource,
|
||||||
private readonly paymentRepo: PaymentRepository,
|
private readonly paymentRepo: PaymentRepository,
|
||||||
private readonly telebirrPaymentStategy: PaymentTelebirrStrategy) {
|
private readonly telebirrPaymentStategy: PaymentTelebirrStrategy) {
|
||||||
@@ -47,7 +50,8 @@ export class PaymentService {
|
|||||||
let redirectUrl: string;
|
let redirectUrl: string;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "booking":
|
case "booking":
|
||||||
redirectUrl = `http://localhost:3001/api/payments/receipts/${orderId}/html`
|
const url = this.configService.get<string>("TELEBIRR_SUCCESS_REDIRECT_BASE_URL")
|
||||||
|
redirectUrl = `${url}/check-status/${orderId}`
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,17 +136,27 @@ export class PaymentService {
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// const templatePath = path.join(
|
async checkStatusAndUpdate(orderId: string) {
|
||||||
// process.cwd(),
|
const resp = await this.paymentRepo.findOneBy({ merchantOrderId: orderId })
|
||||||
// 'src/modules/payment/templates/receipt.hbs',
|
if (!resp) {
|
||||||
// );
|
throw new NotFoundException("order id not found")
|
||||||
|
}
|
||||||
|
const result = await this.telebirrPaymentStategy.queryStatus(resp.merchantOrderId)
|
||||||
|
const bizContent = result.rawResponse.biz_content as {
|
||||||
|
order_status: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ordersStatus = bizContent.order_status
|
||||||
// console.log(templatePath)
|
if (ordersStatus == "PAY_SUCCESS") {
|
||||||
|
await this.datasource.transaction(async (mg) => {
|
||||||
// const source = fs.readFileSync(templatePath, 'utf8');
|
await mg.update(Booking, { id: resp.refId }, { status: "PAID" })
|
||||||
// const template = Handlebars.compile(source);
|
await mg.update(PaymentEntity, { id: resp.id }, { status: "success" })
|
||||||
// return this.getReceiptTemplate();
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: result.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -301,5 +301,4 @@ export class PaymentTelebirrStrategy implements PaymentStrategy {
|
|||||||
return this.config.get<string>('telebirr.publicKey') ?? '';
|
return this.config.get<string>('telebirr.publicKey') ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,16 @@ import { Injectable, } from '@nestjs/common';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
import { TelebirrDto } from '../dto/telebirr.dto';
|
import { TelebirrDto } from '../dto/telebirr.dto';
|
||||||
import { BookingsRepository } from 'src/modules/bookings/bookings.repository';
|
|
||||||
import { PaymentRepository } from '../../payment.repository';
|
import { PaymentRepository } from '../../payment.repository';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { Booking } from 'src/modules/bookings/entities/booking.entity';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TelebirrWebhookService {
|
export class TelebirrWebhookService {
|
||||||
// private readonly logger = new Logger(TelebirrWebhookService.name);
|
// private readonly logger = new Logger(TelebirrWebhookService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly datasource: DataSource,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
private readonly bookingRepo: BookingsRepository,
|
|
||||||
private readonly paymentRepo: PaymentRepository,
|
private readonly paymentRepo: PaymentRepository,
|
||||||
|
|
||||||
) { }
|
) { }
|
||||||
@@ -57,7 +58,8 @@ export class TelebirrWebhookService {
|
|||||||
await this.paymentRepo.update({ id: payment.id }, { status: "success", paidAt: new Date() })
|
await this.paymentRepo.update({ id: payment.id }, { status: "success", paidAt: new Date() })
|
||||||
switch (payment.type) {
|
switch (payment.type) {
|
||||||
case "booking":
|
case "booking":
|
||||||
await this.bookingRepo.update(payment.refId, { paymentStatus: "PAID", })
|
await this.datasource.manager.update(Booking, { id: payment.refId }, { paymentStatus: "PAID", })
|
||||||
|
// await this.bookingRepo.update(payment.refId, { paymentStatus: "PAID", })
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user