From 66a2297773f1fcce24ada12e7b9fe178d7e19bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20K=C3=B6tte?= Date: Fri, 17 Sep 2021 00:41:08 -0700 Subject: [PATCH] Use account number and importer type to identify wealthsimple accounts Fixes #45 --- README.md | 25 +++++++++---------- .../LedgerLookup.swift | 21 ++++++++++++++++ .../MetaDataKeys.swift | 15 +++++++++++ .../WealthsimpleConversionError.swift | 7 ++++++ .../WealthsimpleLedgerMapper.swift | 10 ++++---- 5 files changed, 60 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7e7f024..50ca9e5 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,16 @@ If the commodity in your ledger differs from the symbol used by Wealthsimple, si ### Accounts -For account you need to add two meta data entries: +For Wealthsimple accounts themselves, you need to add this metadata: `importer-type: "wealthsimple"` and `number: "XXX"`. If the account can hold more than one commodity (all accounts except chequing and saving), it needs to follow this structure: `Assets:X:Y:Z:CashAccountName`, `Assets:X:Y:Z:CommodityName`, `Assets:X:Y:Z:OtherCommodityName`. The name of the cash account does not matter, but all other account must end with the commodity symbol (see above). Only add the `importer-type` and `number` to the cash account. + +For account used for transaction to and from your Wealthsimple accounts you need to add two meta data entries: * First is the account type (`wealthsimple-account-type`), you can look up the possible values [here](https://github.com/Nef10/WealthsimpleDownloader/blob/main/Sources/Wealthsimple/Account.swift#L37) -* Second is a key (`wealthsimple-key`): - * For holdings and cash assset accounts this is the symbol of the stock, ETF or currency - * For dividend income accounts this is the symbol as well +* Second is a key (`wealthsimple-key`), for example: + * For dividend income accounts this is the symbol as of the stock or ETF * For the assset account you are going to contribute from, use `contribution` * For the assset account you are going to deposit from, use `deposit` * Use `fee` on an expense account to track the wealthsimple fees - * Use `non resident withholding tax` on an expense account for the tax + * Use `nonResidentWithholdingTax` on an expense account for the tax * In case some transaction does not balance, we will look for an expense account with `rounding` * In case you get a refund, add `refund` to an income account * If you want to track contribution room, use `contribution-room` on an asset and expense account (optional) @@ -43,16 +44,14 @@ Both keys and types can be space separated in case you have multiple Wealthsimpl ``` 2020-07-31 open Assets:Checking:Wealthsimple CAD - wealthsimple-account-type: "ca_cash" - wealthsimple-key: "CAD" + importer-type: "wealthsimple" + number: "A001" 2020-07-31 open Assets:Investment:Wealthsimple:TFSA:Parking CAD - wealthsimple-account-type: "ca_tfsa" - wealthsimple-key: "CAD" - + importer-type: "wealthsimple" + number: "B002" 2020-07-31 open Assets:Investment:Wealthsimple:TFSA:ACWV ACWV - wealthsimple-account-type: "ca_tfsa" - wealthsimple-key: "ACWV" +2020-07-31 open Assets:Investment:Wealthsimple:TFSA:XGRO XGRO 2020-07-31 open Income:Capital:Dividend:ACWV USD wealthsimple-account-type: "ca_tfsa" @@ -62,7 +61,7 @@ Both keys and types can be space separated in case you have multiple Wealthsimpl wealthsimple-account-type: "ca_tfsa" wealthsimple-key: "contribution" -2020-07-31 open Assets:Investment:OtherComany:TFSA +2020-07-31 open Assets:Investment:OtherCompany:TFSA wealthsimple-account-type: "ca_tfsa" wealthsimple-key: "deposit" diff --git a/Sources/SwiftBeanCountWealthsimpleMapper/LedgerLookup.swift b/Sources/SwiftBeanCountWealthsimpleMapper/LedgerLookup.swift index cc13104..698057e 100644 --- a/Sources/SwiftBeanCountWealthsimpleMapper/LedgerLookup.swift +++ b/Sources/SwiftBeanCountWealthsimpleMapper/LedgerLookup.swift @@ -86,6 +86,7 @@ struct LedgerLookup { return symbol } + /// 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 { let symbol = assetSymbol ?? account.currency let accountType = account.accountType.rawValue @@ -100,6 +101,26 @@ struct LedgerLookup { return accountName } + /// Returns account name of matching the Wealthsimple account in the ledger + func ledgerAccountName(of account: Wealthsimple.Account, symbol assetSymbol: String? = nil) throws -> AccountName { + let baseAccount = ledger.accounts.first { + $0.metaData[MetaDataKeys.importerType] == MetaData.importerType && + $0.metaData[MetaDataKeys.number] == account.number + } + guard let accountName = baseAccount?.name else { + throw WealthsimpleConversionError.missingWealthsimpleAccount(account.number) + } + if let symbol = assetSymbol { + let name = "\(accountName.fullName.split(separator: ":").dropLast(1).joined(separator: ":")):\(try ledgerSymbol(for: symbol))" + guard let result = try? AccountName(name) else { + throw WealthsimpleConversionError.missingWealthsimpleAccount(account.number) + } + return result + } else { + return accountName + } + } + func ledgerAccountCommoditySymbol(of account: AccountName) -> String? { let account = ledger.accounts.first { $0.name == account diff --git a/Sources/SwiftBeanCountWealthsimpleMapper/MetaDataKeys.swift b/Sources/SwiftBeanCountWealthsimpleMapper/MetaDataKeys.swift index adf6146..97dd7da 100644 --- a/Sources/SwiftBeanCountWealthsimpleMapper/MetaDataKeys.swift +++ b/Sources/SwiftBeanCountWealthsimpleMapper/MetaDataKeys.swift @@ -5,7 +5,15 @@ // Created by Steffen Kötte on 2020-07-31. // +/// Data in meta data used in the ledger +enum MetaData { + /// + static let importerType = "wealthsimple" +} + +/// Keys for meta data used in the ledger enum MetaDataKeys { + /// Key used to save and lookup the wealthsimple transaction id of transactions in the meta data static let id = "wealthsimple-id" @@ -20,4 +28,11 @@ enum MetaDataKeys { /// Key used to save the symbol of shares for which non resident witholding tax was paid static let symbol = "symbol" + + /// Key used to identify wealthsimple accounts in the ledger + static let importerType = "importer-type" + + /// Key used for wealthsimple account numbers in the ledger + static let number = "number" + } diff --git a/Sources/SwiftBeanCountWealthsimpleMapper/WealthsimpleConversionError.swift b/Sources/SwiftBeanCountWealthsimpleMapper/WealthsimpleConversionError.swift index d39465a..cbe4079 100644 --- a/Sources/SwiftBeanCountWealthsimpleMapper/WealthsimpleConversionError.swift +++ b/Sources/SwiftBeanCountWealthsimpleMapper/WealthsimpleConversionError.swift @@ -13,6 +13,8 @@ public enum WealthsimpleConversionError: Error { case missingCommodity(String) /// an account was not found in the ledger case missingAccount(String, String, String) + /// a wealthsimple account was not found in the ledger + case missingWealthsimpleAccount(String) /// mapping of this transaction type has not been implemented yet case unsupportedTransactionType(String) /// the descriptions of the wealthsimple transactions is not the correct format @@ -32,6 +34,11 @@ extension WealthsimpleConversionError: LocalizedError { The \(category) account for account type \(accountType) and key \(key) was not found in your ledger. \ Please make sure you add the metadata \"\(LedgerLookup.keyMetaDataKey): \"\(key)\" \(LedgerLookup.accountTypeMetaDataKey): \"\(accountType)\"\" to it. """ + case let .missingWealthsimpleAccount(number): + return """ + The account for the wealthsimple account with the number \(number) was not found in your ledger. \ + Please make sure you add the metadata \"\(MetaDataKeys.importerType): \"\(MetaData.importerType)\" \(MetaDataKeys.number): \"\(number)\"\" to it. + """ case let .unsupportedTransactionType(type): return "Transactions of Type \(type) are currently not yet supported" case let .unexpectedDescription(string): diff --git a/Sources/SwiftBeanCountWealthsimpleMapper/WealthsimpleLedgerMapper.swift b/Sources/SwiftBeanCountWealthsimpleMapper/WealthsimpleLedgerMapper.swift index f457f61..063a0ba 100644 --- a/Sources/SwiftBeanCountWealthsimpleMapper/WealthsimpleLedgerMapper.swift +++ b/Sources/SwiftBeanCountWealthsimpleMapper/WealthsimpleLedgerMapper.swift @@ -111,7 +111,7 @@ public struct WealthsimpleLedgerMapper { } } let balance = Balance(date: $0.positionDate, - accountName: try lookup.ledgerAccountName(for: account, ofType: [.asset], symbol: $0.asset.symbol), + accountName: try lookup.ledgerAccountName(of: account, symbol: $0.asset.symbol), amount: balanceAmount) if !lookup.doesBalanceExistInLedger(balance) { balances.append(balance) @@ -119,7 +119,7 @@ public struct WealthsimpleLedgerMapper { } if positions.isEmpty { let balance = Balance(date: Date(), - accountName: try lookup.ledgerAccountName(for: account, ofType: [.asset]), + accountName: try lookup.ledgerAccountName(of: account), amount: Amount(number: 0, commoditySymbol: account.currency, decimalDigits: 0)) if !lookup.doesBalanceExistInLedger(balance) { balances.append(balance) @@ -200,7 +200,7 @@ public struct WealthsimpleLedgerMapper { // swiftlint:disable:next cyclomatic_complexity private func mapTransaction(_ transaction: Wealthsimple.Transaction, in account: Wealthsimple.Account) throws -> (Price?, SwiftBeanCountModel.Transaction) { - let assetAccountName = try lookup.ledgerAccountName(for: account, ofType: [.asset]) + let assetAccountName = try lookup.ledgerAccountName(of: account) var price: Price?, result: STransaction switch transaction.transactionType { case .buy: @@ -236,7 +236,7 @@ public struct WealthsimpleLedgerMapper { private func mapBuy(transaction: WTransaction, in account: WAccount, assetAccountName: AccountName) throws -> (Price, STransaction) { let posting1 = Posting(accountName: assetAccountName, amount: transaction.netCash, price: transaction.useFx ? transaction.fxAmount : nil) - let posting2 = Posting(accountName: try lookup.ledgerAccountName(for: account, ofType: [.asset], symbol: transaction.symbol), + let posting2 = Posting(accountName: try lookup.ledgerAccountName(of: account, symbol: transaction.symbol), amount: try transaction.quantityAmount(lookup: lookup), cost: try Cost(amount: transaction.marketPrice, date: nil, label: nil)) let result = STransaction(metaData: TransactionMetaData(date: transaction.processDate, metaData: [MetaDataKeys.id: transaction.id]), postings: [posting1, posting2]) @@ -307,7 +307,7 @@ public struct WealthsimpleLedgerMapper { private func mapSell(transaction: WTransaction, in account: WAccount, assetAccountName: AccountName) throws -> (Price, STransaction) { let cost = try Cost(amount: nil, date: nil, label: nil) let posting1 = Posting(accountName: assetAccountName, amount: transaction.netCash, price: transaction.useFx ? transaction.fxAmount : nil) - let accountName2 = try lookup.ledgerAccountName(for: account, ofType: [.asset], symbol: transaction.symbol) + let accountName2 = try lookup.ledgerAccountName(of: account, symbol: transaction.symbol) let posting2 = Posting(accountName: accountName2, amount: try transaction.quantityAmount(lookup: lookup), price: transaction.marketPrice, cost: cost) let result = STransaction(metaData: TransactionMetaData(date: transaction.processDate, metaData: [MetaDataKeys.id: transaction.id]), postings: [posting1, posting2]) return (try Price(date: transaction.processDate, commoditySymbol: transaction.symbol, amount: transaction.marketPrice), result)