diff --git a/src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentLinkSettingsSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentLinkSettingsSchemaFix.kt deleted file mode 100644 index b9de570..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentLinkSettingsSchemaFix.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.android.trisolarisserver.config - -import org.springframework.jdbc.core.JdbcTemplate -import org.springframework.stereotype.Component - -@Component -class PayuPaymentLinkSettingsSchemaFix( - private val jdbcTemplate: JdbcTemplate -) : PostgresSchemaFix(jdbcTemplate) { - - override fun runPostgres(jdbcTemplate: JdbcTemplate) { - val hasTable = jdbcTemplate.queryForObject( - """ - select count(*) - from information_schema.tables - where table_name = 'payu_payment_link_settings' - """.trimIndent(), - Int::class.java - ) ?: 0 - if (hasTable == 0) { - logger.info("Creating payu_payment_link_settings table") - jdbcTemplate.execute( - """ - create table payu_payment_link_settings ( - id uuid primary key, - property_id uuid not null unique references property(id) on delete cascade, - merchant_id text not null, - client_id text, - client_secret text, - access_token text, - token_expires_at timestamptz, - is_test boolean not null default false, - updated_at timestamptz not null - ) - """.trimIndent() - ) - } - ensureColumn("payu_payment_link_settings", "client_id", "text") - ensureColumn("payu_payment_link_settings", "client_secret", "text") - ensureColumn("payu_payment_link_settings", "access_token", "text") - ensureColumn("payu_payment_link_settings", "token_expires_at", "timestamptz") - - jdbcTemplate.execute("alter table payu_payment_link_settings alter column access_token drop not null") - } - - private fun ensureColumn(table: String, column: String, type: String) { - val exists = jdbcTemplate.queryForObject( - """ - select count(*) - from information_schema.columns - where table_name = '$table' - and column_name = '$column' - """.trimIndent(), - Int::class.java - ) ?: 0 - if (exists == 0) { - logger.info("Adding $table.$column column") - jdbcTemplate.execute("alter table $table add column $column $type") - } - } -} diff --git a/src/main/kotlin/com/android/trisolarisserver/config/PayuSettingsSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/PayuSettingsSchemaFix.kt deleted file mode 100644 index 85b8c45..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/config/PayuSettingsSchemaFix.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.android.trisolarisserver.config - -import org.springframework.jdbc.core.JdbcTemplate -import org.springframework.stereotype.Component - -@Component -class PayuSettingsSchemaFix( - private val jdbcTemplate: JdbcTemplate -) : PostgresSchemaFix(jdbcTemplate) { - - override fun runPostgres(jdbcTemplate: JdbcTemplate) { - val hasTable = jdbcTemplate.queryForObject( - """ - select count(*) - from information_schema.tables - where table_name = 'payu_settings' - """.trimIndent(), - Int::class.java - ) ?: 0 - if (hasTable == 0) { - logger.info("Creating payu_settings table") - jdbcTemplate.execute( - """ - create table payu_settings ( - id uuid primary key, - property_id uuid not null unique references property(id) on delete cascade, - merchant_key varchar not null, - salt_32 varchar, - salt_256 varchar, - base_url varchar not null, - is_test boolean not null default false, - use_salt_256 boolean not null default true, - updated_at timestamptz not null - ) - """.trimIndent() - ) - } - val hasIsTest = jdbcTemplate.queryForObject( - """ - select count(*) - from information_schema.columns - where table_name = 'payu_settings' - and column_name = 'is_test' - """.trimIndent(), - Int::class.java - ) ?: 0 - if (hasIsTest == 0) { - logger.info("Adding payu_settings.is_test column") - jdbcTemplate.execute("alter table payu_settings add column is_test boolean not null default false") - } - - logger.info("Ensuring payu_settings text column sizes") - jdbcTemplate.execute("alter table payu_settings alter column merchant_key type text") - jdbcTemplate.execute("alter table payu_settings alter column salt_32 type text") - jdbcTemplate.execute("alter table payu_settings alter column salt_256 type text") - jdbcTemplate.execute("alter table payu_settings alter column base_url type text") - } -} diff --git a/src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentAttemptSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/RazorpayPaymentAttemptSchemaFix.kt similarity index 62% rename from src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentAttemptSchemaFix.kt rename to src/main/kotlin/com/android/trisolarisserver/config/RazorpayPaymentAttemptSchemaFix.kt index 3b3add6..5cef0a1 100644 --- a/src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentAttemptSchemaFix.kt +++ b/src/main/kotlin/com/android/trisolarisserver/config/RazorpayPaymentAttemptSchemaFix.kt @@ -4,7 +4,7 @@ import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Component @Component -class PayuPaymentAttemptSchemaFix( +class RazorpayPaymentAttemptSchemaFix( private val jdbcTemplate: JdbcTemplate ) : PostgresSchemaFix(jdbcTemplate) { @@ -13,32 +13,24 @@ class PayuPaymentAttemptSchemaFix( """ select count(*) from information_schema.tables - where table_name = 'payu_payment_attempt' + where table_name = 'razorpay_payment_attempt' """.trimIndent(), Int::class.java ) ?: 0 if (hasTable == 0) { - logger.info("Creating payu_payment_attempt table") + logger.info("Creating razorpay_payment_attempt table") jdbcTemplate.execute( """ - create table payu_payment_attempt ( + create table razorpay_payment_attempt ( id uuid primary key, property_id uuid not null references property(id) on delete cascade, booking_id uuid references booking(id) on delete set null, + event varchar, status varchar, - unmapped_status varchar, amount bigint, currency varchar, - gateway_payment_id varchar, - gateway_txn_id varchar, - bank_ref_num varchar, - mode varchar, - pg_type varchar, - payer_vpa varchar, - payer_name varchar, - payment_source varchar, - error_code varchar, - error_message varchar, + payment_id varchar, + order_id varchar, payload text, received_at timestamptz not null ) diff --git a/src/main/kotlin/com/android/trisolarisserver/config/RazorpayPaymentLinkRequestSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/RazorpayPaymentLinkRequestSchemaFix.kt new file mode 100644 index 0000000..3c08987 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/config/RazorpayPaymentLinkRequestSchemaFix.kt @@ -0,0 +1,41 @@ +package com.android.trisolarisserver.config + +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Component + +@Component +class RazorpayPaymentLinkRequestSchemaFix( + private val jdbcTemplate: JdbcTemplate +) : PostgresSchemaFix(jdbcTemplate) { + + override fun runPostgres(jdbcTemplate: JdbcTemplate) { + val hasTable = jdbcTemplate.queryForObject( + """ + select count(*) + from information_schema.tables + where table_name = 'razorpay_payment_link_request' + """.trimIndent(), + Int::class.java + ) ?: 0 + if (hasTable == 0) { + logger.info("Creating razorpay_payment_link_request table") + jdbcTemplate.execute( + """ + create table razorpay_payment_link_request ( + id uuid primary key, + property_id uuid not null references property(id) on delete cascade, + booking_id uuid not null references booking(id) on delete cascade, + payment_link_id varchar, + amount bigint not null, + currency varchar not null, + status varchar not null, + short_url text, + request_payload text, + response_payload text, + created_at timestamptz not null + ) + """.trimIndent() + ) + } + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/config/PayuQrRequestSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/RazorpayQrRequestSchemaFix.kt similarity index 56% rename from src/main/kotlin/com/android/trisolarisserver/config/PayuQrRequestSchemaFix.kt rename to src/main/kotlin/com/android/trisolarisserver/config/RazorpayQrRequestSchemaFix.kt index f8b5705..4416658 100644 --- a/src/main/kotlin/com/android/trisolarisserver/config/PayuQrRequestSchemaFix.kt +++ b/src/main/kotlin/com/android/trisolarisserver/config/RazorpayQrRequestSchemaFix.kt @@ -4,7 +4,7 @@ import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Component @Component -class PayuQrRequestSchemaFix( +class RazorpayQrRequestSchemaFix( private val jdbcTemplate: JdbcTemplate ) : PostgresSchemaFix(jdbcTemplate) { @@ -13,22 +13,23 @@ class PayuQrRequestSchemaFix( """ select count(*) from information_schema.tables - where table_name = 'payu_qr_request' + where table_name = 'razorpay_qr_request' """.trimIndent(), Int::class.java ) ?: 0 if (hasTable == 0) { - logger.info("Creating payu_qr_request table") + logger.info("Creating razorpay_qr_request table") jdbcTemplate.execute( """ - create table payu_qr_request ( + create table razorpay_qr_request ( id uuid primary key, property_id uuid not null references property(id) on delete cascade, booking_id uuid not null references booking(id) on delete cascade, - txnid varchar not null, + qr_id varchar, amount bigint not null, currency varchar not null, status varchar not null, + image_url text, request_payload text, response_payload text, expiry_at timestamptz, @@ -36,25 +37,6 @@ class PayuQrRequestSchemaFix( ) """.trimIndent() ) - } else { - val hasExpiryAt = jdbcTemplate.queryForObject( - """ - select count(*) - from information_schema.columns - where table_name = 'payu_qr_request' - and column_name = 'expiry_at' - """.trimIndent(), - Int::class.java - ) ?: 0 - if (hasExpiryAt == 0) { - logger.info("Adding expiry_at to payu_qr_request table") - jdbcTemplate.execute( - """ - alter table payu_qr_request - add column expiry_at timestamptz - """.trimIndent() - ) - } } } } diff --git a/src/main/kotlin/com/android/trisolarisserver/config/RazorpaySettingsSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/RazorpaySettingsSchemaFix.kt new file mode 100644 index 0000000..647c268 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/config/RazorpaySettingsSchemaFix.kt @@ -0,0 +1,41 @@ +package com.android.trisolarisserver.config + +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Component + +@Component +class RazorpaySettingsSchemaFix( + private val jdbcTemplate: JdbcTemplate +) : PostgresSchemaFix(jdbcTemplate) { + + override fun runPostgres(jdbcTemplate: JdbcTemplate) { + val hasTable = jdbcTemplate.queryForObject( + """ + select count(*) + from information_schema.tables + where table_name = 'razorpay_settings' + """.trimIndent(), + Int::class.java + ) ?: 0 + if (hasTable == 0) { + logger.info("Creating razorpay_settings table") + jdbcTemplate.execute( + """ + create table razorpay_settings ( + id uuid primary key, + property_id uuid not null unique references property(id) on delete cascade, + key_id varchar not null, + key_secret varchar not null, + webhook_secret varchar, + is_test boolean not null default false, + updated_at timestamptz not null + ) + """.trimIndent() + ) + } + logger.info("Ensuring razorpay_settings text column sizes") + jdbcTemplate.execute("alter table razorpay_settings alter column key_id type text") + jdbcTemplate.execute("alter table razorpay_settings alter column key_secret type text") + jdbcTemplate.execute("alter table razorpay_settings alter column webhook_secret type text") + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/config/PayuWebhookLogSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/RazorpayWebhookLogSchemaFix.kt similarity index 82% rename from src/main/kotlin/com/android/trisolarisserver/config/PayuWebhookLogSchemaFix.kt rename to src/main/kotlin/com/android/trisolarisserver/config/RazorpayWebhookLogSchemaFix.kt index 9e2430d..43a7bb2 100644 --- a/src/main/kotlin/com/android/trisolarisserver/config/PayuWebhookLogSchemaFix.kt +++ b/src/main/kotlin/com/android/trisolarisserver/config/RazorpayWebhookLogSchemaFix.kt @@ -4,7 +4,7 @@ import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Component @Component -class PayuWebhookLogSchemaFix( +class RazorpayWebhookLogSchemaFix( private val jdbcTemplate: JdbcTemplate ) : PostgresSchemaFix(jdbcTemplate) { @@ -13,15 +13,15 @@ class PayuWebhookLogSchemaFix( """ select count(*) from information_schema.tables - where table_name = 'payu_webhook_log' + where table_name = 'razorpay_webhook_log' """.trimIndent(), Int::class.java ) ?: 0 if (hasTable == 0) { - logger.info("Creating payu_webhook_log table") + logger.info("Creating razorpay_webhook_log table") jdbcTemplate.execute( """ - create table payu_webhook_log ( + create table razorpay_webhook_log ( id uuid primary key, property_id uuid not null references property(id) on delete cascade, headers text, diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinkSettingsController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinkSettingsController.kt deleted file mode 100644 index 12ea025..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinkSettingsController.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.android.trisolarisserver.controller - -import com.android.trisolarisserver.component.PropertyAccess -import com.android.trisolarisserver.controller.dto.PayuPaymentLinkSettingsResponse -import com.android.trisolarisserver.controller.dto.PayuPaymentLinkSettingsUpsertRequest -import com.android.trisolarisserver.models.payment.PayuPaymentLinkSettings -import com.android.trisolarisserver.models.property.Role -import com.android.trisolarisserver.repo.PayuPaymentLinkSettingsRepo -import com.android.trisolarisserver.repo.PropertyRepo -import com.android.trisolarisserver.security.MyPrincipal -import org.springframework.http.HttpStatus -import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PutMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import org.springframework.web.server.ResponseStatusException -import java.time.OffsetDateTime -import java.util.UUID - -@RestController -@RequestMapping("/properties/{propertyId}/payu-payment-link-settings") -class PayuPaymentLinkSettingsController( - private val propertyAccess: PropertyAccess, - private val propertyRepo: PropertyRepo, - private val settingsRepo: PayuPaymentLinkSettingsRepo -) { - - @GetMapping - fun getSettings( - @PathVariable propertyId: UUID, - @AuthenticationPrincipal principal: MyPrincipal? - ): PayuPaymentLinkSettingsResponse { - requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER) - val settings = settingsRepo.findByPropertyId(propertyId) - if (settings == null) { - return PayuPaymentLinkSettingsResponse( - propertyId = propertyId, - configured = false, - merchantId = null, - isTest = false, - hasClientId = false, - hasClientSecret = false, - hasAccessToken = false - ) - } - return settings.toResponse() - } - - @PutMapping - fun upsertSettings( - @PathVariable propertyId: UUID, - @AuthenticationPrincipal principal: MyPrincipal?, - @RequestBody request: PayuPaymentLinkSettingsUpsertRequest - ): PayuPaymentLinkSettingsResponse { - requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER) - val property = propertyRepo.findById(propertyId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") - } - val merchantId = request.merchantId.trim().ifBlank { - throw ResponseStatusException(HttpStatus.BAD_REQUEST, "merchantId required") - } - val isTest = request.isTest ?: false - val existing = settingsRepo.findByPropertyId(propertyId) - val updated = if (existing == null) { - PayuPaymentLinkSettings( - property = property, - merchantId = merchantId, - clientId = request.clientId?.trim()?.ifBlank { null }, - clientSecret = request.clientSecret?.trim()?.ifBlank { null }, - accessToken = request.accessToken?.trim()?.ifBlank { null }, - isTest = isTest, - updatedAt = OffsetDateTime.now() - ) - } else { - existing.merchantId = merchantId - val oldClientId = existing.clientId - val oldClientSecret = existing.clientSecret - val oldIsTest = existing.isTest - if (request.clientId != null) existing.clientId = request.clientId.trim().ifBlank { null } - if (request.clientSecret != null) existing.clientSecret = request.clientSecret.trim().ifBlank { null } - if (request.accessToken != null) existing.accessToken = request.accessToken.trim().ifBlank { null } - existing.isTest = isTest - val credsChanged = (request.clientId != null && existing.clientId != oldClientId) || - (request.clientSecret != null && existing.clientSecret != oldClientSecret) || - oldIsTest != isTest - if (credsChanged) { - existing.accessToken = null - existing.tokenExpiresAt = null - } - existing.updatedAt = OffsetDateTime.now() - existing - } - return settingsRepo.save(updated).toResponse() - } -} - -private fun PayuPaymentLinkSettings.toResponse(): PayuPaymentLinkSettingsResponse { - val propertyId = property.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Property id missing") - return PayuPaymentLinkSettingsResponse( - propertyId = propertyId, - configured = true, - merchantId = merchantId, - isTest = isTest, - hasClientId = !clientId.isNullOrBlank(), - hasClientSecret = !clientSecret.isNullOrBlank(), - hasAccessToken = !accessToken.isNullOrBlank() - ) -} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinksController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinksController.kt deleted file mode 100644 index a30c940..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinksController.kt +++ /dev/null @@ -1,217 +0,0 @@ -package com.android.trisolarisserver.controller - -import com.android.trisolarisserver.component.PropertyAccess -import com.android.trisolarisserver.controller.dto.PayuPaymentLinkCreateRequest -import com.android.trisolarisserver.controller.dto.PayuPaymentLinkCreateResponse -import com.android.trisolarisserver.models.booking.BookingStatus -import com.android.trisolarisserver.models.property.Role -import com.android.trisolarisserver.db.repo.BookingRepo -import com.android.trisolarisserver.repo.PaymentRepo -import com.android.trisolarisserver.repo.PayuPaymentLinkSettingsRepo -import com.android.trisolarisserver.repo.RoomStayRepo -import com.android.trisolarisserver.security.MyPrincipal -import com.fasterxml.jackson.databind.ObjectMapper -import org.springframework.http.HttpEntity -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpStatus -import org.springframework.http.MediaType -import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.transaction.annotation.Transactional -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import org.springframework.web.client.RestTemplate -import org.springframework.web.server.ResponseStatusException -import java.time.OffsetDateTime -import java.util.UUID - -@RestController -@RequestMapping("/properties/{propertyId}/bookings/{bookingId}/payments/payu") -class PayuPaymentLinksController( - private val propertyAccess: PropertyAccess, - private val bookingRepo: BookingRepo, - private val roomStayRepo: RoomStayRepo, - private val paymentRepo: PaymentRepo, - private val settingsRepo: PayuPaymentLinkSettingsRepo, - private val restTemplate: RestTemplate, - private val objectMapper: ObjectMapper -) { - - @PostMapping("/link") - @Transactional - fun createPaymentLink( - @PathVariable propertyId: UUID, - @PathVariable bookingId: UUID, - @AuthenticationPrincipal principal: MyPrincipal?, - @RequestBody request: PayuPaymentLinkCreateRequest - ): PayuPaymentLinkCreateResponse { - requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.FINANCE) - - val booking = bookingRepo.findById(bookingId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found") - } - if (booking.property.id != propertyId) { - throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property") - } - if (booking.status == BookingStatus.CANCELLED || booking.status == BookingStatus.NO_SHOW || booking.status == BookingStatus.CHECKED_OUT) { - throw ResponseStatusException(HttpStatus.CONFLICT, "Booking closed") - } - - val settings = settingsRepo.findByPropertyId(propertyId) - ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "PayU payment link settings not configured") - - val stays = roomStayRepo.findByBookingId(bookingId) - val expectedPay = computeExpectedPay(stays, booking.property.timezone) - val collected = paymentRepo.sumAmountByBookingId(bookingId) - val pending = expectedPay - collected - if (pending <= 0) { - throw ResponseStatusException(HttpStatus.CONFLICT, "No pending amount") - } - - val isAmountFilledByCustomer = request.isAmountFilledByCustomer ?: false - val requestedAmount = request.amount?.takeIf { it > 0 } - if (!isAmountFilledByCustomer && requestedAmount == null) { - // default to pending if not open amount - } - if (requestedAmount != null && requestedAmount > pending) { - throw ResponseStatusException(HttpStatus.CONFLICT, "Amount exceeds pending") - } - val amountLong = if (isAmountFilledByCustomer) null else (requestedAmount ?: pending) - - val guest = booking.primaryGuest - ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Booking missing guest") - val customerName = guest.name?.trim()?.ifBlank { null } ?: "Guest" - val customerPhone = guest.phoneE164?.trim()?.ifBlank { null } - ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest phone missing") - val customerEmail = "guest-${bookingId.toString().substring(0, 8)}@hoteltrisolaris.in" - - val body = mutableMapOf( - "description" to (request.description?.trim()?.ifBlank { null } ?: "Booking $bookingId"), - "source" to "API", - "isPartialPaymentAllowed" to (request.isPartialPaymentAllowed ?: false), - "isAmountFilledByCustomer" to isAmountFilledByCustomer, - "customer" to mapOf( - "name" to customerName, - "email" to customerEmail, - "phone" to customerPhone - ), - "udf" to mapOf( - "udf1" to bookingId.toString(), - "udf2" to propertyId.toString(), - "udf3" to (request.udf3?.trim()?.ifBlank { null }), - "udf4" to (request.udf4?.trim()?.ifBlank { null }), - "udf5" to (request.udf5?.trim()?.ifBlank { null }) - ), - "viaEmail" to (request.viaEmail ?: false), - "viaSms" to (request.viaSms ?: false) - ) - request.successUrl?.trim()?.ifBlank { null }?.let { body["successURL"] = it } - request.failureUrl?.trim()?.ifBlank { null }?.let { body["failureURL"] = it } - if (amountLong != null) { - body["subAmount"] = amountLong - } - if (request.minAmountForCustomer != null) { - body["minAmountForCustomer"] = request.minAmountForCustomer - } - request.expiryDate?.trim()?.ifBlank { null }?.let { body["expiryDate"] = it } - - val accessToken = resolveAccessToken(settings) - settingsRepo.save(settings) - val headers = HttpHeaders().apply { - contentType = MediaType.APPLICATION_JSON - set("Authorization", "Bearer $accessToken") - set("merchantId", settings.merchantId) - set("mid", settings.merchantId) - } - val entity = HttpEntity(body, headers) - val response = restTemplate.postForEntity(resolveBaseUrl(settings.isTest), entity, String::class.java) - val responseBody = response.body ?: "" - if (!response.statusCode.is2xxSuccessful) { - throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "PayU request failed") - } - - val paymentLink = extractPaymentLink(responseBody) - return PayuPaymentLinkCreateResponse( - amount = amountLong ?: pending, - currency = booking.property.currency, - paymentLink = paymentLink, - payuResponse = responseBody - ) - } - - private fun resolveBaseUrl(isTest: Boolean): String { - return if (isTest) { - "https://uatoneapi.payu.in/payment-links" - } else { - "https://oneapi.payu.in/payment-links" - } - } - - private fun resolveAccessToken(settings: com.android.trisolarisserver.models.payment.PayuPaymentLinkSettings): String { - val now = OffsetDateTime.now() - val existing = settings.accessToken?.trim()?.ifBlank { null } - val expiresAt = settings.tokenExpiresAt - if (existing != null && expiresAt != null && expiresAt.isAfter(now.plusSeconds(60))) { - return existing - } - val clientId = settings.clientId?.trim()?.ifBlank { null } - val clientSecret = settings.clientSecret?.trim()?.ifBlank { null } - if (clientId == null || clientSecret == null) { - throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Payment link client credentials missing") - } - val tokenResponse = fetchAccessToken(settings.isTest, clientId, clientSecret) - settings.accessToken = tokenResponse.accessToken - settings.tokenExpiresAt = now.plusSeconds(tokenResponse.expiresIn.toLong()) - return tokenResponse.accessToken - } - - private data class TokenResponse(val accessToken: String, val expiresIn: Int) - - private fun fetchAccessToken(isTest: Boolean, clientId: String, clientSecret: String): TokenResponse { - val url = if (isTest) { - "https://uat-accounts.payu.in/oauth/token" - } else { - "https://accounts.payu.in/oauth/token" - } - val form = org.springframework.util.LinkedMultiValueMap().apply { - add("client_id", clientId) - add("client_secret", clientSecret) - add("grant_type", "client_credentials") - add("scope", "create_payment_links") - } - val headers = HttpHeaders().apply { - contentType = MediaType.APPLICATION_FORM_URLENCODED - } - val entity = HttpEntity(form, headers) - val response = restTemplate.postForEntity(url, entity, String::class.java) - val body = response.body ?: "" - if (!response.statusCode.is2xxSuccessful) { - throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "PayU token request failed") - } - return try { - val node = objectMapper.readTree(body) - val token = node.path("access_token").asText(null) - val expiresIn = node.path("expires_in").asInt(0) - if (token.isNullOrBlank() || expiresIn <= 0) { - throw IllegalStateException("Token missing") - } - TokenResponse(token, expiresIn) - } catch (ex: Exception) { - throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "PayU token parse failed") - } - } - - private fun extractPaymentLink(body: String): String? { - if (body.isBlank()) return null - return try { - val node = objectMapper.readTree(body) - val link = node.path("result").path("paymentLink").asText(null) - link?.takeIf { it.isNotBlank() } - } catch (_: Exception) { - null - } - } - -} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuQrPayments.kt b/src/main/kotlin/com/android/trisolarisserver/controller/PayuQrPayments.kt deleted file mode 100644 index 7666274..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuQrPayments.kt +++ /dev/null @@ -1,260 +0,0 @@ -package com.android.trisolarisserver.controller - -import com.android.trisolarisserver.component.PropertyAccess -import com.android.trisolarisserver.controller.dto.PayuQrGenerateRequest -import com.android.trisolarisserver.controller.dto.PayuQrGenerateResponse -import com.android.trisolarisserver.models.booking.BookingStatus -import com.android.trisolarisserver.models.payment.PayuQrRequest -import com.android.trisolarisserver.models.payment.PayuQrStatus -import com.android.trisolarisserver.models.property.Role -import com.android.trisolarisserver.db.repo.BookingRepo -import com.android.trisolarisserver.repo.PaymentRepo -import com.android.trisolarisserver.repo.PayuQrRequestRepo -import com.android.trisolarisserver.repo.PayuSettingsRepo -import com.android.trisolarisserver.repo.RoomStayRepo -import com.android.trisolarisserver.security.MyPrincipal -import org.springframework.http.HttpStatus -import org.springframework.http.MediaType -import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.transaction.annotation.Transactional -import org.springframework.util.LinkedMultiValueMap -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import org.springframework.web.client.RestTemplate -import org.springframework.web.server.ResponseStatusException -import jakarta.servlet.http.HttpServletRequest -import java.security.MessageDigest -import java.time.OffsetDateTime -import java.util.UUID - -@RestController -@RequestMapping("/properties/{propertyId}/bookings/{bookingId}/payments/payu") -class PayuQrPayments( - private val propertyAccess: PropertyAccess, - private val bookingRepo: BookingRepo, - private val roomStayRepo: RoomStayRepo, - private val paymentRepo: PaymentRepo, - private val payuSettingsRepo: PayuSettingsRepo, - private val payuQrRequestRepo: PayuQrRequestRepo, - private val restTemplate: RestTemplate -) { - private val defaultExpirySeconds = 30 * 60 - - @PostMapping("/qr") - @Transactional - fun generateQr( - @PathVariable propertyId: UUID, - @PathVariable bookingId: UUID, - @AuthenticationPrincipal principal: MyPrincipal?, - @RequestBody request: PayuQrGenerateRequest, - httpRequest: HttpServletRequest - ): PayuQrGenerateResponse { - requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.FINANCE) - - val booking = bookingRepo.findById(bookingId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found") - } - if (booking.property.id != propertyId) { - throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property") - } - if (booking.status == BookingStatus.CANCELLED || booking.status == BookingStatus.NO_SHOW || booking.status == BookingStatus.CHECKED_OUT) { - throw ResponseStatusException(HttpStatus.CONFLICT, "Booking closed") - } - - val settings = payuSettingsRepo.findByPropertyId(propertyId) - ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "PayU settings not configured") - val salt = pickSalt(settings) - - val stays = roomStayRepo.findByBookingId(bookingId) - val expectedPay = computeExpectedPay(stays, booking.property.timezone) - val collected = paymentRepo.sumAmountByBookingId(bookingId) - val pending = expectedPay - collected - if (pending <= 0) { - throw ResponseStatusException(HttpStatus.CONFLICT, "No pending amount") - } - val requestedAmount = request.amount?.takeIf { it > 0 } - if (requestedAmount != null && requestedAmount > pending) { - throw ResponseStatusException(HttpStatus.CONFLICT, "Amount exceeds pending") - } - val amountLong = requestedAmount ?: pending - val expirySeconds = request.expirySeconds - ?: request.expiryMinutes?.let { it * 60 } - ?: defaultExpirySeconds - - val existing = payuQrRequestRepo.findTopByBookingIdAndAmountAndCurrencyAndStatusOrderByCreatedAtDesc( - bookingId, - amountLong, - booking.property.currency, - PayuQrStatus.SENT - ) - if (existing != null) { - val expiryAt = existing.expiryAt - val responsePayload = existing.responsePayload - if (expiryAt != null && !responsePayload.isNullOrBlank()) { - val now = OffsetDateTime.now() - if (now.isBefore(expiryAt)) { - return PayuQrGenerateResponse( - txnid = existing.txnid, - amount = amountLong, - currency = booking.property.currency, - payuResponse = responsePayload - ) - } - } - } - - val txnid = "QR${bookingId.toString().substring(0, 8)}${System.currentTimeMillis()}" - val productInfo = "Booking $bookingId" - val guest = booking.primaryGuest - ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Booking missing guest") - val firstname = guest.name?.trim()?.ifBlank { null } ?: "Guest" - val phone = guest.phoneE164?.trim()?.ifBlank { null } - ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest phone missing") - val email = "guest-${bookingId.toString().substring(0, 8)}@hoteltrisolaris.in" - val amount = String.format("%.2f", amountLong.toDouble()) - - val udf1 = bookingId.toString() - val udf2 = propertyId.toString() - val udf3 = request.udf3?.trim()?.ifBlank { "" } ?: "" - val udf4 = request.udf4?.trim()?.ifBlank { "" } ?: "" - val udf5 = request.udf5?.trim()?.ifBlank { "" } ?: "" - val hash = sha512( - listOf( - settings.merchantKey, - txnid, - amount, - productInfo, - firstname, - email, - udf1, - udf2, - udf3, - udf4, - udf5, - "", - "", - "", - "", - "", - salt - ).joinToString("|") - ) - - val form = LinkedMultiValueMap().apply { - set("key", settings.merchantKey) - set("txnid", txnid) - set("amount", amount) - set("productinfo", productInfo) - set("firstname", firstname) - set("email", email) - set("phone", phone) - set("surl", buildReturnUrl(propertyId, true)) - set("furl", buildReturnUrl(propertyId, false)) - set("pg", "DBQR") - set("bankcode", "UPIDBQR") - set("hash", hash) - set("udf1", udf1) - set("udf2", udf2) - set("udf3", udf3) // always - set("udf4", udf4) // always - set("udf5", udf5) // always - set("txn_s2s_flow", "4") - val clientIp = request.clientIp?.trim()?.ifBlank { null } - ?: extractClientIp(httpRequest) - ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "clientIp required") - val deviceInfo = request.deviceInfo?.trim()?.ifBlank { null } - ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "deviceInfo required") - set("s2s_client_ip", clientIp) - set("s2s_device_info", deviceInfo) - set("expiry_time", expirySeconds.toString()) - request.address1?.trim()?.ifBlank { null }?.let { add("address1", it) } - request.address2?.trim()?.ifBlank { null }?.let { add("address2", it) } - request.city?.trim()?.ifBlank { null }?.let { add("city", it) } - request.state?.trim()?.ifBlank { null }?.let { add("state", it) } - request.country?.trim()?.ifBlank { null }?.let { add("country", it) } - request.zipcode?.trim()?.ifBlank { null }?.let { add("zipcode", it) } - } - - val requestPayload = form.entries.joinToString("&") { entry -> - entry.value.joinToString("&") { value -> "${entry.key}=$value" } - } - - val createdAt = OffsetDateTime.now() - val expiryAt = createdAt.plusSeconds(expirySeconds.toLong()) - val record = payuQrRequestRepo.save( - PayuQrRequest( - property = booking.property, - booking = booking, - txnid = txnid, - amount = amountLong, - currency = booking.property.currency, - status = PayuQrStatus.CREATED, - requestPayload = requestPayload, - expiryAt = expiryAt, - createdAt = createdAt - ) - ) - - val headers = org.springframework.http.HttpHeaders().apply { - contentType = MediaType.APPLICATION_FORM_URLENCODED - } - val entity = org.springframework.http.HttpEntity(form, headers) - val response = restTemplate.postForEntity(resolveBaseUrl(settings), entity, String::class.java) - val responseBody = response.body ?: "" - - record.responsePayload = responseBody - record.status = if (response.statusCode.is2xxSuccessful) PayuQrStatus.SENT else PayuQrStatus.FAILED - payuQrRequestRepo.save(record) - - if (!response.statusCode.is2xxSuccessful) { - throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "PayU request failed") - } - - return PayuQrGenerateResponse( - txnid = txnid, - amount = amountLong, - currency = booking.property.currency, - payuResponse = responseBody - ) - } - - private fun pickSalt(settings: com.android.trisolarisserver.models.payment.PayuSettings): String { - val salt = if (settings.useSalt256) settings.salt256 else settings.salt32 - return salt?.trim()?.ifBlank { null } - ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "PayU salt missing") - } - - private fun buildReturnUrl(propertyId: UUID, success: Boolean): String { - val path = if (success) "success" else "failure" - return "https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/$path" - } - - private fun resolveBaseUrl(settings: com.android.trisolarisserver.models.payment.PayuSettings): String { - return if (settings.isTest) { - "https://test.payu.in/_payment" - } else { - "https://secure.payu.in/_payment" - } - } - - private fun sha512(input: String): String { - val bytes = MessageDigest.getInstance("SHA-512").digest(input.toByteArray(Charsets.UTF_8)) - return bytes.joinToString("") { "%02x".format(it) } - } - - private fun extractClientIp(request: HttpServletRequest): String? { - val forwarded = request.getHeader("X-Forwarded-For") - ?.split(",") - ?.firstOrNull() - ?.trim() - ?.ifBlank { null } - if (forwarded != null) return forwarded - val realIp = request.getHeader("X-Real-IP")?.trim()?.ifBlank { null } - if (realIp != null) return realIp - return request.remoteAddr?.trim()?.ifBlank { null } - } - -} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuSettingsController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/PayuSettingsController.kt deleted file mode 100644 index f566fe9..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuSettingsController.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.android.trisolarisserver.controller - -import com.android.trisolarisserver.component.PropertyAccess -import com.android.trisolarisserver.controller.dto.PayuSettingsUpsertRequest -import com.android.trisolarisserver.controller.dto.PayuSettingsResponse -import com.android.trisolarisserver.models.payment.PayuSettings -import com.android.trisolarisserver.models.property.Role -import com.android.trisolarisserver.repo.PayuSettingsRepo -import com.android.trisolarisserver.repo.PropertyRepo -import com.android.trisolarisserver.security.MyPrincipal -import org.springframework.http.HttpStatus -import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PutMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import org.springframework.web.server.ResponseStatusException -import java.time.OffsetDateTime -import java.util.UUID - -@RestController -@RequestMapping("/properties/{propertyId}/payu-settings") -class PayuSettingsController( - private val propertyAccess: PropertyAccess, - private val propertyRepo: PropertyRepo, - private val payuSettingsRepo: PayuSettingsRepo -) { - - @GetMapping - fun getSettings( - @PathVariable propertyId: UUID, - @AuthenticationPrincipal principal: MyPrincipal? - ): PayuSettingsResponse { - requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER) - val settings = payuSettingsRepo.findByPropertyId(propertyId) - if (settings == null) { - return PayuSettingsResponse( - propertyId = propertyId, - configured = false, - merchantKey = null, - isTest = false, - useSalt256 = true, - hasSalt32 = false, - hasSalt256 = false - ) - } - return settings.toResponse() - } - - @PutMapping - fun upsertSettings( - @PathVariable propertyId: UUID, - @AuthenticationPrincipal principal: MyPrincipal?, - @RequestBody request: PayuSettingsUpsertRequest - ): PayuSettingsResponse { - requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER) - val property = propertyRepo.findById(propertyId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") - } - val key = request.merchantKey.trim().ifBlank { - throw ResponseStatusException(HttpStatus.BAD_REQUEST, "merchantKey required") - } - val isTest = request.isTest ?: false - val baseUrl = if (isTest) { - "https://test.payu.in/_payment" - } else { - "https://secure.payu.in/_payment" - } - val existing = payuSettingsRepo.findByPropertyId(propertyId) - val updated = if (existing == null) { - PayuSettings( - property = property, - merchantKey = key, - salt32 = request.salt32?.trim()?.ifBlank { null }, - salt256 = request.salt256?.trim()?.ifBlank { null }, - baseUrl = baseUrl, - isTest = isTest, - useSalt256 = request.useSalt256 ?: true, - updatedAt = OffsetDateTime.now() - ) - } else { - existing.merchantKey = key - if (request.salt32 != null) existing.salt32 = request.salt32.trim().ifBlank { null } - if (request.salt256 != null) existing.salt256 = request.salt256.trim().ifBlank { null } - existing.baseUrl = baseUrl - existing.isTest = isTest - if (request.useSalt256 != null) existing.useSalt256 = request.useSalt256 - existing.updatedAt = OffsetDateTime.now() - existing - } - return payuSettingsRepo.save(updated).toResponse() - } -} - -private fun PayuSettings.toResponse(): PayuSettingsResponse { - val propertyId = property.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Property id missing") - return PayuSettingsResponse( - propertyId = propertyId, - configured = true, - merchantKey = merchantKey, - isTest = isTest, - useSalt256 = useSalt256, - hasSalt32 = !salt32.isNullOrBlank(), - hasSalt256 = !salt256.isNullOrBlank() - ) -} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt b/src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt deleted file mode 100644 index 8860fa6..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt +++ /dev/null @@ -1,181 +0,0 @@ -package com.android.trisolarisserver.controller - -import com.android.trisolarisserver.models.booking.Payment -import com.android.trisolarisserver.models.booking.PaymentMethod -import com.android.trisolarisserver.models.payment.PayuPaymentAttempt -import com.android.trisolarisserver.models.payment.PayuWebhookLog -import com.android.trisolarisserver.db.repo.BookingRepo -import com.android.trisolarisserver.repo.PayuPaymentAttemptRepo -import com.android.trisolarisserver.repo.PayuWebhookLogRepo -import com.android.trisolarisserver.repo.PaymentRepo -import com.android.trisolarisserver.repo.PropertyRepo -import jakarta.servlet.http.HttpServletRequest -import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController -import org.springframework.web.server.ResponseStatusException -import org.springframework.transaction.annotation.Transactional -import java.math.BigDecimal -import java.net.URLDecoder -import java.time.OffsetDateTime -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter -import java.util.UUID - -@RestController -@RequestMapping("/properties/{propertyId}/payu/webhook") -class PayuWebhookCapture( - private val propertyRepo: PropertyRepo, - private val bookingRepo: BookingRepo, - private val paymentRepo: PaymentRepo, - private val payuPaymentAttemptRepo: PayuPaymentAttemptRepo, - private val payuWebhookLogRepo: PayuWebhookLogRepo -) { - - @PostMapping - @ResponseStatus(HttpStatus.NO_CONTENT) - @Transactional - fun capture( - @PathVariable propertyId: UUID, - @RequestBody(required = false) body: String?, - request: HttpServletRequest - ) { - val property = propertyRepo.findById(propertyId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") - } - val headers = request.headerNames.toList().associateWith { request.getHeader(it) } - val headersText = headers.entries.joinToString("\n") { (k, v) -> "$k: $v" } - payuWebhookLogRepo.save( - PayuWebhookLog( - property = property, - headers = headersText, - payload = body, - contentType = request.contentType, - receivedAt = OffsetDateTime.now() - ) - ) - - if (body.isNullOrBlank()) return - val data = parseFormBody(body) - val status = data["status"]?.lowercase() ?: data["unmappedstatus"]?.lowercase() - val isSuccess = status == "success" || status == "captured" - val isRefund = status == "refund" || status == "refunded" - val bookingId = data["udf1"]?.let { runCatching { UUID.fromString(it) }.getOrNull() } - val booking = bookingId?.let { bookingRepo.findById(it).orElse(null) } - if (booking != null && booking.property.id != propertyId) return - - val amountRaw = data["amount"]?.ifBlank { null } ?: data["net_amount_debit"]?.ifBlank { null } - val amount = parseAmount(amountRaw) - val gatewayPaymentId = data["mihpayid"]?.ifBlank { null } - val gatewayTxnId = data["txnid"]?.ifBlank { null } - val bankRef = data["bank_ref_num"]?.ifBlank { null } ?: data["bank_ref_no"]?.ifBlank { null } - val mode = data["mode"]?.ifBlank { null } - val pgType = data["PG_TYPE"]?.ifBlank { null } - val payerVpa = data["field3"]?.ifBlank { null } - val payerName = data["field6"]?.ifBlank { null } - val paymentSource = data["payment_source"]?.ifBlank { null } - val errorCode = data["error"]?.ifBlank { null } - val errorMessage = data["error_Message"]?.ifBlank { null } - val receivedAt = parseAddedOn(data["addedon"], booking?.property?.timezone) - - payuPaymentAttemptRepo.save( - PayuPaymentAttempt( - property = property, - booking = booking, - status = status, - unmappedStatus = data["unmappedstatus"]?.ifBlank { null }, - amount = amount, - currency = booking?.property?.currency ?: property.currency, - gatewayPaymentId = gatewayPaymentId, - gatewayTxnId = gatewayTxnId, - bankRefNum = bankRef, - mode = mode, - pgType = pgType, - payerVpa = payerVpa, - payerName = payerName, - paymentSource = paymentSource, - errorCode = errorCode, - errorMessage = errorMessage, - payload = body, - receivedAt = receivedAt - ) - ) - - if (!isSuccess && !isRefund) return - if (booking == null) return - - if (gatewayPaymentId != null && paymentRepo.findByGatewayPaymentId(gatewayPaymentId) != null) return - if (gatewayPaymentId == null && gatewayTxnId != null && paymentRepo.findByGatewayTxnId(gatewayTxnId) != null) return - - val signedAmount = amount?.let { if (isRefund) -it else it } ?: return - val notes = buildString { - append("payu status=").append(status) - gatewayTxnId?.let { append(" txnid=").append(it) } - bankRef?.let { append(" bank_ref=").append(it) } - } - - paymentRepo.save( - Payment( - property = booking.property, - booking = booking, - amount = signedAmount, - currency = booking.property.currency, - method = PaymentMethod.ONLINE, - gatewayPaymentId = gatewayPaymentId, - gatewayTxnId = gatewayTxnId, - bankRefNum = bankRef, - mode = mode, - pgType = pgType, - payerVpa = payerVpa, - payerName = payerName, - paymentSource = paymentSource, - reference = gatewayPaymentId?.let { "payu:$it" } ?: gatewayTxnId?.let { "payu:$it" }, - notes = notes, - receivedAt = receivedAt - ) - ) - } - - private fun parseFormBody(body: String): Map { - return body.split("&") - .mapNotNull { pair -> - val idx = pair.indexOf("=") - if (idx <= 0) return@mapNotNull null - val key = URLDecoder.decode(pair.substring(0, idx), "UTF-8") - val value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8") - key to value - } - .toMap() - } - - private fun parseAmount(value: String?): Long? { - if (value.isNullOrBlank()) return null - return try { - val bd = BigDecimal(value.trim()).setScale(0, java.math.RoundingMode.HALF_UP) - bd.longValueExact() - } catch (_: Exception) { - null - } - } - - private fun parseAddedOn(value: String?, timezone: String?): OffsetDateTime { - if (value.isNullOrBlank()) return OffsetDateTime.now() - return try { - val local = LocalDateTime.parse(value.trim(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - val zone = try { - if (timezone.isNullOrBlank()) ZoneId.of("Asia/Kolkata") else ZoneId.of(timezone) - } catch (_: Exception) { - ZoneId.of("Asia/Kolkata") - } - local.atZone(zone).toOffsetDateTime() - } catch (_: Exception) { - OffsetDateTime.now(ZoneOffset.UTC) - } - } -} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentLinksController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentLinksController.kt new file mode 100644 index 0000000..4ed1572 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentLinksController.kt @@ -0,0 +1,172 @@ +package com.android.trisolarisserver.controller + +import com.android.trisolarisserver.component.PropertyAccess +import com.android.trisolarisserver.controller.dto.RazorpayPaymentLinkCreateRequest +import com.android.trisolarisserver.controller.dto.RazorpayPaymentLinkCreateResponse +import com.android.trisolarisserver.models.booking.BookingStatus +import com.android.trisolarisserver.models.payment.RazorpayPaymentLinkRequest +import com.android.trisolarisserver.models.property.Role +import com.android.trisolarisserver.db.repo.BookingRepo +import com.android.trisolarisserver.repo.RazorpayPaymentLinkRequestRepo +import com.android.trisolarisserver.repo.RazorpaySettingsRepo +import com.android.trisolarisserver.security.MyPrincipal +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.client.RestTemplate +import org.springframework.web.server.ResponseStatusException +import org.springframework.http.HttpStatus +import java.time.OffsetDateTime +import java.util.Base64 +import java.util.UUID + +@RestController +@RequestMapping("/properties/{propertyId}/bookings/{bookingId}/payments/razorpay") +class RazorpayPaymentLinksController( + private val propertyAccess: PropertyAccess, + private val bookingRepo: BookingRepo, + private val settingsRepo: RazorpaySettingsRepo, + private val linkRequestRepo: RazorpayPaymentLinkRequestRepo, + private val restTemplate: RestTemplate, + private val objectMapper: ObjectMapper +) { + + @PostMapping("/payment-link") + fun createPaymentLink( + @PathVariable propertyId: UUID, + @PathVariable bookingId: UUID, + @RequestBody request: RazorpayPaymentLinkCreateRequest, + principal: MyPrincipal? + ): RazorpayPaymentLinkCreateResponse { + requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF) + val booking = bookingRepo.findById(bookingId).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found") + } + if (booking.property.id != propertyId) { + throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property") + } + if (booking.status == BookingStatus.CANCELLED || booking.status == BookingStatus.NO_SHOW) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Booking is not active") + } + + val settings = settingsRepo.findByPropertyId(propertyId) + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay settings not configured") + + val amount = request.amount ?: 0L + if (amount <= 0) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "amount must be > 0") + } + val currency = booking.property.currency + + val existing = linkRequestRepo.findTopByBookingIdAndAmountAndCurrencyAndStatusOrderByCreatedAtDesc( + bookingId, + amount, + currency, + "created" + ) + if (existing != null && existing.paymentLinkId != null) { + return RazorpayPaymentLinkCreateResponse( + amount = existing.amount, + currency = existing.currency, + paymentLink = existing.shortUrl, + razorpayResponse = existing.responsePayload ?: "{}" + ) + } + + val notes = mapOf( + "bookingId" to bookingId.toString(), + "propertyId" to propertyId.toString() + ) + + val payload = linkedMapOf( + "amount" to amount * 100, + "currency" to currency, + "description" to (request.description ?: "Booking $bookingId"), + "notes" to notes, + "reference_id" to bookingId.toString() + ) + parseExpiryEpoch(request.expiryDate)?.let { payload["expire_by"] = it } + request.isPartialPaymentAllowed?.let { payload["partial_payment"] = it } + request.minAmountForCustomer?.let { payload["first_min_partial_amount"] = it * 100 } + request.successUrl?.let { payload["callback_url"] = it } + if (payload["callback_url"] == null && request.failureUrl != null) { + payload["callback_url"] = request.failureUrl + } + val notify = linkedMapOf() + request.viaEmail?.let { notify["email"] = it } + request.viaSms?.let { notify["sms"] = it } + if (notify.isNotEmpty()) { + payload["notify"] = notify + } + + val requestPayload = objectMapper.writeValueAsString(payload) + val response = postJson(resolveBaseUrl(settings.isTest) + "/payment_links", settings, requestPayload) + val body = response.body ?: "{}" + val node = objectMapper.readTree(body) + val linkId = node.path("id").asText(null) + val status = node.path("status").asText("unknown") + val shortUrl = node.path("short_url").asText(null) + + val record = linkRequestRepo.save( + RazorpayPaymentLinkRequest( + property = booking.property, + booking = booking, + paymentLinkId = linkId, + amount = amount, + currency = currency, + status = status, + shortUrl = shortUrl, + requestPayload = requestPayload, + responsePayload = body, + createdAt = OffsetDateTime.now() + ) + ) + + if (!response.statusCode.is2xxSuccessful) { + record.status = "failed" + linkRequestRepo.save(record) + throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "Razorpay request failed") + } + + return RazorpayPaymentLinkCreateResponse( + amount = amount, + currency = currency, + paymentLink = shortUrl, + razorpayResponse = body + ) + } + + private fun postJson(url: String, settings: com.android.trisolarisserver.models.payment.RazorpaySettings, json: String): ResponseEntity { + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + headers.set(HttpHeaders.AUTHORIZATION, basicAuth(settings.keyId, settings.keySecret)) + return restTemplate.exchange(url, HttpMethod.POST, HttpEntity(json, headers), String::class.java) + } + + private fun resolveBaseUrl(isTest: Boolean): String { + return if (isTest) { + "https://api.razorpay.com/v1" + } else { + "https://api.razorpay.com/v1" + } + } + + private fun basicAuth(keyId: String, keySecret: String): String { + val raw = "$keyId:$keySecret" + val encoded = Base64.getEncoder().encodeToString(raw.toByteArray()) + return "Basic $encoded" + } + + private fun parseExpiryEpoch(value: String?): Long? { + if (value.isNullOrBlank()) return null + return value.trim().toLongOrNull() + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayQrPayments.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayQrPayments.kt new file mode 100644 index 0000000..7bcfef4 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayQrPayments.kt @@ -0,0 +1,162 @@ +package com.android.trisolarisserver.controller + +import com.android.trisolarisserver.component.PropertyAccess +import com.android.trisolarisserver.controller.dto.RazorpayQrGenerateRequest +import com.android.trisolarisserver.controller.dto.RazorpayQrGenerateResponse +import com.android.trisolarisserver.models.booking.BookingStatus +import com.android.trisolarisserver.models.payment.RazorpayQrRequest +import com.android.trisolarisserver.models.property.Role +import com.android.trisolarisserver.db.repo.BookingRepo +import com.android.trisolarisserver.repo.RazorpayQrRequestRepo +import com.android.trisolarisserver.repo.RazorpaySettingsRepo +import com.android.trisolarisserver.security.MyPrincipal +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.client.RestTemplate +import org.springframework.web.server.ResponseStatusException +import org.springframework.http.HttpStatus +import java.time.OffsetDateTime +import java.util.Base64 +import java.util.UUID + +@RestController +@RequestMapping("/properties/{propertyId}/bookings/{bookingId}/payments/razorpay") +class RazorpayQrPayments( + private val propertyAccess: PropertyAccess, + private val bookingRepo: BookingRepo, + private val settingsRepo: RazorpaySettingsRepo, + private val qrRequestRepo: RazorpayQrRequestRepo, + private val restTemplate: RestTemplate, + private val objectMapper: ObjectMapper +) { + + @PostMapping("/qr") + fun createQr( + @PathVariable propertyId: UUID, + @PathVariable bookingId: UUID, + @RequestBody request: RazorpayQrGenerateRequest, + principal: MyPrincipal? + ): RazorpayQrGenerateResponse { + requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF) + val booking = bookingRepo.findById(bookingId).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found") + } + if (booking.property.id != propertyId) { + throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property") + } + if (booking.status == BookingStatus.CANCELLED || booking.status == BookingStatus.NO_SHOW) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Booking is not active") + } + + val settings = settingsRepo.findByPropertyId(propertyId) + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay settings not configured") + + val amount = request.amount ?: 0L + if (amount <= 0) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "amount must be > 0") + } + val currency = booking.property.currency + + val existing = qrRequestRepo.findTopByBookingIdAndAmountAndCurrencyAndStatusOrderByCreatedAtDesc( + bookingId, + amount, + currency, + "active" + ) + if (existing != null && existing.qrId != null) { + return RazorpayQrGenerateResponse( + qrId = existing.qrId, + amount = existing.amount, + currency = existing.currency, + imageUrl = existing.imageUrl, + razorpayResponse = existing.responsePayload ?: "{}" + ) + } + + val expirySeconds = request.expirySeconds ?: request.expiryMinutes?.let { it * 60 } + val expiresAt = expirySeconds?.let { OffsetDateTime.now().plusSeconds(it.toLong()) } + val notes = mapOf( + "bookingId" to bookingId.toString(), + "propertyId" to propertyId.toString() + ) + + val payload = linkedMapOf( + "type" to "upi_qr", + "name" to "Booking $bookingId", + "usage" to "single_use", + "fixed_amount" to true, + "payment_amount" to amount * 100, + "notes" to notes + ) + if (expirySeconds != null) { + payload["close_by"] = OffsetDateTime.now().plusSeconds(expirySeconds.toLong()).toEpochSecond() + } + + val requestPayload = objectMapper.writeValueAsString(payload) + val response = postJson(resolveBaseUrl(settings.isTest) + "/qr_codes", settings, requestPayload) + val body = response.body ?: "{}" + val node = objectMapper.readTree(body) + val qrId = node.path("id").asText(null) + val status = node.path("status").asText("unknown") + val imageUrl = node.path("image_url").asText(null) + + val record = qrRequestRepo.save( + RazorpayQrRequest( + property = booking.property, + booking = booking, + qrId = qrId, + amount = amount, + currency = currency, + status = status, + imageUrl = imageUrl, + requestPayload = requestPayload, + responsePayload = body, + expiryAt = expiresAt + ) + ) + + if (!response.statusCode.is2xxSuccessful) { + record.status = "failed" + qrRequestRepo.save(record) + throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "Razorpay request failed") + } + + return RazorpayQrGenerateResponse( + qrId = qrId, + amount = amount, + currency = currency, + imageUrl = imageUrl, + razorpayResponse = body + ) + } + + private fun postJson(url: String, settings: com.android.trisolarisserver.models.payment.RazorpaySettings, json: String): ResponseEntity { + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + headers.set(HttpHeaders.AUTHORIZATION, basicAuth(settings.keyId, settings.keySecret)) + return restTemplate.exchange(url, HttpMethod.POST, HttpEntity(json, headers), String::class.java) + } + + private fun resolveBaseUrl(isTest: Boolean): String { + return if (isTest) { + "https://api.razorpay.com/v1" + } else { + "https://api.razorpay.com/v1" + } + } + + private fun basicAuth(keyId: String, keySecret: String): String { + val raw = "$keyId:$keySecret" + val encoded = Base64.getEncoder().encodeToString(raw.toByteArray()) + return "Basic $encoded" + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuReturnController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayReturnController.kt similarity index 55% rename from src/main/kotlin/com/android/trisolarisserver/controller/PayuReturnController.kt rename to src/main/kotlin/com/android/trisolarisserver/controller/RazorpayReturnController.kt index 883d13c..0892d1c 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuReturnController.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayReturnController.kt @@ -1,7 +1,6 @@ package com.android.trisolarisserver.controller import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping @@ -10,30 +9,17 @@ import org.springframework.web.bind.annotation.RestController import java.util.UUID @RestController -@RequestMapping("/properties/{propertyId}/payu/return") -class PayuReturnController { - - @GetMapping("/success") - @ResponseStatus(HttpStatus.NO_CONTENT) - fun success(@PathVariable propertyId: UUID) { - // PayU redirect target; no-op. - } - +@RequestMapping("/properties/{propertyId}/razorpay/return") +class RazorpayReturnController { @PostMapping("/success") @ResponseStatus(HttpStatus.NO_CONTENT) - fun successPost(@PathVariable propertyId: UUID) { - // PayU redirect target; no-op. - } - - @GetMapping("/failure") - @ResponseStatus(HttpStatus.NO_CONTENT) - fun failure(@PathVariable propertyId: UUID) { - // PayU redirect target; no-op. + fun success(@PathVariable propertyId: UUID) { + // Razorpay redirect target; no-op. } @PostMapping("/failure") @ResponseStatus(HttpStatus.NO_CONTENT) - fun failurePost(@PathVariable propertyId: UUID) { - // PayU redirect target; no-op. + fun failure(@PathVariable propertyId: UUID) { + // Razorpay redirect target; no-op. } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpaySettingsController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpaySettingsController.kt new file mode 100644 index 0000000..0aa8b95 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpaySettingsController.kt @@ -0,0 +1,92 @@ +package com.android.trisolarisserver.controller + +import com.android.trisolarisserver.component.PropertyAccess +import com.android.trisolarisserver.controller.dto.RazorpaySettingsResponse +import com.android.trisolarisserver.controller.dto.RazorpaySettingsUpsertRequest +import com.android.trisolarisserver.models.payment.RazorpaySettings +import com.android.trisolarisserver.models.property.Role +import com.android.trisolarisserver.repo.PropertyRepo +import com.android.trisolarisserver.repo.RazorpaySettingsRepo +import com.android.trisolarisserver.security.MyPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ResponseStatusException +import org.springframework.http.HttpStatus +import java.time.OffsetDateTime +import java.util.UUID + +@RestController +@RequestMapping("/properties/{propertyId}/razorpay-settings") +class RazorpaySettingsController( + private val propertyAccess: PropertyAccess, + private val propertyRepo: PropertyRepo, + private val settingsRepo: RazorpaySettingsRepo +) { + + @GetMapping + fun getSettings( + @PathVariable propertyId: UUID, + principal: MyPrincipal? + ): RazorpaySettingsResponse { + requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER) + val settings = settingsRepo.findByPropertyId(propertyId) + return if (settings == null) { + RazorpaySettingsResponse( + propertyId = propertyId, + configured = false, + keyId = null, + isTest = false, + hasKeySecret = false, + hasWebhookSecret = false + ) + } else { + settings.toResponse() + } + } + + @PutMapping + fun upsertSettings( + @PathVariable propertyId: UUID, + principal: MyPrincipal?, + @RequestBody request: RazorpaySettingsUpsertRequest + ): RazorpaySettingsResponse { + requireRole(propertyAccess, propertyId, principal, Role.ADMIN) + val property = propertyRepo.findById(propertyId).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") + } + val existing = settingsRepo.findByPropertyId(propertyId) + val updated = if (existing == null) { + RazorpaySettings( + property = property, + keyId = request.keyId.trim(), + keySecret = request.keySecret.trim(), + webhookSecret = request.webhookSecret?.trim(), + isTest = request.isTest ?: false, + updatedAt = OffsetDateTime.now() + ) + } else { + existing.keyId = request.keyId.trim() + existing.keySecret = request.keySecret.trim() + existing.webhookSecret = request.webhookSecret?.trim() + request.isTest?.let { existing.isTest = it } + existing.updatedAt = OffsetDateTime.now() + existing + } + return settingsRepo.save(updated).toResponse() + } +} + +private fun RazorpaySettings.toResponse(): RazorpaySettingsResponse { + return RazorpaySettingsResponse( + propertyId = property.id!!, + configured = true, + keyId = keyId, + isTest = isTest, + hasKeySecret = keySecret.isNotBlank(), + hasWebhookSecret = !webhookSecret.isNullOrBlank() + ) +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayWebhookCapture.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayWebhookCapture.kt new file mode 100644 index 0000000..a8179fd --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayWebhookCapture.kt @@ -0,0 +1,144 @@ +package com.android.trisolarisserver.controller + +import com.android.trisolarisserver.db.repo.BookingRepo +import com.android.trisolarisserver.models.booking.Payment +import com.android.trisolarisserver.models.booking.PaymentMethod +import com.android.trisolarisserver.models.payment.RazorpayPaymentAttempt +import com.android.trisolarisserver.models.payment.RazorpayWebhookLog +import com.android.trisolarisserver.repo.PaymentRepo +import com.android.trisolarisserver.repo.PropertyRepo +import com.android.trisolarisserver.repo.RazorpayPaymentAttemptRepo +import com.android.trisolarisserver.repo.RazorpaySettingsRepo +import com.android.trisolarisserver.repo.RazorpayWebhookLogRepo +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ResponseStatusException +import java.time.OffsetDateTime +import java.util.UUID +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +@RestController +@RequestMapping("/properties/{propertyId}/razorpay/webhook") +class RazorpayWebhookCapture( + private val propertyRepo: PropertyRepo, + private val bookingRepo: BookingRepo, + private val paymentRepo: PaymentRepo, + private val settingsRepo: RazorpaySettingsRepo, + private val razorpayPaymentAttemptRepo: RazorpayPaymentAttemptRepo, + private val razorpayWebhookLogRepo: RazorpayWebhookLogRepo, + private val objectMapper: ObjectMapper +) { + + @PostMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + @Transactional + fun capture( + @PathVariable propertyId: UUID, + @RequestBody(required = false) body: String?, + request: HttpServletRequest + ) { + val property = propertyRepo.findById(propertyId).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") + } + val settings = settingsRepo.findByPropertyId(propertyId) + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay settings not configured") + + val headers = request.headerNames.toList().associateWith { request.getHeader(it) } + val headersText = headers.entries.joinToString("\n") { (k, v) -> "$k: $v" } + razorpayWebhookLogRepo.save( + RazorpayWebhookLog( + property = property, + headers = headersText, + payload = body, + contentType = request.contentType, + receivedAt = OffsetDateTime.now() + ) + ) + + if (body.isNullOrBlank()) return + val signature = request.getHeader("X-Razorpay-Signature") + ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing signature") + val secret = settings.webhookSecret + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Webhook secret not configured") + if (!verifySignature(body, secret, signature)) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid signature") + } + + val root = objectMapper.readTree(body) + val event = root.path("event").asText(null) + val paymentEntity = root.path("payload").path("payment").path("entity") + val orderEntity = root.path("payload").path("order").path("entity") + val paymentId = paymentEntity.path("id").asText(null) + val orderId = paymentEntity.path("order_id").asText(null).ifBlank { null } + ?: orderEntity.path("id").asText(null) + val status = paymentEntity.path("status").asText(null) + val amountPaise = paymentEntity.path("amount").asLong(0) + val currency = paymentEntity.path("currency").asText(property.currency) + val notes = paymentEntity.path("notes") + val bookingId = notes.path("bookingId").asText(null)?.let { runCatching { UUID.fromString(it) }.getOrNull() } + ?: orderEntity.path("receipt").asText(null)?.let { runCatching { UUID.fromString(it) }.getOrNull() } + val booking = bookingId?.let { bookingRepo.findById(it).orElse(null) } + if (booking != null && booking.property.id != propertyId) return + + razorpayPaymentAttemptRepo.save( + RazorpayPaymentAttempt( + property = property, + booking = booking, + event = event, + status = status, + amount = paiseToAmount(amountPaise), + currency = currency, + paymentId = paymentId, + orderId = orderId, + payload = body, + receivedAt = OffsetDateTime.now() + ) + ) + + if (event == null || paymentId == null || booking == null) return + if (event != "payment.captured" && event != "refund.processed") return + + if (paymentRepo.findByGatewayPaymentId(paymentId) != null) return + + val signedAmount = if (event == "refund.processed") -paiseToAmount(amountPaise) else paiseToAmount(amountPaise) + val notesText = "razorpay event=$event status=$status order_id=$orderId" + paymentRepo.save( + Payment( + property = booking.property, + booking = booking, + amount = signedAmount, + currency = booking.property.currency, + method = PaymentMethod.ONLINE, + gatewayPaymentId = paymentId, + gatewayTxnId = orderId, + reference = "razorpay:$paymentId", + notes = notesText, + receivedAt = OffsetDateTime.now() + ) + ) + } + + private fun verifySignature(payload: String, secret: String, signature: String): Boolean { + return try { + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec(secret.toByteArray(), "HmacSHA256")) + val hash = mac.doFinal(payload.toByteArray()).joinToString("") { "%02x".format(it) } + hash.equals(signature, ignoreCase = true) + } catch (_: Exception) { + false + } + } + + private fun paiseToAmount(paise: Long): Long { + return paise / 100 + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/dto/PayuDtos.kt b/src/main/kotlin/com/android/trisolarisserver/controller/dto/PayuDtos.kt deleted file mode 100644 index b2ee6c6..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/controller/dto/PayuDtos.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.android.trisolarisserver.controller.dto - -import java.util.UUID - -data class PayuSettingsUpsertRequest( - val merchantKey: String, - val salt32: String? = null, - val salt256: String? = null, - val isTest: Boolean? = null, - val useSalt256: Boolean? = null -) - -data class PayuSettingsResponse( - val propertyId: UUID, - val configured: Boolean, - val merchantKey: String?, - val isTest: Boolean, - val useSalt256: Boolean, - val hasSalt32: Boolean, - val hasSalt256: Boolean -) - -data class PayuQrGenerateRequest( - val amount: Long? = null, - val customerName: String? = null, - val customerEmail: String? = null, - val customerPhone: String? = null, - val expiryMinutes: Int? = null, - val expirySeconds: Int? = null, - val clientIp: String? = null, - val deviceInfo: String? = null, - val address1: String? = null, - val address2: String? = null, - val city: String? = null, - val state: String? = null, - val country: String? = null, - val zipcode: String? = null, - val udf3: String? = null, - val udf4: String? = null, - val udf5: String? = null -) - -data class PayuQrGenerateResponse( - val txnid: String, - val amount: Long, - val currency: String, - val payuResponse: String -) - -data class PayuPaymentLinkSettingsUpsertRequest( - val merchantId: String, - val clientId: String? = null, - val clientSecret: String? = null, - val accessToken: String? = null, - val isTest: Boolean? = null -) - -data class PayuPaymentLinkSettingsResponse( - val propertyId: UUID, - val configured: Boolean, - val merchantId: String?, - val isTest: Boolean, - val hasClientId: Boolean, - val hasClientSecret: Boolean, - val hasAccessToken: Boolean -) - -data class PayuPaymentLinkCreateRequest( - val amount: Long? = null, - val isAmountFilledByCustomer: Boolean? = null, - val isPartialPaymentAllowed: Boolean? = null, - val minAmountForCustomer: Long? = null, - val description: String? = null, - val expiryDate: String? = null, - val successUrl: String? = null, - val failureUrl: String? = null, - val udf3: String? = null, - val udf4: String? = null, - val udf5: String? = null, - val viaEmail: Boolean? = null, - val viaSms: Boolean? = null -) - -data class PayuPaymentLinkCreateResponse( - val amount: Long, - val currency: String, - val paymentLink: String?, - val payuResponse: String -) diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt new file mode 100644 index 0000000..544e95f --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt @@ -0,0 +1,55 @@ +package com.android.trisolarisserver.controller.dto + +import java.util.UUID + +data class RazorpaySettingsUpsertRequest( + val keyId: String, + val keySecret: String, + val webhookSecret: String? = null, + val isTest: Boolean? = null +) + +data class RazorpaySettingsResponse( + val propertyId: UUID, + val configured: Boolean, + val keyId: String?, + val isTest: Boolean, + val hasKeySecret: Boolean, + val hasWebhookSecret: Boolean +) + +data class RazorpayQrGenerateRequest( + val amount: Long? = null, + val customerName: String? = null, + val customerEmail: String? = null, + val customerPhone: String? = null, + val expiryMinutes: Int? = null, + val expirySeconds: Int? = null +) + +data class RazorpayQrGenerateResponse( + val qrId: String?, + val amount: Long, + val currency: String, + val imageUrl: String?, + val razorpayResponse: String +) + +data class RazorpayPaymentLinkCreateRequest( + val amount: Long? = null, + val isPartialPaymentAllowed: Boolean? = null, + val minAmountForCustomer: Long? = null, + val description: String? = null, + val expiryDate: String? = null, + val successUrl: String? = null, + val failureUrl: String? = null, + val viaEmail: Boolean? = null, + val viaSms: Boolean? = null +) + +data class RazorpayPaymentLinkCreateResponse( + val amount: Long, + val currency: String, + val paymentLink: String?, + val razorpayResponse: String +) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentAttempt.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentAttempt.kt deleted file mode 100644 index eb9fc99..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentAttempt.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.android.trisolarisserver.models.payment - -import com.android.trisolarisserver.models.booking.Booking -import com.android.trisolarisserver.models.property.Property -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.FetchType -import jakarta.persistence.GeneratedValue -import jakarta.persistence.Id -import jakarta.persistence.JoinColumn -import jakarta.persistence.ManyToOne -import jakarta.persistence.Table -import java.time.OffsetDateTime -import java.util.UUID - -@Entity -@Table(name = "payu_payment_attempt") -class PayuPaymentAttempt( - @Id - @GeneratedValue - @Column(columnDefinition = "uuid") - val id: UUID? = null, - - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "property_id", nullable = false) - var property: Property, - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "booking_id") - var booking: Booking? = null, - - @Column(name = "status") - var status: String? = null, - - @Column(name = "unmapped_status") - var unmappedStatus: String? = null, - - @Column(name = "amount") - var amount: Long? = null, - - @Column(name = "currency") - var currency: String? = null, - - @Column(name = "gateway_payment_id") - var gatewayPaymentId: String? = null, - - @Column(name = "gateway_txn_id") - var gatewayTxnId: String? = null, - - @Column(name = "bank_ref_num") - var bankRefNum: String? = null, - - @Column(name = "mode") - var mode: String? = null, - - @Column(name = "pg_type") - var pgType: String? = null, - - @Column(name = "payer_vpa") - var payerVpa: String? = null, - - @Column(name = "payer_name") - var payerName: String? = null, - - @Column(name = "payment_source") - var paymentSource: String? = null, - - @Column(name = "error_code") - var errorCode: String? = null, - - @Column(name = "error_message") - var errorMessage: String? = null, - - @Column(name = "payload", columnDefinition = "text") - var payload: String? = null, - - @Column(name = "received_at", columnDefinition = "timestamptz") - var receivedAt: OffsetDateTime = OffsetDateTime.now() -) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentLinkSettings.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentLinkSettings.kt deleted file mode 100644 index 7ff94ce..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentLinkSettings.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.android.trisolarisserver.models.payment - -import com.android.trisolarisserver.models.property.Property -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.FetchType -import jakarta.persistence.GeneratedValue -import jakarta.persistence.Id -import jakarta.persistence.JoinColumn -import jakarta.persistence.ManyToOne -import jakarta.persistence.Table -import jakarta.persistence.UniqueConstraint -import java.time.OffsetDateTime -import java.util.UUID - -@Entity -@Table( - name = "payu_payment_link_settings", - uniqueConstraints = [UniqueConstraint(columnNames = ["property_id"])] -) -class PayuPaymentLinkSettings( - @Id - @GeneratedValue - @Column(columnDefinition = "uuid") - val id: UUID? = null, - - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "property_id", nullable = false) - var property: Property, - - @Column(name = "merchant_id", nullable = false, columnDefinition = "text") - var merchantId: String, - - @Column(name = "client_id", columnDefinition = "text") - var clientId: String? = null, - - @Column(name = "client_secret", columnDefinition = "text") - var clientSecret: String? = null, - - @Column(name = "access_token", columnDefinition = "text") - var accessToken: String? = null, - - @Column(name = "token_expires_at", columnDefinition = "timestamptz") - var tokenExpiresAt: OffsetDateTime? = null, - - @Column(name = "is_test", nullable = false) - var isTest: Boolean = false, - - @Column(name = "updated_at", columnDefinition = "timestamptz") - var updatedAt: OffsetDateTime = OffsetDateTime.now() -) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuSettings.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuSettings.kt deleted file mode 100644 index f8bc3f0..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuSettings.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.android.trisolarisserver.models.payment - -import com.android.trisolarisserver.models.property.Property -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.FetchType -import jakarta.persistence.GeneratedValue -import jakarta.persistence.Id -import jakarta.persistence.JoinColumn -import jakarta.persistence.ManyToOne -import jakarta.persistence.Table -import jakarta.persistence.UniqueConstraint -import java.time.OffsetDateTime -import java.util.UUID - -@Entity -@Table( - name = "payu_settings", - uniqueConstraints = [UniqueConstraint(columnNames = ["property_id"])] -) -class PayuSettings( - @Id - @GeneratedValue - @Column(columnDefinition = "uuid") - val id: UUID? = null, - - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "property_id", nullable = false) - var property: Property, - - @Column(name = "merchant_key", nullable = false, columnDefinition = "text") - var merchantKey: String, - - @Column(name = "salt_32", columnDefinition = "text") - var salt32: String? = null, - - @Column(name = "salt_256", columnDefinition = "text") - var salt256: String? = null, - - @Column(name = "base_url", nullable = false, columnDefinition = "text") - var baseUrl: String = "https://secure.payu.in/_payment", - - @Column(name = "is_test", nullable = false) - var isTest: Boolean = false, - - @Column(name = "use_salt_256", nullable = false) - var useSalt256: Boolean = true, - - @Column(name = "updated_at", nullable = false, columnDefinition = "timestamptz") - var updatedAt: OffsetDateTime = OffsetDateTime.now() -) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpayPaymentAttempt.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpayPaymentAttempt.kt new file mode 100644 index 0000000..4d414ac --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpayPaymentAttempt.kt @@ -0,0 +1,48 @@ +package com.android.trisolarisserver.models.payment + +import com.android.trisolarisserver.models.booking.Booking +import com.android.trisolarisserver.models.property.Property +import jakarta.persistence.* +import java.time.OffsetDateTime +import java.util.UUID + +@Entity +@Table(name = "razorpay_payment_attempt") +class RazorpayPaymentAttempt( + @Id + @GeneratedValue + @Column(columnDefinition = "uuid") + val id: UUID? = null, + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "property_id", nullable = false) + var property: Property, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "booking_id") + var booking: Booking? = null, + + @Column(name = "event") + var event: String? = null, + + @Column(name = "status") + var status: String? = null, + + @Column(name = "amount") + var amount: Long? = null, + + @Column(name = "currency") + var currency: String? = null, + + @Column(name = "payment_id") + var paymentId: String? = null, + + @Column(name = "order_id") + var orderId: String? = null, + + @Column(name = "payload") + var payload: String? = null, + + @Column(name = "received_at", nullable = false, columnDefinition = "timestamptz") + var receivedAt: OffsetDateTime = OffsetDateTime.now() +) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpayPaymentLinkRequest.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpayPaymentLinkRequest.kt new file mode 100644 index 0000000..2becf96 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpayPaymentLinkRequest.kt @@ -0,0 +1,48 @@ +package com.android.trisolarisserver.models.payment + +import com.android.trisolarisserver.models.booking.Booking +import com.android.trisolarisserver.models.property.Property +import jakarta.persistence.* +import java.time.OffsetDateTime +import java.util.UUID + +@Entity +@Table(name = "razorpay_payment_link_request") +class RazorpayPaymentLinkRequest( + @Id + @GeneratedValue + @Column(columnDefinition = "uuid") + val id: UUID? = null, + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "property_id", nullable = false) + var property: Property, + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "booking_id", nullable = false) + var booking: Booking, + + @Column(name = "payment_link_id") + var paymentLinkId: String? = null, + + @Column(name = "amount") + var amount: Long, + + @Column(name = "currency") + var currency: String, + + @Column(name = "status") + var status: String, + + @Column(name = "short_url") + var shortUrl: String? = null, + + @Column(name = "request_payload") + var requestPayload: String? = null, + + @Column(name = "response_payload") + var responsePayload: String? = null, + + @Column(name = "created_at", nullable = false, columnDefinition = "timestamptz") + val createdAt: OffsetDateTime = OffsetDateTime.now() +) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuQrRequest.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpayQrRequest.kt similarity index 52% rename from src/main/kotlin/com/android/trisolarisserver/models/payment/PayuQrRequest.kt rename to src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpayQrRequest.kt index cf5cafd..cbbc609 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuQrRequest.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpayQrRequest.kt @@ -2,22 +2,13 @@ package com.android.trisolarisserver.models.payment import com.android.trisolarisserver.models.booking.Booking import com.android.trisolarisserver.models.property.Property -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.EnumType -import jakarta.persistence.Enumerated -import jakarta.persistence.FetchType -import jakarta.persistence.GeneratedValue -import jakarta.persistence.Id -import jakarta.persistence.JoinColumn -import jakarta.persistence.ManyToOne -import jakarta.persistence.Table +import jakarta.persistence.* import java.time.OffsetDateTime import java.util.UUID @Entity -@Table(name = "payu_qr_request") -class PayuQrRequest( +@Table(name = "razorpay_qr_request") +class RazorpayQrRequest( @Id @GeneratedValue @Column(columnDefinition = "uuid") @@ -31,23 +22,25 @@ class PayuQrRequest( @JoinColumn(name = "booking_id", nullable = false) var booking: Booking, - @Column(name = "txnid", nullable = false) - var txnid: String, + @Column(name = "qr_id") + var qrId: String? = null, - @Column(name = "amount", nullable = false) + @Column(name = "amount") var amount: Long, - @Column(name = "currency", nullable = false) + @Column(name = "currency") var currency: String, - @Enumerated(EnumType.STRING) - @Column(name = "status", nullable = false) - var status: PayuQrStatus = PayuQrStatus.CREATED, + @Column(name = "status") + var status: String, - @Column(name = "request_payload", columnDefinition = "text") + @Column(name = "image_url") + var imageUrl: String? = null, + + @Column(name = "request_payload") var requestPayload: String? = null, - @Column(name = "response_payload", columnDefinition = "text") + @Column(name = "response_payload") var responsePayload: String? = null, @Column(name = "expiry_at", columnDefinition = "timestamptz") @@ -56,10 +49,3 @@ class PayuQrRequest( @Column(name = "created_at", nullable = false, columnDefinition = "timestamptz") val createdAt: OffsetDateTime = OffsetDateTime.now() ) - -enum class PayuQrStatus { - CREATED, - SENT, - SUCCESS, - FAILED -} diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpaySettings.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpaySettings.kt new file mode 100644 index 0000000..29ccb73 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpaySettings.kt @@ -0,0 +1,34 @@ +package com.android.trisolarisserver.models.payment + +import com.android.trisolarisserver.models.property.Property +import jakarta.persistence.* +import java.time.OffsetDateTime +import java.util.UUID + +@Entity +@Table(name = "razorpay_settings") +class RazorpaySettings( + @Id + @GeneratedValue + @Column(columnDefinition = "uuid") + val id: UUID? = null, + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "property_id", nullable = false, unique = true) + var property: Property, + + @Column(name = "key_id", nullable = false) + var keyId: String, + + @Column(name = "key_secret", nullable = false) + var keySecret: String, + + @Column(name = "webhook_secret") + var webhookSecret: String? = null, + + @Column(name = "is_test", nullable = false) + var isTest: Boolean = false, + + @Column(name = "updated_at", nullable = false, columnDefinition = "timestamptz") + var updatedAt: OffsetDateTime = OffsetDateTime.now() +) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuWebhookLog.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpayWebhookLog.kt similarity index 55% rename from src/main/kotlin/com/android/trisolarisserver/models/payment/PayuWebhookLog.kt rename to src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpayWebhookLog.kt index cd2df42..654d293 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuWebhookLog.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpayWebhookLog.kt @@ -1,20 +1,13 @@ package com.android.trisolarisserver.models.payment import com.android.trisolarisserver.models.property.Property -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.FetchType -import jakarta.persistence.GeneratedValue -import jakarta.persistence.Id -import jakarta.persistence.JoinColumn -import jakarta.persistence.ManyToOne -import jakarta.persistence.Table +import jakarta.persistence.* import java.time.OffsetDateTime import java.util.UUID @Entity -@Table(name = "payu_webhook_log") -class PayuWebhookLog( +@Table(name = "razorpay_webhook_log") +class RazorpayWebhookLog( @Id @GeneratedValue @Column(columnDefinition = "uuid") @@ -24,15 +17,15 @@ class PayuWebhookLog( @JoinColumn(name = "property_id", nullable = false) var property: Property, - @Column(name = "headers", columnDefinition = "text") + @Column(name = "headers") var headers: String? = null, - @Column(name = "payload", columnDefinition = "text") + @Column(name = "payload") var payload: String? = null, @Column(name = "content_type") var contentType: String? = null, @Column(name = "received_at", nullable = false, columnDefinition = "timestamptz") - val receivedAt: OffsetDateTime = OffsetDateTime.now() + var receivedAt: OffsetDateTime = OffsetDateTime.now() ) diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/PayuPaymentAttemptRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/PayuPaymentAttemptRepo.kt deleted file mode 100644 index 135f798..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/repo/PayuPaymentAttemptRepo.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.android.trisolarisserver.repo - -import com.android.trisolarisserver.models.payment.PayuPaymentAttempt -import org.springframework.data.jpa.repository.JpaRepository -import java.util.UUID - -interface PayuPaymentAttemptRepo : JpaRepository diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/PayuPaymentLinkSettingsRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/PayuPaymentLinkSettingsRepo.kt deleted file mode 100644 index 6c2bf33..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/repo/PayuPaymentLinkSettingsRepo.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.android.trisolarisserver.repo - -import com.android.trisolarisserver.models.payment.PayuPaymentLinkSettings -import org.springframework.data.jpa.repository.JpaRepository -import java.util.UUID - -interface PayuPaymentLinkSettingsRepo : JpaRepository { - fun findByPropertyId(propertyId: UUID): PayuPaymentLinkSettings? -} diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/PayuQrRequestRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/PayuQrRequestRepo.kt deleted file mode 100644 index 34138f6..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/repo/PayuQrRequestRepo.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.android.trisolarisserver.repo - -import com.android.trisolarisserver.models.payment.PayuQrRequest -import org.springframework.data.jpa.repository.JpaRepository -import java.util.UUID - -interface PayuQrRequestRepo : JpaRepository { - fun findByBookingId(bookingId: UUID): List - fun findTopByBookingIdAndAmountAndCurrencyAndStatusOrderByCreatedAtDesc( - bookingId: UUID, - amount: Long, - currency: String, - status: com.android.trisolarisserver.models.payment.PayuQrStatus - ): PayuQrRequest? -} diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/PayuSettingsRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/PayuSettingsRepo.kt deleted file mode 100644 index 5b6eeff..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/repo/PayuSettingsRepo.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.android.trisolarisserver.repo - -import com.android.trisolarisserver.models.payment.PayuSettings -import org.springframework.data.jpa.repository.JpaRepository -import java.util.UUID - -interface PayuSettingsRepo : JpaRepository { - fun findByPropertyId(propertyId: UUID): PayuSettings? -} diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/PayuWebhookLogRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/PayuWebhookLogRepo.kt deleted file mode 100644 index 15f62f9..0000000 --- a/src/main/kotlin/com/android/trisolarisserver/repo/PayuWebhookLogRepo.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.android.trisolarisserver.repo - -import com.android.trisolarisserver.models.payment.PayuWebhookLog -import org.springframework.data.jpa.repository.JpaRepository -import java.util.UUID - -interface PayuWebhookLogRepo : JpaRepository diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayPaymentAttemptRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayPaymentAttemptRepo.kt new file mode 100644 index 0000000..9cadf3f --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayPaymentAttemptRepo.kt @@ -0,0 +1,7 @@ +package com.android.trisolarisserver.repo + +import com.android.trisolarisserver.models.payment.RazorpayPaymentAttempt +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface RazorpayPaymentAttemptRepo : JpaRepository diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayPaymentLinkRequestRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayPaymentLinkRequestRepo.kt new file mode 100644 index 0000000..3c6fc9c --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayPaymentLinkRequestRepo.kt @@ -0,0 +1,14 @@ +package com.android.trisolarisserver.repo + +import com.android.trisolarisserver.models.payment.RazorpayPaymentLinkRequest +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface RazorpayPaymentLinkRequestRepo : JpaRepository { + fun findTopByBookingIdAndAmountAndCurrencyAndStatusOrderByCreatedAtDesc( + bookingId: UUID, + amount: Long, + currency: String, + status: String + ): RazorpayPaymentLinkRequest? +} diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayQrRequestRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayQrRequestRepo.kt new file mode 100644 index 0000000..4f869f6 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayQrRequestRepo.kt @@ -0,0 +1,14 @@ +package com.android.trisolarisserver.repo + +import com.android.trisolarisserver.models.payment.RazorpayQrRequest +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface RazorpayQrRequestRepo : JpaRepository { + fun findTopByBookingIdAndAmountAndCurrencyAndStatusOrderByCreatedAtDesc( + bookingId: UUID, + amount: Long, + currency: String, + status: String + ): RazorpayQrRequest? +} diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/RazorpaySettingsRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/RazorpaySettingsRepo.kt new file mode 100644 index 0000000..1cacd77 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/repo/RazorpaySettingsRepo.kt @@ -0,0 +1,9 @@ +package com.android.trisolarisserver.repo + +import com.android.trisolarisserver.models.payment.RazorpaySettings +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface RazorpaySettingsRepo : JpaRepository { + fun findByPropertyId(propertyId: UUID): RazorpaySettings? +} diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayWebhookLogRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayWebhookLogRepo.kt new file mode 100644 index 0000000..1bca384 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayWebhookLogRepo.kt @@ -0,0 +1,7 @@ +package com.android.trisolarisserver.repo + +import com.android.trisolarisserver.models.payment.RazorpayWebhookLog +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface RazorpayWebhookLogRepo : JpaRepository diff --git a/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt b/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt index 395d47f..66e03f1 100644 --- a/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt +++ b/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt @@ -10,8 +10,8 @@ internal object PublicEndpoints { private val roomTypes = Regex("^/properties/[^/]+/room-types$") private val roomTypeImages = Regex("^/properties/[^/]+/room-types/[^/]+/images$") private val iconPngFile = Regex("^/icons/png/[^/]+$") - private val payuWebhook = Regex("^/properties/[^/]+/payu/webhook$") - private val payuReturn = Regex("^/properties/[^/]+/payu/return/(success|failure)$") + private val razorpayWebhook = Regex("^/properties/[^/]+/razorpay/webhook$") + private val razorpayReturn = Regex("^/properties/[^/]+/razorpay/return/(success|failure)$") private val guestDocumentFile = Regex("^/properties/[^/]+/guests/[^/]+/documents/[^/]+/file$") fun isPublic(request: HttpServletRequest): Boolean { @@ -26,8 +26,8 @@ internal object PublicEndpoints { || (roomsByType.matches(path) && method == "GET") || (roomTypes.matches(path) && method == "GET") || roomTypeImages.matches(path) - || (payuWebhook.matches(path) && method == "POST") - || payuReturn.matches(path) + || (razorpayWebhook.matches(path) && method == "POST") + || razorpayReturn.matches(path) || (path == "/image-tags" && method == "GET") || path == "/icons/png" || iconPngFile.matches(path)