Skip to content
This repository was archived by the owner on Nov 23, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
09ad0c2
Update security configuration
mehara-rothila Nov 6, 2025
32d6653
Update invoice and payment controllers
mehara-rothila Nov 6, 2025
fe071c8
Disable hardcoded invoice data seeding
mehara-rothila Nov 9, 2025
2c2518b
Disable method-level security for debugging
mehara-rothila Nov 9, 2025
992cdc6
Fix payment initiation DTO field name
mehara-rothila Nov 9, 2025
29844ed
Add comprehensive invoice response fields
mehara-rothila Nov 9, 2025
4d7f73f
Implement PayHere payment gateway integration
mehara-rothila Nov 9, 2025
fb2b202
Remove hardcoded PayHere credentials from config
mehara-rothila Nov 9, 2025
52cf44c
Update PayHere payment gateway configuration to use environment varia…
DinithEdirisinghe Nov 9, 2025
d77ee78
Add part-payment tracking fields to Invoice entity
mehara-rothila Nov 10, 2025
f2d7dfc
Add ONLINE payment method for gateway integration
mehara-rothila Nov 10, 2025
26f618d
Add requiresDeposit flag to invoice creation DTO
mehara-rothila Nov 10, 2025
672e587
Extend invoice response with part-payment metadata
mehara-rothila Nov 10, 2025
43ab5ae
Implement part-payment calculation and tracking logic
mehara-rothila Nov 10, 2025
a427c31
Fix PayHere hash generation with Base64 merchant secret decoding
mehara-rothila Nov 10, 2025
ec40c7f
Configure PayHere sandbox credentials and endpoints
mehara-rothila Nov 10, 2025
bbbfe26
Add RestTemplate configuration for HTTP client
mehara-rothila Nov 10, 2025
47e0c87
Create notification request DTO for payment events
mehara-rothila Nov 10, 2025
01eb770
Implement notification client for payment alerts
mehara-rothila Nov 10, 2025
3eb97a6
Restrict build trigger to specific branches for pull requests
RandithaK Nov 11, 2025
01bcdc0
Merge pull request #8 from TechTorque-2025/randitha-superbranch
RandithaK Nov 11, 2025
dfff49e
Merge branch 'main' into dev
RandithaK Nov 11, 2025
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
7 changes: 3 additions & 4 deletions .github/workflows/buildtest.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
name: Build and Test Payment Service

on:
push:
branches:
- '**'
pull_request:
branches:
- '**'
- main
- dev
- devOps

jobs:
build-test:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@

/**
* Data seeder for Payment Service - seeds sample invoices, payments, and scheduled payments
* Only runs in 'dev' profile to avoid polluting production data
* DISABLED - Change profile to "dev" to enable seed data
*/
@Configuration
@Profile("dev")
@Profile("seed-data-disabled") // Changed from "dev" to disable
@Slf4j
public class DataSeeder {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.techtorque.payment_service.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
// TEMPORARILY DISABLED FOR DEBUGGING - method-level security was blocking all requests
// @EnableMethodSecurity(prePostEnabled = true)

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method-level security is disabled. This removes authorization checks from all @PreAuthorize annotations. This should not be merged to production branches. Consider fixing the underlying issue causing requests to be blocked rather than disabling security.

Suggested change
// @EnableMethodSecurity(prePostEnabled = true)
@EnableMethodSecurity(prePostEnabled = true)

Copilot uses AI. Check for mistakes.
public class SecurityConfig {

// A more comprehensive whitelist for Swagger/OpenAPI, based on the auth-service config.
Expand All @@ -32,22 +33,18 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

// Set session management to STATELESS
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

// Explicitly disable form login and HTTP Basic authentication
.formLogin(formLogin -> formLogin.disable())
.httpBasic(httpBasic -> httpBasic.disable())
// Set up authorization rules

// TEMPORARILY DISABLE ALL SECURITY FOR DEBUGGING
.authorizeHttpRequests(authz -> authz
// Permit all requests to the Swagger UI and API docs paths
.requestMatchers(SWAGGER_WHITELIST).permitAll()

// All other requests must be authenticated
.anyRequest().authenticated()
)
.anyRequest().permitAll()
);
Comment on lines +41 to +44

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All security is completely disabled, allowing unrestricted access to all endpoints. This is a critical security issue that must not be deployed to any shared environment. Re-enable security before merging.

Copilot uses AI. Check for mistakes.

// Add our custom filter to read headers from the Gateway
.addFilterBefore(new GatewayHeaderFilter(), UsernamePasswordAuthenticationFilter.class);
// .addFilterBefore(new GatewayHeaderFilter(), UsernamePasswordAuthenticationFilter.class);
Comment on lines 46 to +47

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gateway header filter is commented out, which means user authentication headers from the API gateway are not being processed. This breaks the authentication flow. Re-enable this filter when security is restored.

Copilot uses AI. Check for mistakes.

return http.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class InvoiceController {

@Operation(summary = "Create a new invoice")
@PostMapping
@PreAuthorize("hasAnyRole('EMPLOYEE', 'ADMIN')")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'ADMIN', 'SUPER_ADMIN')")
public ResponseEntity<InvoiceResponseDto> createInvoice(
@Valid @RequestBody CreateInvoiceDto dto) {
InvoiceResponseDto invoice = billingService.createInvoice(dto);
Expand All @@ -37,7 +37,7 @@ public ResponseEntity<InvoiceResponseDto> createInvoice(

@Operation(summary = "Get invoice by ID")
@GetMapping("/{invoiceId}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'EMPLOYEE', 'ADMIN')")
@PreAuthorize("hasAnyRole('CUSTOMER', 'EMPLOYEE', 'ADMIN', 'SUPER_ADMIN')")
public ResponseEntity<InvoiceResponseDto> getInvoice(
@PathVariable String invoiceId,
@RequestHeader("X-User-Subject") String userId) {
Expand All @@ -47,26 +47,26 @@ public ResponseEntity<InvoiceResponseDto> getInvoice(

@Operation(summary = "List invoices for the current customer")
@GetMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'EMPLOYEE', 'ADMIN')")
@PreAuthorize("hasAnyRole('CUSTOMER', 'EMPLOYEE', 'ADMIN', 'SUPER_ADMIN')")
public ResponseEntity<List<InvoiceResponseDto>> listInvoices(
@RequestHeader("X-User-Subject") String userId,
@RequestHeader(value = "X-User-Roles", required = false) String userRoles) {

List<InvoiceResponseDto> invoices;
// Admin and Employee can see all invoices, Customer sees only their own
if (userRoles != null && (userRoles.contains("ADMIN") || userRoles.contains("EMPLOYEE"))) {

// Admin, Super Admin and Employee can see all invoices, Customer sees only their own
if (userRoles != null && (userRoles.contains("ADMIN") || userRoles.contains("SUPER_ADMIN") || userRoles.contains("EMPLOYEE"))) {
invoices = billingService.listAllInvoices();
} else {
invoices = billingService.listInvoicesForCustomer(userId);
}

return ResponseEntity.ok(invoices);
}

@Operation(summary = "Send an invoice to a customer via email (employee/admin only)")
@PostMapping("/{invoiceId}/send")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'ADMIN')")
@PreAuthorize("hasAnyRole('EMPLOYEE', 'ADMIN', 'SUPER_ADMIN')")
public ResponseEntity<Map<String, String>> sendInvoiceByEmail(
@PathVariable String invoiceId,
@Valid @RequestBody SendInvoiceDto dto) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public ResponseEntity<List<PaymentResponseDto>> getPaymentHistory(

@Operation(summary = "Get details for a specific payment")
@GetMapping("/{paymentId}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')")
@PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN', 'SUPER_ADMIN')")
public ResponseEntity<PaymentResponseDto> getPaymentDetails(
@PathVariable String paymentId,
@RequestHeader("X-User-Subject") String userId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
@NoArgsConstructor
@AllArgsConstructor
public class PaymentInitiationDto {
private String orderId;
private String invoiceId; // Changed from orderId to match frontend
private BigDecimal amount;
private String currency;
private String itemDescription;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.techtorque.payment_service.dto.notification;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class NotificationRequest {
private String userId;
private String type; // INFO, WARNING, ERROR, SUCCESS
private String message;
private String details;
private String relatedEntityId;
private String relatedEntityType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ public class CreateInvoiceDto {
private List<InvoiceItemRequest> items;

private LocalDate dueDate;

private String notes;


// Part-payment option - if true, invoice will require 50% deposit before work and 50% after
private Boolean requiresDeposit;

@Data
@NoArgsConstructor
@AllArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,33 @@
@AllArgsConstructor
public class InvoiceResponseDto {
private String invoiceId;
private String invoiceNumber;
private String customerId;
private String serviceOrProjectId;
private BigDecimal amount;
private String customerName;
private String customerEmail;
private String serviceId;
private String projectId;
private List<InvoiceItemDto> items;
private BigDecimal subtotal;
private BigDecimal taxAmount;
private BigDecimal discountAmount;
private BigDecimal totalAmount;
private BigDecimal paidAmount;
private BigDecimal balanceAmount;
private InvoiceStatus status;
private LocalDate issueDate;
private String notes;
private LocalDate dueDate;
private LocalDateTime issuedAt;
private LocalDateTime paidAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private List<InvoiceItemDto> items;
private BigDecimal totalPaid;
private BigDecimal balance;

// Part-payment fields
private Boolean requiresDeposit;
private BigDecimal depositAmount;
private BigDecimal depositPaid;
private LocalDateTime depositPaidAt;
private BigDecimal finalAmount;
private BigDecimal finalPaid;
private LocalDateTime finalPaidAt;
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,30 @@ public class Invoice {

@Column(length = 2000)
private String notes;


// Part-payment tracking
@Column(nullable = false)
@Builder.Default
private Boolean requiresDeposit = false;

@Column(precision = 10, scale = 2)
private BigDecimal depositAmount; // 50% deposit

@Column(precision = 10, scale = 2)
private BigDecimal depositPaid;

@Column
private LocalDateTime depositPaidAt;

@Column(precision = 10, scale = 2)
private BigDecimal finalAmount; // 50% final payment

@Column(precision = 10, scale = 2)
private BigDecimal finalPaid;

@Column
private LocalDateTime finalPaidAt;

@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
package com.techtorque.payment_service.entity;
public enum PaymentMethod { CARD, CASH, BANK_TRANSFER }
public enum PaymentMethod { CARD, CASH, BANK_TRANSFER, ONLINE }
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.techtorque.payment_service.service;

import com.techtorque.payment_service.dto.notification.NotificationRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
* Client service for sending notifications to Notification Service
* Handles payment-related notifications (invoice created, payment due, payment received)
*/
@Service
@Slf4j
public class NotificationClient {

private final RestTemplate restTemplate;
private final String notificationServiceUrl;

public NotificationClient(RestTemplate restTemplate,
@Value("${notification.service.url:http://localhost:8088}") String notificationServiceUrl) {
this.restTemplate = restTemplate;
this.notificationServiceUrl = notificationServiceUrl;
}

/**
* Send notification to user asynchronously
* Non-blocking - failures won't affect payment operations
*/
public void sendNotification(NotificationRequest request) {
try {
log.info("Sending payment notification to user: {} - {}", request.getUserId(), request.getMessage());

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<NotificationRequest> entity = new HttpEntity<>(request, headers);

restTemplate.postForEntity(
notificationServiceUrl + "/api/v1/notifications/create",
entity,
Void.class
);

log.debug("Payment notification sent successfully");

} catch (Exception e) {
// Log error but don't throw - notification failure shouldn't break payment operations
log.error("Failed to send payment notification to user {}: {}", request.getUserId(), e.getMessage());
}
}

/**
* Helper method to send invoice notification
*/
public void sendInvoiceNotification(String userId, String type, String message,
String details, String invoiceId) {
NotificationRequest request = NotificationRequest.builder()
.userId(userId)
.type(type)
.message(message)
.details(details)
.relatedEntityId(invoiceId)
.relatedEntityType("INVOICE")
.build();

sendNotification(request);
}

/**
* Helper method to send payment notification
*/
public void sendPaymentNotification(String userId, String type, String message,
String details, String paymentId) {
NotificationRequest request = NotificationRequest.builder()
.userId(userId)
.type(type)
.message(message)
.details(details)
.relatedEntityId(paymentId)
.relatedEntityType("PAYMENT")
.build();

sendNotification(request);
}
}
Loading