Files
imphnen-backend-service/imphnen-dimentorin/src/payments/application/payment_service.rs
T
asepharyana 6d5af29de8 feat(dimentorin): confirm payment auto-confirms linked session
- confirm_payment now transitions linked session pending->confirmed
- e2e verified: book -> create payment -> admin confirm -> session confirmed + payment paid
2026-08-05 09:32:04 +07:00

164 lines
4.9 KiB
Rust

use super::super::domain::{
CreatePaymentCommand, PaymentEntity, PaymentRepository, PaymentService, SERVICE_FEE,
};
use crate::sessions::domain::SessionRepository;
use async_trait::async_trait;
use chrono::{Duration, Utc};
use imphnen_entities::seaorm::auth::mentors::Entity as MentorsEntity;
use imphnen_entities::seaorm::auth::users::Entity as UsersEntity;
use imphnen_utils::AppError;
use sea_orm::prelude::*;
use std::sync::Arc;
use uuid::Uuid;
pub struct PaymentServiceImpl {
payment_repo: Arc<dyn PaymentRepository>,
session_repo: Arc<dyn SessionRepository>,
db: Arc<DatabaseConnection>,
}
impl PaymentServiceImpl {
pub fn new(
payment_repo: Arc<dyn PaymentRepository>,
session_repo: Arc<dyn SessionRepository>,
db: Arc<DatabaseConnection>,
) -> Self {
Self {
payment_repo,
session_repo,
db,
}
}
}
fn generate_external_ref(method: &str, session_id: Uuid) -> String {
match method {
"va" => format!("VA-{}-{}", session_id.to_string().split('-').next().unwrap_or("X"), Utc::now().format("%Y%m%d%H%M%S")),
"qris" => format!("QR-{}", session_id.to_string().replace('-', "").chars().take(16).collect::<String>()),
_ => format!("MANUAL-{}", Utc::now().format("%Y%m%d%H%M%S")),
}
}
#[async_trait]
impl PaymentService for PaymentServiceImpl {
async fn create_payment(
&self,
session_id: Uuid,
mentee_id: Uuid,
cmd: CreatePaymentCommand,
) -> Result<PaymentEntity, AppError> {
// Only a valid session can be paid for.
let session = self
.session_repo
.find_by_id(session_id)
.await
.map_err(|_| AppError::NotFoundError("Session not found".into()))?
.ok_or_else(|| AppError::NotFoundError("Session not found".into()))?;
// The mentee paying must be the session's mentee.
if session.mentee_id != mentee_id {
return Err(AppError::ForbiddenError(
"You can only pay for your own sessions".into(),
));
}
// Load mentor rate from the mentors table (mentors.user_id = the session's
// mentor user id).
let mentor_uuid = session.mentor_id;
let mentor = MentorsEntity::find()
.filter(imphnen_entities::seaorm::auth::mentors::Column::UserId.eq(mentor_uuid))
.one(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?
.ok_or_else(|| AppError::NotFoundError("Mentor not found".into()))?;
let rate = mentor.mentoring_rate.unwrap_or(50_000.0).round() as i64;
let total = rate + SERVICE_FEE;
let method = cmd.method.clone();
let provider = "manual".to_string(); // swap to midtrans/xendit later
let expires_at = Utc::now() + Duration::hours(24);
let payment = PaymentEntity {
id: Uuid::new_v4(),
session_id,
mentee_id,
mentor_id: mentor_uuid,
amount: rate,
service_fee: SERVICE_FEE,
total,
method: method.clone(),
provider,
status: "pending".into(),
external_ref: Some(generate_external_ref(&method, session_id)),
expires_at,
created_at: Utc::now(),
paid_at: None,
};
self.payment_repo.create(payment).await
}
async fn get_payment_by_id(
&self,
id: Uuid,
user_id: Uuid,
) -> Result<PaymentEntity, AppError> {
let payment = self.payment_repo.find_by_id(id).await?;
if payment.mentee_id != user_id {
return Err(AppError::ForbiddenError(
"You can only view your own payments".into(),
));
}
Ok(payment)
}
async fn confirm_payment(
&self,
id: Uuid,
actor_id: Uuid,
) -> Result<PaymentEntity, AppError> {
// Admin / "Admin Pembayaran" only — check role via users table.
let user = UsersEntity::find_by_id(actor_id)
.one(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?
.ok_or_else(|| AppError::NotFoundError("Actor not found".into()))?;
let role_id = user.role_id.ok_or_else(|| {
AppError::ForbiddenError("User has no role assigned".into())
})?;
let roles = imphnen_entities::seaorm::auth::roles::Entity::find_by_id(role_id)
.one(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?
.ok_or_else(|| AppError::ForbiddenError("Role not found".into()))?;
if roles.name != "Admin" && roles.name != "Admin Pembayaran" {
return Err(AppError::ForbiddenError(
"Only payment admin can confirm payments".into(),
));
}
let payment = self.payment_repo.find_by_id(id).await?;
if payment.status != "pending" {
return Err(AppError::ConflictError(
"Payment is not pending".into(),
));
}
let paid = self.payment_repo
.update_status(id, "paid", Some(payment.external_ref.clone().unwrap_or_default()))
.await?;
// Confirm the linked session so mentor/mentee can proceed with the call.
if let Some(mut session) = self.session_repo.find_by_id(payment.session_id).await? {
session.status = "confirmed".to_string();
session.updated_at = Utc::now();
self.session_repo.update(payment.session_id, session).await?;
}
Ok(paid)
}
async fn get_mentee_payments(
&self,
mentee_id: Uuid,
) -> Result<Vec<PaymentEntity>, AppError> {
self.payment_repo.find_by_mentee(mentee_id).await
}
}