From 4ecf9bc22ed885f57f7c1082bd684159d6b8799f Mon Sep 17 00:00:00 2001 From: vamsii777 Date: Tue, 18 Feb 2025 18:01:16 +0530 Subject: [PATCH 1/3] Add Currency enum with comprehensive international currency support - Create `Currency` enum representing Razorpay-supported currencies - Implement detailed currency metadata including: * ISO 4217 currency codes * Currency descriptions * Decimal place exponents * Currency symbols - Add utility methods for currency conversion and representation - Update Payment capture method to include currency parameter --- Sources/RazorpayKit/Models/Currency.swift | 276 ++++++++++++++++++++ Sources/RazorpayKit/Resources/Payment.swift | 8 +- 2 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 Sources/RazorpayKit/Models/Currency.swift diff --git a/Sources/RazorpayKit/Models/Currency.swift b/Sources/RazorpayKit/Models/Currency.swift new file mode 100644 index 0000000..3b5fa56 --- /dev/null +++ b/Sources/RazorpayKit/Models/Currency.swift @@ -0,0 +1,276 @@ +/// Represents currencies supported by Razorpay for international payments +/// +/// This enum contains all the currencies that can be used for payments through Razorpay's payment gateway. +/// Each case represents a currency with its ISO 4217 currency code. +/// +/// ## Usage Example +/// ```swift +/// let paymentData: [String: Any] = [ +/// "amount": 1000, +/// "currency": Currency.indianRupee.rawValue +/// ] +/// ``` +public enum Currency: String, CaseIterable { + /// Indian Rupee (₹) + case indianRupee = "INR" + + /// United States Dollar ($) + case usDollar = "USD" + + /// Euro (€) + case euro = "EUR" + + /// British Pound Sterling (£) + case britishPound = "GBP" + + /// Japanese Yen (¥) + case japaneseYen = "JPY" + + /// Canadian Dollar (C$) + case canadianDollar = "CAD" + + /// Australian Dollar (A$) + case australianDollar = "AUD" + + /// New Zealand Dollar (NZ$) + case newZealandDollar = "NZD" + + /// Singapore Dollar (S$) + case singaporeDollar = "SGD" + + /// Hong Kong Dollar (HK$) + case hongKongDollar = "HKD" + + /// Malaysian Ringgit (RM) + case malaysianRinggit = "MYR" + + /// United Arab Emirates Dirham (د.إ) + case uaeDirham = "AED" + + /// Swiss Franc (Fr) + case swissFranc = "CHF" + + /// Chinese Yuan (¥) + case chineseYuan = "CNY" + + /// South African Rand (R) + case southAfricanRand = "ZAR" + + /// Sri Lankan Rupee (Rs) + case sriLankanRupee = "LKR" + + /// Swedish Krona (kr) + case swedishKrona = "SEK" + + /// Saudi Riyal (﷼) + case saudiRiyal = "SAR" + + /// Thai Baht (฿) + case thaiBaht = "THB" + + /// Russian Ruble (₽) + case russianRuble = "RUB" + + /// Turkish Lira (₺) + case turkishLira = "TRY" + + /// Israeli New Shekel (₪) + case israeliNewShekel = "ILS" + + /// Danish Krone (kr) + case danishKrone = "DKK" + + /// Polish Złoty (zł) + case polishZloty = "PLN" + + /// Norwegian Krone (kr) + case norwegianKrone = "NOK" + + /// Hungarian Forint (Ft) + case hungarianForint = "HUF" + + /// Czech Koruna (Kč) + case czechKoruna = "CZK" + + /// Qatari Riyal (﷼) + case qatariRiyal = "QAR" + + /// Kuwaiti Dinar (د.ك) + case kuwaitiDinar = "KWD" + + /// Bahraini Dinar (.د.ب) + case bahrainiDinar = "BHD" + + /// Omani Rial (﷼) + case omaniRial = "OMR" + + /// Mexican Peso ($) + case mexicanPeso = "MXN" + + /// Taiwan New Dollar (NT$) + case taiwanDollar = "TWD" + + /// South Korean Won (₩) + case southKoreanWon = "KRW" + + /// Indonesian Rupiah (Rp) + case indonesianRupiah = "IDR" + + /// Philippine Peso (₱) + case philippinePeso = "PHP" + + /// Vietnamese Dong (₫) + case vietnameseDong = "VND" + + /// Bangladeshi Taka (৳) + case bangladeshiTaka = "BDT" + + /// Brazilian Real (R$) + case brazilianReal = "BRL" + + /// Egyptian Pound (£) + case egyptianPound = "EGP" + + /// Nigerian Naira (₦) + case nigerianNaira = "NGN" + + /// Ghanaian Cedi (₵) + case ghanaianCedi = "GHS" + + /// Kenyan Shilling (KSh) + case kenyanShilling = "KES" +} + +extension Currency { + /// Returns a description of the currency including its name and code + public var description: String { + switch self { + case .indianRupee: return "Indian Rupee (INR)" + case .usDollar: return "US Dollar (USD)" + case .euro: return "Euro (EUR)" + case .britishPound: return "British Pound Sterling (GBP)" + case .japaneseYen: return "Japanese Yen (JPY)" + case .canadianDollar: return "Canadian Dollar (CAD)" + case .australianDollar: return "Australian Dollar (AUD)" + case .newZealandDollar: return "New Zealand Dollar (NZD)" + case .singaporeDollar: return "Singapore Dollar (SGD)" + case .hongKongDollar: return "Hong Kong Dollar (HKD)" + case .malaysianRinggit: return "Malaysian Ringgit (MYR)" + case .uaeDirham: return "UAE Dirham (AED)" + case .swissFranc: return "Swiss Franc (CHF)" + case .chineseYuan: return "Chinese Yuan (CNY)" + case .southAfricanRand: return "South African Rand (ZAR)" + case .sriLankanRupee: return "Sri Lankan Rupee (LKR)" + case .swedishKrona: return "Swedish Krona (SEK)" + case .saudiRiyal: return "Saudi Riyal (SAR)" + case .thaiBaht: return "Thai Baht (THB)" + case .russianRuble: return "Russian Ruble (RUB)" + case .turkishLira: return "Turkish Lira (TRY)" + case .israeliNewShekel: return "Israeli New Shekel (ILS)" + case .danishKrone: return "Danish Krone (DKK)" + case .polishZloty: return "Polish Złoty (PLN)" + case .norwegianKrone: return "Norwegian Krone (NOK)" + case .hungarianForint: return "Hungarian Forint (HUF)" + case .czechKoruna: return "Czech Koruna (CZK)" + case .qatariRiyal: return "Qatari Riyal (QAR)" + case .kuwaitiDinar: return "Kuwaiti Dinar (KWD)" + case .bahrainiDinar: return "Bahraini Dinar (BHD)" + case .omaniRial: return "Omani Rial (OMR)" + case .mexicanPeso: return "Mexican Peso (MXN)" + case .taiwanDollar: return "Taiwan New Dollar (TWD)" + case .southKoreanWon: return "South Korean Won (KRW)" + case .indonesianRupiah: return "Indonesian Rupiah (IDR)" + case .philippinePeso: return "Philippine Peso (PHP)" + case .vietnameseDong: return "Vietnamese Dong (VND)" + case .bangladeshiTaka: return "Bangladeshi Taka (BDT)" + case .brazilianReal: return "Brazilian Real (BRL)" + case .egyptianPound: return "Egyptian Pound (EGP)" + case .nigerianNaira: return "Nigerian Naira (NGN)" + case .ghanaianCedi: return "Ghanaian Cedi (GHS)" + case .kenyanShilling: return "Kenyan Shilling (KES)" + } + } + + /// The exponent for the currency, indicating the number of decimal places + /// Used to convert currency units to their smallest denomination + /// For example: + /// - INR has exponent 2, so ₹1 = 100 paise + /// - JPY has exponent 0, so ¥1 = 1 yen + public var exponent: Int { + switch self { + // Zero decimal currencies + case .japaneseYen, .hungarianForint, .taiwanDollar, .southKoreanWon, + .indonesianRupiah, .vietnameseDong: + return 0 + + // Three decimal currencies + case .bahrainiDinar, .kuwaitiDinar, .omaniRial: + return 3 + + // Two decimal currencies (most common) + default: + return 2 + } + } + + /// Converts a decimal amount to the smallest currency unit + /// For example: + /// - convertToSmallestUnit(10.5) for INR returns 1050 (paise) + /// - convertToSmallestUnit(10.5) for JPY returns 10 (yen) + /// - convertToSmallestUnit(10.5) for BHD returns 10500 (fils) + public func convertToSmallestUnit(_ amount: Decimal) -> Int { + let multiplier = pow(10, exponent) + return NSDecimalNumber(decimal: amount * multiplier).intValue + } + + /// Converts an amount in smallest currency unit back to decimal + /// For example: + /// - convertFromSmallestUnit(1050) for INR returns 10.50 (rupees) + /// - convertFromSmallestUnit(10) for JPY returns 10.00 (yen) + /// - convertFromSmallestUnit(10500) for BHD returns 10.500 (dinar) + public func convertFromSmallestUnit(_ amount: Int) -> Decimal { + let divisor = Decimal(pow(10, exponent)) + return Decimal(amount) / divisor + } +} + +// Example usage extension +extension Currency { + /// Returns the symbol for the currency + public var symbol: String { + switch self { + case .indianRupee: return "₹" + case .usDollar, .mexicanPeso: return "$" + case .euro: return "€" + case .britishPound, .egyptianPound: return "£" + case .japaneseYen, .chineseYuan: return "¥" + case .malaysianRinggit: return "RM" + case .uaeDirham: return "د.إ" + case .swissFranc: return "Fr" + case .southAfricanRand: return "R" + case .sriLankanRupee: return "Rs" + case .swedishKrona, .norwegianKrone, .danishKrone: return "kr" + case .saudiRiyal, .qatariRiyal, .omaniRial: return "﷼" + case .thaiBaht: return "฿" + case .russianRuble: return "₽" + case .turkishLira: return "₺" + case .israeliNewShekel: return "₪" + case .polishZloty: return "zł" + case .hungarianForint: return "Ft" + case .czechKoruna: return "Kč" + case .kuwaitiDinar: return "د.ك" + case .bahrainiDinar: return ".د.ب" + case .taiwanDollar: return "NT$" + case .southKoreanWon: return "₩" + case .indonesianRupiah: return "Rp" + case .philippinePeso: return "₱" + case .vietnameseDong: return "₫" + case .bangladeshiTaka: return "৳" + case .brazilianReal: return "R$" + case .nigerianNaira: return "₦" + case .ghanaianCedi: return "₵" + case .kenyanShilling: return "KSh" + default: return self.rawValue + } + } +} \ No newline at end of file diff --git a/Sources/RazorpayKit/Resources/Payment.swift b/Sources/RazorpayKit/Resources/Payment.swift index 94aaca1..9cb685a 100644 --- a/Sources/RazorpayKit/Resources/Payment.swift +++ b/Sources/RazorpayKit/Resources/Payment.swift @@ -14,7 +14,7 @@ import AsyncHTTPClient /// ### Payment Management /// - ``all(queryParams:extraHeaders:)`` /// - ``fetch(paymentID:queryParams:extraHeaders:)`` -/// - ``capture(paymentID:amount:data:extraHeaders:)`` +/// - ``capture(paymentID:amount:currency:data:extraHeaders:)`` /// - ``edit(paymentID:data:extraHeaders:)`` /// /// ### Refunds @@ -67,11 +67,12 @@ public protocol PaymentRoutes: RazorpayAPIRoute { /// - Parameters: /// - paymentID: The unique identifier of the payment to capture /// - amount: The amount to capture in smallest currency unit (e.g., paise for INR) + /// - currency: The currency of the payment /// - data: Additional data for the capture request /// - extraHeaders: Optional additional headers for the request /// - Returns: A dictionary containing the capture response /// - Throws: An error if the capture fails - func capture(paymentID: String, amount: Int, data: [String: Any], extraHeaders: [String: String]?) async throws -> [String: Any] + func capture(paymentID: String, amount: Int, currency: Currency, data: [String: Any], extraHeaders: [String: String]?) async throws -> [String: Any] /// Initiates a refund for a payment with the given ID and amount. /// - Parameters: @@ -275,9 +276,10 @@ public struct RazorpayPaymentRoutes: PaymentRoutes { return try await RZPRUTL.processResponse(response) } - public func capture(paymentID: String, amount: Int, data: [String: Any], extraHeaders: [String: String]? = nil) async throws -> [String: Any] { + public func capture(paymentID: String, amount: Int, currency: Currency, data: [String: Any], extraHeaders: [String: String]? = nil) async throws -> [String: Any] { var captureData = data captureData["amount"] = amount + captureData["currency"] = currency.rawValue let url = "\(APIConstants.v1)\(APIConstants.paymentURL)/\(paymentID)/capture" let requestBody = try HTTPClientRequest.Body.json(captureData) let response = try await client.sendRequest(method: .POST, path: url, body: requestBody, headers: RZPRUTL.convertToHTTPHeaders(extraHeaders)) From 4618468fad433b796efa2cf898bdd96460cdd8ec Mon Sep 17 00:00:00 2001 From: vamsii777 Date: Tue, 18 Feb 2025 18:04:12 +0530 Subject: [PATCH 2/3] Add backward-compatible payment capture method with default currency - Introduce deprecated capture method for backward compatibility - Default to Indian Rupee (INR) when no currency is specified - Maintain existing method signature while supporting new currency-aware capture --- Sources/RazorpayKit/Resources/Payment.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/RazorpayKit/Resources/Payment.swift b/Sources/RazorpayKit/Resources/Payment.swift index 9cb685a..e0a29a1 100644 --- a/Sources/RazorpayKit/Resources/Payment.swift +++ b/Sources/RazorpayKit/Resources/Payment.swift @@ -276,6 +276,12 @@ public struct RazorpayPaymentRoutes: PaymentRoutes { return try await RZPRUTL.processResponse(response) } + @available(*, deprecated, message: "Use capture(paymentID:amount:currency:data:extraHeaders:) instead") + public func capture(paymentID: String, amount: Int, data: [String: Any], extraHeaders: [String: String]? = nil) async throws -> [String: Any] { + // Default to INR for backward compatibility + return try await capture(paymentID: paymentID, amount: amount, currency: .indianRupee, data: data, extraHeaders: extraHeaders) + } + public func capture(paymentID: String, amount: Int, currency: Currency, data: [String: Any], extraHeaders: [String: String]? = nil) async throws -> [String: Any] { var captureData = data captureData["amount"] = amount From 41f1c5c7006ffb2396ea17d394f388bf17414f69 Mon Sep 17 00:00:00 2001 From: vamsii777 Date: Tue, 18 Feb 2025 18:27:01 +0530 Subject: [PATCH 3/3] Refactor Currency and Payment Capture Methods - Update Currency conversion methods to use Decimal and Double for precise calculations - Simplify Payment capture method by removing optional data parameter - Add Foundation import to Currency model - Update test suite with new capture payment method signature --- Sources/RazorpayKit/Models/Currency.swift | 7 ++++--- Sources/RazorpayKit/Resources/Payment.swift | 8 ++++---- Tests/RazorpayKitTests/RazorpayKitTests.swift | 7 +++++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Sources/RazorpayKit/Models/Currency.swift b/Sources/RazorpayKit/Models/Currency.swift index 3b5fa56..3550318 100644 --- a/Sources/RazorpayKit/Models/Currency.swift +++ b/Sources/RazorpayKit/Models/Currency.swift @@ -1,3 +1,4 @@ +import Foundation /// Represents currencies supported by Razorpay for international payments /// /// This enum contains all the currencies that can be used for payments through Razorpay's payment gateway. @@ -219,7 +220,7 @@ extension Currency { /// - convertToSmallestUnit(10.5) for JPY returns 10 (yen) /// - convertToSmallestUnit(10.5) for BHD returns 10500 (fils) public func convertToSmallestUnit(_ amount: Decimal) -> Int { - let multiplier = pow(10, exponent) + let multiplier = Decimal(pow(10.0, Double(exponent))) return NSDecimalNumber(decimal: amount * multiplier).intValue } @@ -229,7 +230,7 @@ extension Currency { /// - convertFromSmallestUnit(10) for JPY returns 10.00 (yen) /// - convertFromSmallestUnit(10500) for BHD returns 10.500 (dinar) public func convertFromSmallestUnit(_ amount: Int) -> Decimal { - let divisor = Decimal(pow(10, exponent)) + let divisor = Decimal(pow(10.0, Double(exponent))) return Decimal(amount) / divisor } } @@ -273,4 +274,4 @@ extension Currency { default: return self.rawValue } } -} \ No newline at end of file +} diff --git a/Sources/RazorpayKit/Resources/Payment.swift b/Sources/RazorpayKit/Resources/Payment.swift index e0a29a1..0e9c95f 100644 --- a/Sources/RazorpayKit/Resources/Payment.swift +++ b/Sources/RazorpayKit/Resources/Payment.swift @@ -72,7 +72,7 @@ public protocol PaymentRoutes: RazorpayAPIRoute { /// - extraHeaders: Optional additional headers for the request /// - Returns: A dictionary containing the capture response /// - Throws: An error if the capture fails - func capture(paymentID: String, amount: Int, currency: Currency, data: [String: Any], extraHeaders: [String: String]?) async throws -> [String: Any] + func capture(paymentID: String, amount: Int, currency: Currency, extraHeaders: [String: String]?) async throws -> [String: Any] /// Initiates a refund for a payment with the given ID and amount. /// - Parameters: @@ -279,11 +279,11 @@ public struct RazorpayPaymentRoutes: PaymentRoutes { @available(*, deprecated, message: "Use capture(paymentID:amount:currency:data:extraHeaders:) instead") public func capture(paymentID: String, amount: Int, data: [String: Any], extraHeaders: [String: String]? = nil) async throws -> [String: Any] { // Default to INR for backward compatibility - return try await capture(paymentID: paymentID, amount: amount, currency: .indianRupee, data: data, extraHeaders: extraHeaders) + return try await capture(paymentID: paymentID, amount: amount, currency: .indianRupee, extraHeaders: extraHeaders) } - public func capture(paymentID: String, amount: Int, currency: Currency, data: [String: Any], extraHeaders: [String: String]? = nil) async throws -> [String: Any] { - var captureData = data + public func capture(paymentID: String, amount: Int, currency: Currency, extraHeaders: [String: String]? = nil) async throws -> [String: Any] { + var captureData = [String: Any]() captureData["amount"] = amount captureData["currency"] = currency.rawValue let url = "\(APIConstants.v1)\(APIConstants.paymentURL)/\(paymentID)/capture" diff --git a/Tests/RazorpayKitTests/RazorpayKitTests.swift b/Tests/RazorpayKitTests/RazorpayKitTests.swift index 357c186..8e13aed 100644 --- a/Tests/RazorpayKitTests/RazorpayKitTests.swift +++ b/Tests/RazorpayKitTests/RazorpayKitTests.swift @@ -72,6 +72,13 @@ struct RazorpayKitTests { #expect(response["id"] as? String == paymentId, "Fetched payment ID should match the requested payment ID") } + @Test("Capture Payment") + func capturePayment() async throws { + let paymentId = "pay_29QQoUBi66xm2f" + let response = try await razorpayClient.payment.capture(paymentID: paymentId, amount: 100, currency: .indianRupee) + #expect(response["id"] != nil, "Captured payment should have an ID") + } + @Test func refundPayment() async throws { let paymentId = "pay_29QQoUBi66xm2f" let refundData: [String: Any] = ["amount": 100]