diff --git a/conf/lab.ini b/conf/lab.ini index f9325dc6..f2ddee9f 100644 --- a/conf/lab.ini +++ b/conf/lab.ini @@ -111,7 +111,7 @@ enable_register = true enable_export = true [exchange_rates] -# Exchange rates data source, supports "euro_central_bank", "czech_national_bank" currently +# Exchange rates data source, supports "euro_central_bank", "czech_national_bank", "national_bank_of_poland" currently data_source = euro_central_bank # Requesting exchange rates data timeout (milliseconds), default is 10000 (10 seconds) diff --git a/pkg/exchangerates/exchange_rates_datasource_container.go b/pkg/exchangerates/exchange_rates_datasource_container.go index 6b5b3c88..8863144a 100644 --- a/pkg/exchangerates/exchange_rates_datasource_container.go +++ b/pkg/exchangerates/exchange_rates_datasource_container.go @@ -23,6 +23,9 @@ func InitializeExchangeRatesDataSource(config *settings.Config) error { } else if config.ExchangeRatesDataSource == settings.CzechNationalBankDataSource { Container.Current = &CzechNationalBankDataSource{} return nil + } else if config.ExchangeRatesDataSource == settings.NationalBankOfPolandDataSource { + Container.Current = &NationalBankOfPolandDataSource{} + return nil } return errs.ErrInvalidExchangeRatesDataSource diff --git a/pkg/exchangerates/national_bank_of_poland_datasource.go b/pkg/exchangerates/national_bank_of_poland_datasource.go new file mode 100644 index 00000000..5b7cf0c4 --- /dev/null +++ b/pkg/exchangerates/national_bank_of_poland_datasource.go @@ -0,0 +1,155 @@ +package exchangerates + +import ( + "bytes" + "encoding/xml" + "math" + "time" + + "github.com/mayswind/lab/pkg/core" + "github.com/mayswind/lab/pkg/errs" + "github.com/mayswind/lab/pkg/log" + "github.com/mayswind/lab/pkg/models" + "github.com/mayswind/lab/pkg/utils" + "github.com/mayswind/lab/pkg/validators" +) + +const nationalBankOfPolandDailyExchangeRateUrl = "https://www.nbp.pl/kursy/xml/en/lastaen.xml" +const nationalBankOfPolandInconvertibleCurrencyExchangeRateUrl = "https://www.nbp.pl/kursy/xml/en/lastben.xml" +const nationalBankOfPolandExchangeRateReferenceUrl = "https://www.nbp.pl/homen.aspx?f=/kursy/kursyen.htm" +const nationalBankOfPolandDataSource = "Narodowy Bank Polski" +const nationalBankOfPolandBaseCurrency = "PLN" + +const nationalBankOfPolandDataUpdateDateFormat = "2006-01-02 15:04" +const nationalBankOfPolandDataUpdateDateTimezone = "Europe/Warsaw" + +// NationalBankOfPolandDataSource defines the structure of exchange rates data source of National Bank of Poland +type NationalBankOfPolandDataSource struct { + ExchangeRatesDataSource +} + +// NationalBankOfPolandExchangeRateData represents the whole data from National Bank of Poland +type NationalBankOfPolandExchangeRateData struct { + XMLName xml.Name `xml:"exchange_rates"` + Date string `xml:"date,attr"` + AllExchangeRates []*NationalBankOfPolandExchangeRate `xml:"mid-rate"` +} + +// NationalBankOfPolandExchangeRate represents the exchange rate data from National Bank of Poland +type NationalBankOfPolandExchangeRate struct { + Currency string `xml:"code,attr"` + Units string `xml:"units,attr"` + Rate string `xml:",chardata"` +} + +// ToLatestExchangeRateResponse returns a view-object according to original data from National Bank of Poland +func (e *NationalBankOfPolandExchangeRateData) ToLatestExchangeRateResponse(c *core.Context) *models.LatestExchangeRateResponse { + if len(e.AllExchangeRates) < 1 { + log.ErrorfWithRequestId(c, "[national_bank_of_poland_datasource.ToLatestExchangeRateResponse] all exchange rates is empty") + return nil + } + + exchangeRates := make(models.LatestExchangeRateSlice, 0, len(e.AllExchangeRates)) + + for i := 0; i < len(e.AllExchangeRates); i++ { + exchangeRate := e.AllExchangeRates[i] + + if _, exists := validators.AllCurrencyNames[exchangeRate.Currency]; !exists { + continue + } + + finalExchangeRate := exchangeRate.ToLatestExchangeRate(c) + + if finalExchangeRate == nil { + continue + } + + exchangeRates = append(exchangeRates, finalExchangeRate) + } + + timezone, err := time.LoadLocation(nationalBankOfPolandDataUpdateDateTimezone) + + if err != nil { + log.ErrorfWithRequestId(c, "[national_bank_of_poland_datasource.ToLatestExchangeRateResponse] failed to get timezone, timezone name is %s", nationalBankOfPolandDataUpdateDateTimezone) + return nil + } + + updateDateTime := e.Date + " 12:15" // Table A of the average foreign currency exchange rates is published (updated) on the NBP website on business days, between 11:45 a.m. and 12:15 p.m. + updateTime, err := time.ParseInLocation(nationalBankOfPolandDataUpdateDateFormat, updateDateTime, timezone) + + if err != nil { + log.ErrorfWithRequestId(c, "[national_bank_of_poland_datasource.ToLatestExchangeRateResponse] failed to parse update date, datetime is %s", updateDateTime) + return nil + } + + latestExchangeRateResp := &models.LatestExchangeRateResponse{ + DataSource: nationalBankOfPolandDataSource, + ReferenceUrl: nationalBankOfPolandExchangeRateReferenceUrl, + UpdateTime: updateTime.Unix(), + BaseCurrency: nationalBankOfPolandBaseCurrency, + ExchangeRates: exchangeRates, + } + + return latestExchangeRateResp +} + +// ToLatestExchangeRate returns a data pair according to original data from National Bank of Poland +func (e *NationalBankOfPolandExchangeRate) ToLatestExchangeRate(c *core.Context) *models.LatestExchangeRate { + amount, err := utils.StringToInt64(e.Units) + + if err != nil { + log.WarnfWithRequestId(c, "[national_bank_of_poland_datasource.ToLatestExchangeRate] failed to parse amount, currency is %s, amount is %s", e.Currency, e.Units) + return nil + } + + rate, err := utils.StringToFloat64(e.Rate) + + if err != nil { + log.WarnfWithRequestId(c, "[national_bank_of_poland_datasource.ToLatestExchangeRate] failed to parse rate, currency is %s, rate is %s", e.Currency, e.Rate) + return nil + } + + if rate <= 0 { + log.WarnfWithRequestId(c, "[national_bank_of_poland_datasource.ToLatestExchangeRate] rate is invalid, currency is %s, rate is %s", e.Currency, e.Rate) + return nil + } + + finalRate := float64(amount) / rate + + if math.IsInf(finalRate, 0) { + return nil + } + + return &models.LatestExchangeRate{ + Currency: e.Currency, + Rate: utils.Float64ToString(finalRate), + } +} + +// GetRequestUrls returns the National Bank of Poland data source urls +func (e *NationalBankOfPolandDataSource) GetRequestUrls() []string { + return []string{nationalBankOfPolandInconvertibleCurrencyExchangeRateUrl, nationalBankOfPolandDailyExchangeRateUrl} +} + +// Parse returns the common response entity according to the National Bank of Poland data source raw response +func (e *NationalBankOfPolandDataSource) Parse(c *core.Context, content []byte) (*models.LatestExchangeRateResponse, error) { + nationalBankOfPolandData := &NationalBankOfPolandExchangeRateData{} + + xmlDecoder := xml.NewDecoder(bytes.NewReader(content)) + xmlDecoder.CharsetReader = utils.IdentReader + err := xmlDecoder.Decode(&nationalBankOfPolandData) + + if err != nil { + log.ErrorfWithRequestId(c, "[national_bank_of_poland_datasource.Parse] failed to parse xml data, content is %s, because %s", string(content), err.Error()) + return nil, errs.ErrFailedToRequestRemoteApi + } + + latestExchangeRateResponse := nationalBankOfPolandData.ToLatestExchangeRateResponse(c) + + if latestExchangeRateResponse == nil { + log.ErrorfWithRequestId(c, "[national_bank_of_poland_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/settings/setting.go b/pkg/settings/setting.go index 19ef07cf..4a60d1ab 100644 --- a/pkg/settings/setting.go +++ b/pkg/settings/setting.go @@ -64,8 +64,9 @@ const ( // Exchange rates data source types const ( - EuroCentralBankDataSource string = "euro_central_bank" - CzechNationalBankDataSource string = "czech_national_bank" + EuroCentralBankDataSource string = "euro_central_bank" + CzechNationalBankDataSource string = "czech_national_bank" + NationalBankOfPolandDataSource string = "national_bank_of_poland" ) const ( @@ -415,6 +416,8 @@ func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectio config.ExchangeRatesDataSource = EuroCentralBankDataSource } else if getConfigItemStringValue(configFile, sectionName, "data_source") == CzechNationalBankDataSource { config.ExchangeRatesDataSource = CzechNationalBankDataSource + } else if getConfigItemStringValue(configFile, sectionName, "data_source") == NationalBankOfPolandDataSource { + config.ExchangeRatesDataSource = NationalBankOfPolandDataSource } else { return errs.ErrInvalidExchangeRatesDataSource } diff --git a/pkg/utils/io.go b/pkg/utils/io.go new file mode 100644 index 00000000..623d7f27 --- /dev/null +++ b/pkg/utils/io.go @@ -0,0 +1,8 @@ +package utils + +import "io" + +// IdentReader returns the original io reader +func IdentReader(encoding string, input io.Reader) (io.Reader, error) { + return input, nil +}