Implement bank transfer app flows
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import Foundation
|
||||
|
||||
private let BML_API_BASE = "https://www.bankofmaldives.com.mv/internetbanking"
|
||||
private let BML_API_UA = "bml-mobile-banking/348 (Apple; iOS 17.0; iPhone)"
|
||||
private let BML_API_VERSION = "2.1.44.348"
|
||||
|
||||
final class BmlAccountClient {
|
||||
|
||||
private let session: BmlSession
|
||||
private let urlSession: URLSession = {
|
||||
let cfg = URLSessionConfiguration.default
|
||||
cfg.timeoutIntervalForRequest = 30
|
||||
return URLSession(configuration: cfg)
|
||||
}()
|
||||
|
||||
init(session: BmlSession) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
// MARK: - Dashboard / Accounts
|
||||
|
||||
func fetchAccounts(
|
||||
loginTag: String,
|
||||
profileName: String = "Personal",
|
||||
profileId: String = ""
|
||||
) async throws -> [BankAccount] {
|
||||
let data = try await apiGet("/api/mobile/dashboard")
|
||||
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
root["success"] as? Bool == true,
|
||||
let payload = root["payload"] as? [String: Any],
|
||||
let dashboard = payload["dashboard"] as? [[String: Any]] else {
|
||||
return []
|
||||
}
|
||||
return parseDashboard(dashboard, loginTag: loginTag, profileName: profileName, profileId: profileId)
|
||||
}
|
||||
|
||||
// MARK: - Profile check (lightweight ping to verify session)
|
||||
|
||||
func checkProfile() async throws {
|
||||
let data = try await apiGet("/api/mobile/profile")
|
||||
if let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
root["success"] as? Bool == false {
|
||||
throw BmlError.sessionExpired
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User info
|
||||
|
||||
func fetchUserInfo() async throws -> BmlUserInfo? {
|
||||
let data = try await apiGet("/api/mobile/userinfo")
|
||||
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
root["success"] as? Bool == true,
|
||||
let payload = root["payload"] as? [String: Any],
|
||||
let user = payload["user"] as? [String: Any] else { return nil }
|
||||
return BmlUserInfo(
|
||||
fullName: (user["fullname"] as? String ?? "").trimmingCharacters(in: .whitespaces),
|
||||
email: (user["email"] as? String ?? "").trimmingCharacters(in: .whitespaces),
|
||||
mobile: (user["mobile_phone"] as? String ?? "").trimmingCharacters(in: .whitespaces),
|
||||
customerId: (user["customer_number"] as? String ?? "").trimmingCharacters(in: .whitespaces),
|
||||
idCard: (user["idcard"] as? String ?? "").trimmingCharacters(in: .whitespaces),
|
||||
birthdate: (user["birthdate"] as? String ?? "").trimmingCharacters(in: .whitespaces)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Loan detail
|
||||
|
||||
func fetchLoanDetail(internalId: String) async throws -> BmlLoanDetail? {
|
||||
let data = try await apiGet("/api/mobile/account/\(internalId)")
|
||||
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
root["success"] as? Bool == true,
|
||||
let p = root["payload"] as? [String: Any] else { return nil }
|
||||
return BmlLoanDetail(
|
||||
loanAmount: p["loanAmount"] as? Double ?? 0,
|
||||
outstandingAmt: p["outstandingAmt"] as? Double ?? 0,
|
||||
repayAmount: p["repayAmount"] as? Double ?? 0,
|
||||
intRate: p["intRate"] as? Double ?? 0,
|
||||
loanStatus: p["loanStatus"] as? String ?? "",
|
||||
startDate: p["startDate"] as? String ?? "",
|
||||
endDate: p["endDate"] as? String ?? "",
|
||||
noOfRepayOverdue: p["noOfRepayOverdue"] as? Int ?? 0,
|
||||
overdueAmount: p["overdueAmount"] as? Double ?? 0
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Transfer OTP channels
|
||||
|
||||
func fetchTransferChannels() async throws -> [BmlOtpChannel] {
|
||||
let data = try await apiGet("/api/mobile/transfer")
|
||||
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
root["success"] as? Bool == true,
|
||||
let arr = (root["payload"] as? [String: Any])?["transfer"] as? [String: Any],
|
||||
let channels = arr["otpChannel"] as? [[String: Any]] else { return [] }
|
||||
return channels.map {
|
||||
BmlOtpChannel(
|
||||
channel: $0["channel"] as? String ?? "",
|
||||
description: $0["description"] as? String ?? "",
|
||||
masked: $0["masked"] as? String ?? ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Dashboard parsing
|
||||
|
||||
private func parseDashboard(
|
||||
_ dashboard: [[String: Any]],
|
||||
loginTag: String,
|
||||
profileName: String,
|
||||
profileId: String
|
||||
) -> [BankAccount] {
|
||||
var casaAccounts: [BankAccount] = []
|
||||
var cardAccounts: [BankAccount] = []
|
||||
var loanAccounts: [BankAccount] = []
|
||||
|
||||
for item in dashboard {
|
||||
let currency = item["currency"] as? String ?? "MVR"
|
||||
let accountType = item["account_type"] as? String ?? "CASA"
|
||||
let product = item["product"] as? String ?? ""
|
||||
let accountNum = Self.stringValue(item["account"])
|
||||
let status = item["account_status"] as? String ?? "Active"
|
||||
let internalId = Self.stringValue(item["id"])
|
||||
|
||||
switch accountType {
|
||||
case "CASA":
|
||||
let avail = item["availableBalance"] as? Double ?? 0
|
||||
casaAccounts.append(BankAccount(
|
||||
id: "BML_\(accountNum)_\(loginTag)",
|
||||
bank: "BML",
|
||||
profileName: profileName,
|
||||
profileType: "BML",
|
||||
productCode: item["product_code"] as? String ?? "",
|
||||
accountNumber: accountNum,
|
||||
accountBriefName: item["alias"] as? String ?? "",
|
||||
currencyName: currency,
|
||||
accountTypeName: product,
|
||||
availableBalance: avail,
|
||||
currentBalance: item["ledgerBalance"] as? Double ?? 0,
|
||||
blockedAmount: item["lockedAmount"] as? Double ?? 0,
|
||||
mvrBalance: currency == "MVR" ? avail : nil,
|
||||
statusDesc: status,
|
||||
profileImageHash: nil,
|
||||
loginTag: loginTag,
|
||||
profileId: profileId,
|
||||
internalId: internalId
|
||||
))
|
||||
|
||||
case "Loan":
|
||||
let outstanding = abs(item["availableBalance"] as? Double ?? 0)
|
||||
loanAccounts.append(BankAccount(
|
||||
id: "BML_LOAN_\(accountNum)_\(loginTag)",
|
||||
bank: "BML",
|
||||
profileName: profileName,
|
||||
profileType: "BML_LOAN",
|
||||
productCode: item["product_code"] as? String ?? "",
|
||||
accountNumber: accountNum,
|
||||
accountBriefName: item["alias"] as? String ?? "",
|
||||
currencyName: currency,
|
||||
accountTypeName: product,
|
||||
availableBalance: outstanding,
|
||||
currentBalance: outstanding,
|
||||
blockedAmount: 0,
|
||||
mvrBalance: nil,
|
||||
statusDesc: status,
|
||||
profileImageHash: nil,
|
||||
loginTag: loginTag,
|
||||
profileId: profileId,
|
||||
internalId: internalId
|
||||
))
|
||||
|
||||
case "Card":
|
||||
let isPrepaid = item["prepaid_card"] as? Bool ?? false
|
||||
let isVisible = item["account_visible"] as? Bool ?? false
|
||||
let cardBalance = item["cardBalance"] as? [String: Any]
|
||||
let avail = cardBalance?["AvailableLimit"] as? Double ?? 0
|
||||
let current = cardBalance?["CurrentBalance"] as? Double ?? 0
|
||||
let cardType: String = isPrepaid ? "BML_PREPAID" : isVisible ? "BML_CREDIT" : "BML_DEBIT"
|
||||
let alias = (item["alias"] as? String ?? "").isEmpty ? product : (item["alias"] as? String ?? product)
|
||||
cardAccounts.append(BankAccount(
|
||||
id: "BML_CARD_\(accountNum)_\(loginTag)",
|
||||
bank: "BML",
|
||||
profileName: profileName,
|
||||
profileType: cardType,
|
||||
productCode: item["product_code"] as? String ?? "",
|
||||
accountNumber: accountNum,
|
||||
accountBriefName: alias,
|
||||
currencyName: currency,
|
||||
accountTypeName: product,
|
||||
availableBalance: avail,
|
||||
currentBalance: current,
|
||||
blockedAmount: 0,
|
||||
mvrBalance: currency == "MVR" ? avail : nil,
|
||||
statusDesc: status,
|
||||
profileImageHash: nil,
|
||||
loginTag: loginTag,
|
||||
profileId: profileId,
|
||||
internalId: internalId
|
||||
))
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return casaAccounts + cardAccounts + loanAccounts
|
||||
}
|
||||
|
||||
private static func stringValue(_ value: Any?) -> String {
|
||||
switch value {
|
||||
case let string as String:
|
||||
return string
|
||||
case let int as Int:
|
||||
return String(int)
|
||||
case let int64 as Int64:
|
||||
return String(int64)
|
||||
case let double as Double:
|
||||
return double.rounded() == double ? String(Int64(double)) : String(double)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HTTP
|
||||
|
||||
private func apiGet(_ path: String) async throws -> Data {
|
||||
var req = URLRequest(url: URL(string: "\(BML_API_BASE)\(path)")!)
|
||||
req.setValue("Bearer \(session.accessToken)", forHTTPHeaderField: "Authorization")
|
||||
req.setValue(BML_API_UA, forHTTPHeaderField: "User-Agent")
|
||||
req.setValue(BML_API_VERSION, forHTTPHeaderField: "x-app-version")
|
||||
|
||||
let (data, response) = try await urlSession.data(for: req)
|
||||
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
|
||||
if code == 401 || code == 419 { throw BmlError.sessionExpired }
|
||||
if code >= 500 { throw BmlError.networkError("Server error HTTP \(code)") }
|
||||
return data
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user