diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Repository/BankTransferUserDataRepository.swift b/Packages/KeyAppKit/Sources/BankTransfer/Repository/BankTransferUserDataRepository.swift index e0c2345d33..4cecbded55 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Repository/BankTransferUserDataRepository.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Repository/BankTransferUserDataRepository.swift @@ -19,7 +19,7 @@ public protocol BankTransferUserDataRepository { func clearCache() async func getWallet(userId: String) async throws -> UserWallet? - - func withdrawalInfo() async throws -> WithdrawalInfo? + + func getWithdrawalInfo(userId: String) async throws -> WithdrawalInfo? func save(_ info: WithdrawalInfo) async throws } diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Service/BankTransferServiceImpl.swift b/Packages/KeyAppKit/Sources/BankTransfer/Service/BankTransferServiceImpl.swift index de4b91359f..4422f91249 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Service/BankTransferServiceImpl.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Service/BankTransferServiceImpl.swift @@ -168,9 +168,10 @@ extension BankTransferServiceImpl { ) ) } - + public func getWithdrawalInfo() async throws -> Provider.WithdrawalInfo? { - try await repository.withdrawalInfo() + guard let userId = subject.value.value.userId else { throw BankTransferError.missingUserId } + return try await repository.getWithdrawalInfo(userId: userId) } public func saveWithdrawalInfo(info: Provider.WithdrawalInfo) async throws { diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Mock/MockStrigaRemoteProvider.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Mock/MockStrigaRemoteProvider.swift index d327642e9a..fd73258bac 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Mock/MockStrigaRemoteProvider.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Mock/MockStrigaRemoteProvider.swift @@ -170,4 +170,10 @@ public final class MockStrigaRemoteProvider: StrigaRemoteProvider { public func exchangeRates() async throws -> StrigaExchangeRatesResponse { return ["USDCEUR": StrigaExchangeRates(price: "0.9", buy: "0.9", sell: "0.88", timestamp: Int(Date().timeIntervalSince1970), currency: "Euros")] } + + public func getAccountStatement(userId: String, accountId: String, startDate: Date, endDate: Date, page: Int) async throws -> StrigaGetAccountStatementResponse { + StrigaGetAccountStatementResponse(transactions: [ + StrigaGetAccountStatementResponse.Transaction(id: "a25e0dd1-8f4f-441d-a671-2f7d1e9738e6", txType: "SEPA_PAYIN_COMPLETED", bankingSenderBic: "BUKBGB22", bankingSenderIban: "GB29NWBK60161331926819") + ]) + } } diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaEndpoint.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaEndpoint.swift index 15faecfb4c..420e5d8632 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaEndpoint.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaEndpoint.swift @@ -316,6 +316,30 @@ struct StrigaEndpoint: HTTPEndpoint { body: nil ) } + + static func getAccountStatement( + baseURL: String, + keyPair: KeyPair, + userId: String, + accountId: String, + startDate: Date, + endDate: Date, + page: Int + ) throws -> Self { + try StrigaEndpoint( + baseURL: baseURL, + path: "/wallets/get/account/statement", + method: .post, + keyPair: keyPair, + body: [ + "userId": .init(userId), + "accountId": .init(accountId), + "startDate": .init(startDate.millisecondsSince1970), + "endDate": .init(endDate.millisecondsSince1970), + "page": .init(page) + ] as [String: KeyAppNetworking.AnyEncodable] + ) + } } extension KeyPair { diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaGetAccountStatementResponse.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaGetAccountStatementResponse.swift new file mode 100644 index 0000000000..a021219a64 --- /dev/null +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaGetAccountStatementResponse.swift @@ -0,0 +1,12 @@ +import Foundation + +public struct StrigaGetAccountStatementResponse: Codable { + public let transactions: [Transaction] + + public struct Transaction: Codable { + let id: String + let txType: String + let bankingSenderBic: String? + let bankingSenderIban: String? + } +} diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaProviderError.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaProviderError.swift index fbb2d8945d..369af3e4c2 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaProviderError.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Models/StrigaProviderError.swift @@ -4,4 +4,5 @@ public enum StrigaProviderError: Error { case invalidRequest(String) case invalidResponse case invalidRateTokens + case missingAccountId } diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProvider.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProvider.swift index 09bf3e9a0e..e3c79a7e05 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProvider.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProvider.swift @@ -91,4 +91,13 @@ public protocol StrigaRemoteProvider: AnyObject { /// Exchange Rates /// - SeeAlso: [Exchange Rates](https://www.sandbox.striga.com/api/v1/trade/rates) func exchangeRates() async throws -> StrigaExchangeRatesResponse + + /// Get Account Statement + /// - Parameter userId: The Id of the user who is sending this transaction + /// - Parameter accountId: Unique Id of an account belonging to any wallet of this user + /// - Parameter startDate: Format as a UNIX Epoch timestamp with ms precision + /// - Parameter endDate: Format as a UNIX Epoch timestamp with ms precision + /// - Parameter page: Page number + /// - SeeAlso: [Get Account Statement](https://docs.striga.com/reference/get-account-statement) + func getAccountStatement(userId: String, accountId: String, startDate: Date, endDate: Date, page: Int) async throws -> StrigaGetAccountStatementResponse } diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProviderImpl.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProviderImpl.swift index 3595b8de43..4b5582cdea 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProviderImpl.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/DataProviders/StrigaRemoteProviderImpl.swift @@ -262,6 +262,12 @@ extension StrigaRemoteProviderImpl: StrigaRemoteProvider { let endpoint = try StrigaEndpoint.exchangeRates(baseURL: baseURL, keyPair: keyPair) return try await httpClient.request(endpoint: endpoint, responseModel: StrigaExchangeRatesResponse.self) } + + public func getAccountStatement(userId: String, accountId: String, startDate: Date, endDate: Date, page: Int) async throws -> StrigaGetAccountStatementResponse { + guard let keyPair else { throw BankTransferError.invalidKeyPair } + let endpoint = try StrigaEndpoint.getAccountStatement(baseURL: baseURL, keyPair: keyPair, userId: userId, accountId: accountId, startDate: startDate, endDate: endDate, page: page) + return try await httpClient.request(endpoint: endpoint, responseModel: StrigaGetAccountStatementResponse.self) + } } // MARK: - Error response diff --git a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/StrigaBankTransferUserDataRepository.swift b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/StrigaBankTransferUserDataRepository.swift index 3ef8157f87..93c64bd6bd 100644 --- a/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/StrigaBankTransferUserDataRepository.swift +++ b/Packages/KeyAppKit/Sources/BankTransfer/Striga/Repository/StrigaBankTransferUserDataRepository.swift @@ -421,17 +421,44 @@ private extension UserWallet { } } -extension StrigaBankTransferUserDataRepository { - +public extension StrigaBankTransferUserDataRepository { // MARK: - Withdrawal - public func withdrawalInfo() async throws -> WithdrawalInfo? { - await localProvider.getCachedWithdrawalInfo()// ?? - /// GetAccountStatement here -// WithdrawalInfo(IBAN: "IBAN", BIC: "BIC", receiver: "Receiver") + func getWithdrawalInfo(userId: String) async throws -> StrigaWithdrawalInfo? { + if let cached = await localProvider.getCachedWithdrawalInfo() { + return cached + } else { + guard let accountId = await localProvider.getCachedUserData()?.wallet?.accounts.eur?.accountID else { + throw StrigaProviderError.missingAccountId + } + + let transactions = try await remoteProvider.getAccountStatement( + userId: userId, + accountId: accountId, + startDate: Constants.startDate, + endDate: Date(), + page: 1 + ).transactions + let completedTransaction = transactions.first(where: { $0.txType == Constants.sepaPayoutCompleted }) + let initialTransaction = transactions + .first(where: { $0.id == completedTransaction?.id && $0.txType == Constants.sepaPayoutInitiated }) ?? + transactions.first(where: { $0.txType == Constants.sepaPayinCompleted }) + + let regData = await localProvider.getCachedRegistrationData() + + let info = WithdrawalInfo( + IBAN: initialTransaction?.bankingSenderIban, + BIC: initialTransaction?.bankingSenderBic, + receiver: [regData?.firstName, regData?.lastName].compactMap { $0 }.joined(separator: " ") + ) + if info.IBAN != nil && info.BIC != nil { + try? await save(info) + } + return info + } } - - public func save(_ info: StrigaWithdrawalInfo) async throws { + + func save(_ info: StrigaWithdrawalInfo) async throws { try await localProvider.save( withdrawalInfo: .init( IBAN: info.IBAN, @@ -441,3 +468,10 @@ extension StrigaBankTransferUserDataRepository { ) } } + +private enum Constants { + static let startDate = Date(timeIntervalSince1970: 1687564800) // 24.06.2023 + static let sepaPayoutCompleted = "SEPA_PAYOUT_COMPLETED" + static let sepaPayoutInitiated = "SEPA_PAYOUT_INITIATED" + static let sepaPayinCompleted = "SEPA_PAYIN_COMPLETED" +} diff --git a/Packages/KeyAppKit/Tests/UnitTests/BankTransferTests/StrigaRemoteProviderTests.swift b/Packages/KeyAppKit/Tests/UnitTests/BankTransferTests/StrigaRemoteProviderTests.swift index 46ec4c9343..086dff61b8 100644 --- a/Packages/KeyAppKit/Tests/UnitTests/BankTransferTests/StrigaRemoteProviderTests.swift +++ b/Packages/KeyAppKit/Tests/UnitTests/BankTransferTests/StrigaRemoteProviderTests.swift @@ -515,6 +515,49 @@ final class StrigaRemoteProviderTests: XCTestCase { XCTAssertEqual((error as NSError).code, NSURLErrorTimedOut) } } + + func testGetAccountStatement_SuccessfulEmptyResponse() async throws { + // Arrange + let mockData = """ + {"transactions":[],"count":0,"total":0} + """ + let provider = try getMockProvider(responseString: mockData, statusCode: 200) + + let result = try await provider.getAccountStatement( + userId: "cecaea44-47f2-439b-99a1-a35fefaf1eb6", + accountId: "4dc6ecb29d74198e9e507f8025cad011", + startDate: Date(timeIntervalSince1970: 1687564800), + endDate: Date(), + page: 1 + ) + + // Assert + XCTAssertEqual(result.transactions.isEmpty, true) + } + + func testGetAccountStatement_SuccessfulResponse() async throws { + // Arrange + let mockData = """ + {"transactions":[{"id":"af9d7fa5-4676-4190-ada1-06615c018f99","accountId":"d23cd18146112c1547be09a11ec2b7fb","syncedOwnerId":"54dd6616-f959-41b6-9d96-6d9fddaa3473","sourceSyncedOwnerId":"54dd6616-f959-41b6-9d96-6d9fddaa3473","destinationSyncedOwnerId":"54dd6616-f959-41b6-9d96-6d9fddaa3473","debit":"5000","timestamp":"2023-07-20T13:54:36.826Z","txType":"EXCHANGE_DEBIT","txSubType":"CURRENCY_EXCHANGE_PENDING","memo":"Swap 5000 EUR to USDC","memoPayer":"Simulate Payin","exchangeRate":"1","balanceBefore":{"amount":"5000","currency":"cents"},"balanceAfter":{"amount":"0","currency":"cents"}},{"id":"a25e0dd1-8f4f-441d-a671-2f7d1e9738e6","accountId":"d23cd18146112c1547be09a11ec2b7fb","syncedOwnerId":"54dd6616-f959-41b6-9d96-6d9fddaa3473","sourceSyncedOwnerId":"54dd6616-f959-41b6-9d96-6d9fddaa3473","credit":"5000","timestamp":"2023-07-20T13:54:36.321Z","txType":"SEPA_PAYIN_COMPLETED","memo":"Simulate Payin","exchangeRate":"1","balanceBefore":{"amount":"0","currency":"cents"},"balanceAfter":{"amount":"5000","currency":"cents"},"bankingTransactionId":"a25e0dd1-8f4f-441d-a671-2f7d1e9738e6","bankingTransactionShortId":"20230720-MQ3R2H","bankingSenderBic":"BUKBGB22","bankingSenderIban":"GB29NWBK60161331926819","bankingSenderName":"Boris Johnson","bankingPaymentType":"SEPA","bankingSenderInformation":null,"bankingSenderRoutingCodes":[],"bankingSenderAccountNumber":null,"bankingTransactionDateTime":"2023-07-20T13:54:35.904836","bankingTransactionReference":"Simulate Payin"},{"id":"7c2075cb-7037-45f2-aeb6-dc1a61334f1a","accountId":"d23cd18146112c1547be09a11ec2b7fb","syncedOwnerId":"54dd6616-f959-41b6-9d96-6d9fddaa3473","sourceSyncedOwnerId":"54dd6616-f959-41b6-9d96-6d9fddaa3473","destinationSyncedOwnerId":"54dd6616-f959-41b6-9d96-6d9fddaa3473","debit":"1000","timestamp":"2023-07-20T13:54:21.603Z","txType":"EXCHANGE_DEBIT","txSubType":"CURRENCY_EXCHANGE_PENDING","memo":"Swap 1000 EUR to USDC","memoPayer":"Simulate Payin","exchangeRate":"1","balanceBefore":{"amount":"1000","currency":"cents"},"balanceAfter":{"amount":"0","currency":"cents"}},{"id":"5c29131d-186e-47ad-a9c2-252368fc88ea","accountId":"d23cd18146112c1547be09a11ec2b7fb","syncedOwnerId":"54dd6616-f959-41b6-9d96-6d9fddaa3473","sourceSyncedOwnerId":"54dd6616-f959-41b6-9d96-6d9fddaa3473","credit":"1000","timestamp":"2023-07-20T13:54:20.988Z","txType":"SEPA_PAYIN_COMPLETED","memo":"Simulate Payin","exchangeRate":"1","balanceBefore":{"amount":"0","currency":"cents"},"balanceAfter":{"amount":"1000","currency":"cents"},"bankingTransactionId":"5c29131d-186e-47ad-a9c2-252368fc88ea","bankingTransactionShortId":"20230720-RAF998","bankingSenderBic":"BUKBGB22","bankingSenderIban":"GB29NWBK60161331926819","bankingSenderName":"Boris Johnson","bankingPaymentType":"SEPA","bankingSenderInformation":null,"bankingSenderRoutingCodes":[],"bankingSenderAccountNumber":null,"bankingTransactionDateTime":"2023-07-20T13:54:19.754214","bankingTransactionReference":"Simulate Payin"}],"count":4,"total":4} + """ + let provider = try getMockProvider(responseString: mockData, statusCode: 200) + + let result = try await provider.getAccountStatement( + userId: "cecaea44-47f2-439b-99a1-a35fefaf1eb6", + accountId: "4dc6ecb29d74198e9e507f8025cad011", + startDate: Date(timeIntervalSince1970: 1687564800), + endDate: Date(), + page: 1 + ) + debugPrint(result.transactions.first(where: { $0.txType == "SEPA_PAYIN_COMPLETED" })) + // Assert + XCTAssertEqual(result.transactions.isEmpty, false) + XCTAssertEqual(result.transactions.contains(where: { $0.txType == "SEPA_PAYOUT_COMPLETED" }), false) + XCTAssertEqual(result.transactions.contains(where: { $0.txType == "SEPA_PAYOUT_INITIATED" }), false) + XCTAssertEqual(result.transactions.contains(where: { $0.txType == "SEPA_PAYIN_COMPLETED" }), true) + XCTAssertNotNil(result.transactions.first(where: { $0.txType == "SEPA_PAYIN_COMPLETED" })?.bankingSenderIban) + XCTAssertNotNil(result.transactions.first(where: { $0.txType == "SEPA_PAYIN_COMPLETED" })?.bankingSenderBic) + } // MARK: - Helper Methods diff --git a/p2p_wallet/Scenes/Main/BankTransfer/WithdrawCalculator/WithdrawCalculatorViewModel.swift b/p2p_wallet/Scenes/Main/BankTransfer/WithdrawCalculator/WithdrawCalculatorViewModel.swift index d76f1ee0b9..2f2325298a 100644 --- a/p2p_wallet/Scenes/Main/BankTransfer/WithdrawCalculator/WithdrawCalculatorViewModel.swift +++ b/p2p_wallet/Scenes/Main/BankTransfer/WithdrawCalculator/WithdrawCalculatorViewModel.swift @@ -23,7 +23,7 @@ final class WithdrawCalculatorViewModel: BaseViewModel, ObservableObject { let allButtonPressed = PassthroughSubject() let openBankTransfer = PassthroughSubject() let openWithdraw = PassthroughSubject() - let proceedBankTransfer = PassthroughSubject() + let proceedWithdraw = PassthroughSubject() @Published var actionData = WithdrawCalculatorAction.zero @Published var isLoading = false @@ -160,34 +160,35 @@ private extension WithdrawCalculatorViewModel { .withLatestFrom(bankTransferService.value.state) .sinkAsync { [weak self] state in guard let self else { return } - self.openWithdraw.send(StrigaWithdrawalInfo(IBAN: "4560001 5001 5800 1234 0000 11", BIC: "TRONLOLIPOPXXX", receiver: "Lord Voldemort")) -// if state.value.kycStatus != .approved { -// self.openBankTransfer.send() -// } else if state.value.isIBANNotReady { -// self.isLoading = true -// await self.bankTransferService.value.reload() -// self.proceedBankTransfer.send() -// } - // todo add get statement request + if state.value.kycStatus != .approved { + self.openBankTransfer.send() + } else if state.value.isIBANNotReady { + self.isLoading = true + await self.bankTransferService.value.reload() + self.isLoading = false + self.proceedWithdraw.send() + } else { + self.proceedWithdraw.send() + } } .store(in: &subscriptions) - proceedBankTransfer + proceedWithdraw .withLatestFrom(bankTransferService.value.state) .sinkAsync { [weak self] state in guard let self else { return } if state.value.isIBANNotReady { self.notificationService.showDefaultErrorNotification() } else { - // iban and bic - // todo add get statement request + let info = await self.getWithdrawalInfo() + self.openWithdraw.send(info) } } .store(in: &subscriptions) $arePricesLoading .filter { $0 } - .map { _ in return WithdrawCalculatorAction(isEnabled: false, title: L10n.gettingRates) } + .map { _ in WithdrawCalculatorAction(isEnabled: false, title: L10n.gettingRates) } .receive(on: RunLoop.main) .assignWeak(to: \.actionData, on: self) .store(in: &subscriptions) @@ -279,6 +280,15 @@ private extension WithdrawCalculatorViewModel { func cancelUpdate() { exchangeRatesTimer?.invalidate() } + + func getWithdrawalInfo() async -> StrigaWithdrawalInfo { + self.isLoading = true + let info = try? await self.bankTransferService.value.getWithdrawalInfo() + let regData = try? await self.bankTransferService.value.getRegistrationData() + let receiver = [regData?.firstName, regData?.lastName].compactMap { $0 }.joined(separator: " ") + self.isLoading = false + return info ?? StrigaWithdrawalInfo(receiver: receiver) + } } private enum Constants {