From a4b26374f4b9b40e694bcee13d3df5417aaebf92 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sun, 17 Nov 2024 22:07:53 +0800 Subject: [PATCH] add Central Bank of Myanmar exchange rates data source --- conf/ezbookkeeping.ini | 1 + pkg/api/exchange_rates_test.go | 16 ++ .../central_bank_of_myanmar_datasource.go | 139 ++++++++++++++++++ ...central_bank_of_myanmar_datasource_test.go | 121 +++++++++++++++ .../exchange_rates_datasource_container.go | 3 + pkg/settings/setting.go | 2 + 6 files changed, 282 insertions(+) create mode 100644 pkg/exchangerates/central_bank_of_myanmar_datasource.go create mode 100644 pkg/exchangerates/central_bank_of_myanmar_datasource_test.go diff --git a/conf/ezbookkeeping.ini b/conf/ezbookkeeping.ini index 3325d725..01159ad6 100644 --- a/conf/ezbookkeeping.ini +++ b/conf/ezbookkeeping.ini @@ -348,6 +348,7 @@ custom_map_tile_server_default_zoom_level = 14 # "national_bank_of_georgia": https://nbg.gov.ge/en/monetary-policy/currency # "central_bank_of_hungary": https://www.mnb.hu/en/arfolyamok # "bank_of_israel": https://www.boi.org.il/en/economic-roles/financial-markets/exchange-rates/ +# "central_bank_of_myanmar": https://forex.cbm.gov.mm/index.php/fxrate # "norges_bank": https://www.norges-bank.no/en/topics/Statistics/exchange_rates/ # "national_bank_of_poland": https://nbp.pl/en/statistic-and-financial-reporting/rates/ # "national_bank_of_romania": https://www.bnr.ro/Exchange-rates-1224.aspx diff --git a/pkg/api/exchange_rates_test.go b/pkg/api/exchange_rates_test.go index 838ef984..f747dd7f 100644 --- a/pkg/api/exchange_rates_test.go +++ b/pkg/api/exchange_rates_test.go @@ -150,6 +150,22 @@ func TestExchangeRatesApiLatestExchangeRateHandler_BankOfIsraelDataSource(t *tes checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates) } +func TestExchangeRatesApiLatestExchangeRateHandler_CentralBankOfMyanmarDataSource(t *testing.T) { + exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.CentralBankOfMyanmarDataSource) + + if exchangeRateResponse == nil { + return + } + + assert.Equal(t, "MMK", exchangeRateResponse.BaseCurrency) + + supportedCurrencyCodes := []string{"AUD", "BDT", "BND", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK", + "EGP", "EUR", "GBP", "HKD", "IDR", "ILS", "INR", "JPY", "KES", "KHR", "KRW", "KWD", "LAK", "LKR", + "MYR", "NOK", "NPR", "NZD", "PHP", "PKR", "RSD", "RUB", "SAR", "SEK", "SGD", "THB", "USD", "VND", "ZAR"} + + checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates) +} + func TestExchangeRatesApiLatestExchangeRateHandler_NorgesBankDataSource(t *testing.T) { exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.NorgesBankDataSource) diff --git a/pkg/exchangerates/central_bank_of_myanmar_datasource.go b/pkg/exchangerates/central_bank_of_myanmar_datasource.go new file mode 100644 index 00000000..146776cf --- /dev/null +++ b/pkg/exchangerates/central_bank_of_myanmar_datasource.go @@ -0,0 +1,139 @@ +package exchangerates + +import ( + "encoding/json" + "math" + "net/http" + "strings" + + "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 centralBankOfMyanmarExchangeRateUrl = "https://forex.cbm.gov.mm/api/latest" +const centralBankOfMyanmarExchangeRateReferenceUrl = "https://forex.cbm.gov.mm/index.php/fxrate" +const centralBankOfMyanmarDataSource = "မြန်မာနိုင်ငံတော်ဗဟိုဘဏ်" +const centralBankOfMyanmarBaseCurrency = "MMK" + +var centralBankOfMyanmarSpecialCurrencyUnits = map[string]int32{ + "JPY": 100, + "KHR": 100, + "IDR": 100, + "KRW": 100, + "LAK": 100, + "VND": 100, +} + +// CentralBankOfMyanmarDataSource defines the structure of exchange rates data source of central bank of Myanmar +type CentralBankOfMyanmarDataSource struct { + ExchangeRatesDataSource +} + +// CentralBankOfMyanmarExchangeRate represents the exchange rate data from central bank of Myanmar +type CentralBankOfMyanmarExchangeRate struct { + Timestamp string `json:"timestamp"` + ExchangeRates map[string]string `json:"rates"` +} + +// ToLatestExchangeRateResponse returns a view-object according to original data from central bank of Myanmar +func (e *CentralBankOfMyanmarExchangeRate) ToLatestExchangeRateResponse(c core.Context) *models.LatestExchangeRateResponse { + exchangeRates := make(models.LatestExchangeRateSlice, 0, len(e.ExchangeRates)) + + for currencyCode, exchangeRate := range e.ExchangeRates { + if _, exists := validators.AllCurrencyNames[currencyCode]; !exists { + continue + } + + finalExchangeRate := e.BuildLatestExchangeRate(c, currencyCode, exchangeRate) + + if finalExchangeRate == nil { + continue + } + + exchangeRates = append(exchangeRates, finalExchangeRate) + } + + updateTime, err := utils.StringToInt64(e.Timestamp) + + if err != nil { + log.Errorf(c, "[central_bank_of_myanmar_datasource.ToLatestExchangeRateResponse] failed to parse timestamp, timestamp is %s", e.Timestamp) + return nil + } + + latestExchangeRateResp := &models.LatestExchangeRateResponse{ + DataSource: centralBankOfMyanmarDataSource, + ReferenceUrl: centralBankOfMyanmarExchangeRateReferenceUrl, + UpdateTime: updateTime, + BaseCurrency: centralBankOfMyanmarBaseCurrency, + ExchangeRates: exchangeRates, + } + + return latestExchangeRateResp +} + +// BuildLatestExchangeRate returns a data pair according to original data from central bank of Myanmar +func (e *CentralBankOfMyanmarExchangeRate) BuildLatestExchangeRate(c core.Context, currencyCode string, exchangeRate string) *models.LatestExchangeRate { + rate, err := utils.StringToFloat64(strings.ReplaceAll(exchangeRate, ",", "")) + + if err != nil { + log.Warnf(c, "[central_bank_of_myanmar_datasource.BuildLatestExchangeRate] failed to parse rate, currency is %s, rate is %s", currencyCode, exchangeRate) + return nil + } + + if rate <= 0 { + log.Warnf(c, "[central_bank_of_myanmar_datasource.BuildLatestExchangeRate] rate is invalid, currency is %s, rate is %s", currencyCode, exchangeRate) + return nil + } + + unit, has := centralBankOfMyanmarSpecialCurrencyUnits[currencyCode] + + if !has { + unit = 1 + } + + finalRate := float64(unit) / rate + + if math.IsInf(finalRate, 0) { + return nil + } + + return &models.LatestExchangeRate{ + Currency: currencyCode, + Rate: utils.Float64ToString(finalRate), + } +} + +// BuildRequests returns the central bank of Myanmar exchange rates http requests +func (e *CentralBankOfMyanmarDataSource) BuildRequests() ([]*http.Request, error) { + req, err := http.NewRequest("GET", centralBankOfMyanmarExchangeRateUrl, nil) + + if err != nil { + return nil, err + } + + return []*http.Request{req}, nil +} + +// Parse returns the common response entity according to the central bank of Myanmar data source raw response +func (e *CentralBankOfMyanmarDataSource) Parse(c core.Context, content []byte) (*models.LatestExchangeRateResponse, error) { + centralBankOfMyanmarData := &CentralBankOfMyanmarExchangeRate{} + err := json.Unmarshal(content, centralBankOfMyanmarData) + + if err != nil { + log.Errorf(c, "[central_bank_of_myanmar_datasource.Parse] failed to parse xml data, content is %s, because %s", string(content), err.Error()) + return nil, errs.ErrFailedToRequestRemoteApi + } + + latestExchangeRateResponse := centralBankOfMyanmarData.ToLatestExchangeRateResponse(c) + + if latestExchangeRateResponse == nil { + log.Errorf(c, "[central_bank_of_myanmar_datasource.Parse] failed to parse latest exchange rate data, content is %s", string(content)) + return nil, errs.ErrFailedToRequestRemoteApi + } + + return latestExchangeRateResponse, nil +} diff --git a/pkg/exchangerates/central_bank_of_myanmar_datasource_test.go b/pkg/exchangerates/central_bank_of_myanmar_datasource_test.go new file mode 100644 index 00000000..90ef48d8 --- /dev/null +++ b/pkg/exchangerates/central_bank_of_myanmar_datasource_test.go @@ -0,0 +1,121 @@ +package exchangerates + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/models" +) + +const centralBankOfMyanmarMinimumRequiredContent = "{\n" + + " \"timestamp\": \"1731571200\",\n" + + " \"rates\": {\n" + + " \"USD\": \"2,100.0\",\n" + + " \"JPY\": \"1,347.6\"\n" + + " }\n" + + "}" + +func TestCentralBankOfMyanmarDataSource_StandardDataExtractBaseCurrency(t *testing.T) { + dataSource := &CentralBankOfMyanmarDataSource{} + context := core.NewNullContext() + + actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(centralBankOfMyanmarMinimumRequiredContent)) + assert.Equal(t, nil, err) + assert.Equal(t, "MMK", actualLatestExchangeRateResponse.BaseCurrency) +} + +func TestCentralBankOfMyanmarDataSource_StandardDataExtractUpdateTime(t *testing.T) { + dataSource := &CentralBankOfMyanmarDataSource{} + context := core.NewNullContext() + + actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(centralBankOfMyanmarMinimumRequiredContent)) + assert.Equal(t, nil, err) + assert.Equal(t, int64(1731571200), actualLatestExchangeRateResponse.UpdateTime) +} + +func TestCentralBankOfMyanmarDataSource_StandardDataExtractExchangeRates(t *testing.T) { + dataSource := &CentralBankOfMyanmarDataSource{} + context := core.NewNullContext() + + actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(centralBankOfMyanmarMinimumRequiredContent)) + assert.Equal(t, nil, err) + assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{ + Currency: "JPY", + Rate: "0.07420599584446423", + }) + assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{ + Currency: "USD", + Rate: "0.0004761904761904762", + }) +} + +func TestCentralBankOfMyanmarDataSource_BlankContent(t *testing.T) { + dataSource := &CentralBankOfMyanmarDataSource{} + context := core.NewNullContext() + + _, err := dataSource.Parse(context, []byte("")) + assert.NotEqual(t, nil, err) +} + +func TestCentralBankOfMyanmarDataSource_EmptyData(t *testing.T) { + dataSource := &CentralBankOfMyanmarDataSource{} + context := core.NewNullContext() + + _, err := dataSource.Parse(context, []byte("{}")) + assert.NotEqual(t, nil, err) +} + +func TestCentralBankOfMyanmarDataSource_EmptyExchangeRatesData(t *testing.T) { + dataSource := &CentralBankOfMyanmarDataSource{} + context := core.NewNullContext() + + _, err := dataSource.Parse(context, []byte("{\n"+ + " \"timestamp\": \"1731571200\"\n"+ + "}")) + + _, err = dataSource.Parse(context, []byte("{\n"+ + " \"timestamp\": \"1731571200\",\n"+ + " \"rates\": {\n"+ + " }\n"+ + "}")) + assert.Nil(t, nil, err) +} + +func TestCentralBankOfMyanmarDataSource_InvalidCurrency(t *testing.T) { + dataSource := &CentralBankOfMyanmarDataSource{} + context := core.NewNullContext() + + actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+ + " \"timestamp\": \"1731571200\",\n"+ + " \"rates\": {\n"+ + " \"XXX\": \"1\"\n"+ + " }\n"+ + "}")) + assert.Equal(t, nil, err) + assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0) +} + +func TestCentralBankOfMyanmarDataSource_InvalidRate(t *testing.T) { + dataSource := &CentralBankOfMyanmarDataSource{} + context := core.NewNullContext() + + actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("{\n"+ + " \"timestamp\": \"1731571200\",\n"+ + " \"rates\": {\n"+ + " \"USD\": null\n"+ + " }\n"+ + "}")) + assert.Equal(t, nil, err) + assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0) + + actualLatestExchangeRateResponse, err = dataSource.Parse(context, []byte("{\n"+ + " \"timestamp\": \"1731571200\",\n"+ + " \"rates\": {\n"+ + " \"USD\": \"0\"\n"+ + " }\n"+ + "}")) + assert.Equal(t, nil, err) + assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0) +} diff --git a/pkg/exchangerates/exchange_rates_datasource_container.go b/pkg/exchangerates/exchange_rates_datasource_container.go index 94d7dc27..037db730 100644 --- a/pkg/exchangerates/exchange_rates_datasource_container.go +++ b/pkg/exchangerates/exchange_rates_datasource_container.go @@ -41,6 +41,9 @@ func InitializeExchangeRatesDataSource(config *settings.Config) error { } else if config.ExchangeRatesDataSource == settings.BankOfIsraelDataSource { Container.Current = &BankOfIsraelDataSource{} return nil + } else if config.ExchangeRatesDataSource == settings.CentralBankOfMyanmarDataSource { + Container.Current = &CentralBankOfMyanmarDataSource{} + return nil } else if config.ExchangeRatesDataSource == settings.NorgesBankDataSource { Container.Current = &NorgesBankDataSource{} return nil diff --git a/pkg/settings/setting.go b/pkg/settings/setting.go index 84bc94a0..d4923fdc 100644 --- a/pkg/settings/setting.go +++ b/pkg/settings/setting.go @@ -108,6 +108,7 @@ const ( NationalBankOfGeorgiaDataSource string = "national_bank_of_georgia" CentralBankOfHungaryDataSource string = "central_bank_of_hungary" BankOfIsraelDataSource string = "bank_of_israel" + CentralBankOfMyanmarDataSource string = "central_bank_of_myanmar" NorgesBankDataSource string = "norges_bank" NationalBankOfPolandDataSource string = "national_bank_of_poland" NationalBankOfRomaniaDataSource string = "national_bank_of_romania" @@ -894,6 +895,7 @@ func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectio dataSource == NationalBankOfGeorgiaDataSource || dataSource == CentralBankOfHungaryDataSource || dataSource == BankOfIsraelDataSource || + dataSource == CentralBankOfMyanmarDataSource || dataSource == NorgesBankDataSource || dataSource == NationalBankOfPolandDataSource || dataSource == NationalBankOfRomaniaDataSource ||