Skip to content

Commit

Permalink
Add first tests, some refactoring
Browse files Browse the repository at this point in the history
Refs #1
  • Loading branch information
Nef10 committed Sep 18, 2021
1 parent 66a2297 commit e6eab7d
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 26 deletions.
71 changes: 52 additions & 19 deletions Sources/SwiftBeanCountWealthsimpleMapper/LedgerLookup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import Foundation
import SwiftBeanCountModel
import Wealthsimple

protocol WealthsimpleAccountRepresentable {
var number: String { get }
var accountType: Wealthsimple.Account.AccountType { get }
var currency: String { get }
}

/// To lookup things in the ledger
struct LedgerLookup {

/// Key used to look up assets for wealthsimple symbols in the ledger
static let symbolMetaDataKey = "wealthsimple-symbol"

/// Key used to look up accounts by keys (e.g. symbols or transactions types) in the ledger
static let keyMetaDataKey = "wealthsimple-key"

Expand All @@ -30,17 +33,30 @@ struct LedgerLookup {
self.ledger = ledger
}

/// Checks if a transaction with a certain wealthsimple id as meta data already exists in the ledger
/// The check only checks based on the wealthsimple id - not any date, amount or other property
/// - Parameter transaction: transaction to check - should have MetaDataKeys.id set as meta data
/// - Returns: if a transaction with this id is already in the ledger
func doesTransactionExistInLedger(_ transaction: SwiftBeanCountModel.Transaction) -> Bool {
self.ledger.transactions.contains {
$0.metaData.metaData[MetaDataKeys.id] == transaction.metaData.metaData[MetaDataKeys.id] ||
$0.metaData.metaData[MetaDataKeys.nrwtId] == transaction.metaData.metaData[MetaDataKeys.id]
guard let id = transaction.metaData.metaData[MetaDataKeys.id] else {
return false
}
return self.ledger.transactions.contains {
$0.metaData.metaData[MetaDataKeys.id] == id ||
$0.metaData.metaData[MetaDataKeys.nrwtId] == id
}
}

/// Checks if a specific price is already in the ledger
/// - Parameter price: price to check
/// - Returns: if the price exists
func doesPriceExistInLedger(_ price: SwiftBeanCountModel.Price) -> Bool {
ledger.prices.contains(price)
}

/// Checks if a specific balance exists in the ledger
/// - Parameter balance: balance to check
/// - Returns: if the balance already exists
func doesBalanceExistInLedger(_ balance: Balance) -> Bool {
guard let account = ledger.accounts.first(where: { $0.name == balance.accountName }) else {
return false
Expand All @@ -67,16 +83,21 @@ struct LedgerLookup {
}
}

func ledgerSymbol(for asset: Asset) throws -> String {
func commoditySymbol(for asset: Asset) throws -> CommoditySymbol {
if asset.type == .currency {
return asset.symbol
} else {
return try ledgerSymbol(for: asset.symbol)
return try commoditySymbol(for: asset.symbol)
}
}

func ledgerSymbol(for assetSymbol: String) throws -> String {
var commodity = ledger.commodities.first { $0.metaData[Self.symbolMetaDataKey] == assetSymbol }
/// Finds the right CommoditySymbol from the ledger to use for a given asset symbol
/// The user can specify this via Self.symbolMetaDataKey, otherwise it try to use the commodity with the same symbol
/// - Parameter assetSymbol: asset symbol to find the commodity for
/// - Throws: WealthsimpleConversionError if the commodity cannot be found in the ledger
/// - Returns: CommoditySymbol
func commoditySymbol(for assetSymbol: String) throws -> CommoditySymbol {
var commodity = ledger.commodities.first { $0.metaData[MetaDataKeys.commoditySymbol] == assetSymbol }
if commodity == nil {
commodity = ledger.commodities.first { $0.symbol == assetSymbol }
}
Expand All @@ -87,7 +108,11 @@ struct LedgerLookup {
}

/// Returns account name to use for a certain type of posting - not including the Wealthsimple accounts themselves
func ledgerAccountName(for account: Wealthsimple.Account, ofType type: [SwiftBeanCountModel.AccountType], symbol assetSymbol: String? = nil) throws -> AccountName {
func ledgerAccountName(
for account: WealthsimpleAccountRepresentable,
ofType type: [SwiftBeanCountModel.AccountType],
symbol assetSymbol: String? = nil
) throws -> AccountName {
let symbol = assetSymbol ?? account.currency
let accountType = account.accountType.rawValue
let account = ledger.accounts.first {
Expand All @@ -102,7 +127,12 @@ struct LedgerLookup {
}

/// Returns account name of matching the Wealthsimple account in the ledger
func ledgerAccountName(of account: Wealthsimple.Account, symbol assetSymbol: String? = nil) throws -> AccountName {
/// - Parameters:
/// - account: Account to get the name for
/// - assetSymbol: Assets symbol in the account. If not specified cash account will be returned
/// - Throws: WealthsimpleConversionError if the account cannot be found
/// - Returns: Name of the matching account
func ledgerAccountName(of account: WealthsimpleAccountRepresentable, symbol assetSymbol: String? = nil) throws -> AccountName {
let baseAccount = ledger.accounts.first {
$0.metaData[MetaDataKeys.importerType] == MetaData.importerType &&
$0.metaData[MetaDataKeys.number] == account.number
Expand All @@ -111,20 +141,23 @@ struct LedgerLookup {
throw WealthsimpleConversionError.missingWealthsimpleAccount(account.number)
}
if let symbol = assetSymbol {
let name = "\(accountName.fullName.split(separator: ":").dropLast(1).joined(separator: ":")):\(try ledgerSymbol(for: symbol))"
let name = "\(accountName.fullName.split(separator: ":").dropLast(1).joined(separator: ":")):\(try commoditySymbol(for: symbol))"
guard let result = try? AccountName(name) else {
throw WealthsimpleConversionError.missingWealthsimpleAccount(account.number)
throw WealthsimpleConversionError.invalidCommoditySymbol(symbol)
}
return result
} else {
return accountName
}
}

func ledgerAccountCommoditySymbol(of account: AccountName) -> String? {
let account = ledger.accounts.first {
$0.name == account
}
return account?.commoditySymbol
/// Get the CommoditySymbol of a specified account in the ledger
/// - Parameter account: name of the account to check
/// - Returns:CommoditySymbol or nil if either the account could not be found or did not have a commodity assigned
func ledgerAccountCommoditySymbol(of account: AccountName) -> CommoditySymbol? {
ledger.accounts.first { $0.name == account }?.commoditySymbol
}
}

extension Wealthsimple.Account: WealthsimpleAccountRepresentable {
}
5 changes: 4 additions & 1 deletion Sources/SwiftBeanCountWealthsimpleMapper/MetaDataKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

/// Data in meta data used in the ledger
enum MetaData {
///
/// name of the importer
static let importerType = "wealthsimple"
}

Expand Down Expand Up @@ -35,4 +35,7 @@ enum MetaDataKeys {
/// Key used for wealthsimple account numbers in the ledger
static let number = "number"

/// Key used to look up commodities for wealthsimple symbols in the ledger
static let commoditySymbol = "wealthsimple-symbol"

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ extension Wealthsimple.Transaction {
}

func quantitySymbol(lookup: LedgerLookup) throws -> String {
cashTypes.contains(transactionType) ? symbol : try lookup.ledgerSymbol(for: symbol)
cashTypes.contains(transactionType) ? symbol : try lookup.commoditySymbol(for: symbol)
}

func quantityAmount(lookup: LedgerLookup) throws -> Amount {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

/// Errors which can happen when transforming downloaded wealthsimple data into SwiftBeanCountModel types
public enum WealthsimpleConversionError: Error {
public enum WealthsimpleConversionError: Error, Equatable {
/// a commodity was not found in the ledger
case missingCommodity(String)
/// an account was not found in the ledger
Expand All @@ -22,13 +22,15 @@ public enum WealthsimpleConversionError: Error {
/// the account of the postion or transaction is not contained in the account property
/// Did you forget to set it to the downloaded accounts before attempting mapping?
case accountNotFound(String)
/// A commodity symbol was used which cannot be used as account name string
case invalidCommoditySymbol(String)
}

extension WealthsimpleConversionError: LocalizedError {
public var errorDescription: String? {
switch self {
case let .missingCommodity(symbol):
return "The Commodity \(symbol) was not found in your ledger. Please make sure you add the metadata \"\(LedgerLookup.symbolMetaDataKey): \"\(symbol)\"\" to it."
return "The Commodity \(symbol) was not found in your ledger. Please make sure you add the metadata \"\(MetaDataKeys.commoditySymbol): \"\(symbol)\"\" to it."
case let .missingAccount(key, category, accountType):
return """
The \(category) account for account type \(accountType) and key \(key) was not found in your ledger. \
Expand All @@ -45,6 +47,8 @@ extension WealthsimpleConversionError: LocalizedError {
return "Wealthsimple returned an unexpected description for a transaction: \(string)"
case let .accountNotFound(accountId):
return "Wealthsimple returned an element from an account with id \(accountId) which was not found."
case let .invalidCommoditySymbol(symbol):
return "Could not generate account for commodity \(symbol). For the mapping to work commodity symbols must only contain charaters allowed in account names."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ public struct WealthsimpleLedgerMapper {
var balances = [Balance]()
try positions.forEach {
let price = Self.amount(for: $0.priceAmount, in: $0.priceCurrency)
let balanceAmount = Self.amount(for: $0.quantity, in: try lookup.ledgerSymbol(for: $0.asset))
let balanceAmount = Self.amount(for: $0.quantity, in: try lookup.commoditySymbol(for: $0.asset))
if $0.asset.type != .currency {
let price = try Price(date: $0.positionDate, commoditySymbol: try lookup.ledgerSymbol(for: $0.asset), amount: price)
let price = try Price(date: $0.positionDate, commoditySymbol: try lookup.commoditySymbol(for: $0.asset), amount: price)
if !lookup.doesPriceExistInLedger(price) {
prices.append(price)
}
Expand Down
2 changes: 2 additions & 0 deletions Tests/.swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
disabled_rules:
- force_try
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation
import XCTest

extension XCTestCase {

func assert<T, E: Error & Equatable>(_ expression: @autoclosure () throws -> T, throws expectedError: E, in file: StaticString = #file, line: UInt = #line) {
var caughtError: Error?

XCTAssertThrowsError(try expression(), file: file, line: line) {
caughtError = $0
}

guard let error = caughtError as? E else {
XCTFail("Unexpected error type, got \(type(of: caughtError!)) instead of \(E.self)", file: file, line: line)
return
}

XCTAssertEqual(error, expectedError, file: file, line: line)
}

}
Loading

0 comments on commit e6eab7d

Please sign in to comment.