Skip to content

Commit

Permalink
add Bank of Israel exchange rates data source
Browse files Browse the repository at this point in the history
  • Loading branch information
mayswind committed Nov 11, 2024
1 parent bff6ca7 commit 2d0e2e0
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 0 deletions.
1 change: 1 addition & 0 deletions conf/ezbookkeeping.ini
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ custom_map_tile_server_default_zoom_level = 14
# "reserve_bank_of_australia": https://www.rba.gov.au/statistics/frequency/exchange-rates.html
# "czech_national_bank": https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/
# "national_bank_of_poland": https://nbp.pl/en/statistic-and-financial-reporting/rates/
# "bank_of_israel": https://www.boi.org.il/en/economic-roles/financial-markets/exchange-rates/
# "international_monetary_fund": https://www.imf.org/external/np/fin/data/param_rms_mth.aspx
data_source = euro_central_bank

Expand Down
143 changes: 143 additions & 0 deletions pkg/exchangerates/bank_of_israel_datasource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package exchangerates

import (
"encoding/xml"
"math"
"time"

"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)

const bankOfIsraelExchangeRateUrl = "https://boi.org.il/PublicApi/GetExchangeRates?asXml=true"
const bankOfIsraelExchangeRateReferenceUrl = "https://www.boi.org.il/en/economic-roles/financial-markets/exchange-rates/"
const bankOfIsraelDataSource = "Bank of Israel"
const bankOfIsraelBaseCurrency = "ILS"

const bankOfIsraelDataUpdateDateFormat = "2006-01-02T15:04:05.9999999Z"

// BankOfIsraelDataSource defines the structure of exchange rates data source of bank of Israel
type BankOfIsraelDataSource struct {
ExchangeRatesDataSource
}

// bankOfIsraelExchangeRateData represents the whole data from bank of Israel
type bankOfIsraelExchangeRateData struct {
XMLName xml.Name `xml:"ExchangeRatesResponseCollectioDTO"`
AllExchangeRates []*bankOfIsraelExchangeRate `xml:"ExchangeRates>ExchangeRateResponseDTO"`
}

// bankOfIsraelExchangeRate represents the exchange rate data from bank of Israel
type bankOfIsraelExchangeRate struct {
Currency string `xml:"Key"`
Rate string `xml:"CurrentExchangeRate"`
LastUpdate string `xml:"LastUpdate"`
Unit string `xml:"Unit"`
}

// ToLatestExchangeRateResponse returns a view-object according to original data from bank of Israel
func (e *bankOfIsraelExchangeRateData) ToLatestExchangeRateResponse(c core.Context) *models.LatestExchangeRateResponse {
if len(e.AllExchangeRates) < 1 {
log.Errorf(c, "[bank_of_israel_datasource.ToLatestExchangeRateResponse] all exchange rates is empty")
return nil
}

latestUpdateDate := ""
exchangeRates := make(models.LatestExchangeRateSlice, 0, len(e.AllExchangeRates))

for i := 0; i < len(e.AllExchangeRates); i++ {
exchangeRate := e.AllExchangeRates[i]

if latestUpdateDate == "" {
latestUpdateDate = exchangeRate.LastUpdate
}

if _, exists := validators.AllCurrencyNames[exchangeRate.Currency]; !exists {
continue
}

rate, err := utils.StringToFloat64(exchangeRate.Rate)

if err != nil {
log.Warnf(c, "[bank_of_israel_datasource.ToLatestExchangeRateResponse] failed to parse rate, rate is %s", exchangeRate.Rate)
continue
}

if rate <= 0 {
log.Warnf(c, "[bank_of_israel_datasource.ToLatestExchangeRateResponse] rate is invalid, rate is %s", exchangeRate.Rate)
continue
}

unit, err := utils.StringToFloat64(exchangeRate.Unit)

if err != nil {
log.Warnf(c, "[bank_of_israel_datasource.ToLatestExchangeRateResponse] failed to parse unit, unit is %s", exchangeRate.Unit)
continue
}

finalRate := unit / rate

if math.IsInf(finalRate, 0) {
continue
}

exchangeRates = append(exchangeRates, &models.LatestExchangeRate{
Currency: exchangeRate.Currency,
Rate: utils.Float64ToString(finalRate),
})
}

updateTime, err := time.Parse(bankOfIsraelDataUpdateDateFormat, latestUpdateDate)

if err != nil {
log.Errorf(c, "[bank_of_israel_datasource.ToLatestExchangeRateResponse] failed to parse update date, datetime is %s", latestUpdateDate)
return nil
}

latestExchangeRateResp := &models.LatestExchangeRateResponse{
DataSource: bankOfIsraelDataSource,
ReferenceUrl: bankOfIsraelExchangeRateReferenceUrl,
UpdateTime: updateTime.Unix(),
BaseCurrency: bankOfIsraelBaseCurrency,
ExchangeRates: exchangeRates,
}

return latestExchangeRateResp
}

// ToLatestExchangeRate returns a data pair according to original data from bank of Israel
func (e *bankOfIsraelExchangeRate) ToLatestExchangeRate() *models.LatestExchangeRate {
return &models.LatestExchangeRate{
Currency: e.Currency,
Rate: e.Rate,
}
}

// GetRequestUrls returns the bank of Israel data source urls
func (e *BankOfIsraelDataSource) GetRequestUrls() []string {
return []string{bankOfIsraelExchangeRateUrl}
}

// Parse returns the common response entity according to the bank of Israel data source raw response
func (e *BankOfIsraelDataSource) Parse(c core.Context, content []byte) (*models.LatestExchangeRateResponse, error) {
bankOfIsraelData := &bankOfIsraelExchangeRateData{}
err := xml.Unmarshal(content, bankOfIsraelData)

if err != nil {
log.Errorf(c, "[bank_of_israel_datasource.Parse] failed to parse xml data, content is %s, because %s", string(content), err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}

latestExchangeRateResponse := bankOfIsraelData.ToLatestExchangeRateResponse(c)

if latestExchangeRateResponse == nil {
log.Errorf(c, "[bank_of_israel_datasource.Parse] failed to parse latest exchange rate data, content is %s", string(content))
return nil, errs.ErrFailedToRequestRemoteApi
}

return latestExchangeRateResponse, nil
}
171 changes: 171 additions & 0 deletions pkg/exchangerates/bank_of_israel_datasource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package exchangerates

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)

const bankOfIsraelMinimumRequiredContent = "" +
"<ExchangeRatesResponseCollectioDTO xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://schemas.datacontract.org/2004/07/BOI.Core.Models.HotData\">\n" +
" <ExchangeRates>\n" +
" <ExchangeRateResponseDTO>\n" +
" <CurrentExchangeRate>3.733</CurrentExchangeRate>\n" +
" <Key>USD</Key>\n" +
" <LastUpdate>2024-11-11T13:26:05.6590204Z</LastUpdate>\n" +
" <Unit>1</Unit>\n" +
" </ExchangeRateResponseDTO>\n" +
" <ExchangeRateResponseDTO>\n" +
" <CurrentExchangeRate>2.4287</CurrentExchangeRate>\n" +
" <Key>JPY</Key>\n" +
" <LastUpdate>2024-11-11T13:26:05.6590204Z</LastUpdate>\n" +
" <Unit>100</Unit>\n" +
" </ExchangeRateResponseDTO>\n" +
" </ExchangeRates>\n" +
"</ExchangeRatesResponseCollectioDTO>"

func TestBankOfIsraelDataSource_StandardDataExtractBaseCurrency(t *testing.T) {
dataSource := &BankOfIsraelDataSource{}
context := core.NewNullContext()

actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(bankOfIsraelMinimumRequiredContent))
assert.Equal(t, nil, err)
assert.Equal(t, "ILS", actualLatestExchangeRateResponse.BaseCurrency)
}

func TestBankOfIsraelDataSource_StandardDataExtractExchangeRates(t *testing.T) {
dataSource := &BankOfIsraelDataSource{}
context := core.NewNullContext()

actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(bankOfIsraelMinimumRequiredContent))
assert.Equal(t, nil, err)
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "USD",
Rate: "0.2678810608090008",
})
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "JPY",
Rate: "41.17429077284144",
})
}

func TestBankOfIsraelDataSource_BlankContent(t *testing.T) {
dataSource := &BankOfIsraelDataSource{}
context := core.NewNullContext()

_, err := dataSource.Parse(context, []byte(""))
assert.NotEqual(t, nil, err)
}

func TestBankOfIsraelDataSource_EmptyExchangeRatesResponseCollectioDTO(t *testing.T) {
dataSource := &BankOfIsraelDataSource{}
context := core.NewNullContext()

_, err := dataSource.Parse(context, []byte("<ExchangeRatesResponseCollectioDTO xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://schemas.datacontract.org/2004/07/BOI.Core.Models.HotData\">\n"+
"</ExchangeRatesResponseCollectioDTO>"))
assert.NotEqual(t, nil, err)
}

func TestBankOfIsraelDataSource_InvalidCurrency(t *testing.T) {
dataSource := &BankOfIsraelDataSource{}
context := core.NewNullContext()

actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<ExchangeRatesResponseCollectioDTO xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://schemas.datacontract.org/2004/07/BOI.Core.Models.HotData\">\n"+
" <ExchangeRates>\n"+
" <ExchangeRateResponseDTO>\n"+
" <CurrentExchangeRate>1</CurrentExchangeRate>\n"+
" <Key>XXX</Key>\n"+
" <LastUpdate>2024-11-11T13:26:05.6590204Z</LastUpdate>\n"+
" <Unit>1</Unit>\n"+
" </ExchangeRateResponseDTO>\n"+
" </ExchangeRates>\n"+
"</ExchangeRatesResponseCollectioDTO>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}

func TestBankOfIsraelDataSource_EmptyRate(t *testing.T) {
dataSource := &BankOfIsraelDataSource{}
context := core.NewNullContext()

actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<ExchangeRatesResponseCollectioDTO xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://schemas.datacontract.org/2004/07/BOI.Core.Models.HotData\">\n"+
" <ExchangeRates>\n"+
" <ExchangeRateResponseDTO>\n"+
" <Key>USD</Key>\n"+
" <LastUpdate>2024-11-11T13:26:05.6590204Z</LastUpdate>\n"+
" <Unit>1</Unit>\n"+
" </ExchangeRateResponseDTO>\n"+
" </ExchangeRates>\n"+
"</ExchangeRatesResponseCollectioDTO>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}

func TestBankOfIsraelDataSource_InvalidRate(t *testing.T) {
dataSource := &BankOfIsraelDataSource{}
context := core.NewNullContext()

actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<ExchangeRatesResponseCollectioDTO xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://schemas.datacontract.org/2004/07/BOI.Core.Models.HotData\">\n"+
" <ExchangeRates>\n"+
" <ExchangeRateResponseDTO>\n"+
" <CurrentExchangeRate>null</CurrentExchangeRate>\n"+
" <Key>USD</Key>\n"+
" <LastUpdate>2024-11-11T13:26:05.6590204Z</LastUpdate>\n"+
" <Unit>1</Unit>\n"+
" </ExchangeRateResponseDTO>\n"+
" </ExchangeRates>\n"+
"</ExchangeRatesResponseCollectioDTO>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)

actualLatestExchangeRateResponse, err = dataSource.Parse(context, []byte("<ExchangeRatesResponseCollectioDTO xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://schemas.datacontract.org/2004/07/BOI.Core.Models.HotData\">\n"+
" <ExchangeRates>\n"+
" <ExchangeRateResponseDTO>\n"+
" <CurrentExchangeRate>0</CurrentExchangeRate>\n"+
" <Key>USD</Key>\n"+
" <LastUpdate>2024-11-11T13:26:05.6590204Z</LastUpdate>\n"+
" <Unit>1</Unit>\n"+
" </ExchangeRateResponseDTO>\n"+
" </ExchangeRates>\n"+
"</ExchangeRatesResponseCollectioDTO>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}

func TestBankOfIsraelDataSource_EmptyUnit(t *testing.T) {
dataSource := &BankOfIsraelDataSource{}
context := core.NewNullContext()

actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<ExchangeRatesResponseCollectioDTO xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://schemas.datacontract.org/2004/07/BOI.Core.Models.HotData\">\n"+
" <ExchangeRates>\n"+
" <ExchangeRateResponseDTO>\n"+
" <CurrentExchangeRate>1</CurrentExchangeRate>\n"+
" <Key>USD</Key>\n"+
" <LastUpdate>2024-11-11T13:26:05.6590204Z</LastUpdate>\n"+
" </ExchangeRateResponseDTO>\n"+
" </ExchangeRates>\n"+
"</ExchangeRatesResponseCollectioDTO>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}

func TestBankOfIsraelDataSource_InvalidUnit(t *testing.T) {
dataSource := &BankOfIsraelDataSource{}
context := core.NewNullContext()

actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<ExchangeRatesResponseCollectioDTO xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://schemas.datacontract.org/2004/07/BOI.Core.Models.HotData\">\n"+
" <ExchangeRates>\n"+
" <ExchangeRateResponseDTO>\n"+
" <CurrentExchangeRate>1</CurrentExchangeRate>\n"+
" <Key>USD</Key>\n"+
" <LastUpdate>2024-11-11T13:26:05.6590204Z</LastUpdate>\n"+
" <Unit>null</Unit>\n"+
" </ExchangeRateResponseDTO>\n"+
" </ExchangeRates>\n"+
"</ExchangeRatesResponseCollectioDTO>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
3 changes: 3 additions & 0 deletions pkg/exchangerates/exchange_rates_datasource_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ func InitializeExchangeRatesDataSource(config *settings.Config) error {
} else if config.ExchangeRatesDataSource == settings.NationalBankOfPolandDataSource {
Container.Current = &NationalBankOfPolandDataSource{}
return nil
} else if config.ExchangeRatesDataSource == settings.BankOfIsraelDataSource {
Container.Current = &BankOfIsraelDataSource{}
return nil
} else if config.ExchangeRatesDataSource == settings.InternationalMonetaryFundDataSource {
Container.Current = &InternationalMonetaryFundDataSource{}
return nil
Expand Down
3 changes: 3 additions & 0 deletions pkg/settings/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const (
ReserveBankOfAustraliaDataSource string = "reserve_bank_of_australia"
CzechNationalBankDataSource string = "czech_national_bank"
NationalBankOfPolandDataSource string = "national_bank_of_poland"
BankOfIsraelDataSource string = "bank_of_israel"
InternationalMonetaryFundDataSource string = "international_monetary_fund"
)

Expand Down Expand Up @@ -887,6 +888,8 @@ func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectio
config.ExchangeRatesDataSource = CzechNationalBankDataSource
} else if dataSource == NationalBankOfPolandDataSource {
config.ExchangeRatesDataSource = NationalBankOfPolandDataSource
} else if dataSource == BankOfIsraelDataSource {
config.ExchangeRatesDataSource = BankOfIsraelDataSource
} else if dataSource == InternationalMonetaryFundDataSource {
config.ExchangeRatesDataSource = InternationalMonetaryFundDataSource
} else {
Expand Down

0 comments on commit 2d0e2e0

Please sign in to comment.