diff --git a/build.gradle.kts b/build.gradle.kts index 7a967ed..7d7f387 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,24 +9,17 @@ plugins { val ktorVersion = "1.6.3" val libraryVersion = "1.0.1" - android { namespace = "com.example.zarinpal" compileSdk = 34 - publishing { - singleVariant("release") { - withSourcesJar() - } - } - defaultConfig { - aarMetadata { - minCompileSdk = 24 - } minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") + aarMetadata { + minCompileSdk = 24 + } } buildTypes { @@ -47,6 +40,12 @@ android { kotlinOptions { jvmTarget = "1.8" } + + publishing { + singleVariant("release") { + withSourcesJar() + } + } } tasks.register("javadocJar") { @@ -64,20 +63,24 @@ publishing { afterEvaluate { from(components["release"]) } + artifact(tasks["javadocJar"]) { classifier = "javadoc" } pom { dependencies { - implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.ktor:ktor-client-android:$ktorVersion") - implementation("io.ktor:ktor-client-serialization:$ktorVersion") - implementation("io.ktor:ktor-client-logging:$ktorVersion") - implementation("ch.qos.logback:logback-classic:1.2.3") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0") + listOf( + "io.ktor:ktor-client-core:$ktorVersion", + "io.ktor:ktor-client-android:$ktorVersion", + "io.ktor:ktor-client-serialization:$ktorVersion", + "io.ktor:ktor-client-logging:$ktorVersion", + "ch.qos.logback:logback-classic:1.2.3", + "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0" + ).forEach { + implementation(it) + } } - } } } @@ -86,23 +89,29 @@ publishing { maven { url = uri("https://maven.pkg.github.com/alirezabashi98/zarinpal-sdk") credentials { - credentials.username = System.getenv("GITHUB_USER") - credentials.password = System.getenv("GITHUB_TOKEN") + username = System.getenv("GITHUB_USER") + password = System.getenv("GITHUB_TOKEN") } } } } + dependencies { - implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.ktor:ktor-client-android:$ktorVersion") - implementation("io.ktor:ktor-client-serialization:$ktorVersion") - implementation("io.ktor:ktor-client-logging:$ktorVersion") - implementation("ch.qos.logback:logback-classic:1.2.3") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0") - implementation("androidx.core:core-ktx:1.13.0") + listOf( + "io.ktor:ktor-client-core:$ktorVersion", + "io.ktor:ktor-client-android:$ktorVersion", + "io.ktor:ktor-client-serialization:$ktorVersion", + "io.ktor:ktor-client-logging:$ktorVersion", + "ch.qos.logback:logback-classic:1.2.3", + "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0", + "androidx.core:core-ktx:1.13.0" + ).forEach { + implementation(it) + } } tasks.register("copyLibs") { from(configurations.getByName("implementation")) into("libs") -} \ No newline at end of file +} + diff --git a/src/main/java/com/example/zarinpal/ZarinPal.kt b/src/main/java/com/example/zarinpal/ZarinPal.kt index a8ed3e9..ebdcf4d 100644 --- a/src/main/java/com/example/zarinpal/ZarinPal.kt +++ b/src/main/java/com/example/zarinpal/ZarinPal.kt @@ -23,37 +23,28 @@ import com.example.zarinpal.utils.Validator * ZarinPal class handles all interactions with the ZarinPal API. * Provides methods for creating payments, verifying payments, refunds, reversals, and more. */ -class ZarinPal(config: Config) { +class ZarinPal(private val config: Config) { - // Instance of PaymentService used for API calls private val service = PaymentService.create(config) - // Configuration object containing sandbox mode and other settings - private val config = config - - /** - * Creates a new payment and returns the response. - * @param paymentRequest Request data for creating the payment. - * @param redirectUrl Callback function to handle the payment gateway URL and status. - * @return [CreatePaymentDataResponse] containing details of the created payment. - */ suspend fun createPayment( paymentRequest: CreatePaymentRequest, redirectUrl: (paymentGatewayUri: String, status: Int) -> Unit ): CreatePaymentDataResponse? { - Validator.validateMerchantId(paymentRequest.merchantId ?: config.merchantId) - Validator.validateCallbackUrl(paymentRequest.callbackUrl) - Validator.validateAmount(paymentRequest.amount) - Validator.validateMobile(paymentRequest.metadata?.mobile) - Validator.validateEmail(paymentRequest.metadata?.email) - Validator.validateCardPan(paymentRequest.cardPan) + with(Validator) { + validateMerchantId(paymentRequest.merchantId ?: config.merchantId) + validateCallbackUrl(paymentRequest.callbackUrl) + validateAmount(paymentRequest.amount) + validateMobile(paymentRequest.metadata?.mobile) + validateEmail(paymentRequest.metadata?.email) + validateCardPan(paymentRequest.cardPan) + } - val paymentResponse = - service.createPayment(paymentRequest) + val paymentResponse = service.createPayment(paymentRequest) val paymentGatewayUri = HttpRoutes.getRedirectUrl( sandBox = paymentRequest.sandBox ?: config.sandBox, - authority = paymentResponse?.authority ?: "" + authority = paymentResponse?.authority.orEmpty() ) redirectUrl(paymentGatewayUri, paymentResponse?.code ?: 0) @@ -61,89 +52,58 @@ class ZarinPal(config: Config) { return paymentResponse } - /** - * Verifies a payment using the authority code. - * @param paymentVerifyRequest Request data for verifying the payment. - * @return [PaymentVerificationDataResponse] containing verification details. - */ - suspend fun paymentVerify( - paymentVerifyRequest: PaymentVerifyRequest - ): PaymentVerificationDataResponse? { - Validator.validateMerchantId(paymentVerifyRequest.merchantId ?: config.merchantId) - Validator.validateAuthority(paymentVerifyRequest.authority) - Validator.validateAmount(paymentVerifyRequest.amount) + suspend fun paymentVerify(paymentVerifyRequest: PaymentVerifyRequest): PaymentVerificationDataResponse? { + with(Validator) { + validateMerchantId(paymentVerifyRequest.merchantId ?: config.merchantId) + validateAuthority(paymentVerifyRequest.authority) + validateAmount(paymentVerifyRequest.amount) + } return service.paymentVerify(paymentVerifyRequest) } - /** - * Inquires about a payment status. - * @param paymentInquiryRequest Request data for the payment inquiry. - * @return [PaymentInquiryDataResponse] containing inquiry details. - */ - suspend fun paymentInquiry( - paymentInquiryRequest: PaymentInquiryRequest - ): PaymentInquiryDataResponse? { - Validator.validateMerchantId(paymentInquiryRequest.merchantId ?: config.merchantId) - Validator.validateAuthority(paymentInquiryRequest.authority) + suspend fun paymentInquiry(paymentInquiryRequest: PaymentInquiryRequest): PaymentInquiryDataResponse? { + with(Validator) { + validateMerchantId(paymentInquiryRequest.merchantId ?: config.merchantId) + validateAuthority(paymentInquiryRequest.authority) + } return service.paymentInquiry(paymentInquiryRequest) } - - /** - * Retrieves unverified payments. - * @param paymentUnVerifiedRequest Optional request data for retrieving unverified payments. - * @return [PaymentUnVerifiedDataResponse] containing unverified payment details. - */ suspend fun paymentUnVerified( paymentUnVerifiedRequest: PaymentUnVerifiedRequest = PaymentUnVerifiedRequest() ): PaymentUnVerifiedDataResponse? { Validator.validateMerchantId(paymentUnVerifiedRequest.merchantId ?: config.merchantId) - return service.paymentUnVerified(paymentUnVerifiedRequest) } - /** - * Retrieves unverified payments. - * @param paymentUnVerifiedRequest Optional request data for retrieving unverified payments. - * @return [PaymentUnVerifiedDataResponse] containing unverified payment details. - */ - suspend fun paymentReverse( - paymentReverseRequest: PaymentReverseRequest - ): PaymentReverseDataResponse? { - Validator.validateMerchantId(paymentReverseRequest.merchantId ?: config.merchantId) - Validator.validateAuthority(paymentReverseRequest.authority) + suspend fun paymentReverse(paymentReverseRequest: PaymentReverseRequest): PaymentReverseDataResponse? { + with(Validator) { + validateMerchantId(paymentReverseRequest.merchantId ?: config.merchantId) + validateAuthority(paymentReverseRequest.authority) + } return service.paymentReverse(paymentReverseRequest) } - /** - * Retrieves transaction history. - * @param transactionRequest Request data for fetching transactions. - * @return A list of [Session] objects containing transaction details. - */ - suspend fun getTransactions( - transactionRequest: TransactionRequest - ): List? { - Validator.validateTerminalId(transactionRequest.terminalId) - Validator.validateLimit(transactionRequest.limit) - Validator.validateOffset(transactionRequest.offset) + suspend fun getTransactions(transactionRequest: TransactionRequest): List? { + with(Validator) { + validateTerminalId(transactionRequest.terminalId) + validateLimit(transactionRequest.limit) + validateOffset(transactionRequest.offset) + } return service.getTransactions(transactionRequest) } - /** - * Processes a payment refund. - * @param paymentRefundRequest Request data for refunding the payment. - * @return [PaymentRefundResponse] containing refund details. - */ - suspend fun paymentRefund( - paymentRefundRequest: PaymentRefundRequest - ): PaymentRefundResponse? { - Validator.validateSessionId(paymentRefundRequest.sessionId) - Validator.validateAmount(paymentRefundRequest.amount, minAmount = 20_000) + suspend fun paymentRefund(paymentRefundRequest: PaymentRefundRequest): PaymentRefundResponse? { + with(Validator) { + validateSessionId(paymentRefundRequest.sessionId) + validateAmount(paymentRefundRequest.amount, minAmount = 20_000) + } return service.paymentRefund(paymentRefundRequest) } -} \ No newline at end of file +} + diff --git a/src/main/java/com/example/zarinpal/data/remote/HttpRoutes.kt b/src/main/java/com/example/zarinpal/data/remote/HttpRoutes.kt index d6d258b..1b67461 100644 --- a/src/main/java/com/example/zarinpal/data/remote/HttpRoutes.kt +++ b/src/main/java/com/example/zarinpal/data/remote/HttpRoutes.kt @@ -1,19 +1,10 @@ package com.example.zarinpal.data.remote /** - * This object contains constants and functions related to the HTTP routes for interacting with the ZarinPal payment gateway. - * - * @property BASE_URL The base URL for the live ZarinPal payment gateway. - * @property BASE_URL_SANDBOX The base URL for the sandbox (test) environment of the ZarinPal payment gateway. - * @property START_PAY_URL The URL endpoint for starting a payment. - * @property BASE_URL_GRAPH The base URL for the GraphQL API of ZarinPal. - * @property PAYMENT The endpoint for creating a payment request. - * @property PAYMENT_VERIFY The endpoint for verifying a payment. - * @property PAYMENT_INQUIRY The endpoint for inquiring about a payment. - * @property PAYMENT_UN_VERIFIED The endpoint for querying unverified payments. - * @property PAYMENT_REVERSE The endpoint for reversing a payment. + * Utility object for building HTTP routes related to the ZarinPal payment gateway. */ object HttpRoutes { + private const val BASE_URL = "https://payment.zarinpal.com" private const val BASE_URL_SANDBOX = "https://sandbox.zarinpal.com" private const val START_PAY_URL = "/pg/StartPay/" @@ -23,68 +14,27 @@ object HttpRoutes { private const val PAYMENT = "/pg/v4/payment/request.json" private const val PAYMENT_VERIFY = "/pg/v4/payment/verify.json" private const val PAYMENT_INQUIRY = "/pg/v4/payment/inquiry.json" - private const val PAYMENT_UN_VERIFIED = "/pg/v4/payment/unVerified.json" + private const val PAYMENT_UNVERIFIED = "/pg/v4/payment/unVerified.json" private const val PAYMENT_REVERSE = "/pg/v4/payment/reverse.json" - /** - * Generates the URL for creating a payment request. - * - * @param sandBox A boolean indicating whether to use the sandbox (test) environment. - * @return The full URL for the payment request. - */ - fun createPayment(sandBox: Boolean): String { - return (if (sandBox) BASE_URL_SANDBOX else BASE_URL) + PAYMENT - } + private fun baseUrl(sandbox: Boolean): String = + if (sandbox) BASE_URL_SANDBOX else BASE_URL + + fun createPayment(sandbox: Boolean): String = + baseUrl(sandbox) + PAYMENT - /** - * Generates the URL for verifying a payment. - * - * @param sandBox A boolean indicating whether to use the sandbox (test) environment. - * @return The full URL for the payment verification request. - */ - fun paymentVerify(sandBox: Boolean): String { - return (if (sandBox) BASE_URL_SANDBOX else BASE_URL) + PAYMENT_VERIFY - } + fun paymentVerify(sandbox: Boolean): String = + baseUrl(sandbox) + PAYMENT_VERIFY - /** - * Generates the URL for inquiring about a payment. - * - * @param sandBox A boolean indicating whether to use the sandbox (test) environment. - * @return The full URL for the payment inquiry request. - */ - fun paymentInquiry(sandBox: Boolean): String { - return (if (sandBox) BASE_URL_SANDBOX else BASE_URL) + PAYMENT_INQUIRY - } + fun paymentInquiry(sandbox: Boolean): String = + baseUrl(sandbox) + PAYMENT_INQUIRY - /** - * Generates the URL for querying unverified payments. - * - * @param sandBox A boolean indicating whether to use the sandbox (test) environment. - * @return The full URL for the unverified payment query request. - */ - fun paymentUnVerified(sandBox: Boolean): String { - return (if (sandBox) BASE_URL_SANDBOX else BASE_URL) + PAYMENT_UN_VERIFIED - } + fun paymentUnverified(sandbox: Boolean): String = + baseUrl(sandbox) + PAYMENT_UNVERIFIED - /** - * Generates the URL for reversing a payment. - * - * @param sandBox A boolean indicating whether to use the sandbox (test) environment. - * @return The full URL for the payment reversal request. - */ - fun paymentReverse(sandBox: Boolean): String { - return (if (sandBox) BASE_URL_SANDBOX else BASE_URL) + PAYMENT_REVERSE - } + fun paymentReverse(sandbox: Boolean): String = + baseUrl(sandbox) + PAYMENT_REVERSE - /** - * Generates the URL for redirecting a user to the payment page with the provided authority. - * - * @param authority The payment authority returned by the payment gateway. - * @param sandBox A boolean indicating whether to use the sandbox (test) environment. - * @return The full URL for redirecting the user to the payment page. - */ - fun getRedirectUrl(authority: String, sandBox: Boolean): String { - if ((authority ?: "").isEmpty()) return "" - return (if (sandBox) BASE_URL_SANDBOX else BASE_URL) + START_PAY_URL + authority - } -} \ No newline at end of file + fun getRedirectUrl(authority: String, sandbox: Boolean): String = + if (authority.isBlank()) "" else baseUrl(sandbox) + START_PAY_URL + authority +} diff --git a/src/main/java/com/example/zarinpal/data/remote/PaymentServiceImpl.kt b/src/main/java/com/example/zarinpal/data/remote/PaymentServiceImpl.kt index 045a035..588a49d 100644 --- a/src/main/java/com/example/zarinpal/data/remote/PaymentServiceImpl.kt +++ b/src/main/java/com/example/zarinpal/data/remote/PaymentServiceImpl.kt @@ -1,270 +1,147 @@ package com.example.zarinpal.data.remote - import com.example.zarinpal.data.remote.dto.Config -import com.example.zarinpal.data.remote.dto.create.CreatePaymentDataResponse -import com.example.zarinpal.data.remote.dto.create.CreatePaymentRequest -import com.example.zarinpal.data.remote.dto.create.CreatePaymentResponse -import com.example.zarinpal.data.remote.dto.inquiry.PaymentInquiryDataResponse -import com.example.zarinpal.data.remote.dto.inquiry.PaymentInquiryRequest -import com.example.zarinpal.data.remote.dto.inquiry.PaymentInquiryResponse -import com.example.zarinpal.data.remote.dto.refund.GraphRefundModel -import com.example.zarinpal.data.remote.dto.refund.PaymentRefundRequest -import com.example.zarinpal.data.remote.dto.refund.PaymentRefundResponse -import com.example.zarinpal.data.remote.dto.refund.PaymentRefundResponseModel -import com.example.zarinpal.data.remote.dto.reverse.PaymentReverseDataResponse -import com.example.zarinpal.data.remote.dto.reverse.PaymentReverseRequest -import com.example.zarinpal.data.remote.dto.reverse.PaymentReverseResponse -import com.example.zarinpal.data.remote.dto.transaction.GraphTransactionModel -import com.example.zarinpal.data.remote.dto.transaction.Session -import com.example.zarinpal.data.remote.dto.transaction.TransactionRequest -import com.example.zarinpal.data.remote.dto.transaction.TransactionResponse -import com.example.zarinpal.data.remote.dto.unVerified.PaymentUnVerifiedDataResponse -import com.example.zarinpal.data.remote.dto.unVerified.PaymentUnVerifiedRequest -import com.example.zarinpal.data.remote.dto.unVerified.PaymentUnVerifiedResponse -import com.example.zarinpal.data.remote.dto.verification.PaymentVerificationDataResponse -import com.example.zarinpal.data.remote.dto.verification.PaymentVerificationResponse -import com.example.zarinpal.data.remote.dto.verification.PaymentVerifyRequest +import com.example.zarinpal.data.remote.dto.create.* +import com.example.zarinpal.data.remote.dto.inquiry.* +import com.example.zarinpal.data.remote.dto.refund.* +import com.example.zarinpal.data.remote.dto.reverse.* +import com.example.zarinpal.data.remote.dto.transaction.* +import com.example.zarinpal.data.remote.dto.unVerified.* +import com.example.zarinpal.data.remote.dto.verification.* import io.ktor.client.HttpClient -import io.ktor.client.features.ClientRequestException -import io.ktor.client.features.RedirectResponseException -import io.ktor.client.features.ServerResponseException -import io.ktor.client.request.header -import io.ktor.client.request.post -import io.ktor.client.request.url +import io.ktor.client.call.body +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.plugins.RedirectResponseException +import io.ktor.client.plugins.ServerResponseException +import io.ktor.client.request.* import io.ktor.client.statement.readText import org.json.JSONArray import org.json.JSONObject -/** - * This class handles the communication with the Payment API. - * It provides methods for creating, verifying, inquiring, refunding, and reversing payments, - * as well as fetching transaction details and handling unverified payments. - */ class PaymentServiceImpl( private val client: HttpClient, private val config: Config ) : PaymentService { - /** - * Creates a new payment. - * @param paymentRequest The request object containing payment details. - * @return A [CreatePaymentDataResponse] object containing the response data, or null if the request fails. - */ - override suspend fun createPayment(paymentRequest: CreatePaymentRequest): CreatePaymentDataResponse? { - return handleRequestWithErrorHandling { - val route = HttpRoutes.createPayment(paymentRequest.sandBox ?: config.sandBox) - - val response = client.post { - url(route) - body = paymentRequest.copyWithConfig(config) - } - response.data + override suspend fun createPayment(request: CreatePaymentRequest): CreatePaymentDataResponse? = + handleRequest { + client.post { + url(HttpRoutes.createPayment(request.sandBox ?: config.sandBox)) + setBody(request.copyWithConfig(config)) + }.data } - } - - /** - * Verifies a payment after it has been made. - * @param paymentVerifyRequest The request object containing payment verification details. - * @return A [PaymentVerificationDataResponse] object containing the verification result, or null if the request fails. - */ - override suspend fun paymentVerify(paymentVerifyRequest: PaymentVerifyRequest): PaymentVerificationDataResponse? { - return handleRequestWithErrorHandling { - val route = HttpRoutes.paymentVerify(paymentVerifyRequest.sandBox ?: config.sandBox) - val response = client.post { - url(route) - body = paymentVerifyRequest.copyWithConfig(config) - } - response.data + override suspend fun paymentVerify(request: PaymentVerifyRequest): PaymentVerificationDataResponse? = + handleRequest { + client.post { + url(HttpRoutes.paymentVerify(request.sandBox ?: config.sandBox)) + setBody(request.copyWithConfig(config)) + }.data } - } - - /** - * Inquires about the status of a payment. - * @param paymentInquiryRequest The request object containing payment inquiry details. - * @return A [PaymentInquiryDataResponse] object containing the inquiry result, or null if the request fails. - */ - override suspend fun paymentInquiry(paymentInquiryRequest: PaymentInquiryRequest): PaymentInquiryDataResponse? { - return handleRequestWithErrorHandling { - val route = HttpRoutes.paymentInquiry(paymentInquiryRequest.sandBox ?: config.sandBox) - val response = client.post { - url(route) - body = paymentInquiryRequest.copyWithConfig(config) - } - response.data + override suspend fun paymentInquiry(request: PaymentInquiryRequest): PaymentInquiryDataResponse? = + handleRequest { + client.post { + url(HttpRoutes.paymentInquiry(request.sandBox ?: config.sandBox)) + setBody(request.copyWithConfig(config)) + }.data } - } - /** - * Retrieves unverified payments. - * @param paymentUnVerifiedRequest The request object containing unverified payment details. - * @return A [PaymentUnVerifiedDataResponse] object containing unverified payments, or null if the request fails. - */ - override suspend fun paymentUnVerified(paymentUnVerifiedRequest: PaymentUnVerifiedRequest): PaymentUnVerifiedDataResponse? { - return handleRequestWithErrorHandling { - val route = - HttpRoutes.paymentUnVerified(paymentUnVerifiedRequest.sandBox ?: config.sandBox) - val response = client.post { - url(route) - body = paymentUnVerifiedRequest.copyWithConfig(config) - } - response.data + override suspend fun paymentUnVerified(request: PaymentUnVerifiedRequest): PaymentUnVerifiedDataResponse? = + handleRequest { + client.post { + url(HttpRoutes.paymentUnVerified(request.sandBox ?: config.sandBox)) + setBody(request.copyWithConfig(config)) + }.data } - } - - /** - * Reverses a payment. - * @param paymentReverseRequest The request object containing payment reversal details. - * @return A [PaymentReverseDataResponse] object containing the reversal result, or null if the request fails. - */ - override suspend fun paymentReverse(paymentReverseRequest: PaymentReverseRequest): PaymentReverseDataResponse? { - return handleRequestWithErrorHandling { - val route = - HttpRoutes.paymentReverse(paymentReverseRequest.sandBox ?: config.sandBox) - val response = client.post { - url(route) - body = paymentReverseRequest.copyWithConfig(config) - } - response.data + override suspend fun paymentReverse(request: PaymentReverseRequest): PaymentReverseDataResponse? = + handleRequest { + client.post { + url(HttpRoutes.paymentReverse(request.sandBox ?: config.sandBox)) + setBody(request.copyWithConfig(config)) + }.data } - } - /** - * Retrieves a list of transactions. - * @param transactionRequest The request object containing transaction filter details. - * @return A list of [Session] objects representing the transactions, or null if the request fails. - */ - override suspend fun getTransactions(transactionRequest: TransactionRequest): List? { - return handleRequestWithErrorHandling { + override suspend fun getTransactions(request: TransactionRequest): List? = + handleRequest { val query = """ - query Sessions(${'$'}terminal_id: ID!, ${'$'}filter: FilterEnum, ${'$'}id: ID, ${'$'}reference_id: String, ${'$'}rrn: String, ${'$'}card_pan: String, ${'$'}email: String, ${'$'}mobile: CellNumber, ${'$'}description: String, ${'$'}limit: Int, ${'$'}offset: Int) { Session(terminal_id: ${'$'}terminal_id, filter: ${'$'}filter, id: ${'$'}id, reference_id: ${'$'}reference_id, rrn: ${'$'}rrn, card_pan: ${'$'}card_pan, email: ${'$'}email, mobile: ${'$'}mobile, description: ${'$'}description, limit: ${'$'}limit, offset: ${'$'}offset) { id, status, amount, description, created_at } } - """.trimIndent() + query Sessions(\$terminal_id: ID!, \$filter: FilterEnum, \$id: ID, \$reference_id: String, \$rrn: String, \$card_pan: String, \$email: String, \$mobile: CellNumber, \$description: String, \$limit: Int, \$offset: Int) { + Session(terminal_id: \$terminal_id, filter: \$filter, id: \$id, reference_id: \$reference_id, rrn: \$rrn, card_pan: \$card_pan, email: \$email, mobile: \$mobile, description: \$description, limit: \$limit, offset: \$offset) { + id + status + amount + description + created_at + } + } + """.trimIndent() - val token = transactionRequest.token ?: config.token - val response = client.post { + client.post { url(HttpRoutes.BASE_URL_GRAPH) - header("Authorization", "Bearer $token") - body = GraphTransactionModel(query = query, variables = transactionRequest) - } - response.data?.session + header("Authorization", "Bearer ${request.token ?: config.token}") + setBody(GraphTransactionModel(query, request)) + }.data?.session } - } - /** - * Refunds a payment. - * @param paymentRefundRequest The request object containing refund details. - * @return A [PaymentRefundResponse] object containing the refund result, or null if the request fails. - */ - override suspend fun paymentRefund(paymentRefundRequest: PaymentRefundRequest): PaymentRefundResponse? { - return handleRequestWithErrorHandling { + override suspend fun paymentRefund(request: PaymentRefundRequest): PaymentRefundResponse? = + handleRequest { val query = """ - mutation AddRefund(${'$'}session_id: ID!,${'$'}amount: BigInteger!,${'$'}description: String,${'$'}method: InstantPayoutActionTypeEnum,${'$'}reason: RefundReasonEnum) {resource: AddRefund(session_id: ${'$'}session_id,amount: ${'$'}amount,description: ${'$'}description,method: ${'$'}method,reason: ${'$'}reason) {terminal_id,id,amount,timeline {refund_amount,refund_time,refund_status}}} - """.trimIndent() + mutation AddRefund(\$session_id: ID!, \$amount: BigInteger!, \$description: String, \$method: InstantPayoutActionTypeEnum, \$reason: RefundReasonEnum) { + resource: AddRefund(session_id: \$session_id, amount: \$amount, description: \$description, method: \$method, reason: \$reason) { + terminal_id + id + amount + timeline { + refund_amount + refund_time + refund_status + } + } + } + """.trimIndent() - val token = paymentRefundRequest.token ?: config.token - val response = client.post { + client.post { url(HttpRoutes.BASE_URL_GRAPH) - header("Authorization", "Bearer $token") - body = GraphRefundModel(query = query, variables = paymentRefundRequest) - } - response.data.resource + header("Authorization", "Bearer ${request.token ?: config.token}") + setBody(GraphRefundModel(query, request)) + }.data?.resource } - } - - /** - * Handles errors and retries requests if necessary. - * @param request A suspending function that performs the request. - * @return The result of the request, or throws an exception if an error occurs. - */ - private suspend fun handleRequestWithErrorHandling(request: suspend () -> T): T { - return try { + private suspend fun handleRequest(request: suspend () -> T): T = + try { request() } catch (e: RedirectResponseException) { - // 3xx - responses - val errorResponse = e.response.readText() - throw Exception(processErrorResponse(errorResponse) ?: e.response.status.description) + throw Exception(extractError(e.response.readText()) ?: e.response.status.description) } catch (e: ClientRequestException) { - // 4xx - responses - val errorResponse = e.response.readText() - throw Exception(processErrorResponse(errorResponse) ?: e.response.status.description) + throw Exception(extractError(e.response.readText()) ?: e.response.status.description) } catch (e: ServerResponseException) { - // 5xx - responses - val errorResponse = e.response.readText() - throw Exception(processErrorResponse(errorResponse) ?: e.response.status.description) + throw Exception(extractError(e.response.readText()) ?: e.response.status.description) } catch (e: Exception) { throw e } - } - - /** - * Processes error responses from the API. - * @param jsonString The JSON string containing the error response. - * @return A readable error message, or null if the error cannot be processed. - */ - private fun processErrorResponse(jsonString: String?): String? { - // Implementation of error response processing - if (jsonString == null) return null - try { - val jsonObject = JSONObject(jsonString) - - // If 'errors' is an object - if (jsonObject.has("errors") && !jsonObject.isNull("errors") && jsonObject.get("errors") is JSONObject) { - val errorObject = jsonObject.getJSONObject("errors") - val message :String?= errorObject.optString("message",null) - val faMessage :String?= errorObject.optString("fa_message",null) - - if(!(faMessage ?: message).isNullOrBlank() && !(faMessage ?: message).isNullOrEmpty()) return faMessage ?: message - - val messageJsonObject = jsonObject.getString("message") - val faMessageJsonObject :String?= jsonObject.optString("fa_message",null) - - return faMessageJsonObject ?: messageJsonObject - } + private fun extractError(json: String?): String? { + if (json.isNullOrBlank()) return null - // If 'errors' is an array - else if (jsonObject.has("errors") && jsonObject.get("errors") is JSONArray) { - val errorsArray = jsonObject.getJSONArray("errors") - - // Iterate through each error in the array - for (i in 0 until errorsArray.length()) { - val errorObject = errorsArray.getJSONObject(i) - - // Check if there's a readable_code error - if (errorObject.has("readable_code")) { - val message = errorObject.getString("message") - val faMessage :String?= errorObject.optString("fa_message", null) - - return faMessage ?: message - } - // Check if there's a validation error - else if (errorObject.has("validation")) { - val validationArray = errorObject.getJSONArray("validation") - for (j in 0 until validationArray.length()) { - val validationObject = validationArray.getJSONObject(j) - val message = validationObject.getString("message") - - // Use the validation message - val faMessage :String?= validationObject.optString("fa_message", null) - return faMessage ?: message - } - } - // Check if there's a other error - else { - val message = errorObject.getString("message") - val faMessage :String?= errorObject.optString("fa_message", null) - - return faMessage ?: message - } - } + return try { + val obj = JSONObject(json) + when (val errors = obj.opt("errors")) { + is JSONObject -> errors.optString("fa_message") ?: errors.optString("message") + is JSONArray -> (0 until errors.length()) + .mapNotNull { errors.optJSONObject(it) } + .flatMap { listOfNotNull( + it.optString("fa_message"), + it.optString("message"), + it.optJSONArray("validation")?.optJSONObject(0)?.optString("fa_message"), + it.optJSONArray("validation")?.optJSONObject(0)?.optString("message") + ) } + .firstOrNull() + else -> obj.optString("fa_message") ?: obj.optString("message") } - } catch (ex: Exception) { - return null + } catch (_: Exception) { + null } - return null } -} \ No newline at end of file +} + diff --git a/src/main/java/com/example/zarinpal/data/remote/enum/FilterEnum.kt b/src/main/java/com/example/zarinpal/data/remote/enum/FilterEnum.kt index 6f132f6..0655d1a 100644 --- a/src/main/java/com/example/zarinpal/data/remote/enum/FilterEnum.kt +++ b/src/main/java/com/example/zarinpal/data/remote/enum/FilterEnum.kt @@ -2,12 +2,15 @@ package com.example.zarinpal.data.remote.enum import kotlinx.serialization.Serializable +/** + * Specifies the available filter options for querying payment records. + */ @Serializable enum class FilterEnum { - ALL, - PAID, - VERIFIED, - TRASH, - ACTIVE, - REFUNDED -} \ No newline at end of file + ALL, // Includes all payment records + PAID, // Payments that have been successfully made + VERIFIED, // Payments that have been confirmed as verified + TRASH, // Payments that are marked as discarded + ACTIVE, // Ongoing or currently valid payment transactions + REFUNDED // Payments that have been successfully refunded +} diff --git a/src/main/java/com/example/zarinpal/data/remote/enum/MethodEnum.kt b/src/main/java/com/example/zarinpal/data/remote/enum/MethodEnum.kt index 1de4c88..4b7b076 100644 --- a/src/main/java/com/example/zarinpal/data/remote/enum/MethodEnum.kt +++ b/src/main/java/com/example/zarinpal/data/remote/enum/MethodEnum.kt @@ -2,8 +2,11 @@ package com.example.zarinpal.data.remote.enum import kotlinx.serialization.Serializable +/** + * Defines supported payment methods. + */ @Serializable enum class MethodEnum { - PAYA, - CARD -} \ No newline at end of file + PAYA, // Interbank transfer using the PAYA system + CARD // Payment via bank card +} diff --git a/src/main/java/com/example/zarinpal/data/remote/enum/ReasonEnum.kt b/src/main/java/com/example/zarinpal/data/remote/enum/ReasonEnum.kt index 7fd406d..2df8f38 100644 --- a/src/main/java/com/example/zarinpal/data/remote/enum/ReasonEnum.kt +++ b/src/main/java/com/example/zarinpal/data/remote/enum/ReasonEnum.kt @@ -2,11 +2,13 @@ package com.example.zarinpal.data.remote.enum import kotlinx.serialization.Serializable - +/** + * Enum representing reasons for payment-related actions such as refunds or cancellations. + */ @Serializable enum class ReasonEnum { - DUPLICATE_TRANSACTION, - SUSPICIOUS_TRANSACTION, - CUSTOMER_REQUEST, - OTHER -} \ No newline at end of file + DUPLICATE_TRANSACTION, // Transaction was submitted more than once + SUSPICIOUS_TRANSACTION, // Transaction flagged as suspicious + CUSTOMER_REQUEST, // Action initiated by customer request + OTHER // Any other unspecified reason +} diff --git a/src/main/java/com/example/zarinpal/utils/Validator.kt b/src/main/java/com/example/zarinpal/utils/Validator.kt index 0fc467f..bed3869 100644 --- a/src/main/java/com/example/zarinpal/utils/Validator.kt +++ b/src/main/java/com/example/zarinpal/utils/Validator.kt @@ -1,189 +1,91 @@ package com.example.zarinpal.utils /** - * This object contains functions for validating various input fields used in the ZarinPal payment gateway system. + * Provides validation functions for input fields used in the ZarinPal payment system. */ -class Validator { +object Validator { - companion object { - - /** - * Validates the provided merchant ID. It must be in the correct UUID format. - * - * @param merchantId The merchant ID to validate. - * @throws IllegalArgumentException if the merchant ID is null or invalid. - */ - fun validateMerchantId(merchantId: String) { - val pattern = "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$".toRegex() - if (!pattern.matches(merchantId)) { - throw IllegalArgumentException("Invalid merchant_id format. It should be a valid UUID.") - } + fun validateMerchantId(merchantId: String) { + require(merchantId.matches(Regex("^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"))) { + "Invalid merchant ID format. Must be a valid UUID." } + } - /** - * Validates the authority string. It must start with 'A' or 'S' followed by 35 alphanumeric characters. - * - * @param authority The authority string to validate. - * @throws IllegalArgumentException if the authority format is invalid. - */ - fun validateAuthority(authority: String) { - val pattern = "^[AS][0-9a-zA-Z]{35}$".toRegex() - if (!pattern.matches(authority)) { - throw IllegalArgumentException("Invalid authority format. It should be a string starting with 'A' or 'S' followed by 35 alphanumeric characters.") - } + fun validateAuthority(authority: String) { + require(authority.matches(Regex("^[AS][0-9a-zA-Z]{35}$"))) { + "Invalid authority format. Must start with 'A' or 'S' followed by 35 alphanumeric characters." } + } - /** - * Validates the payment amount. It must be greater than or equal to the minimum amount. - * - * @param amount The amount to validate. - * @param minAmount The minimum allowed amount (default is 1000). - * @throws IllegalArgumentException if the amount is less than the minimum. - */ - fun validateAmount(amount: Int, minAmount: Int = 1000) { - if (amount < minAmount) { - throw IllegalArgumentException("Amount must be at least $minAmount.") - } - } + fun validateAmount(amount: Int, minAmount: Int = 1000) { + require(amount >= minAmount) { "Amount must be at least $minAmount." } + } - /** - * Validates the callback URL. It must start with "http://" or "https://". - * - * @param callbackUrl The callback URL to validate. - * @throws IllegalArgumentException if the callback URL format is invalid. - */ - fun validateCallbackUrl(callbackUrl: String) { - val pattern = "^https?:\\/\\/.*$".toRegex() - if (!pattern.matches(callbackUrl)) { - throw IllegalArgumentException("Invalid callback URL format. It should start with http:// or https://.") - } + fun validateCallbackUrl(callbackUrl: String) { + require(callbackUrl.matches(Regex("^https?://.*$"))) { + "Invalid callback URL. Must start with http:// or https://." } + } - /** - * Validates the mobile number. It must start with "09" and contain 11 digits. - * - * @param mobile The mobile number to validate. - * @throws IllegalArgumentException if the mobile number is invalid. - */ - fun validateMobile(mobile: String?) { - val pattern = "^09[0-9]{9}$".toRegex() - if (mobile != null && !pattern.matches(mobile)) { - throw IllegalArgumentException("Invalid mobile number format.") - } + fun validateMobile(mobile: String?) { + if (mobile != null) { + require(mobile.matches(Regex("^09[0-9]{9}$"))) { "Invalid mobile number format." } } + } - /** - * Validates the email address. It must follow a valid email format. - * - * @param email The email address to validate. - * @throws IllegalArgumentException if the email format is invalid. - */ - fun validateEmail(email: String?) { - val pattern = "^[^\\s@]+@[^(\\s@)]+\\.[^\\s@]+$".toRegex() - if (email != null && !pattern.matches(email)) { - throw IllegalArgumentException("Invalid email format.") - } + fun validateEmail(email: String?) { + if (email != null) { + require(email.matches(Regex("^[^\s@]+@[^\s@]+\.[^\s@]+$"))) { "Invalid email format." } } + } - /** - * Validates the currency. Allowed values are "IRR" or "IRT". - * - * @param currency The currency to validate. - * @throws IllegalArgumentException if the currency is not in the allowed list. - */ - fun validateCurrency(currency: String?) { - val validCurrencies = listOf("IRR", "IRT") - if (currency != null && currency !in validCurrencies) { - throw IllegalArgumentException("Invalid currency format. Allowed values are 'IRR' or 'IRT'.") + fun validateCurrency(currency: String?) { + if (currency != null) { + require(currency == "IRR" || currency == "IRT") { + "Invalid currency format. Allowed values are 'IRR' or 'IRT'." } } + } - /** - * Validates the wages list. Each wage must include a valid IBAN, positive amount, and a description shorter than 255 characters. - * - * @param wages The list of wages to validate. - * @throws IllegalArgumentException if any of the wage entries is invalid. - */ - fun validateWages(wages: List>?) { - wages?.forEach { wage -> - val iban = wage["iban"] as String - val amount = wage["amount"] as Double - val description = wage["description"] as String + fun validateWages(wages: List>?) { + wages?.forEach { wage -> + val iban = wage["iban"] as? String ?: throw IllegalArgumentException("Wage IBAN is required.") + val amount = wage["amount"] as? Double ?: throw IllegalArgumentException("Wage amount is required.") + val description = wage["description"] as? String ?: throw IllegalArgumentException("Wage description is required.") - val ibanPattern = "^[A-Z]{2}[0-9]{2}[0-9A-Z]{1,30}$".toRegex() - if (!ibanPattern.matches(iban)) { - throw IllegalArgumentException("Invalid IBAN format in wages.") - } - if (amount <= 0) { - throw IllegalArgumentException("Wage amount must be greater than zero.") - } - if (description.length > 255) { - throw IllegalArgumentException("Wage description must be provided and less than 255 characters.") - } + require(iban.matches(Regex("^[A-Z]{2}[0-9]{2}[0-9A-Z]{1,30}$"))) { + "Invalid IBAN format in wages." } + require(amount > 0) { "Wage amount must be greater than zero." } + require(description.length <= 255) { "Wage description must be 255 characters or fewer." } } + } - /** - * Validates the terminal ID. It must contain only digits and cannot be empty. - * - * @param terminalId The terminal ID to validate. - * @throws IllegalArgumentException if the terminal ID is empty or contains non-digit characters. - */ - fun validateTerminalId(terminalId: String) { - val regex = Regex("^[0-9]+$") // Matches only digits. - if (terminalId.isEmpty() || !regex.matches(terminalId)) { - throw IllegalArgumentException("Terminal ID must contain only digits and cannot be empty.") - } + fun validateTerminalId(terminalId: String) { + require(terminalId.isNotEmpty() && terminalId.matches(Regex("^[0-9]+$"))) { + "Terminal ID must contain only digits and cannot be empty." } + } - /** - * Validates the session ID to ensure it is not empty. - * - * @param sessionId The session ID to validate. - * @throws IllegalArgumentException if the session ID is empty. - */ - fun validateSessionId(sessionId: String) { - val regex = Regex("^[0-9]+$") // Matches only digits. - if (sessionId.isEmpty() || !regex.matches(sessionId)) { - throw IllegalArgumentException("Session ID is required.") - } + fun validateSessionId(sessionId: String) { + require(sessionId.isNotEmpty() && sessionId.matches(Regex("^[0-9]+$"))) { + "Session ID must contain only digits and cannot be empty." } + } - /** - * Validates the limit value to ensure it is a positive integer. - * - * @param limit The limit value to validate. - * @throws IllegalArgumentException if the limit is not positive. - */ - fun validateLimit(limit: Int) { - if (limit <= 0) { - throw IllegalArgumentException("Limit must be a positive integer.") - } - } + fun validateLimit(limit: Int) { + require(limit > 0) { "Limit must be a positive integer." } + } - /** - * Validates the offset value to ensure it is a non-negative integer. - * - * @param offset The offset value to validate. - * @throws IllegalArgumentException if the offset is negative. - */ - fun validateOffset(offset: Int) { - if (offset < 0) { - throw IllegalArgumentException("Offset must be a non-negative integer.") - } - } + fun validateOffset(offset: Int) { + require(offset >= 0) { "Offset must be a non-negative integer." } + } - /** - * Validates the card PAN (Primary Account Number) to ensure it is a 16-digit number. - * - * @param cardPan The card PAN value to validate. - * @throws IllegalArgumentException if the card PAN format is invalid. - */ - fun validateCardPan(cardPan: String?) { - val pattern = "^[0-9]{16}$".toRegex() - if (cardPan != null && !pattern.matches(cardPan)) { - throw IllegalArgumentException("Invalid card PAN format. It should be a 16-digit number.") + fun validateCardPan(cardPan: String?) { + if (cardPan != null) { + require(cardPan.matches(Regex("^[0-9]{16}$"))) { + "Invalid card PAN format. Must be a 16-digit number." } } } -} +}