Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers(HttpMethod.GET, "/api/appointment-types/**", "/appointment-types/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/availability/doctor/**", "/availability/doctor/**").permitAll()

.requestMatchers(HttpMethod.POST, "/api/appointments/book", "/appointments/book").hasAnyRole("PATIENT", "ADMIN")
.requestMatchers(HttpMethod.GET, "/api/appointments/user/**", "/appointments/user/**").hasAnyRole("PATIENT", "ADMIN")
.requestMatchers(HttpMethod.POST, "/api/appointments/book", "/appointments/book").permitAll()
.requestMatchers(HttpMethod.GET, "/api/appointments/user/**", "/appointments/user/**").permitAll()

.requestMatchers(HttpMethod.POST, "/api/doctors/**", "/doctors/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/doctors/**", "/doctors/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/doctors/**", "/doctors/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/api/appointment-types/**", "/appointment-types/**").hasRole("ADMIN")

.requestMatchers("/api/chat/**", "/chat/**").authenticated()
.requestMatchers("/api/chat/**", "/chat/**").permitAll()

.anyRequest().authenticated()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ public ResponseEntity<?> bookAppointment(@RequestBody AppointmentRequestDTO dto,

@GetMapping("/user/success")
public ResponseEntity<List<AppointmentDTO>> getSuccessfulAppointments() {
return ResponseEntity.ok(appointmentService.getSuccessfulAppointments());
System.out.println("=== /api/appointments/user/success HIT by Patient ===");
try {
List<AppointmentDTO> list = appointmentService.getSuccessfulAppointments();
System.out.println("=== Returning " + list.size() + " appointments ===");
return ResponseEntity.ok(list);
} catch (Exception e) {
System.out.println("=== ERROR in getSuccessfulAppointments: " + e.getMessage() + " ===");
e.printStackTrace();
throw e;
}
}

@GetMapping("/admin/all")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,101 +5,117 @@
import com.example.mainservice.entity.enums.PaymentStatus;
import com.example.mainservice.repository.AppointmentRepository;
import com.example.mainservice.repository.PaymentRepository;
import com.example.mainservice.service.DoctorAvailabilityService;
import com.example.mainservice.service.PaymentService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;

import java.time.LocalDateTime;
import java.util.Map;

@RestController
@CrossOrigin(origins = "*") // Allows the frontend to freely call the success verification
@RequestMapping("/api/payments")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class PaymentController {

private final PaymentService paymentService;
private final AppointmentRepository appointmentRepository;
private final PaymentRepository paymentRepository;
private final DoctorAvailabilityService doctorAvailabilityService;

// Build payment form for frontend
@GetMapping("/pay/{appointmentId}")
public String pay(@PathVariable Long appointmentId,
@RequestParam(required = false) String amount) {
System.out.println("=== /pay called for appointmentId: " + appointmentId + " with amount: " + amount);
System.out.println("=== /pay called for appointmentId: " + appointmentId + " | amount: " + amount + " ===");
return paymentService.buildPaymentForm(appointmentId, amount);
}

// PayHere IPN notification
// Webhook called by PayHere servers in background
@PostMapping("/notify")
public String notifyPayment(
public void handlePayHereNotification(
@RequestParam("merchant_id") String merchantId,
@RequestParam("order_id") String orderId,
@RequestParam("payhere_amount") String amount,
@RequestParam("payhere_amount") String payhereAmount,
@RequestParam("payhere_currency") String payhereCurrency,
@RequestParam("status_code") String statusCode,
@RequestParam("md5sig") String md5sig,
@RequestParam("payment_id") String paymentId,
@RequestParam("method") String method) {
@RequestParam(value = "custom_1", required = false) String custom1,
@RequestParam(value = "custom_2", required = false) String custom2
) {
System.out.println("=== PAYHERE NOTIFICATION RECEIVED ===");
System.out.println("Order ID: " + orderId);
System.out.println("Status Code: " + statusCode);

System.out.println("=== PayHere IPN Notification Received ===");
System.out.println("merchant_id: " + merchantId);
System.out.println("order_id: " + orderId);
System.out.println("amount: " + amount);
System.out.println("status_code: " + statusCode);
System.out.println("md5sig: " + md5sig);
System.out.println("payment_id: " + paymentId);
System.out.println("method: " + method);
// 1. Find the payment and appointment
Payment payment = paymentRepository.findByOrderId(orderId).orElse(null);
if (payment == null) {
System.err.println("Order not found: " + orderId);
return;
}

try {
// Extract numeric appointment ID from order_id
// Example: "ORDER_5_1769372867357" -> "5"
String[] parts = orderId.split("_");
if (parts.length < 2) {
throw new RuntimeException("Invalid order_id format: " + orderId);
}
Long appointmentId = Long.parseLong(parts[1]);
Appointment appointment = payment.getAppointment();

// Find appointment by extracted ID
Appointment appointment = appointmentRepository.findById(appointmentId)
.orElseThrow(() -> new RuntimeException("Appointment not found with ID: " + appointmentId));
// 2. Validate hash (optional but recommended for production)
// boolean isValid = paymentService.validateHash(...);
// if (!isValid) return;

// Create Payment entity
Payment payment = new Payment();
payment.setAppointment(appointment);
payment.setAmount(Double.parseDouble(amount));
payment.setOrderId(orderId);
payment.setTransactionId(paymentId);
payment.setPaymentGateway("PayHere");
// 3. Update status based on statusCode
// 2 = Success, 0 = Pending, -1 = Canceled, -2 = Failed, -3 = Chargedback
if ("2".equals(statusCode)) {
payment.setPaymentStatus(PaymentStatus.SUCCESS);
appointment.setPaymentStatus(PaymentStatus.SUCCESS);
} else if ("0".equals(statusCode)) {
payment.setPaymentStatus(PaymentStatus.PENDING);
} else {
payment.setPaymentStatus(PaymentStatus.FAILED);
appointment.setPaymentStatus(PaymentStatus.FAILED);
}

// Set payment and appointment status
if ("2".equals(statusCode)) { // 2 = success
if (appointment.getAvailability() != null) {
doctorAvailabilityService.markSlotBooked(appointment.getAvailability().getId());
}
payment.setPaymentStatus(PaymentStatus.SUCCESS);
appointment.setPaymentStatus(PaymentStatus.SUCCESS);
System.out.println("Payment SUCCESS for appointment " + appointmentId);
} else {
if (appointment.getAvailability() != null) {
doctorAvailabilityService.markSlotAvailable(appointment.getAvailability().getId());
paymentRepository.save(payment);
appointmentRepository.save(appointment);
System.out.println("=== PAYMENT SAVED -> " + payment.getPaymentStatus() + " ===");
}

// IMPORTANT: For development mostly!
// Forces all pending payments to SUCCESS so you don't need real PayHere IPN configured.
@GetMapping("/dev-success-all")
public String forceSuccessAll() {
System.out.println("=== DEV: FORCING ALL PENDING PAYMENTS TO SUCCESS ===");
var payments = paymentRepository.findAll();
int count = 0;
for (Payment p : payments) {
if (p.getPaymentStatus() == PaymentStatus.PENDING) {
p.setPaymentStatus(PaymentStatus.SUCCESS);
p.setPaymentDate(LocalDateTime.now());
paymentRepository.save(p);

Appointment a = p.getAppointment();
if (a != null) {
a.setPaymentStatus(PaymentStatus.SUCCESS);
appointmentRepository.save(a);
}
payment.setPaymentStatus(PaymentStatus.FAILED);
appointment.setPaymentStatus(PaymentStatus.FAILED);
System.out.println("Payment FAILED for appointment " + appointmentId);
count++;
}
}
return "Updated " + count + " pending payments to SUCCESS.";
}

// Save payment and update appointment
@GetMapping("/success/{orderId}")
public ResponseEntity<Map<String, String>> verifyPaymentSuccess(@PathVariable String orderId) {
System.out.println("=== FRONTEND VERIFY SUCCESS FOR ORDER: " + orderId + " ===");
Payment payment = paymentRepository.findByOrderId(orderId).orElse(null);
if (payment != null && payment.getPaymentStatus() == PaymentStatus.PENDING) {
payment.setPaymentStatus(PaymentStatus.SUCCESS);
paymentRepository.save(payment);
appointmentRepository.save(appointment);

System.out.println("Payment saved: " + payment);
System.out.println("Appointment updated: " + appointment);

} catch (Exception e) {
System.out.println("Error handling PayHere IPN: " + e.getMessage());
e.printStackTrace();

Appointment a = payment.getAppointment();
if (a != null) {
a.setPaymentStatus(PaymentStatus.SUCCESS);
appointmentRepository.save(a);
}
return ResponseEntity.ok(Map.of("message", "Payment manually verified on frontend"));
}

// Return 'ok' to PayHere
return "ok";
return ResponseEntity.ok(Map.of("message", "Payment already processed or not found"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ public class Payment {
@Enumerated(EnumType.STRING)
private PaymentStatus paymentStatus;

@Builder.Default
private LocalDateTime paymentDate = LocalDateTime.now();
}
Loading
Loading