From bd785ab5f42ec38c1d3d20efdececc01ff7ee45e Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sun, 4 Aug 2024 01:08:28 +0300 Subject: [PATCH 01/22] Add etranport client and implement Rest API calls --- etransport/client.go | 89 ++++ etransport/client_test.go | 150 +++++++ etransport/codes.go | 877 ++++++++++++++++++++++++++++++++++++++ etransport/declaration.go | 161 +++++++ etransport/rest.go | 240 +++++++++++ 5 files changed, 1517 insertions(+) create mode 100644 etransport/client.go create mode 100644 etransport/client_test.go create mode 100644 etransport/codes.go create mode 100644 etransport/declaration.go create mode 100644 etransport/rest.go diff --git a/etransport/client.go b/etransport/client.go new file mode 100644 index 0000000..24557ef --- /dev/null +++ b/etransport/client.go @@ -0,0 +1,89 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package etransport + +import ( + "context" + + xoauth2 "golang.org/x/oauth2" + + "github.com/printesoi/e-factura-go/client" +) + +// ClientConfig is the config used to create a Client +type ClientConfig struct { + // the client to use for making requests to the ANAF APIs protected with OAuth2. + ApiClient *client.ApiClient +} + +// ClientConfigOption allows gradually modifying a ClientConfig +type ClientConfigOption func(*ClientConfig) + +// ClientApiClient sets the ApiClient to use for APIs protected with OAuth2. +func ClientApiClient(apiClient *client.ApiClient) ClientConfigOption { + return func(c *ClientConfig) { + c.ApiClient = apiClient + } +} + +// Client is a client that talks to ANAF e-transport APIs. +type Client struct { + apiClient *client.ApiClient +} + +// NewProductionClient creates a new basic Client for the ANAF e-transport production APIs. +func NewProductionClient(ctx context.Context, tokenSource xoauth2.TokenSource) (*Client, error) { + apiClient, err := client.NewApiClient( + client.ApiClientContext(ctx), + client.ApiClientProductionEnvironment(true), + client.ApiClientOAuth2TokenSource(tokenSource), + ) + if err != nil { + return nil, err + } + + return &Client{ + apiClient: apiClient, + }, nil +} + +// NewSandboxClient creates a new basic Client for the ANAF e-transport sandbox(test) APIs. +func NewSandboxClient(ctx context.Context, tokenSource xoauth2.TokenSource) (*Client, error) { + apiClient, err := client.NewApiClient( + client.ApiClientContext(ctx), + client.ApiClientSandboxEnvironment(true), + client.ApiClientOAuth2TokenSource(tokenSource), + ) + if err != nil { + return nil, err + } + + return &Client{ + apiClient: apiClient, + }, nil +} + +// NewClient allow for more control than NewProductionClient and NewSandboxClient +// by passing custom ApiClient to this Client. +func NewClient(opts ...ClientConfigOption) (*Client, error) { + cfg := &ClientConfig{} + for _, opt := range opts { + opt(cfg) + } + + return &Client{ + apiClient: cfg.ApiClient, + }, nil +} diff --git a/etransport/client_test.go b/etransport/client_test.go new file mode 100644 index 0000000..02b9930 --- /dev/null +++ b/etransport/client_test.go @@ -0,0 +1,150 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package etransport + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + xoauth2 "golang.org/x/oauth2" + + "github.com/printesoi/e-factura-go/oauth2" +) + +func getTestCIF() string { + return os.Getenv("ETRANSPORT_TEST_CIF") +} + +// setupTestEnvOAuth2Config creates a OAuth2Config from the environment. +// If skipIfEmptyEnv if set to false and the env variables +// ETRANSPORT_TEST_CLIENT_ID, ETRANSPORT_TEST_CLIENT_SECRET, +// ETRANSPORT_TEST_REDIRECT_URL are not set, this method returns an error. +// If skipIfEmptyEnv is set to true and the env vars +// are not set, this method returns a nil config. +func setupTestEnvOAuth2Config(skipIfEmptyEnv bool) (oauth2Cfg *oauth2.Config, err error) { + clientID := os.Getenv("ETRANSPORT_TEST_CLIENT_ID") + clientSecret := os.Getenv("ETRANSPORT_TEST_CLIENT_SECRET") + if clientID == "" || clientSecret == "" { + if skipIfEmptyEnv { + return + } + err = errors.New("invalid oauth2 credentials") + return + } + + redirectURL := os.Getenv("ETRANSPORT_TEST_REDIRECT_URL") + if redirectURL == "" { + err = errors.New("invalid redirect URL") + return + } + + if cfg, er := oauth2.MakeConfig( + oauth2.ConfigCredentials(clientID, clientSecret), + oauth2.ConfigRedirectURL(redirectURL), + ); er != nil { + err = er + return + } else { + oauth2Cfg = &cfg + } + return +} + +// setupRealClient creates a real sandboxed Client (a client that talks to the +// ANAF TEST APIs). +func setupRealClient(skipIfEmptyEnv bool, oauth2Cfg *oauth2.Config) (*Client, error) { + if oauth2Cfg == nil { + cfg, err := setupTestEnvOAuth2Config(skipIfEmptyEnv) + if err != nil { + return nil, err + } else if cfg == nil { + return nil, nil + } else { + oauth2Cfg = cfg + } + } + + tokenJSON := os.Getenv("ETRANSPORT_TEST_INITIAL_TOKEN_JSON") + if tokenJSON == "" { + return nil, errors.New("Invalid initial token json") + } + + token, err := oauth2.TokenFromJSON([]byte(tokenJSON)) + if err != nil { + return nil, err + } + + onTokenChanged := func(ctx context.Context, token *xoauth2.Token) error { + tokenJSON, _ := json.Marshal(token) + fmt.Printf("[E-TRANSPORT] token changed: %s\n", string(tokenJSON)) + return nil + } + + ctx := context.Background() + client, err := NewSandboxClient(ctx, oauth2Cfg.TokenSourceWithChangedHandler(ctx, token, onTokenChanged)) + return client, err +} + +func TestGenerateAuthCodeURL(t *testing.T) { + assert := assert.New(t) + + cfg, err := setupTestEnvOAuth2Config(true) + if !assert.NoError(err) { + return + } + if cfg == nil { + t.Skipf("Skipping test, no credentials to create oauth2 config") + } + + uuid, err := uuid.NewRandom() + if !assert.NoError(err) { + return + } + + url := cfg.AuthCodeURL(uuid.String()) + assert.True(url != "") + fmt.Printf("%s\n", url) +} + +func TestExchangeCode(t *testing.T) { + assert := assert.New(t) + + cfg, err := setupTestEnvOAuth2Config(true) + if !assert.NoError(err) { + return + } + if cfg == nil { + t.Skipf("Skipping test, no credentials to create oauth2 config") + } + + code := os.Getenv("ETRANSPORT_TEST_EXCHANGE_CODE") + if code == "" { + t.Skipf("Skipping test, no exchange code") + } + + token, err := cfg.Exchange(context.Background(), code) + if !assert.NoError(err) { + return + } + + tj, _ := json.Marshal(token) + fmt.Printf("%s\n", string(tj)) +} diff --git a/etransport/codes.go b/etransport/codes.go new file mode 100644 index 0000000..6824660 --- /dev/null +++ b/etransport/codes.go @@ -0,0 +1,877 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package etransport + +type DeclPostIncidentType string + +const ( + // "D" - Singura valoare posibilă pentru atributul "declPostAvarie" + // (declarare până la sfârșitul următoarei zile lucrătoare repunerii în + // funcțiune a sistemului, cf. art. 8, alin. 1^3 al OUG41/2022 aşa cum este + // modificată prin OUG132/2022), dacă este cazul. Pentru declarare normală + // - anterioară punerii în mişcare a vehiculului pe parcurs rutier naţional + // - atributul nu trebuie specificat. + DeclPostIncident DeclPostIncidentType = "D" +) + +type OpType string + +const ( + // Valori posibile pentru câmpul codTipOperatiune: + // "10" AIC - Achiziţie intracomunitară + OpTypeAIC OpType = "10" + // "12" LHI - Operațiuni în sistem lohn (UE) - intrare + OpTypeLHI OpType = "12" + // "14" SCI - Stocuri la dispoziția clientului (Call-off stock) - intrare + OpTypeSCI OpType = "14" + // "20" LIC - Livrare intracomunitară + OpTypeLIC OpType = "20" + // "22" LHE - Operațiuni în sistem lohn (UE) - ieșire + OpTypeLHE OpType = "22" + // "24" SCE - Stocuri la dispoziția clientului (Call-off stock) - ieșire + OpTypeSCE OpType = "24" + // "30" TTN - Transport pe teritoriul naţional + OpTypeTTN OpType = "30" + // "40" IMP - Import + OpTypeIMP OpType = "40" + // "50" EXP - Export + OpTypeEXP OpType = "50" + // "60" DIN - Tranzacţie intracomunitară - Intrare pentru depozitare/formare nou transport + OpTypeDIN OpType = "60" + // "70" DIE - Tranzacţie intracomunitară - Ieşire după depozitare/formare nou transport + OpTypeDIE OpType = "70" +) + +// pentru codTipOperatiune = "10" AIC - Achiziţie intracomunitară: +// +// "101" Comercializare +// "201" Producție +// "301" Gratuități +// "401" Echipament comercial +// "501" Mijloace fixe +// "601" Consum propriu +// "703" Operațiuni de livrare cu instalare +// "801" Leasing financiar/operațional +// "802" Bunuri în garanție +// "901" Operațiuni scutite +// "1001" Investiție in curs +// "1101" Donații, ajutoare +// "9901" Altele +// +// pentru codTipOperatiune = "12" LHI - Operațiuni în sistem lohn (UE) - intrare: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "14" SCI - Stocuri la dispoziția clientului (Call-off stock) - intrare: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "20" LIC - Livrare intracomunitară: +// +// "101" Comercializare +// "301" Gratuități +// "703" Operațiuni de livrare cu instalare +// "801" Leasing financiar/operațional +// "802" Bunuri în garanție +// "9901" Altele +// +// pentru codTipOperatiune = "22" LHE - Operațiuni în sistem lohn (UE) - ieşire: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "24" SCE - Stocuri la dispoziția clientului (Call-off stock) - ieşire: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "30" TTN - Transport pe teritoriul naţional: +// +// "101" Comercializare +// "704" Transfer între gestiuni +// "705" Bunuri puse la dispoziția clientului +// "9901" Altele +// +// pentru codTipOperatiune = "40" IMP - Import: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "50" EXP - Export: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "60" DIN - Tranzactie intracomunitara - Intrare pentru depozitare/formare nou transport: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "70" DIE - Tranzactie intracomunitara - Iesire dupa depozitare/formare nou transport: +// +// "9999" Același cu operațiunea +// +// TODO: enum +type OpReason string + +type CountryCodeType string + +const ( + // RO - Romania + CountryCodeRO CountryCodeType = "RO" + // AD - Andorra + CountryCodeAD CountryCodeType = "AD" + // AE - United Arab Emirates + CountryCodeAE CountryCodeType = "AE" + // AF - Afghanistan + CountryCodeAF CountryCodeType = "AF" + // AG - Antigua and Barbuda + CountryCodeAG CountryCodeType = "AG" + // AI - Anguilla + CountryCodeAI CountryCodeType = "AI" + // AL - Albania + CountryCodeAL CountryCodeType = "AL" + // AM - Armenia + CountryCodeAM CountryCodeType = "AM" + // AO - Angola + CountryCodeAO CountryCodeType = "AO" + // AQ - Antarctica + CountryCodeAQ CountryCodeType = "AQ" + // AR - Argentina + CountryCodeAR CountryCodeType = "AR" + // AS - American Samoa + CountryCodeAS CountryCodeType = "AS" + // AT - Austria + CountryCodeAT CountryCodeType = "AT" + // AU - Australia + CountryCodeAU CountryCodeType = "AU" + // AW - Aruba + CountryCodeAW CountryCodeType = "AW" + // AX - Aland Islands + CountryCodeAX CountryCodeType = "AX" + // AZ - Azerbaijan + CountryCodeAZ CountryCodeType = "AZ" + // BA - Bosnia and Herzegovina + CountryCodeBA CountryCodeType = "BA" + // BB - Barbados + CountryCodeBB CountryCodeType = "BB" + // BD - Bangladesh + CountryCodeBD CountryCodeType = "BD" + // BE - Belgium + CountryCodeBE CountryCodeType = "BE" + // BF - Burkina Faso + CountryCodeBF CountryCodeType = "BF" + // BG - Bulgaria + CountryCodeBG CountryCodeType = "BG" + // BH - Bahrain + CountryCodeBH CountryCodeType = "BH" + // BI - Burundi + CountryCodeBI CountryCodeType = "BI" + // BJ - Benin + CountryCodeBJ CountryCodeType = "BJ" + // BL - Saint Barthélemy + CountryCodeBL CountryCodeType = "BL" + // BM - Bermuda + CountryCodeBM CountryCodeType = "BM" + // BN - Brunei Darussalam + CountryCodeBN CountryCodeType = "BN" + // BO - Bolivia, Plurinational State of + CountryCodeBO CountryCodeType = "BO" + // BQ - Bonaire, Sint Eustatius and Saba + CountryCodeBQ CountryCodeType = "BQ" + // BR - Brazil + CountryCodeBR CountryCodeType = "BR" + // BS - Bahamas + CountryCodeBS CountryCodeType = "BS" + // BT - Bhutan + CountryCodeBT CountryCodeType = "BT" + // BV - Bouvet Island + CountryCodeBV CountryCodeType = "BV" + // BW - Botswana + CountryCodeBW CountryCodeType = "BW" + // BY - Belarus + CountryCodeBY CountryCodeType = "BY" + // BZ - Belize + CountryCodeBZ CountryCodeType = "BZ" + // CA - Canada + CountryCodeCA CountryCodeType = "CA" + // CC - Cocos (Keeling) Islands + CountryCodeCC CountryCodeType = "CC" + // CD - Congo, the Democratic Republic of the + CountryCodeCD CountryCodeType = "CD" + // CF - Central African Republic + CountryCodeCF CountryCodeType = "CF" + // CG - Congo + CountryCodeCG CountryCodeType = "CG" + // CH - Switzerland + CountryCodeCH CountryCodeType = "CH" + // CI - Côte d'Ivoire + CountryCodeCI CountryCodeType = "CI" + // CK - Cook Islands + CountryCodeCK CountryCodeType = "CK" + // CL - Chile + CountryCodeCL CountryCodeType = "CL" + // CM - Cameroon + CountryCodeCM CountryCodeType = "CM" + // CN - China + CountryCodeCN CountryCodeType = "CN" + // CO - Colombia + CountryCodeCO CountryCodeType = "CO" + // CR - Costa Rica + CountryCodeCR CountryCodeType = "CR" + // CU - Cuba + CountryCodeCU CountryCodeType = "CU" + // CV - Cabo Verde + CountryCodeCV CountryCodeType = "CV" + // CW - Curaçao + CountryCodeCW CountryCodeType = "CW" + // CX - Christmas Island + CountryCodeCX CountryCodeType = "CX" + // CY - Cyprus + CountryCodeCY CountryCodeType = "CY" + // CZ - Czechia + CountryCodeCZ CountryCodeType = "CZ" + // DE - Germany + CountryCodeDE CountryCodeType = "DE" + // DJ - Djibouti + CountryCodeDJ CountryCodeType = "DJ" + // DK - Denmark + CountryCodeDK CountryCodeType = "DK" + // DM - Dominica + CountryCodeDM CountryCodeType = "DM" + // DO - Dominican Republic + CountryCodeDO CountryCodeType = "DO" + // DZ - Algeria + CountryCodeDZ CountryCodeType = "DZ" + // EC - Ecuador + CountryCodeEC CountryCodeType = "EC" + // EE - Estonia + CountryCodeEE CountryCodeType = "EE" + // EG - Egypt + CountryCodeEG CountryCodeType = "EG" + // EH - Western Sahara + CountryCodeEH CountryCodeType = "EH" + // EL - Greece + CountryCodeEL CountryCodeType = "EL" + // ER - Eritrea + CountryCodeER CountryCodeType = "ER" + // ES - Spain + CountryCodeES CountryCodeType = "ES" + // ET - Ethiopia + CountryCodeET CountryCodeType = "ET" + // FI - Finland + CountryCodeFI CountryCodeType = "FI" + // FJ - Fiji + CountryCodeFJ CountryCodeType = "FJ" + // FK - Falkland Islands (Malvinas) + CountryCodeFK CountryCodeType = "FK" + // FM - Micronesia, Federated States of + CountryCodeFM CountryCodeType = "FM" + // FO - Faroe Islands + CountryCodeFO CountryCodeType = "FO" + // FR - France + CountryCodeFR CountryCodeType = "FR" + // GA - Gabon + CountryCodeGA CountryCodeType = "GA" + // GB - United Kingdom of Great Britain and Northern Ireland + CountryCodeGB CountryCodeType = "GB" + // GD - Grenada + CountryCodeGD CountryCodeType = "GD" + // GE - Georgia + CountryCodeGE CountryCodeType = "GE" + // GF - French Guiana + CountryCodeGF CountryCodeType = "GF" + // GG - Guernsey + CountryCodeGG CountryCodeType = "GG" + // GH - Ghana + CountryCodeGH CountryCodeType = "GH" + // GI - Gibraltar + CountryCodeGI CountryCodeType = "GI" + // GL - Greenland + CountryCodeGL CountryCodeType = "GL" + // GM - Gambia + CountryCodeGM CountryCodeType = "GM" + // GN - Guinea + CountryCodeGN CountryCodeType = "GN" + // GP - Guadeloupe + CountryCodeGP CountryCodeType = "GP" + // GQ - Equatorial Guinea + CountryCodeGQ CountryCodeType = "GQ" + // GS - South Georgia and the South Sandwich Islands + CountryCodeGS CountryCodeType = "GS" + // GT - Guatemala + CountryCodeGT CountryCodeType = "GT" + // GU - Guam + CountryCodeGU CountryCodeType = "GU" + // GW - Gu inea-Bissau + CountryCodeGW CountryCodeType = "GW" + // GY - Guyana + CountryCodeGY CountryCodeType = "GY" + // HK - Hong Kong + CountryCodeHK CountryCodeType = "HK" + // HM - Heard Island and McDonald Islands + CountryCodeHM CountryCodeType = "HM" + // HN - Honduras + CountryCodeHN CountryCodeType = "HN" + // HR - Croatia + CountryCodeHR CountryCodeType = "HR" + // HT - Haiti + CountryCodeHT CountryCodeType = "HT" + // HU - Hungary + CountryCodeHU CountryCodeType = "HU" + // ID - Indonesia + CountryCodeID CountryCodeType = "ID" + // IE - Ireland + CountryCodeIE CountryCodeType = "IE" + // IL - Israel + CountryCodeIL CountryCodeType = "IL" + // IM - Isle of Man + CountryCodeIM CountryCodeType = "IM" + // IN - India + CountryCodeIN CountryCodeType = "IN" + // IO - British Indian Ocean Territory + CountryCodeIO CountryCodeType = "IO" + // IQ - Iraq + CountryCodeIQ CountryCodeType = "IQ" + // IR - Iran, Islamic Republic of + CountryCodeIR CountryCodeType = "IR" + // IS - Iceland + CountryCodeIS CountryCodeType = "IS" + // IT - Italy + CountryCodeIT CountryCodeType = "IT" + // JE - Jersey + CountryCodeJE CountryCodeType = "JE" + // JM - Jamaica + CountryCodeJM CountryCodeType = "JM" + // JO - Jordan + CountryCodeJO CountryCodeType = "JO" + // JP - Japan + CountryCodeJP CountryCodeType = "JP" + // KE - Kenya + CountryCodeKE CountryCodeType = "KE" + // KG - Kyrgyzstan + CountryCodeKG CountryCodeType = "KG" + // KH - Cambodia + CountryCodeKH CountryCodeType = "KH" + // KI - Kiribati + CountryCodeKI CountryCodeType = "KI" + // KM - Comoros + CountryCodeKM CountryCodeType = "KM" + // KN - Saint Kitts and Nevis + CountryCodeKN CountryCodeType = "KN" + // KP - Korea, Democratic People's Republic of + CountryCodeKP CountryCodeType = "KP" + // KR - Korea, Republic of + CountryCodeKR CountryCodeType = "KR" + // KW - Kuwait + CountryCodeKW CountryCodeType = "KW" + // KY - Cayman Islands + CountryCodeKY CountryCodeType = "KY" + // KZ - Kazakhstan + CountryCodeKZ CountryCodeType = "KZ" + // LA - Lao People's Democratic Republic + CountryCodeLA CountryCodeType = "LA" + // LB - Lebanon + CountryCodeLB CountryCodeType = "LB" + // LC - Saint Lucia + CountryCodeLC CountryCodeType = "LC" + // LI - Liechtenstein + CountryCodeLI CountryCodeType = "LI" + // LK - Sri Lanka + CountryCodeLK CountryCodeType = "LK" + // LR - Liberia + CountryCodeLR CountryCodeType = "LR" + // LS - Lesotho + CountryCodeLS CountryCodeType = "LS" + // LT - Lithuania + CountryCodeLT CountryCodeType = "LT" + // LU - Luxembourg + CountryCodeLU CountryCodeType = "LU" + // LV - Latvia + CountryCodeLV CountryCodeType = "LV" + // LY - Libya + CountryCodeLY CountryCodeType = "LY" + // MA - Morocco + CountryCodeMA CountryCodeType = "MA" + // MC - Monaco + CountryCodeMC CountryCodeType = "MC" + // MD - Moldova, Republic of + CountryCodeMD CountryCodeType = "MD" + // ME - Montenegro + CountryCodeME CountryCodeType = "ME" + // MF - Saint Martin (French part) + CountryCodeMF CountryCodeType = "MF" + // MG - Madagascar + CountryCodeMG CountryCodeType = "MG" + // MH - Marshall Islands + CountryCodeMH CountryCodeType = "MH" + // MK - Macedonia, the former Yugoslav Republic of + CountryCodeMK CountryCodeType = "MK" + // ML - Mali + CountryCodeML CountryCodeType = "ML" + // MM - Myanmar + CountryCodeMM CountryCodeType = "MM" + // MN - Mongolia + CountryCodeMN CountryCodeType = "MN" + // MO - Macao + CountryCodeMO CountryCodeType = "MO" + // MP - Northern Mariana Islands + CountryCodeMP CountryCodeType = "MP" + // MQ - Martinique + CountryCodeMQ CountryCodeType = "MQ" + // MR - Mauritania + CountryCodeMR CountryCodeType = "MR" + // MS - Montserrat + CountryCodeMS CountryCodeType = "MS" + // MT - Malta + CountryCodeMT CountryCodeType = "MT" + // MU - Mauritius + CountryCodeMU CountryCodeType = "MU" + // MV - Maldives + CountryCodeMV CountryCodeType = "MV" + // MW - Malawi + CountryCodeMW CountryCodeType = "MW" + // MX - Mexico + CountryCodeMX CountryCodeType = "MX" + // MY - Malaysia + CountryCodeMY CountryCodeType = "MY" + // MZ - Mozambique + CountryCodeMZ CountryCodeType = "MZ" + // NA - Namibia + CountryCodeNA CountryCodeType = "NA" + // NC - New Caledonia + CountryCodeNC CountryCodeType = "NC" + // NE - Niger + CountryCodeNE CountryCodeType = "NE" + // NF - Norfolk Island + CountryCodeNF CountryCodeType = "NF" + // NG - Nigeria + CountryCodeNG CountryCodeType = "NG" + // NI - Nicaragua + CountryCodeNI CountryCodeType = "NI" + // NL - Netherlands + CountryCodeNL CountryCodeType = "NL" + // NO - Norway + CountryCodeNO CountryCodeType = "NO" + // NP - Nepal + CountryCodeNP CountryCodeType = "NP" + // NR - Nauru + CountryCodeNR CountryCodeType = "NR" + // NU - Niue + CountryCodeNU CountryCodeType = "NU" + // NZ - New Zealand + CountryCodeNZ CountryCodeType = "NZ" + // OM - Oman + CountryCodeOM CountryCodeType = "OM" + // PA - Panama + CountryCodePA CountryCodeType = "PA" + // PE - Peru + CountryCodePE CountryCodeType = "PE" + // PF - French Polynesia + CountryCodePF CountryCodeType = "PF" + // PG - Papua New Guinea + CountryCodePG CountryCodeType = "PG" + // PH - Philippines + CountryCodePH CountryCodeType = "PH" + // PK - Pakistan + CountryCodePK CountryCodeType = "PK" + // PL - Poland + CountryCodePL CountryCodeType = "PL" + // PM - Saint Pierre and Miquelon + CountryCodePM CountryCodeType = "PM" + // PN - Pitcairn + CountryCodePN CountryCodeType = "PN" + // PR - Puerto Rico + CountryCodePR CountryCodeType = "PR" + // PS - Palestine, State of + CountryCodePS CountryCodeType = "PS" + // PT - Portugal + CountryCodePT CountryCodeType = "PT" + // PW - Palau + CountryCodePW CountryCodeType = "PW" + // PY - Paraguay + CountryCodePY CountryCodeType = "PY" + // QA - Qatar + CountryCodeQA CountryCodeType = "QA" + // RE - Réunion + CountryCodeRE CountryCodeType = "RE" + // RS - Serbia + CountryCodeRS CountryCodeType = "RS" + // RU - Russian Federation + CountryCodeRU CountryCodeType = "RU" + // RW - Rwanda + CountryCodeRW CountryCodeType = "RW" + // SA - Saudi Arabia + CountryCodeSA CountryCodeType = "SA" + // SB - Solomon Islands + CountryCodeSB CountryCodeType = "SB" + // SC - Seychelles + CountryCodeSC CountryCodeType = "SC" + // SD - Sudan + CountryCodeSD CountryCodeType = "SD" + // SE - Sweden + CountryCodeSE CountryCodeType = "SE" + // SG - Singapore + CountryCodeSG CountryCodeType = "SG" + // SH - Saint Helena, Ascension and Tristan da Cunha + CountryCodeSH CountryCodeType = "SH" + // SI - Slovenia + CountryCodeSI CountryCodeType = "SI" + // SJ - Svalbard and Jan Mayen + CountryCodeSJ CountryCodeType = "SJ" + // SK - Slovakia + CountryCodeSK CountryCodeType = "SK" + // SL - Sierra Leone + CountryCodeSL CountryCodeType = "SL" + // SM - San Marino + CountryCodeSM CountryCodeType = "SM" + // SN - Senegal + CountryCodeSN CountryCodeType = "SN" + // SO - Somalia + CountryCodeSO CountryCodeType = "SO" + // SR - Suriname + CountryCodeSR CountryCodeType = "SR" + // SS - South Sudan + CountryCodeSS CountryCodeType = "SS" + // ST - Sao Tome and Principe + CountryCodeST CountryCodeType = "ST" + // SV - El Salvador + CountryCodeSV CountryCodeType = "SV" + // SX - Sint Maarten (Dutch part) + CountryCodeSX CountryCodeType = "SX" + // SY - Syrian Arab Republic + CountryCodeSY CountryCodeType = "SY" + // SZ - Swaziland + CountryCodeSZ CountryCodeType = "SZ" + // TC - Turks and Caicos Islands + CountryCodeTC CountryCodeType = "TC" + // TD - Chad + CountryCodeTD CountryCodeType = "TD" + // TF - French Southern Territories + CountryCodeTF CountryCodeType = "TF" + // TG - Togo + CountryCodeTG CountryCodeType = "TG" + // TH - Thailand + CountryCodeTH CountryCodeType = "TH" + // TJ - Tajikistan + CountryCodeTJ CountryCodeType = "TJ" + // TK - Tokelau + CountryCodeTK CountryCodeType = "TK" + // TL - Timor-Leste + CountryCodeTL CountryCodeType = "TL" + // TM - Turkmenistan + CountryCodeTM CountryCodeType = "TM" + // TN - Tunisia + CountryCodeTN CountryCodeType = "TN" + // TO - Tonga + CountryCodeTO CountryCodeType = "TO" + // TR - Turkey + CountryCodeTR CountryCodeType = "TR" + // TT - Trinidad and Tobago + CountryCodeTT CountryCodeType = "TT" + // TV - Tuvalu + CountryCodeTV CountryCodeType = "TV" + // TW - Taiwan, Province of China + CountryCodeTW CountryCodeType = "TW" + // TZ - Tanzania, United Republic of + CountryCodeTZ CountryCodeType = "TZ" + // UA - Ukraine + CountryCodeUA CountryCodeType = "UA" + // UG - Uganda + CountryCodeUG CountryCodeType = "UG" + // UM - United States Minor Outlying Islands + CountryCodeUM CountryCodeType = "UM" + // US - United States of America + CountryCodeUS CountryCodeType = "US" + // UY - Uruguay + CountryCodeUY CountryCodeType = "UY" + // UZ - Uzbekistan + CountryCodeUZ CountryCodeType = "UZ" + // VA - Holy See + CountryCodeVA CountryCodeType = "VA" + // VC - Saint Vincent and the Grenadines + CountryCodeVC CountryCodeType = "VC" + // VE - Venezuela, Bolivarian Republic of + CountryCodeVE CountryCodeType = "VE" + // VG - Virgin Islands, British + CountryCodeVG CountryCodeType = "VG" + // VI - Virgin Islands, U.S. + CountryCodeVI CountryCodeType = "VI" + // VN - Viet Nam + CountryCodeVN CountryCodeType = "VN" + // VU - Vanuatu + CountryCodeVU CountryCodeType = "VU" + // WF - Wallis and Futuna + CountryCodeWF CountryCodeType = "WF" + // WS - Samoa + CountryCodeWS CountryCodeType = "WS" + // YE - Yemen + CountryCodeYE CountryCodeType = "YE" + // YT - Mayotte + CountryCodeYT CountryCodeType = "YT" + // ZA - South Africa + CountryCodeZA CountryCodeType = "ZA" + // ZM - Zambia + CountryCodeZM CountryCodeType = "ZM" + // ZW - Zimbabwe + CountryCodeZW CountryCodeType = "ZW" + // 1A - Kosovo + CountryCode1A CountryCodeType = "1A" +) + +type BCPCodeType string + +const ( +// "1" Petea (HU) +// "2" Borș (HU) +// "3" Vărșand (HU) +// "4" Nădlac (HU) +// "5" Calafat (BG) +// "6" Bechet (BG) +// "7" Turnu Măgurele (BG) +// "8" Zimnicea (BG) +// "9" Giurgiu (BG) +// "10" Ostrov (BG) +// "11" Negru Vodă (BG) +// "12" Vama Veche (BG) +// "13" Călărași (BG) +// "14" Corabia (BG) +// "15" Oltenița (BG) +// "16" Carei (HU) +// "17" Cenad (HU) +// "18" Episcopia Bihor (HU) +// "19" Salonta (HU) +// "20" Săcuieni (HU) +// "21" Turnu (HU) +// "22" Urziceni (HU) +// "23" Valea lui Mihai (HU) +// "24" Vladimirescu (HU) +// "25" Porțile de Fier 1 (RS) +// "26" Naidăș (RS) +// "27" Stamora Moravița (RS) +// "28" Jimbolia (RS) +// "29" Halmeu (UA) +// "30" Stânca Costești (MD) +// "31" Sculeni (MD) +// "32" Albița (MD) +// "33" Oancea (MD) +// "34" Galați Giurgiulești (MD) +// "35" Constanța Sud Agigea (-) +// "36" Siret (UA) +// "37" Nădlac 2 - A1 (HU) +// "38" Borș 2 - A3 (HU) +// TODO: enum values +) + +type CustomsOfficeCodeType string + +const ( +// Valori posibile pentru câmpul codBirouVamal (BVI/F - Birou Vamal de Interior/Frontieră): +// "12801" BVI Alba Iulia (ROBV0300) +// "22801" BVI Arad (ROTM0200) +// "22901" BVF Arad Aeroport (ROTM0230) +// "22902" BVF Zona Liberă Curtici (ROTM2300) +// "32801" BVI Pitești (ROCR7000) +// "42801" BVI Bacău (ROIS0600) +// "42901" BVF Bacău Aeroport (ROIS0620) +// "52801" BVI Oradea (ROCJ6570) +// "52901" BVF Oradea Aeroport (ROCJ6580) +// "62801" BVI Bistriţa-Năsăud (ROCJ0400) +// "72801" BVI Botoşani (ROIS1600) +// "72901" BVF Stanca Costeşti (ROIS1610) +// "72902" BVF Rădăuţi Prut (ROIS1620) +// "82801" BVI Braşov (ROBV0900) +// "92901" BVF Zona Liberă Brăila (ROGL0710) +// "92902" BVF Brăila (ROGL0700) +// "102801" BVI Buzău (ROGL1500) +// "112801" BVI Reșița (ROTM7600) +// "112901" BVF Naidăș (ROTM6100) +// "122801" BVI Cluj Napoca (ROCJ1800) +// "122901" BVF Cluj Napoca Aero (ROCJ1810) +// "132901" BVF Constanţa Sud Agigea (ROCT1900) +// "132902" BVF Mihail Kogălniceanu (ROCT5100) +// "132903" BVF Mangalia (ROCT5400) +// "132904" BVF Constanţa Port (ROCT1970) +// "142801" BVI Sfântu Gheorghe (ROBV7820) +// "152801" BVI Târgoviște (ROBU8600) +// "162801" BVI Craiova (ROCR2100) +// "162901" BVF Craiova Aeroport (ROCR2110) +// "162902" BVF Bechet (ROCR1720) +// "162903" BVF Calafat (ROCR1700) +// "172901" BVF Zona Liberă Galaţi (ROGL3810) +// "172902" BVF Giurgiuleşti (ROGL3850) +// "172903" BVF Oancea (ROGL3610) +// "172904" BVF Galaţi (ROGL3800) +// "182801" BVI Târgu Jiu (ROCR8810) +// "192801" BVI Miercurea Ciuc (ROBV5600) +// "202801" BVI Deva (ROTM8100) +// "212801" BVI Slobozia (ROCT8220) +// "222901" BVF Iaşi Aero (ROIS4660) +// "222902" BVF Sculeni (ROIS4990) +// "222903" BVF Iaşi (ROIS4650) +// "232801" BVI Antrepozite/Ilfov (ROBU1200) +// "232901" BVF Otopeni Călători (ROBU1030) +// "242801" BVI Baia Mare (ROCJ0500) +// "242901" BVF Aero Baia Mare (ROCJ0510) +// "242902" BVF Sighet (ROCJ8000) +// "252901" BVF Orşova (ROCR7280) +// "252902" BVF Porţile De Fier I (ROCR7270) +// "252903" BVF Porţile De Fier II (ROCR7200) +// "252904" BVF Drobeta Turnu Severin (ROCR9000) +// "262801" BVI Târgu Mureş (ROBV8800) +// "262901" BVF Târgu Mureş Aeroport (ROBV8820) +// "272801" BVI Piatra Neamţ (ROIS7400) +// "282801" BVI Corabia (ROCR2000) +// "282802" BVI Olt (ROCR8210) +// "292801" BVI Ploiești (ROBU7100) +// "302801" BVI Satu-Mare (ROCJ7810) +// "302901" BVF Halmeu (ROCJ4310) +// "302902" BVF Aeroport Satu Mare (ROCJ7830) +// "312801" BVI Zalău (ROCJ9700) +// "322801" BVI Sibiu (ROBV7900) +// "322901" BVF Sibiu Aeroport (ROBV7910) +// "332801" BVI Suceava (ROIS8230) +// "332901" BVF Dorneşti (ROIS2700) +// "332902" BVF Siret (ROIS8200) +// "332903" BVF Suceava Aero (ROIS8250) +// "332904" BVF Vicovu De Sus (ROIS9620) +// "342801" BVI Alexandria (ROCR0310) +// "342901" BVF Turnu Măgurele (ROCR9100) +// "342902" BVF Zimnicea (ROCR5800) +// "352802" BVI Timişoara Bază (ROTM8720) +// "352901" BVF Jimbolia (ROTM5010) +// "352902" BVF Moraviţa (ROTM5510) +// "352903" BVF Timişoara Aeroport (ROTM8730) +// "362901" BVF Sulina (ROCT8300) +// "362902" BVF Aeroport Delta Dunării Tulcea (ROGL8910) +// "362903" BVF Tulcea (ROGL8900) +// "362904" BVF Isaccea (ROGL8920) +// "372801" BVI Vaslui (ROIS9610) +// "372901" BVF Fălciu (-) +// "372902" BVF Albiţa (ROIS0100) +// "382801" BVI Râmnicu Vâlcea (ROCR7700) +// "392801" BVI Focșani (ROGL3600) +// "402801" BVI Bucureşti Poştă (ROBU1380) +// "402802" BVI Târguri și Expoziții (ROBU1400) +// "402901" BVF Băneasa (ROBU1040) +// "512801" BVI Călăraşi (ROCT1710) +// "522801" BVI Giurgiu (ROBU3910) +// "522901" BVF Zona Liberă Giurgiu (ROBU3980) +// TODO: enum values +) + +type CountyCodeType string + +const ( +// Valori posibile pentru câmpul codJudetType: +// "1" Alba +// "2" Arad +// "3" Argeş +// "4" Bacău +// "5" Bihor +// "6" Bistriţa-Năsăud +// "7" Botoşani +// "8" Braşov +// "9" Brăila +// "10" Buzău +// "11" Caraş-Severin +// "51" Călăraşi +// "12" Cluj +// "13" Constanţa +// "14" Covasna +// "16" Dolj +// "15" Dâmboviţa +// "17" Galaţi +// "52" Giurgiu +// "18" Gorj +// "19" Harghita +// "20" Hunedoara +// "21" Ialomiţa +// "22" Iaşi +// "23" Ilfov +// "24" Maramureş +// "25" Mehedinţi +// "40" Municipiul Bucureşti +// "26" Mureş +// "27" Neamţ +// "28" Olt +// "29" Prahova +// "30" Satu Mare +// "32" Sibiu +// "33" Suceava +// "31" Sălaj +// "34" Teleorman +// "35" Timiş +// "36" Tulcea +// "37" Vaslui +// "39" Vrancea +// "38" Vâlcea +) + +type OpPurposeCodeType string + +const ( +// Câmpul codScopOperatiune ia valori diferite, în funcţie de valoarea câmpului codTipOperatiune, astfel: +// - pentru codTipOperatiune = "10" AIC - Achiziţie intracomunitară: +// "101" Comercializare +// "201" Producție +// "301" Gratuități +// "401" Echipament comercial +// "501" Mijloace fixe +// "601" Consum propriu +// "703" Operațiuni de livrare cu instalare +// "801" Leasing financiar/operațional +// "802" Bunuri în garanție +// "901" Operațiuni scutite +// "1001" Investiție in curs +// "1101" Donații, ajutoare +// "9901" Altele +// - pentru codTipOperatiune = "12" LHI - Operațiuni în sistem lohn (UE) - intrare: +// "9999" Același cu operațiunea +// - pentru codTipOperatiune = "14" SCI - Stocuri la dispoziția clientului (Call-off stock) - intrare: +// "9999" Același cu operațiunea +// - pentru codTipOperatiune = "20" LIC - Livrare intracomunitară: +// "101" Comercializare +// "301" Gratuități +// "703" Operațiuni de livrare cu instalare +// "801" Leasing financiar/operațional +// "802" Bunuri în garanție +// "9901" Altele +// - pentru codTipOperatiune = "22" LHE - Operațiuni în sistem lohn (UE) - ieşire: +// "9999" Același cu operațiunea +// - pentru codTipOperatiune = "24" SCE - Stocuri la dispoziția clientului (Call-off stock) - ieşire: +// "9999" Același cu operațiunea +// - pentru codTipOperatiune = "30" TTN - Transport pe teritoriul naţional: +// "101" Comercializare +// "704" Transfer între gestiuni +// "705" Bunuri puse la dispoziția clientului +// "9901" Altele +// - pentru codTipOperatiune = "40" IMP - Import: +// "9999" Același cu operațiunea +// - pentru codTipOperatiune = "50" EXP - Export: +// "9999" Același cu operațiunea +// - pentru codTipOperatiune = "60" DIN - Tranzactie intracomunitara - Intrare pentru depozitare/formare nou transport: +// "9999" Același cu operațiunea +// - pentru codTipOperatiune = "70" DIE - Tranzactie intracomunitara - Iesire dupa depozitare/formare nou transport: +// "9999" Același cu operațiunea +) + +// Valori posibile pentru câmpul codUnitateMasura: UN/ECE Recommendation N°20 and UN/ECE Recommendation N°21 — Unit codes +type UnitMeasureCodeType string + +type DocumentType string + +const ( +// Valori posibile pentru câmpul tipDocument: +// "10" CMR +// "20" Factura +// "30" Aviz de însoțire a mărfii +// "9999" Altele +) diff --git a/etransport/declaration.go b/etransport/declaration.go new file mode 100644 index 0000000..9cba164 --- /dev/null +++ b/etransport/declaration.go @@ -0,0 +1,161 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package etransport + +import ( + "github.com/shopspring/decimal" + + ixml "github.com/printesoi/e-factura-go/xml" + "github.com/printesoi/xml-go" +) + +type PostingDeclaration struct { + // CUI/CIF/CNP + DeclarantCode string `xml:"codDeclarant,attr"` + // + DeclarantRef string `xml:"refDeclarant,attr,omitempty"` + DeclPostIncident DeclPostIncidentType `xml:"declPostAvarie,attr,omitempty"` + + Notification *PostingDeclarationNotification `xml:"notificare,omitempty"` + Deletion *PostingDeclarationDeletion `xml:"stergere,omitempty"` + Confirmation *PostingDeclarationConfirmation `xml:"confirmare,omitempty"` + VehicleChange *PostingDeclarationVehicleChange `xml:"modifVehicul,omitempty"` + + // Name of node. + XMLName xml.Name `xml:"eTransport"` + // xmlns attr. Will be automatically set in MarshalXML + Namespace string `xml:"xmlns,attr"` + // xmlns:xsi attr. Will be automatically set in MarshalXML + NamespaceXSI string `xml:"xmlns:xsi,attr"` + // generated with... Will be automatically set in MarshalXML if empty. + Comment string `xml:",comment"` +} + +// Prefill sets the NS, NScac, NScbc and Comment properties for ensuring that +// the required attributes and properties are set for a valid UBL XML. +func (pd *PostingDeclaration) Prefill() { + pd.Namespace = "mfp:anaf:dgti:eTransport:declaratie:v2" + pd.NamespaceXSI = "http://www.w3.org/2001/XMLSchema-instance" +} + +func (pd PostingDeclaration) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + type eTransport PostingDeclaration + pd.Prefill() + start.Name.Local = "eTransport" + return e.EncodeElement(eTransport(pd), start) +} + +// XML returns the XML encoding of the PostingDeclaration +func (pd PostingDeclaration) XML() ([]byte, error) { + return ixml.MarshalXMLWithHeader(pd) +} + +// XMLIndent works like XML, but each XML element begins on a new +// indented line that starts with prefix and is followed by one or more +// copies of indent according to the nesting depth. +func (pd PostingDeclaration) XMLIndent(prefix, indent string) ([]byte, error) { + return ixml.MarshalIndentXMLWithHeader(pd, prefix, indent) +} + +type UitType string + +type PostingDeclarationNotification struct { + OpType OpType `xml:"codTipOperatiune,attr"` + + // Cardinality: 0..1 + Correction *PostingDeclarationNotificationCorrection `xml:"corectie,omitempty"` + // Cardinality: 1..n + TransportedGoods []PostingDeclarationNotificationTransportedGood `xml:"bunuriTransportate"` + // Cardinality: 1..1 + CommercialPartner PostingDeclarationNotificationCommercialPartner `xml:"partenerComercial"` + // Cardinality: 1..1 + TransportData PostingDeclarationNotificationTransportData `xml:"dateTransport"` + // Cardinality: 1..1 + RouteStartPlace PostingDeclationPlace `xml:"locStartTraseuRutier"` + // Cardinality: 1..1 + RouteEndPlace PostingDeclationPlace `xml:"locFinalTraseuRutier"` + // Cardinality: 1..n + TransportDocuments []PostingDeclarationTransportDocument `xml:"documenteTransport"` + // Cardinality: 0..n + PrevNotifications []string `xml:"notificareAnterioara,omitempty"` +} + +type PostingDeclarationNotificationCorrection struct { + Uit UitType `xml:"uit,attr"` +} + +type PostingDeclarationNotificationTransportedGood struct { + OpPurposeCode OpPurposeCodeType `xml:"codScopOperatiune,attr"` + TariffCode string `xml:"codTarifar,attr,omitempty"` + GoodName string `xml:"denumireMarfa,attr"` + Quantity decimal.Decimal `xml:"cantitate,attr"` + UnitMeasureCode UnitMeasureCodeType `xml:"codUnitateMasura,attr"` + NetWeight *decimal.Decimal `xml:"greutateNeta,attr,omitempty"` + GrossWeight decimal.Decimal `xml:"greutateBruta,attr"` + LeiValueNoVAT *decimal.Decimal `xml:"valoareLeiFaraTva,attr,omitempty"` + DeclarantRef string `xml:"refDeclaranti,attr,omitempty"` +} + +type PostingDeclarationNotificationCommercialPartner struct { + CountryCode CountryCodeType `xml:"codTara,attr"` + Code string `xml:"cod,attr,omitempty"` + Name string `xml:"denumire,attr"` +} + +type PostingDeclarationNotificationTransportData struct { + LicensePlate string `xml:"nrVehicul,attr"` + TrailerLicensePlate1 string `xml:"nrRemorca1,attr,omitempty"` + TrailerLicensePlate2 string `xml:"nrRemorca2,attr,omitempty"` + TransportOrgCountryCode CountryCodeType `xml:"codTaraOrgTransport,attr"` + TransportOrgCode string `xml:"codOrgTransport,attr,omitempty"` + TransportOrgName string `xml:"denumireOrgTransport,attr"` + // TODO: type + TransportDate string `xml:"dataTransport,attr"` +} + +type PostingDeclationPlace struct { + Location *PostingDeclationLocation `xml:"locatie,omitempty"` + BCPCode BCPCodeType `xml:"codPtf,attr,omitempty"` + CustomsOfficeCode CustomsOfficeCodeType `xml:"codBirouVamal,attr,omitempty"` +} + +type PostingDeclationLocation struct { + CountyCode CountyCodeType `xml:"codJudet,attr"` + LocalityName string `xml:"denumireLocalitate,attr"` + StreetName string `xml:"denumireStrada,attr"` + StreetNo string `xml:"numar,attr,omitempty"` + Building string `xml:"bloc,attr,omitempty"` + BuildingEntrance string `xml:"scara,attr,omitempty"` + Floor string `xml:"etaj,attr,omitempty"` + Apartment string `xml:"apartament,attr,omitempty"` + OtherInfo string `xml:"alteInfo,attr,omitempty"` + ZipCode string `xml:"codPostal,attr,omitempty"` +} + +type PostingDeclarationTransportDocument struct { + DocumentType DocumentType `xml:"tipDocument,attr"` + DocumentNo string `xml:"numarDocument,attr,omitempty"` + DocumentDate string `xml:"dataDocument,attr"` + Remarks string `xml:"observatii,attr,omitempty"` +} + +type PostingDeclarationDeletion struct { +} + +type PostingDeclarationConfirmation struct { +} + +type PostingDeclarationVehicleChange struct { +} diff --git a/etransport/rest.go b/etransport/rest.go new file mode 100644 index 0000000..57e4e59 --- /dev/null +++ b/etransport/rest.go @@ -0,0 +1,240 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package etransport + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "strings" + + ierrors "github.com/printesoi/e-factura-go/internal/errors" + ixml "github.com/printesoi/e-factura-go/xml" +) + +const ( + apiBase = "ETRANSPORT/ws/v1/" + apiPathUploadV2 = apiBase + "upload/%s/%s/2" + apiPathMessageList = apiBase + "lista/%d/%s" + apiPathMessageState = apiBase + "stareMesaj/%d" + apiPathInfo = apiBase + "info" +) + +type Message struct { + Uit string `json:"uit"` + Cod_decl string `json:"cod_decl"` + Ref_decl string `json:"ref_decl"` + Sursa string `json:"sursa"` + Id_incarcare string `json:"id_incarcare"` + Data_creare string `json:"data_creare"` + Tip_op string `json:"tip_op"` + Data_transp string `json:"data_transp"` + Pc_tara string `json:"pc_tara"` + Pc_cod string `json:"pc_cod"` + Pc_den string `json:"pc_den"` + Tr_tara string `json:"tr_tara"` + Tr_cod string `json:"tr_cod"` + Tr_den string `json:"tr_den"` + Nr_veh string `json:"nr_veh"` + Nr_rem1 string `json:"nr_rem1"` + Nr_rem2 string `json:"nr_rem2"` + Mesaje []MessageError `json:"mesaje,omitempty"` +} + +type MessageError struct { + Type string `json:"tip"` + Message string `json:"mesaj"` +} + +// MessagesListResponse is the parsed response from the list messages endpoint. +type MessagesListResponse struct { + Errors []struct { + ErrorMessage string `json:"errorMessage"` + } `json:"errors,omitempty"` + Messages []Message `json:"mesaje,omitempty"` + Serial string `json:"serial"` + CUI string `json:"cui"` + Title string `json:"titlu"` + DateResponse string `json:"dateResponse"` + ExecutionStatus int32 `json:"ExecutionStatus"` + TraceID string `json:"trace_id"` +} + +// IsOk returns true if the response corresponding to fetching messages list +// was successful. +func (r *MessagesListResponse) IsOk() bool { + return r != nil && (len(r.Errors) == 0 || len(r.Errors) == 1 && strings.HasPrefix(r.Errors[0].ErrorMessage, "Nu exista mesaje in ")) +} + +// GetMessagesList fetches the list of messages for a provided cif and number +// of days. +// NOTE: If there are no messages for the given interval, ANAF APIs +// return an error. For this case, the response.IsOk() returns true and the +// Messages slice is empty, since I don't think that no messages should result +// in an error. +func (c *Client) GetMessagesList( + ctx context.Context, cif string, numDays int, +) (response *MessagesListResponse, err error) { + path := fmt.Sprintf(apiPathMessageList, numDays, cif) + req, er := c.apiClient.NewRequest(ctx, http.MethodGet, path, nil, nil) + if err = er; err != nil { + return + } + + res := new(MessagesListResponse) + if err = c.apiClient.DoUnmarshalJSON(req, res, func(r *http.Response, _ any) error { + for _, em := range res.Errors { + if limit, ok := ierrors.ErrorMessageMatchLimitExceeded(em.ErrorMessage); ok { + return ierrors.NewLimitExceededError(r, limit, errors.New(em.ErrorMessage)) + } + } + return nil + }); err == nil { + response = res + } + return +} + +type ( + GetMessageStateCode string + + GetMessageStateResponse struct { + Errors []struct { + ErrorMessage string `json:"errorMessage"` + } `json:"errors,omitempty"` + State GetMessageStateCode `json:"stare"` + DateResponse string `json:"dateResponse"` + ExecutionStatus int32 `json:"ExecutionStatus"` + TraceID string `json:"trace_id"` + } +) + +const ( + GetMessageStateCodeOk GetMessageStateCode = "ok" + GetMessageStateCodeNok GetMessageStateCode = "nok" + GetMessageStateCodeProcessing GetMessageStateCode = "in prelucrare" + GetMessageStateCodeInvalidXML GetMessageStateCode = "XML cu erori nepreluat de sistem" +) + +// IsOk returns true if the message state if ok (processed, and can be +// downloaded). +func (r *GetMessageStateResponse) IsOk() bool { + return r != nil && r.State == GetMessageStateCodeOk +} + +// IsNok returns true if the message state is nok (there was an error +// processing the invoice). +func (r *GetMessageStateResponse) IsNok() bool { + return r != nil && r.State == GetMessageStateCodeNok +} + +// IsProcessing returns true if the message state is processing. +func (r *GetMessageStateResponse) IsProcessing() bool { + return r != nil && r.State == GetMessageStateCodeProcessing +} + +// IsInvalidXML returns true if the message state is processing. +func (r *GetMessageStateResponse) IsInvalidXML() bool { + return r != nil && r.State == GetMessageStateCodeInvalidXML +} + +// GetMessageState fetch the state of a message. The uploadIndex must a result +// from an upload operation. +func (c *Client) GetMessageState( + ctx context.Context, uploadIndex int64, +) (response *GetMessageStateResponse, err error) { + path := fmt.Sprintf(apiPathMessageState, uploadIndex) + req, er := c.apiClient.NewRequest(ctx, http.MethodGet, path, nil, nil) + if err = er; err != nil { + return + } + + res := new(GetMessageStateResponse) + if err = c.apiClient.DoUnmarshalJSON(req, res, func(r *http.Response, _ any) error { + for _, em := range res.Errors { + if limit, ok := ierrors.ErrorMessageMatchLimitExceeded(em.ErrorMessage); ok { + return ierrors.NewLimitExceededError(r, limit, errors.New(em.ErrorMessage)) + } + } + return nil + }); err == nil { + response = res + } + return +} + +type ( + uploadStandard string + + UploadV2Response struct { + DateResponse string `json:"dateResponse"` + ExecutionStatus int32 `json:"ExecutionStatus"` + UploadIndex int64 `json:"index_incarcare"` + UIT string `json:"UIT"` + TraceID string `json:"trace_id"` + DeclarantRef string `json:"ref_declarant"` + Attention string `json:"atentie,omitempty"` + Errors []struct { + ErrorMessage string `json:"errorMessage"` + } `json:"errors,omitempty"` + } +) + +// IsOk returns true if the response corresponding to fetching messages list +// was successful. +func (r *UploadV2Response) IsOk() bool { + return r != nil && r.ExecutionStatus == 0 +} + +const ( + uploadStandardETransp uploadStandard = "ETRANSP" +) + +func (c *Client) UploadV2XML( + ctx context.Context, xml io.Reader, cif string, +) (response *UploadV2Response, err error) { + path := fmt.Sprintf(apiPathUploadV2, uploadStandardETransp, cif) + req, er := c.apiClient.NewRequest(ctx, http.MethodPost, path, nil, xml) + if err = er; err != nil { + return + } + req.Header.Set("Content-Type", "application/xml") + + res := new(UploadV2Response) + if err = c.apiClient.DoUnmarshalJSON(req, res, func(r *http.Response, _ any) error { + for _, em := range res.Errors { + if limit, ok := ierrors.ErrorMessageMatchLimitExceeded(em.ErrorMessage); ok { + return ierrors.NewLimitExceededError(r, limit, errors.New(em.ErrorMessage)) + } + } + return nil + }); err == nil { + response = res + } + return +} + +func (c *Client) UploadPostingDeclarationV2( + ctx context.Context, decl PostingDeclaration, cif string, +) (response *UploadV2Response, err error) { + xmlReader, err := ixml.MarshalXMLToReader(decl) + if err != nil { + return nil, err + } + + return c.UploadV2XML(ctx, xmlReader, cif) +} From 46e99f9d76b680a2763469172653eb53cea4b9e2 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sun, 4 Aug 2024 16:47:53 +0300 Subject: [PATCH 02/22] Implement all posting declaration payloads --- etransport/codes.go | 11 ++- etransport/declaration.go | 145 ++++++++++++++++++++++++++++++-------- etransport/rest.go | 48 +++++++------ 3 files changed, 153 insertions(+), 51 deletions(-) diff --git a/etransport/codes.go b/etransport/codes.go index 6824660..fe9ea8a 100644 --- a/etransport/codes.go +++ b/etransport/codes.go @@ -29,7 +29,6 @@ const ( type OpType string const ( - // Valori posibile pentru câmpul codTipOperatiune: // "10" AIC - Achiziţie intracomunitară OpTypeAIC OpType = "10" // "12" LHI - Operațiuni în sistem lohn (UE) - intrare @@ -833,7 +832,7 @@ const ( // "1001" Investiție in curs // "1101" Donații, ajutoare // "9901" Altele -// - pentru codTipOperatiune = "12" LHI - Operațiuni în sistem lohn (UE) - intrare: +// - pentru codTipOperatiune = "12" LHI - Operațiuni în sistem lohn (UE) - intrare: // "9999" Același cu operațiunea // - pentru codTipOperatiune = "14" SCI - Stocuri la dispoziția clientului (Call-off stock) - intrare: // "9999" Același cu operațiunea @@ -875,3 +874,11 @@ const ( // "30" Aviz de însoțire a mărfii // "9999" Altele ) + +type ConfirmationType string + +const ( + ConfirmationTypeConfirmed ConfirmationType = "10" + ConfirmationTypePartiallyConfirmed ConfirmationType = "20" + ConfirmationTypeUnconfirmed ConfirmationType = "30" +) diff --git a/etransport/declaration.go b/etransport/declaration.go index 9cba164..d5c2f6b 100644 --- a/etransport/declaration.go +++ b/etransport/declaration.go @@ -15,46 +15,118 @@ package etransport import ( + "errors" + "github.com/shopspring/decimal" ixml "github.com/printesoi/e-factura-go/xml" "github.com/printesoi/xml-go" ) +// PostingDeclaration is the object that represents an e-transport posting +// declaration payload for the upload v2 endpoint. type PostingDeclaration struct { // CUI/CIF/CNP DeclarantCode string `xml:"codDeclarant,attr"` - // - DeclarantRef string `xml:"refDeclarant,attr,omitempty"` + // Reference for the declarant + DeclarantRef string `xml:"refDeclarant,attr,omitempty"` + // DeclPostIncident this must be set only if the posting declaration is + // uploaded after the transport already had place, otherwise leave this + // empty. DeclPostIncident DeclPostIncidentType `xml:"declPostAvarie,attr,omitempty"` - Notification *PostingDeclarationNotification `xml:"notificare,omitempty"` - Deletion *PostingDeclarationDeletion `xml:"stergere,omitempty"` - Confirmation *PostingDeclarationConfirmation `xml:"confirmare,omitempty"` - VehicleChange *PostingDeclarationVehicleChange `xml:"modifVehicul,omitempty"` + declarationType postingDeclarationType `xml:"-"` + declarationData any `xml:"-"` +} + +type postingDeclarationType int + +const ( + postingDeclarationTypeNA postingDeclarationType = iota + postingDeclarationTypeNotification + postingDeclarationTypeDeletion + postingDeclarationTypeConfirmation + postingDeclarationTypeVehicleChange +) + +// SetNotification set the given PostingDeclarationNotification as the +// PostingDeclaration payload. +func (pd *PostingDeclaration) SetNotification(notification PostingDeclarationNotification) *PostingDeclaration { + pd.declarationType = postingDeclarationTypeNotification + pd.declarationData = notification + return pd +} + +// SetDeletion set the given PostingDeclarationDeletion as the +// PostingDeclaration payload. +func (pd *PostingDeclaration) SetDeletion(deletion PostingDeclarationDeletion) *PostingDeclaration { + pd.declarationType = postingDeclarationTypeDeletion + pd.declarationData = deletion + return pd +} - // Name of node. - XMLName xml.Name `xml:"eTransport"` - // xmlns attr. Will be automatically set in MarshalXML - Namespace string `xml:"xmlns,attr"` - // xmlns:xsi attr. Will be automatically set in MarshalXML - NamespaceXSI string `xml:"xmlns:xsi,attr"` - // generated with... Will be automatically set in MarshalXML if empty. - Comment string `xml:",comment"` +// SetConfirmation set the given PostingDeclarationConfirmation as the +// PostingDeclaration payload. +func (pd *PostingDeclaration) SetConfirmation(confirmation PostingDeclarationConfirmation) *PostingDeclaration { + pd.declarationType = postingDeclarationTypeConfirmation + pd.declarationData = confirmation + return pd } -// Prefill sets the NS, NScac, NScbc and Comment properties for ensuring that -// the required attributes and properties are set for a valid UBL XML. -func (pd *PostingDeclaration) Prefill() { - pd.Namespace = "mfp:anaf:dgti:eTransport:declaratie:v2" - pd.NamespaceXSI = "http://www.w3.org/2001/XMLSchema-instance" +// SetVehicleChange set the given PostingDeclarationVehicleChange as the +// PostingDeclaration payload. +func (pd *PostingDeclaration) SetVehicleChange(vehicleChange PostingDeclarationVehicleChange) *PostingDeclaration { + pd.declarationType = postingDeclarationTypeVehicleChange + pd.declarationData = vehicleChange + return pd } func (pd PostingDeclaration) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - type eTransport PostingDeclaration - pd.Prefill() + type postingDeclaration PostingDeclaration + var eTransport struct { + postingDeclaration + + Notification *PostingDeclarationNotification `xml:"notificare,omitempty"` + Deletion *PostingDeclarationDeletion `xml:"stergere,omitempty"` + Confirmation *PostingDeclarationConfirmation `xml:"confirmare,omitempty"` + VehicleChange *PostingDeclarationVehicleChange `xml:"modifVehicul,omitempty"` + + // Name of node. + XMLName xml.Name `xml:"eTransport"` + // xmlns attr. Will be automatically set in MarshalXML + Namespace string `xml:"xmlns,attr"` + // xmlns:xsi attr. Will be automatically set in MarshalXML + NamespaceXSI string `xml:"xmlns:xsi,attr"` + // generated with... comment + Comment string `xml:",comment"` + } + + eTransport.postingDeclaration = postingDeclaration(pd) + eTransport.Namespace = "mfp:anaf:dgti:eTransport:declaratie:v2" + eTransport.NamespaceXSI = "http://www.w3.org/2001/XMLSchema-instance" + switch pd.declarationType { + case postingDeclarationTypeNotification: + notification, _ := pd.declarationData.(PostingDeclarationNotification) + eTransport.Notification = ¬ification + + case postingDeclarationTypeDeletion: + deletion, _ := pd.declarationData.(PostingDeclarationDeletion) + eTransport.Deletion = &deletion + + case postingDeclarationTypeConfirmation: + confirmation, _ := pd.declarationData.(PostingDeclarationConfirmation) + eTransport.Confirmation = &confirmation + + case postingDeclarationTypeVehicleChange: + confirmation, _ := pd.declarationData.(PostingDeclarationVehicleChange) + eTransport.VehicleChange = &confirmation + + default: + return errors.New("payload not set for posting declaration") + } + start.Name.Local = "eTransport" - return e.EncodeElement(eTransport(pd), start) + return e.EncodeElement(eTransport, start) } // XML returns the XML encoding of the PostingDeclaration @@ -69,7 +141,7 @@ func (pd PostingDeclaration) XMLIndent(prefix, indent string) ([]byte, error) { return ixml.MarshalIndentXMLWithHeader(pd, prefix, indent) } -type UitType string +type UITType string type PostingDeclarationNotification struct { OpType OpType `xml:"codTipOperatiune,attr"` @@ -89,11 +161,11 @@ type PostingDeclarationNotification struct { // Cardinality: 1..n TransportDocuments []PostingDeclarationTransportDocument `xml:"documenteTransport"` // Cardinality: 0..n - PrevNotifications []string `xml:"notificareAnterioara,omitempty"` + PrevNotifications []PostingDeclarationNotificationPrevNotification `xml:"notificareAnterioara,omitempty"` } type PostingDeclarationNotificationCorrection struct { - Uit UitType `xml:"uit,attr"` + UIT UITType `xml:"uit,attr"` } type PostingDeclarationNotificationTransportedGood struct { @@ -116,13 +188,12 @@ type PostingDeclarationNotificationCommercialPartner struct { type PostingDeclarationNotificationTransportData struct { LicensePlate string `xml:"nrVehicul,attr"` - TrailerLicensePlate1 string `xml:"nrRemorca1,attr,omitempty"` - TrailerLicensePlate2 string `xml:"nrRemorca2,attr,omitempty"` + Trailer1LicensePlate string `xml:"nrRemorca1,attr,omitempty"` + Trailer2LicensePlate string `xml:"nrRemorca2,attr,omitempty"` TransportOrgCountryCode CountryCodeType `xml:"codTaraOrgTransport,attr"` TransportOrgCode string `xml:"codOrgTransport,attr,omitempty"` TransportOrgName string `xml:"denumireOrgTransport,attr"` - // TODO: type - TransportDate string `xml:"dataTransport,attr"` + TransportDate string `xml:"dataTransport,attr"` } type PostingDeclationPlace struct { @@ -151,11 +222,27 @@ type PostingDeclarationTransportDocument struct { Remarks string `xml:"observatii,attr,omitempty"` } +type PostingDeclarationNotificationPrevNotification struct { + UIT UITType `xml:"uit,attr"` + Remarks string `xml:"observatii,attr,omitempty"` + DeclarantRef string `xml:"refDeclarant,attr,omitempty"` +} + type PostingDeclarationDeletion struct { + UIT UITType `xml:"uit,attr"` } type PostingDeclarationConfirmation struct { + UIT UITType `xml:"uit,attr"` + ConfirmationType ConfirmationType `xml:"tipConfirmare,attr"` + Remarks string `xml:"observatii,attr,omitempty"` } type PostingDeclarationVehicleChange struct { + UIT UITType `xml:"uit,attr"` + LicensePlate string `xml:"nrVehicul,attr"` + Trailer1LicensePlate string `xml:"nrRemorca1,attr,omitempty"` + Trailer2LicensePlate string `xml:"nrRemorca2,attr,omitempty"` + ChangeData string `xml:"dataModificare,attr"` + Remarks string `xml:"observatii,attr,omitempty"` } diff --git a/etransport/rest.go b/etransport/rest.go index 57e4e59..2ce6f56 100644 --- a/etransport/rest.go +++ b/etransport/rest.go @@ -35,29 +35,37 @@ const ( ) type Message struct { - Uit string `json:"uit"` - Cod_decl string `json:"cod_decl"` - Ref_decl string `json:"ref_decl"` - Sursa string `json:"sursa"` - Id_incarcare string `json:"id_incarcare"` - Data_creare string `json:"data_creare"` - Tip_op string `json:"tip_op"` - Data_transp string `json:"data_transp"` - Pc_tara string `json:"pc_tara"` - Pc_cod string `json:"pc_cod"` - Pc_den string `json:"pc_den"` - Tr_tara string `json:"tr_tara"` - Tr_cod string `json:"tr_cod"` - Tr_den string `json:"tr_den"` - Nr_veh string `json:"nr_veh"` - Nr_rem1 string `json:"nr_rem1"` - Nr_rem2 string `json:"nr_rem2"` - Mesaje []MessageError `json:"mesaje,omitempty"` + UIT UITType `json:"uit"` + DeclarantCode string `json:"cod_decl"` + DeclarantRef string `json:"ref_decl"` + Source string `json:"sursa"` + UploadID string `json:"id_incarcare"` + CreatedDate string `json:"data_creare"` + OpType string `json:"tip_op"` + TransportDate string `json:"data_transp"` + CommercialPartnerCountryCode CountryCodeType `json:"pc_tara,omitempty"` + CommercialPartnerCode string `json:"pc_cod,omitempty"` + CommercialPartnerName string `json:"pc_den,omitempty"` + TransportOrgCountryCode CountryCodeType `json:"tr_tara,omitempty"` + TransportOrgCode string `json:"tr_cod,omitempty"` + TransportOrgName string `json:"tr_den,omitempty"` + LicensePlace string `json:"nr_veh,omitempty"` + Trailer1LicensePlate string `json:"nr_rem1,omitempty"` + Trailer2LicensePlate string `json:"nr_rem2,omitempty"` + Messages []MessageError `json:"mesaje,omitempty"` } +type MessageErrorType string + +const ( + MessageErrorTypeErr MessageErrorType = "ERR" + MessageErrorTypeWarn MessageErrorType = "WARN" + MessageErrorTypeInfo MessageErrorType = "INFO" +) + type MessageError struct { - Type string `json:"tip"` - Message string `json:"mesaj"` + Type MessageErrorType `json:"tip"` + Message string `json:"mesaj"` } // MessagesListResponse is the parsed response from the list messages endpoint. From bca0494f0c877765b12970b9a886ede975b94023 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sun, 4 Aug 2024 19:14:22 +0300 Subject: [PATCH 03/22] Rename PostingDeclaration to PostingDeclarationV2 and add some const values for codes --- etransport/codes.go | 140 +++++++++++++++++++++++++------------- etransport/declaration.go | 61 +++++++++-------- etransport/rest.go | 66 ++++++++++-------- 3 files changed, 162 insertions(+), 105 deletions(-) diff --git a/etransport/codes.go b/etransport/codes.go index fe9ea8a..5858dd1 100644 --- a/etransport/codes.go +++ b/etransport/codes.go @@ -769,49 +769,90 @@ const ( type CountyCodeType string const ( -// Valori posibile pentru câmpul codJudetType: -// "1" Alba -// "2" Arad -// "3" Argeş -// "4" Bacău -// "5" Bihor -// "6" Bistriţa-Năsăud -// "7" Botoşani -// "8" Braşov -// "9" Brăila -// "10" Buzău -// "11" Caraş-Severin -// "51" Călăraşi -// "12" Cluj -// "13" Constanţa -// "14" Covasna -// "16" Dolj -// "15" Dâmboviţa -// "17" Galaţi -// "52" Giurgiu -// "18" Gorj -// "19" Harghita -// "20" Hunedoara -// "21" Ialomiţa -// "22" Iaşi -// "23" Ilfov -// "24" Maramureş -// "25" Mehedinţi -// "40" Municipiul Bucureşti -// "26" Mureş -// "27" Neamţ -// "28" Olt -// "29" Prahova -// "30" Satu Mare -// "32" Sibiu -// "33" Suceava -// "31" Sălaj -// "34" Teleorman -// "35" Timiş -// "36" Tulcea -// "37" Vaslui -// "39" Vrancea -// "38" Vâlcea + // "40" Municipiul Bucureşti + CountyCodeB CountyCodeType = "40" + // "1" Alba + CountryCodeAB CountyCodeType = "1" + // "2" Arad + CountyCodeAR CountyCodeType = "2" + // "3" Argeş + CountyCodeAG CountyCodeType = "3" + // "4" Bacău + CountyCodeBC CountyCodeType = "4" + // "5" Bihor + CountyCodeBH CountyCodeType = "5" + // "6" Bistriţa-Năsăud + CountyCodeBN CountyCodeType = "6" + // "7" Botoşani + CountyCodeBT CountyCodeType = "7" + // "8" Braşov + CountyCodeBV CountyCodeType = "8" + // "9" Brăila + CountyCodeBR CountyCodeType = "9" + // "10" Buzău + CountyCodeBZ CountyCodeType = "10" + // "11" Caraş-Severin + CountyCodeCS CountyCodeType = "11" + // "51" Călăraşi + CountyCodeCL CountyCodeType = "51" + // "12" Cluj + CountyCodeCJ CountyCodeType = "12" + // "13" Constanţa + CountyCodeCT CountyCodeType = "13" + // "14" Covasna + CountyCodeCV CountyCodeType = "14" + // "15" Dâmboviţa + CountyCodeDB CountyCodeType = "15" + // "16" Dolj + CountyCodeDJ CountyCodeType = "16" + // "17" Galaţi + CountyCodeGL CountyCodeType = "17" + // "52" Giurgiu + CountyCodeGR CountyCodeType = "52" + // "18" Gorj + CountyCodeGJ CountyCodeType = "18" + // "19" Harghita + CountyCodeHR CountyCodeType = "19" + // "20" Hunedoara + CountyCodeHD CountyCodeType = "20" + // "21" Ialomiţa + CountyCodeIL CountyCodeType = "21" + // "22" Iaşi + CountyCodeIS CountyCodeType = "22" + // "23" Ilfov + CountyCodeIF CountyCodeType = "23" + // "24" Maramureş + CountyCodeMM CountyCodeType = "24" + // "25" Mehedinţi + CountyCodeMH CountyCodeType = "25" + // "26" Mureş + CountyCodeMS CountyCodeType = "26" + // "27" Neamţ + CountyCodeNT CountyCodeType = "27" + // "28" Olt + CountyCodeOT CountyCodeType = "28" + // "29" Prahova + CountyCodePH CountyCodeType = "29" + // "30" Satu Mare + CountyCodeSM CountyCodeType = "30" + // "31" Sălaj + CountyCodeSJ CountyCodeType = "31" + // "32" Sibiu + CountyCodeSB CountyCodeType = "32" + // "33" Suceava + CountyCodeSV CountyCodeType = "33" + // "34" Teleorman + CountyCodeTR CountyCodeType = "34" + // "35" Timiş + CountyCodeTM CountyCodeType = "35" + // "36" Tulcea + CountyCodeTL CountyCodeType = "36" + // "37" Vaslui + CountyCodeVS CountyCodeType = "37" + // "38" Vâlcea + CountyCodeVL CountyCodeType = "38" + // "39" Vrancea + CountyCodeVN CountyCodeType = "39" ) type OpPurposeCodeType string @@ -868,11 +909,14 @@ type UnitMeasureCodeType string type DocumentType string const ( -// Valori posibile pentru câmpul tipDocument: -// "10" CMR -// "20" Factura -// "30" Aviz de însoțire a mărfii -// "9999" Altele + // "10" CMR + DocumentTypeCMR DocumentType = "10" + // "20" Factura + DocumentTypeInvoice DocumentType = "20" + // "30" Aviz de însoțire a mărfii + DocumentTypeDeliveryNote DocumentType = "30" + // "9999" Altele + DocumentTypeOther DocumentType = "9999" ) type ConfirmationType string diff --git a/etransport/declaration.go b/etransport/declaration.go index d5c2f6b..2023a7a 100644 --- a/etransport/declaration.go +++ b/etransport/declaration.go @@ -23,9 +23,14 @@ import ( "github.com/printesoi/xml-go" ) -// PostingDeclaration is the object that represents an e-transport posting +const ( + nsETransportDeclV2 = "mfp:anaf:dgti:eTransport:declaratie:v2" + nsXSI = "http://www.w3.org/2001/XMLSchema-instance" +) + +// PostingDeclarationV2 is the object that represents an e-transport posting // declaration payload for the upload v2 endpoint. -type PostingDeclaration struct { +type PostingDeclarationV2 struct { // CUI/CIF/CNP DeclarantCode string `xml:"codDeclarant,attr"` // Reference for the declarant @@ -35,8 +40,8 @@ type PostingDeclaration struct { // empty. DeclPostIncident DeclPostIncidentType `xml:"declPostAvarie,attr,omitempty"` - declarationType postingDeclarationType `xml:"-"` - declarationData any `xml:"-"` + declarationType postingDeclarationType + declarationPayload any } type postingDeclarationType int @@ -50,39 +55,39 @@ const ( ) // SetNotification set the given PostingDeclarationNotification as the -// PostingDeclaration payload. -func (pd *PostingDeclaration) SetNotification(notification PostingDeclarationNotification) *PostingDeclaration { +// PostingDeclarationV2 payload. +func (pd *PostingDeclarationV2) SetNotification(notification PostingDeclarationNotification) *PostingDeclarationV2 { pd.declarationType = postingDeclarationTypeNotification - pd.declarationData = notification + pd.declarationPayload = notification return pd } // SetDeletion set the given PostingDeclarationDeletion as the -// PostingDeclaration payload. -func (pd *PostingDeclaration) SetDeletion(deletion PostingDeclarationDeletion) *PostingDeclaration { +// PostingDeclarationV2 payload. +func (pd *PostingDeclarationV2) SetDeletion(deletion PostingDeclarationDeletion) *PostingDeclarationV2 { pd.declarationType = postingDeclarationTypeDeletion - pd.declarationData = deletion + pd.declarationPayload = deletion return pd } // SetConfirmation set the given PostingDeclarationConfirmation as the -// PostingDeclaration payload. -func (pd *PostingDeclaration) SetConfirmation(confirmation PostingDeclarationConfirmation) *PostingDeclaration { +// PostingDeclarationV2 payload. +func (pd *PostingDeclarationV2) SetConfirmation(confirmation PostingDeclarationConfirmation) *PostingDeclarationV2 { pd.declarationType = postingDeclarationTypeConfirmation - pd.declarationData = confirmation + pd.declarationPayload = confirmation return pd } // SetVehicleChange set the given PostingDeclarationVehicleChange as the -// PostingDeclaration payload. -func (pd *PostingDeclaration) SetVehicleChange(vehicleChange PostingDeclarationVehicleChange) *PostingDeclaration { +// PostingDeclarationV2 payload. +func (pd *PostingDeclarationV2) SetVehicleChange(vehicleChange PostingDeclarationVehicleChange) *PostingDeclarationV2 { pd.declarationType = postingDeclarationTypeVehicleChange - pd.declarationData = vehicleChange + pd.declarationPayload = vehicleChange return pd } -func (pd PostingDeclaration) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - type postingDeclaration PostingDeclaration +func (pd PostingDeclarationV2) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + type postingDeclaration PostingDeclarationV2 var eTransport struct { postingDeclaration @@ -102,23 +107,23 @@ func (pd PostingDeclaration) MarshalXML(e *xml.Encoder, start xml.StartElement) } eTransport.postingDeclaration = postingDeclaration(pd) - eTransport.Namespace = "mfp:anaf:dgti:eTransport:declaratie:v2" - eTransport.NamespaceXSI = "http://www.w3.org/2001/XMLSchema-instance" + eTransport.Namespace = nsETransportDeclV2 + eTransport.NamespaceXSI = nsXSI switch pd.declarationType { case postingDeclarationTypeNotification: - notification, _ := pd.declarationData.(PostingDeclarationNotification) + notification, _ := pd.declarationPayload.(PostingDeclarationNotification) eTransport.Notification = ¬ification case postingDeclarationTypeDeletion: - deletion, _ := pd.declarationData.(PostingDeclarationDeletion) + deletion, _ := pd.declarationPayload.(PostingDeclarationDeletion) eTransport.Deletion = &deletion case postingDeclarationTypeConfirmation: - confirmation, _ := pd.declarationData.(PostingDeclarationConfirmation) + confirmation, _ := pd.declarationPayload.(PostingDeclarationConfirmation) eTransport.Confirmation = &confirmation case postingDeclarationTypeVehicleChange: - confirmation, _ := pd.declarationData.(PostingDeclarationVehicleChange) + confirmation, _ := pd.declarationPayload.(PostingDeclarationVehicleChange) eTransport.VehicleChange = &confirmation default: @@ -129,15 +134,15 @@ func (pd PostingDeclaration) MarshalXML(e *xml.Encoder, start xml.StartElement) return e.EncodeElement(eTransport, start) } -// XML returns the XML encoding of the PostingDeclaration -func (pd PostingDeclaration) XML() ([]byte, error) { +// XML returns the XML encoding of the PostingDeclarationV2 +func (pd PostingDeclarationV2) XML() ([]byte, error) { return ixml.MarshalXMLWithHeader(pd) } // XMLIndent works like XML, but each XML element begins on a new // indented line that starts with prefix and is followed by one or more // copies of indent according to the nesting depth. -func (pd PostingDeclaration) XMLIndent(prefix, indent string) ([]byte, error) { +func (pd PostingDeclarationV2) XMLIndent(prefix, indent string) ([]byte, error) { return ixml.MarshalIndentXMLWithHeader(pd, prefix, indent) } @@ -243,6 +248,6 @@ type PostingDeclarationVehicleChange struct { LicensePlate string `xml:"nrVehicul,attr"` Trailer1LicensePlate string `xml:"nrRemorca1,attr,omitempty"` Trailer2LicensePlate string `xml:"nrRemorca2,attr,omitempty"` - ChangeData string `xml:"dataModificare,attr"` + ChangeDate string `xml:"dataModificare,attr"` Remarks string `xml:"observatii,attr,omitempty"` } diff --git a/etransport/rest.go b/etransport/rest.go index 2ce6f56..5a285d5 100644 --- a/etransport/rest.go +++ b/etransport/rest.go @@ -68,6 +68,18 @@ type MessageError struct { Message string `json:"mesaj"` } +func (me MessageError) IsErr() bool { + return me.Type == MessageErrorTypeErr +} + +func (me MessageError) IsWarn() bool { + return me.Type == MessageErrorTypeWarn +} + +func (me MessageError) IsInfo() bool { + return me.Type == MessageErrorTypeInfo +} + // MessagesListResponse is the parsed response from the list messages endpoint. type MessagesListResponse struct { Errors []struct { @@ -117,19 +129,17 @@ func (c *Client) GetMessagesList( return } -type ( - GetMessageStateCode string +type GetMessageStateResponse struct { + Errors []struct { + ErrorMessage string `json:"errorMessage"` + } `json:"errors,omitempty"` + State GetMessageStateCode `json:"stare"` + DateResponse string `json:"dateResponse"` + ExecutionStatus int32 `json:"ExecutionStatus"` + TraceID string `json:"trace_id"` +} - GetMessageStateResponse struct { - Errors []struct { - ErrorMessage string `json:"errorMessage"` - } `json:"errors,omitempty"` - State GetMessageStateCode `json:"stare"` - DateResponse string `json:"dateResponse"` - ExecutionStatus int32 `json:"ExecutionStatus"` - TraceID string `json:"trace_id"` - } -) +type GetMessageStateCode string const ( GetMessageStateCodeOk GetMessageStateCode = "ok" @@ -185,22 +195,18 @@ func (c *Client) GetMessageState( return } -type ( - uploadStandard string - - UploadV2Response struct { - DateResponse string `json:"dateResponse"` - ExecutionStatus int32 `json:"ExecutionStatus"` - UploadIndex int64 `json:"index_incarcare"` - UIT string `json:"UIT"` - TraceID string `json:"trace_id"` - DeclarantRef string `json:"ref_declarant"` - Attention string `json:"atentie,omitempty"` - Errors []struct { - ErrorMessage string `json:"errorMessage"` - } `json:"errors,omitempty"` - } -) +type UploadV2Response struct { + DateResponse string `json:"dateResponse"` + ExecutionStatus int32 `json:"ExecutionStatus"` + UploadIndex int64 `json:"index_incarcare"` + UIT string `json:"UIT"` + TraceID string `json:"trace_id"` + DeclarantRef string `json:"ref_declarant"` + Attention string `json:"atentie,omitempty"` + Errors []struct { + ErrorMessage string `json:"errorMessage"` + } `json:"errors,omitempty"` +} // IsOk returns true if the response corresponding to fetching messages list // was successful. @@ -208,6 +214,8 @@ func (r *UploadV2Response) IsOk() bool { return r != nil && r.ExecutionStatus == 0 } +type uploadStandard string + const ( uploadStandardETransp uploadStandard = "ETRANSP" ) @@ -237,7 +245,7 @@ func (c *Client) UploadV2XML( } func (c *Client) UploadPostingDeclarationV2( - ctx context.Context, decl PostingDeclaration, cif string, + ctx context.Context, decl PostingDeclarationV2, cif string, ) (response *UploadV2Response, err error) { xmlReader, err := ixml.MarshalXMLToReader(decl) if err != nil { From bab268dbee9f82158e61e427832dc2fa5eace787 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sun, 4 Aug 2024 20:22:49 +0300 Subject: [PATCH 04/22] Add some info in the README about e-transport --- README.md | 128 +++++++++++++++++++++++++++++++++++---------- etransport/rest.go | 31 ++++++++--- 2 files changed, 125 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index c159f70..3411030 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # e-factura-go [![Tests](https://github.com/printesoi/e-factura-go/actions/workflows/tests.yml/badge.svg)](https://github.com/printesoi/e-factura-go/actions/workflows/test.yml) [![Coverage Status](https://coveralls.io/repos/github/printesoi/e-factura-go/badge.svg)](https://coveralls.io/github/printesoi/e-factura-go) [![Go Report Card](https://goreportcard.com/badge/github.com/printesoi/e-factura-go)](https://goreportcard.com/report/github.com/printesoi/e-factura-go) -Package efactura provides a client for using the ANAF e-factura API. +Package e-factura-go provides a client for using the e-factura and e-transport APIs. ## NOTICE ## @@ -17,35 +17,15 @@ go get github.com/printesoi/e-factura-go will resolve and add the package to the current development module, along with its dependencies. -Alternatively the same can be achieved if you use import in a package: +## oauth2 ## -```go -import "github.com/printesoi/e-factura-go" -``` - -and run `go get` without parameters. - -Finally, to use the top-of-trunk version of this repo, use the following command: - -```bash -go get github.com/printesoi/e-factura-go@main -``` - -## Usage ## - -This package can be use both for interacting with (calling) the ANAF e-factura -API via the Client object and for generating an Invoice UBL XML. +Construct the required OAuth2 config needed for an e-factura or e-transport Client: ```go import ( - "github.com/printesoi/e-factura-go/efactura" efactura_oauth2 "github.com/printesoi/e-factura-go/oauth2" ) -``` - -Construct the required OAuth2 config needed for the Client: -```go oauth2Cfg, err := efactura_oauth2.MakeConfig( efactura_oauth2.ConfigCredentials(anafAppClientID, anafApplientSecret), efactura_oauth2.ConfigRedirectURL(anafAppRedirectURL), @@ -85,11 +65,20 @@ if err != nil { } ``` +## e-factura ## + +This package can be use both for interacting with (calling) the ANAF e-factura +API via the Client object and for generating an Invoice UBL XML. + Construct a new simple client for production environment: ```go +import ( + "github.com/printesoi/e-factura-go/efactura" +) + client, err := efactura.NewProductionClient( - context.Background(), + context.Background(), efactura_oauth2.TokenSource(token), ) if err != nil { @@ -101,7 +90,7 @@ Construct a new simple client for sandbox (test) environment: ```go client, err := efactura.NewSandboxClient( - context.Background(), + context.Background(), efactura_oauth2.TokenSource(token), ) if err != nil { @@ -144,7 +133,7 @@ to load the Go embedded copy of the timezone database. ### Upload invoice ### ```go -var invoice Invoice +var invoice efactura.Invoice // Build invoice (manually, or with the InvoiceBuilder) uploadRes, err := client.UploadInvoice(ctx, invoice, "123456789") @@ -267,7 +256,7 @@ limit: ```go import ( - efactura_errors "github.com/printesoi/e-factura-go/errors" + efactura_errors "github.com/printesoi/e-factura-go/errors" ) resp, err := client.GetMessageState(ctx, uploadIndex) @@ -330,6 +319,91 @@ if err := efactura.UnmarshalInvoice(data, &invoice); err != nil { **NOTE** Only use efactura.UnmarshalInvoice, because `encoding/xml` package cannot unmarshal a struct like efactura.Invoice due to namespace prefixes! +## e-Transport ## + +The `etransport` package can be used for interacting with (calling) the +e-transport API via the Client object. + +Construct a new simple client for production environment: + +```go +import ( + "github.com/printesoi/e-factura-go/etransport" +) + +client, err := etransport.NewProductionClient( + context.Background(), + efactura_oauth2.TokenSource(token), +) +if err != nil { + // Handle error +} +``` + +Construct a new simple client for sandbox (test) environment: + +```go +client, err := etransport.NewSandboxClient( + context.Background(), + efactura_oauth2.TokenSource(token), +) +if err != nil { + // Handle error +} +``` + +### Upload declaration ### + +```go +var declaration etransport.PostingDeclarationV2 +// Build posting declaration + +uploadRes, err := client.UploadPostingDeclarationV2(ctx, declaration, "123456789") +if err != nil { + // Handle error +} +if uploadRes.IsOk() { + fmt.Printf("Upload index: %d\n", uploadRes.GetUploadIndex()) +} else { + // The upload was not successful, check uploadRes.Errors +} +``` + +### Get message state ### + +you can check the message state for an upload index resulted from an upload: + +```go +resp, err := client.GetMessageState(ctx, uploadIndex) +if err != nil { + // Handle error +} +switch { +case resp.IsOk(): + // Uploaded declaration was processed, we can now use the UIT. +case resp.IsNok(): + // Processing failed +case resp.IsProcessing(): + // The message/declaration is still processing +case resp.IsInvalidXML(): + // The uploaded XML is invalid +``` + +### Get messages list ### + +```go +numDays := 7 // Between 1 and 60 +resp, err := client.GetMessagesList(ctx, "123456789", numDays) +if err != nil { + // Handle error +} +if resp.IsOk() { + for _, message := range resp.Messages { + // Process message + } +} +``` + ## Tasks ## - [ ] e-factura CLI tool diff --git a/etransport/rest.go b/etransport/rest.go index 5a285d5..9e80e9f 100644 --- a/etransport/rest.go +++ b/etransport/rest.go @@ -196,13 +196,13 @@ func (c *Client) GetMessageState( } type UploadV2Response struct { - DateResponse string `json:"dateResponse"` - ExecutionStatus int32 `json:"ExecutionStatus"` - UploadIndex int64 `json:"index_incarcare"` - UIT string `json:"UIT"` - TraceID string `json:"trace_id"` - DeclarantRef string `json:"ref_declarant"` - Attention string `json:"atentie,omitempty"` + DateResponse string `json:"dateResponse"` + ExecutionStatus int32 `json:"ExecutionStatus"` + UploadIndex int64 `json:"index_incarcare"` + UIT UITType `json:"UIT"` + TraceID string `json:"trace_id"` + DeclarantRef string `json:"ref_declarant"` + Attention string `json:"atentie,omitempty"` Errors []struct { ErrorMessage string `json:"errorMessage"` } `json:"errors,omitempty"` @@ -214,6 +214,23 @@ func (r *UploadV2Response) IsOk() bool { return r != nil && r.ExecutionStatus == 0 } +// GetUploadIndex returns the upload index (should only be called when +// IsOk() == true). +func (r *UploadV2Response) GetUploadIndex() int64 { + if r == nil { + return 0 + } + return r.UploadIndex +} + +// GetUIT returns the UIT (should only be called when IsOk() == true). +func (r *UploadV2Response) GetUIT() UITType { + if r == nil { + return "" + } + return r.UIT +} + type uploadStandard string const ( From c9f456a98a385b52ece29865d4efa8cd4a8214c1 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sun, 4 Aug 2024 20:47:43 +0300 Subject: [PATCH 05/22] Make Decimal and Date to types package --- efactura/builders.go | 99 ++++++------- efactura/builders_test.go | 253 +++++++++++++++++---------------- efactura/invoice.go | 31 ++-- efactura/xml_types.go | 88 +----------- etransport/declaration.go | 11 +- types/date.go | 104 ++++++++++++++ {efactura => types}/decimal.go | 8 +- 7 files changed, 308 insertions(+), 286 deletions(-) create mode 100644 types/date.go rename {efactura => types}/decimal.go (98%) diff --git a/efactura/builders.go b/efactura/builders.go index 9b39300..31a9358 100644 --- a/efactura/builders.go +++ b/efactura/builders.go @@ -17,21 +17,22 @@ package efactura import ( ierrors "github.com/printesoi/e-factura-go/internal/errors" "github.com/printesoi/e-factura-go/internal/ptr" + "github.com/printesoi/e-factura-go/types" ) // InvoiceLineAllowanceChargeBuilder builds an InvoiceLineAllowanceCharge object type InvoiceLineAllowanceChargeBuilder struct { chargeIndicator bool currencyID CurrencyCodeType - amount Decimal - baseAmount *Decimal + amount types.Decimal + baseAmount *types.Decimal allowanceChargeReasonCode *string allowanceChargeReason *string } // NewInvoiceLineAllowanceChargeBuilder creates a new generic // InvoiceLineAllowanceChargeBuilder. -func NewInvoiceLineAllowanceChargeBuilder(chargeIndicator bool, currencyID CurrencyCodeType, amount Decimal) *InvoiceLineAllowanceChargeBuilder { +func NewInvoiceLineAllowanceChargeBuilder(chargeIndicator bool, currencyID CurrencyCodeType, amount types.Decimal) *InvoiceLineAllowanceChargeBuilder { b := new(InvoiceLineAllowanceChargeBuilder) return b.WithChargeIndicator(chargeIndicator). WithCurrencyID(currencyID).WithAmount(amount) @@ -40,14 +41,14 @@ func NewInvoiceLineAllowanceChargeBuilder(chargeIndicator bool, currencyID Curre // NewInvoiceLineAllowanceBuilder creates a new InvoiceLineAllowanceChargeBuilder // builder that will build InvoiceLineAllowanceCharge object correspoding to an // allowance (ChargeIndicator = false) -func NewInvoiceLineAllowanceBuilder(currencyID CurrencyCodeType, amount Decimal) *InvoiceLineAllowanceChargeBuilder { +func NewInvoiceLineAllowanceBuilder(currencyID CurrencyCodeType, amount types.Decimal) *InvoiceLineAllowanceChargeBuilder { return NewInvoiceLineAllowanceChargeBuilder(false, currencyID, amount) } // NewInvoiceLineChargeBuilder creates a new InvoiceLineAllowanceChargeBuilder // builder that will build InvoiceLineAllowanceCharge object correspoding to a // charge (ChargeIndicator = true) -func NewInvoiceLineChargeBuilder(currencyID CurrencyCodeType, amount Decimal) *InvoiceLineAllowanceChargeBuilder { +func NewInvoiceLineChargeBuilder(currencyID CurrencyCodeType, amount types.Decimal) *InvoiceLineAllowanceChargeBuilder { return NewInvoiceLineAllowanceChargeBuilder(true, currencyID, amount) } @@ -61,12 +62,12 @@ func (b *InvoiceLineAllowanceChargeBuilder) WithCurrencyID(currencyID CurrencyCo return b } -func (b *InvoiceLineAllowanceChargeBuilder) WithAmount(amount Decimal) *InvoiceLineAllowanceChargeBuilder { +func (b *InvoiceLineAllowanceChargeBuilder) WithAmount(amount types.Decimal) *InvoiceLineAllowanceChargeBuilder { b.amount = amount return b } -func (b *InvoiceLineAllowanceChargeBuilder) WithBaseAmount(amount Decimal) *InvoiceLineAllowanceChargeBuilder { +func (b *InvoiceLineAllowanceChargeBuilder) WithBaseAmount(amount types.Decimal) *InvoiceLineAllowanceChargeBuilder { b.baseAmount = amount.Ptr() return b } @@ -118,11 +119,11 @@ type InvoiceLineBuilder struct { note string currencyID CurrencyCodeType unitCode UnitCodeType - invoicedQuantity Decimal - baseQuantity *Decimal + invoicedQuantity types.Decimal + baseQuantity *types.Decimal - grossPriceAmount Decimal - priceDeduction Decimal + grossPriceAmount types.Decimal + priceDeduction types.Decimal invoicePeriod *InvoiceLinePeriod allowancesCharges []InvoiceLineAllowanceCharge @@ -161,22 +162,22 @@ func (b *InvoiceLineBuilder) WithUnitCode(unitCode UnitCodeType) *InvoiceLineBui return b } -func (b *InvoiceLineBuilder) WithInvoicedQuantity(quantity Decimal) *InvoiceLineBuilder { +func (b *InvoiceLineBuilder) WithInvoicedQuantity(quantity types.Decimal) *InvoiceLineBuilder { b.invoicedQuantity = quantity return b } -func (b *InvoiceLineBuilder) WithBaseQuantity(quantity Decimal) *InvoiceLineBuilder { +func (b *InvoiceLineBuilder) WithBaseQuantity(quantity types.Decimal) *InvoiceLineBuilder { b.baseQuantity = &quantity return b } -func (b *InvoiceLineBuilder) WithGrossPriceAmount(priceAmount Decimal) *InvoiceLineBuilder { +func (b *InvoiceLineBuilder) WithGrossPriceAmount(priceAmount types.Decimal) *InvoiceLineBuilder { b.grossPriceAmount = priceAmount return b } -func (b *InvoiceLineBuilder) WithPriceDeduction(deduction Decimal) *InvoiceLineBuilder { +func (b *InvoiceLineBuilder) WithPriceDeduction(deduction types.Decimal) *InvoiceLineBuilder { b.priceDeduction = deduction return b } @@ -261,7 +262,7 @@ func (b InvoiceLineBuilder) Build() (line InvoiceLine, err error) { Quantity: b.invoicedQuantity, UnitCode: b.unitCode, } - var netPriceAmount Decimal + var netPriceAmount types.Decimal if b.priceDeduction.IsZero() { netPriceAmount = b.grossPriceAmount } else { @@ -308,7 +309,7 @@ func (b InvoiceLineBuilder) Build() (line InvoiceLine, err error) { // Invoiced quantity * (Item net price / item price base quantity) // + Sum of invoice line charge amount // - Sum of invoice line allowance amount - baseQuantity := D(1) + baseQuantity := types.D(1) if b.baseQuantity != nil { baseQuantity = *b.baseQuantity } @@ -336,16 +337,16 @@ func (b InvoiceLineBuilder) Build() (line InvoiceLine, err error) { type InvoiceDocumentAllowanceChargeBuilder struct { chargeIndicator bool currencyID CurrencyCodeType - amount Decimal + amount types.Decimal taxCategory InvoiceTaxCategory - baseAmount *Decimal + baseAmount *types.Decimal allowanceChargeReasonCode *string allowanceChargeReason *string } // NewInvoiceDocumentAllowanceChargeBuilder creates a new generic // InvoiceDocumentAllowanceChargeBuilder. -func NewInvoiceDocumentAllowanceChargeBuilder(chargeIndicator bool, currencyID CurrencyCodeType, amount Decimal, taxCategory InvoiceTaxCategory) *InvoiceDocumentAllowanceChargeBuilder { +func NewInvoiceDocumentAllowanceChargeBuilder(chargeIndicator bool, currencyID CurrencyCodeType, amount types.Decimal, taxCategory InvoiceTaxCategory) *InvoiceDocumentAllowanceChargeBuilder { b := new(InvoiceDocumentAllowanceChargeBuilder) return b.WithChargeIndicator(chargeIndicator).WithCurrencyID(currencyID). WithAmount(amount).WithTaxCategory(taxCategory) @@ -354,14 +355,14 @@ func NewInvoiceDocumentAllowanceChargeBuilder(chargeIndicator bool, currencyID C // NewInvoiceDocumentAllowanceBuilder creates a new InvoiceDocumentAllowanceChargeBuilder // builder that will build InvoiceDocumentAllowanceCharge object correspoding to an // allowance (ChargeIndicator = false) -func NewInvoiceDocumentAllowanceBuilder(currencyID CurrencyCodeType, amount Decimal, taxCategory InvoiceTaxCategory) *InvoiceDocumentAllowanceChargeBuilder { +func NewInvoiceDocumentAllowanceBuilder(currencyID CurrencyCodeType, amount types.Decimal, taxCategory InvoiceTaxCategory) *InvoiceDocumentAllowanceChargeBuilder { return NewInvoiceDocumentAllowanceChargeBuilder(false, currencyID, amount, taxCategory) } // NewInvoiceDocumentChargeBuilder creates a new InvoiceDocumentAllowanceChargeBuilder // builder that will build InvoiceDocumentAllowanceCharge object correspoding to a // charge (ChargeIndicator = true) -func NewInvoiceDocumentChargeBuilder(currencyID CurrencyCodeType, amount Decimal, taxCategory InvoiceTaxCategory) *InvoiceDocumentAllowanceChargeBuilder { +func NewInvoiceDocumentChargeBuilder(currencyID CurrencyCodeType, amount types.Decimal, taxCategory InvoiceTaxCategory) *InvoiceDocumentAllowanceChargeBuilder { return NewInvoiceDocumentAllowanceChargeBuilder(true, currencyID, amount, taxCategory) } @@ -375,7 +376,7 @@ func (b *InvoiceDocumentAllowanceChargeBuilder) WithCurrencyID(currencyID Curren return b } -func (b *InvoiceDocumentAllowanceChargeBuilder) WithAmount(amount Decimal) *InvoiceDocumentAllowanceChargeBuilder { +func (b *InvoiceDocumentAllowanceChargeBuilder) WithAmount(amount types.Decimal) *InvoiceDocumentAllowanceChargeBuilder { b.amount = amount return b } @@ -385,7 +386,7 @@ func (b *InvoiceDocumentAllowanceChargeBuilder) WithTaxCategory(taxCategory Invo return b } -func (b *InvoiceDocumentAllowanceChargeBuilder) WithBaseAmount(amount Decimal) *InvoiceDocumentAllowanceChargeBuilder { +func (b *InvoiceDocumentAllowanceChargeBuilder) WithBaseAmount(amount types.Decimal) *InvoiceDocumentAllowanceChargeBuilder { b.baseAmount = amount.Ptr() return b } @@ -442,13 +443,13 @@ type taxExemptionReason struct { // InvoiceBuilder builds an Invoice object type InvoiceBuilder struct { id string - issueDate Date - dueDate *Date + issueDate types.Date + dueDate *types.Date invoiceType InvoiceTypeCodeType documentCurrencyID CurrencyCodeType taxCurrencyID CurrencyCodeType - taxCurrencyExchangeRate Decimal + taxCurrencyExchangeRate types.Decimal taxExeptionReasons map[TaxCategoryCodeType]taxExemptionReason @@ -467,7 +468,7 @@ type InvoiceBuilder struct { allowancesCharges []InvoiceDocumentAllowanceCharge invoiceLines []InvoiceLine - expectedTaxInclusiveAmount *Decimal + expectedTaxInclusiveAmount *types.Decimal } func NewInvoiceBuilder(id string) (b *InvoiceBuilder) { @@ -480,12 +481,12 @@ func (b *InvoiceBuilder) WithID(id string) *InvoiceBuilder { return b } -func (b *InvoiceBuilder) WithIssueDate(date Date) *InvoiceBuilder { +func (b *InvoiceBuilder) WithIssueDate(date types.Date) *InvoiceBuilder { b.issueDate = date return b } -func (b *InvoiceBuilder) WithDueDate(date Date) *InvoiceBuilder { +func (b *InvoiceBuilder) WithDueDate(date types.Date) *InvoiceBuilder { b.dueDate = &date return b } @@ -500,7 +501,7 @@ func (b *InvoiceBuilder) WithDocumentCurrencyCode(currencyID CurrencyCodeType) * return b } -func (b *InvoiceBuilder) WithDocumentToTaxCurrencyExchangeRate(rate Decimal) *InvoiceBuilder { +func (b *InvoiceBuilder) WithDocumentToTaxCurrencyExchangeRate(rate types.Decimal) *InvoiceBuilder { b.taxCurrencyExchangeRate = rate return b } @@ -610,7 +611,7 @@ func (b *InvoiceBuilder) AddTaxExemptionReason(taxCategoryCode TaxCategoryCodeTy // the tax inclusive amount generated is different than the given amount, the // BT-114 term will be set (Payable rounding amount) and Payable Amount // (BT-115) is adjusted with the difference. -func (b *InvoiceBuilder) WithExpectedTaxInclusiveAmount(amount Decimal) *InvoiceBuilder { +func (b *InvoiceBuilder) WithExpectedTaxInclusiveAmount(amount types.Decimal) *InvoiceBuilder { b.expectedTaxInclusiveAmount = amount.Ptr() return b } @@ -671,7 +672,7 @@ func (b InvoiceBuilder) Build() (retInvoice Invoice, err error) { // amountToTaxAmount converts an Amount assumed to be in the // DocumentCurrencyCode to an amount in TaxCurrencyCode - amountToTaxAmount := func(a Decimal) Decimal { + amountToTaxAmount := func(a types.Decimal) types.Decimal { if taxCurrencyID == invoice.DocumentCurrencyCode { return a } @@ -682,14 +683,14 @@ func (b InvoiceBuilder) Build() (retInvoice Invoice, err error) { invoice.InvoiceLines = b.invoiceLines var ( - lineExtensionAmount = Zero - allowanceTotalAmount = Zero - chargeTotalAmount = Zero - taxExclusiveAmount = Zero - taxInclusiveAmount = Zero - prepaidAmount = Zero - payableRoundingAmount = Zero - payableAmount = Zero + lineExtensionAmount = types.Zero + allowanceTotalAmount = types.Zero + chargeTotalAmount = types.Zero + taxExclusiveAmount = types.Zero + taxInclusiveAmount = types.Zero + prepaidAmount = types.Zero + payableRoundingAmount = types.Zero + payableAmount = types.Zero ) taxCategoryMap := make(taxCategoryMap) @@ -707,7 +708,7 @@ func (b InvoiceBuilder) Build() (retInvoice Invoice, err error) { } } for i, allowanceCharge := range invoice.AllowanceCharges { - var amount Decimal + var amount types.Decimal if allowanceCharge.ChargeIndicator { amount = allowanceCharge.Amount.Amount chargeTotalAmount = chargeTotalAmount.Add(allowanceCharge.Amount.Amount) @@ -721,7 +722,7 @@ func (b InvoiceBuilder) Build() (retInvoice Invoice, err error) { } } - taxTotal, taxTotalTaxCurrency := Zero, Zero + taxTotal, taxTotalTaxCurrency := types.Zero, types.Zero var taxSubtotals []InvoiceTaxSubtotal for _, taxCategorySummary := range taxCategoryMap.getSummaries() { @@ -854,17 +855,17 @@ func makeTaxCategoryKeyLine(category InvoiceLineTaxCategory) taxCategoryKey { type taxCategorySummary struct { category InvoiceTaxCategory - baseAmount Decimal + baseAmount types.Decimal } -func (s taxCategorySummary) getTaxAmount() Decimal { - return s.baseAmount.Mul(s.category.Percent.Value()).Div(D(100)).Round(2) +func (s taxCategorySummary) getTaxAmount() types.Decimal { + return s.baseAmount.Mul(s.category.Percent.Value()).Div(types.D(100)).Round(2) } // taxCategoryMap is not concurency-safe type taxCategoryMap map[taxCategoryKey]*taxCategorySummary -func (m *taxCategoryMap) add(k taxCategoryKey, category InvoiceTaxCategory, amount Decimal) bool { +func (m *taxCategoryMap) add(k taxCategoryKey, category InvoiceTaxCategory, amount types.Decimal) bool { if category.TaxScheme.ID == TaxSchemeIDVAT { percent := category.Percent.Value() if !category.ID.TaxRateExempted() { @@ -889,7 +890,7 @@ func (m *taxCategoryMap) add(k taxCategoryKey, category InvoiceTaxCategory, amou return true } -func (m *taxCategoryMap) addDocumentTaxCategory(category InvoiceTaxCategory, amount Decimal) bool { +func (m *taxCategoryMap) addDocumentTaxCategory(category InvoiceTaxCategory, amount types.Decimal) bool { if m == nil { return false } @@ -898,7 +899,7 @@ func (m *taxCategoryMap) addDocumentTaxCategory(category InvoiceTaxCategory, amo return m.add(k, category, amount) } -func (m *taxCategoryMap) addLineTaxCategory(category InvoiceLineTaxCategory, amount Decimal) bool { +func (m *taxCategoryMap) addLineTaxCategory(category InvoiceLineTaxCategory, amount types.Decimal) bool { if m == nil { return false } diff --git a/efactura/builders_test.go b/efactura/builders_test.go index 6e5535d..45f35db 100644 --- a/efactura/builders_test.go +++ b/efactura/builders_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/printesoi/e-factura-go/text" + "github.com/printesoi/e-factura-go/types" ) const ( @@ -126,10 +127,10 @@ func getInvoiceCustomerParty() InvoiceCustomerParty { func TestInvoiceLineBuilder(t *testing.T) { assert := assert.New(t) - a := func(d Decimal) string { + a := func(d types.Decimal) string { return d.StringFixed(2) } - d := func(d Decimal) string { + d := func(d types.Decimal) string { return d.String() } @@ -142,16 +143,16 @@ func TestInvoiceLineBuilder(t *testing.T) { ID string CurrencyID CurrencyCodeType UnitCode UnitCodeType - Quantity Decimal - BaseQuantity Decimal - GrossPrice Decimal - PriceDeduction Decimal - Allowances []Decimal - Charges []Decimal + Quantity types.Decimal + BaseQuantity types.Decimal + GrossPrice types.Decimal + PriceDeduction types.Decimal + Allowances []types.Decimal + Charges []types.Decimal ItemName string TaxCategory InvoiceLineTaxCategory - ExpectedLineAmount Decimal + ExpectedLineAmount types.Decimal } tests := []lineTest{ { @@ -159,131 +160,131 @@ func TestInvoiceLineBuilder(t *testing.T) { ID: "01.1", CurrencyID: CurrencyEUR, UnitCode: "XGR", - Quantity: D(5), - GrossPrice: D(12), + Quantity: types.D(5), + GrossPrice: types.D(12), ItemName: text.Transliterate("Sticle cu vin"), TaxCategory: InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(25), + Percent: types.D(25), }, - ExpectedLineAmount: D(60), + ExpectedLineAmount: types.D(60), }, { // A.1.4 Exemplul 1. Linia 2 ID: "01.2", CurrencyID: CurrencyEUR, UnitCode: "XBX", - Quantity: D(1), - GrossPrice: D(90), + Quantity: types.D(1), + GrossPrice: types.D(90), ItemName: text.Transliterate("Vin - cutie de 6"), TaxCategory: InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(25), + Percent: types.D(25), }, - ExpectedLineAmount: D(90), + ExpectedLineAmount: types.D(90), }, { // A.1.4 Exemplul 2 ID: "02", CurrencyID: CurrencyEUR, UnitCode: "C62", - Quantity: D(10_000), - BaseQuantity: D(1_000), - GrossPrice: D(4.5), + Quantity: types.D(10_000), + BaseQuantity: types.D(1_000), + GrossPrice: types.D(4.5), ItemName: text.Transliterate("Șurub"), TaxCategory: InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(25), + Percent: types.D(25), }, - ExpectedLineAmount: D(45), + ExpectedLineAmount: types.D(45), }, { // A.1.4 Exemplul 3 ID: "03", CurrencyID: CurrencyEUR, UnitCode: "58", - Quantity: D(1.3), - GrossPrice: D(10), - PriceDeduction: D(0.5), + Quantity: types.D(1.3), + GrossPrice: types.D(10), + PriceDeduction: types.D(0.5), ItemName: text.Transliterate("Pui"), TaxCategory: InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(12.5), + Percent: types.D(12.5), }, - ExpectedLineAmount: D(12.35), + ExpectedLineAmount: types.D(12.35), }, { // A.1.5 Exemplul 4 (Reduceri, deduceri şi taxe suplimentare). Line 1 ID: "04.1", CurrencyID: CurrencyEUR, UnitCode: "XBX", - Quantity: D(25), - GrossPrice: D(9.5), - PriceDeduction: D(1), + Quantity: types.D(25), + GrossPrice: types.D(9.5), + PriceDeduction: types.D(1), ItemName: text.Transliterate("Stilou"), - Charges: []Decimal{ - D(10), + Charges: []types.Decimal{ + types.D(10), }, TaxCategory: InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(25), + Percent: types.D(25), }, - ExpectedLineAmount: D(222.50), + ExpectedLineAmount: types.D(222.50), }, { // A.1.5 Exemplul 4 (Reduceri, deduceri şi taxe suplimentare). Line 2 ID: "04.2", CurrencyID: CurrencyEUR, UnitCode: "RM", - Quantity: D(15), - GrossPrice: D(4.5), + Quantity: types.D(15), + GrossPrice: types.D(4.5), ItemName: text.Transliterate("Hârtie"), - Allowances: []Decimal{ - D(3.38), + Allowances: []types.Decimal{ + types.D(3.38), }, TaxCategory: InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(25), + Percent: types.D(25), }, - ExpectedLineAmount: D(64.12), + ExpectedLineAmount: types.D(64.12), }, { // A.1.6 Exemplul 5 (Linie a facturii negativă). Linia 1 ID: "05.1", CurrencyID: CurrencyEUR, UnitCode: "XBX", - Quantity: D(25), - GrossPrice: D(9.5), - PriceDeduction: D(1), + Quantity: types.D(25), + GrossPrice: types.D(9.5), + PriceDeduction: types.D(1), ItemName: text.Transliterate("Stilou"), TaxCategory: InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(25), + Percent: types.D(25), }, - ExpectedLineAmount: D(212.50), + ExpectedLineAmount: types.D(212.50), }, { // A.1.6 Exemplul 5 (Linie a facturii negativă). Linia 2 ID: "05.2", CurrencyID: CurrencyEUR, UnitCode: "XBX", - Quantity: D(-10), - GrossPrice: D(9.5), - PriceDeduction: D(1), + Quantity: types.D(-10), + GrossPrice: types.D(9.5), + PriceDeduction: types.D(1), ItemName: text.Transliterate("Stilou"), TaxCategory: InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(25), + Percent: types.D(25), }, - ExpectedLineAmount: D(-85), + ExpectedLineAmount: types.D(-85), }, } for _, t := range tests { @@ -332,10 +333,10 @@ func TestInvoiceLineBuilder(t *testing.T) { func TestInvoiceBuilder(t *testing.T) { assert := assert.New(t) - a := func(d Decimal) string { + a := func(d types.Decimal) string { return d.StringFixed(2) } - d := func(d Decimal) string { + d := func(d types.Decimal) string { return d.String() } @@ -353,14 +354,14 @@ func TestInvoiceBuilder(t *testing.T) { standardTaxCategory := InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(25), + Percent: types.D(25), } line1, err := NewInvoiceLineBuilder("1", documentCurrencyID). WithUnitCode("XBX"). - WithInvoicedQuantity(D(25)). - WithGrossPriceAmount(D(9.5)). - WithPriceDeduction(D(1)). + WithInvoicedQuantity(types.D(25)). + WithGrossPriceAmount(types.D(9.5)). + WithPriceDeduction(types.D(1)). WithItemName("Stilouri"). WithItemTaxCategory(standardTaxCategory). Build() @@ -370,9 +371,9 @@ func TestInvoiceBuilder(t *testing.T) { line2, err := NewInvoiceLineBuilder("2", documentCurrencyID). WithUnitCode("XBX"). - WithInvoicedQuantity(D(-10)). - WithGrossPriceAmount(D(9.5)). - WithPriceDeduction(D(1)). + WithInvoicedQuantity(types.D(-10)). + WithGrossPriceAmount(types.D(9.5)). + WithPriceDeduction(types.D(1)). WithItemName("Stilouri"). WithItemTaxCategory(standardTaxCategory). Build() @@ -381,8 +382,8 @@ func TestInvoiceBuilder(t *testing.T) { } invoiceBuilder := NewInvoiceBuilder("test.example.05"). - WithIssueDate(MakeDate(2024, 3, 1)). - WithDueDate(MakeDate(2024, 3, 31)). + WithIssueDate(types.MakeDate(2024, 3, 1)). + WithDueDate(types.MakeDate(2024, 3, 31)). WithInvoiceTypeCode(InvoiceTypeCommercialInvoice). WithDocumentCurrencyCode(documentCurrencyID). WithSupplier(getInvoiceSupplierParty()). @@ -394,10 +395,10 @@ func TestInvoiceBuilder(t *testing.T) { // Invoice lines if assert.Equal(2, len(invoice.InvoiceLines), "should have correct number of lines") { line1 := invoice.InvoiceLines[0] - assert.Equal(a(D(212.5)), a(line1.LineExtensionAmount.Amount)) + assert.Equal(a(types.D(212.5)), a(line1.LineExtensionAmount.Amount)) line2 := invoice.InvoiceLines[1] - assert.Equal(a(D(-85)), a(line2.LineExtensionAmount.Amount)) + assert.Equal(a(types.D(-85)), a(line2.LineExtensionAmount.Amount)) } // VAT details (BG-23) @@ -405,26 +406,26 @@ func TestInvoiceBuilder(t *testing.T) { if assert.Equal(1, len(invoice.TaxTotal[0].TaxSubtotals)) { subtotal := invoice.TaxTotal[0].TaxSubtotals[0] assert.Equal(TaxCategoryVATStandardRate, subtotal.TaxCategory.ID) - assert.Equal(a(D(25)), a(subtotal.TaxCategory.Percent)) - assert.Equal(a(D(127.5)), a(subtotal.TaxableAmount.Amount)) - assert.Equal(a(D(31.88)), a(subtotal.TaxAmount.Amount)) + assert.Equal(a(types.D(25)), a(subtotal.TaxCategory.Percent)) + assert.Equal(a(types.D(127.5)), a(subtotal.TaxableAmount.Amount)) + assert.Equal(a(types.D(31.88)), a(subtotal.TaxAmount.Amount)) } // BT-110 if assert.NotNil(invoice.TaxTotal[0].TaxAmount, "BT-110 must exist") { - assert.Equal(a(D(31.88)), a(invoice.TaxTotal[0].TaxAmount.Amount), "BT-110 incorrect value") + assert.Equal(a(types.D(31.88)), a(invoice.TaxTotal[0].TaxAmount.Amount), "BT-110 incorrect value") } } // Document totals (BG-22) // BT-106 - assert.Equal(a(D(127.5)), a(invoice.LegalMonetaryTotal.LineExtensionAmount.Amount), "BT-106 incorrect value") + assert.Equal(a(types.D(127.5)), a(invoice.LegalMonetaryTotal.LineExtensionAmount.Amount), "BT-106 incorrect value") // BT-109 - assert.Equal(a(D(127.5)), a(invoice.LegalMonetaryTotal.TaxExclusiveAmount.Amount), "BT-109 incorrect value") + assert.Equal(a(types.D(127.5)), a(invoice.LegalMonetaryTotal.TaxExclusiveAmount.Amount), "BT-109 incorrect value") // BT-112 - assert.Equal(a(D(159.38)), a(invoice.LegalMonetaryTotal.TaxInclusiveAmount.Amount), "BT-112 incorrect value") + assert.Equal(a(types.D(159.38)), a(invoice.LegalMonetaryTotal.TaxInclusiveAmount.Amount), "BT-112 incorrect value") // BT-115 - assert.Equal(a(D(159.38)), a(invoice.LegalMonetaryTotal.PayableAmount.Amount), "BT-115 incorrect value") + assert.Equal(a(types.D(159.38)), a(invoice.LegalMonetaryTotal.PayableAmount.Amount), "BT-115 incorrect value") } } { @@ -434,13 +435,13 @@ func TestInvoiceBuilder(t *testing.T) { line1, err := NewInvoiceLineBuilder("1", documentCurrencyID). WithUnitCode("H87"). - WithInvoicedQuantity(D(5)). - WithGrossPriceAmount(D(25.0)). + WithInvoicedQuantity(types.D(5)). + WithGrossPriceAmount(types.D(25.0)). WithItemName(text.Transliterate("Cerneală pentru imprimantă")). WithItemTaxCategory(InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(25), + Percent: types.D(25), }). Build() if assert.NoError(err) { @@ -449,13 +450,13 @@ func TestInvoiceBuilder(t *testing.T) { line2, err := NewInvoiceLineBuilder("2", documentCurrencyID). WithUnitCode("H87"). - WithInvoicedQuantity(D(1)). - WithGrossPriceAmount(D(24.0)). + WithInvoicedQuantity(types.D(1)). + WithGrossPriceAmount(types.D(24.0)). WithItemName(text.Transliterate("Imprimare afiș")). WithItemTaxCategory(InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(10), + Percent: types.D(10), }). Build() if assert.NoError(err) { @@ -464,13 +465,13 @@ func TestInvoiceBuilder(t *testing.T) { line3, err := NewInvoiceLineBuilder("3", documentCurrencyID). WithUnitCode("H87"). - WithInvoicedQuantity(D(1)). - WithGrossPriceAmount(D(136.0)). + WithInvoicedQuantity(types.D(1)). + WithGrossPriceAmount(types.D(136.0)). WithItemName(text.Transliterate("Scaun de birou")). WithItemTaxCategory(InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(25), + Percent: types.D(25), }). Build() if assert.NoError(err) { @@ -479,8 +480,8 @@ func TestInvoiceBuilder(t *testing.T) { line4, err := NewInvoiceLineBuilder("4", documentCurrencyID). WithUnitCode("H87"). - WithInvoicedQuantity(D(1)). - WithGrossPriceAmount(D(95.0)). + WithInvoicedQuantity(types.D(1)). + WithGrossPriceAmount(types.D(95.0)). WithItemName(text.Transliterate("Tastatură fără fir")). WithItemTaxCategory(InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, @@ -493,8 +494,8 @@ func TestInvoiceBuilder(t *testing.T) { line5, err := NewInvoiceLineBuilder("5", documentCurrencyID). WithUnitCode("H87"). - WithInvoicedQuantity(D(1)). - WithGrossPriceAmount(D(53.0)). + WithInvoicedQuantity(types.D(1)). + WithGrossPriceAmount(types.D(53.0)). WithItemName(text.Transliterate("Cablu de adaptare")). WithItemTaxCategory(InvoiceLineTaxCategory{ TaxScheme: TaxSchemeVAT, @@ -506,8 +507,8 @@ func TestInvoiceBuilder(t *testing.T) { } invoiceBuilder := NewInvoiceBuilder("test.example.07"). - WithIssueDate(MakeDate(2024, 3, 1)). - WithDueDate(MakeDate(2024, 4, 1)). + WithIssueDate(types.MakeDate(2024, 3, 1)). + WithDueDate(types.MakeDate(2024, 4, 1)). WithInvoiceTypeCode(InvoiceTypeSelfBilledInvoice). WithDocumentCurrencyCode(documentCurrencyID). WithSupplier(getInvoiceSupplierParty()). @@ -517,11 +518,11 @@ func TestInvoiceBuilder(t *testing.T) { documentAllowance, err := NewInvoiceDocumentAllowanceBuilder( documentCurrencyID, - D(15), + types.D(15), InvoiceTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(25), + Percent: types.D(25), }, ).WithAllowanceChargeReason("Motivul C").Build() if assert.NoError(err) { @@ -530,11 +531,11 @@ func TestInvoiceBuilder(t *testing.T) { documentCharge, err := NewInvoiceDocumentChargeBuilder( documentCurrencyID, - D(35), + types.D(35), InvoiceTaxCategory{ TaxScheme: TaxSchemeVAT, ID: TaxCategoryVATStandardRate, - Percent: D(25), + Percent: types.D(25), }, ).WithAllowanceChargeReason("Motivul B").Build() if assert.NoError(err) { @@ -543,7 +544,7 @@ func TestInvoiceBuilder(t *testing.T) { if documentCurrencyID != CurrencyRON { invoiceBuilder.WithTaxCurrencyCode(CurrencyRON) - invoiceBuilder.WithDocumentToTaxCurrencyExchangeRate(D(4.9691)) + invoiceBuilder.WithDocumentToTaxCurrencyExchangeRate(types.D(4.9691)) } return invoiceBuilder.Build() } @@ -555,54 +556,54 @@ func TestInvoiceBuilder(t *testing.T) { // Invoice lines if assert.Equal(5, len(invoice.InvoiceLines)) { line1 := invoice.InvoiceLines[0] - assert.Equal(a(D(125.0)), a(line1.LineExtensionAmount.Amount)) + assert.Equal(a(types.D(125.0)), a(line1.LineExtensionAmount.Amount)) assert.Equal(TaxCategoryVATStandardRate, line1.Item.TaxCategory.ID) - assert.Equal(d(D(25.0)), d(line1.Item.TaxCategory.Percent)) + assert.Equal(d(types.D(25.0)), d(line1.Item.TaxCategory.Percent)) line2 := invoice.InvoiceLines[1] - assert.Equal(a(D(24.0)), a(line2.LineExtensionAmount.Amount)) + assert.Equal(a(types.D(24.0)), a(line2.LineExtensionAmount.Amount)) assert.Equal(TaxCategoryVATStandardRate, line2.Item.TaxCategory.ID) - assert.Equal(d(D(10.0)), d(line2.Item.TaxCategory.Percent)) + assert.Equal(d(types.D(10.0)), d(line2.Item.TaxCategory.Percent)) line3 := invoice.InvoiceLines[2] - assert.Equal(a(D(136.0)), a(line3.LineExtensionAmount.Amount)) + assert.Equal(a(types.D(136.0)), a(line3.LineExtensionAmount.Amount)) assert.Equal(TaxCategoryVATStandardRate, line3.Item.TaxCategory.ID) - assert.Equal(d(D(25.0)), d(line3.Item.TaxCategory.Percent)) + assert.Equal(d(types.D(25.0)), d(line3.Item.TaxCategory.Percent)) line4 := invoice.InvoiceLines[3] - assert.Equal(a(D(95.0)), a(line4.LineExtensionAmount.Amount)) + assert.Equal(a(types.D(95.0)), a(line4.LineExtensionAmount.Amount)) assert.Equal(TaxCategoryVATExempt, line4.Item.TaxCategory.ID) - assert.Equal(d(D(0.0)), d(line4.Item.TaxCategory.Percent)) + assert.Equal(d(types.D(0.0)), d(line4.Item.TaxCategory.Percent)) line5 := invoice.InvoiceLines[4] - assert.Equal(a(D(53.0)), a(line5.LineExtensionAmount.Amount)) + assert.Equal(a(types.D(53.0)), a(line5.LineExtensionAmount.Amount)) assert.Equal(TaxCategoryVATExempt, line5.Item.TaxCategory.ID) - assert.Equal(d(D(0.0)), d(line5.Item.TaxCategory.Percent)) + assert.Equal(d(types.D(0.0)), d(line5.Item.TaxCategory.Percent)) } // Document totals (BG-22) // BT-106 - assert.Equal(a(D(433.0)), a(invoice.LegalMonetaryTotal.LineExtensionAmount.Amount), "BT-106 incorrect value") + assert.Equal(a(types.D(433.0)), a(invoice.LegalMonetaryTotal.LineExtensionAmount.Amount), "BT-106 incorrect value") // BT-107 if allowanceTotalAmount := invoice.LegalMonetaryTotal.AllowanceTotalAmount; assert.NotNil(allowanceTotalAmount, "BT-107 must be non-nil") { - assert.Equal(a(D(15)), a(allowanceTotalAmount.Amount), "BT-107 incorrect value") + assert.Equal(a(types.D(15)), a(allowanceTotalAmount.Amount), "BT-107 incorrect value") } // BT-108 if chargeTotalAmount := invoice.LegalMonetaryTotal.ChargeTotalAmount; assert.NotNil(chargeTotalAmount, "BT-108 must be non-nil") { - assert.Equal(a(D(35)), a(chargeTotalAmount.Amount), "BT-108 incorrect value") + assert.Equal(a(types.D(35)), a(chargeTotalAmount.Amount), "BT-108 incorrect value") } // BT-109 - assert.Equal(a(D(453.0)), a(invoice.LegalMonetaryTotal.TaxExclusiveAmount.Amount), "BT-109 incorrect value") + assert.Equal(a(types.D(453.0)), a(invoice.LegalMonetaryTotal.TaxExclusiveAmount.Amount), "BT-109 incorrect value") // BT-110 if assert.Equal(1, len(invoice.TaxTotal), "Must have only one TaxTotal") && assert.NotNil(invoice.TaxTotal[0].TaxAmount, "BT-110 must exist") { - assert.Equal(a(D(72.65)), a(invoice.TaxTotal[0].TaxAmount.Amount), "BT-110 incorrect value") + assert.Equal(a(types.D(72.65)), a(invoice.TaxTotal[0].TaxAmount.Amount), "BT-110 incorrect value") } // BT-112 - assert.Equal(a(D(525.65)), a(invoice.LegalMonetaryTotal.TaxInclusiveAmount.Amount), "BT-112 incorrect value") + assert.Equal(a(types.D(525.65)), a(invoice.LegalMonetaryTotal.TaxInclusiveAmount.Amount), "BT-112 incorrect value") // BT-115 - assert.Equal(a(D(525.65)), a(invoice.LegalMonetaryTotal.PayableAmount.Amount), "BT-115 incorrect value") + assert.Equal(a(types.D(525.65)), a(invoice.LegalMonetaryTotal.PayableAmount.Amount), "BT-115 incorrect value") } } { @@ -613,62 +614,62 @@ func TestInvoiceBuilder(t *testing.T) { // Invoice lines if assert.Equal(5, len(invoice.InvoiceLines)) { line1 := invoice.InvoiceLines[0] - assert.Equal(a(D(125.0)), a(line1.LineExtensionAmount.Amount)) + assert.Equal(a(types.D(125.0)), a(line1.LineExtensionAmount.Amount)) assert.Equal(TaxCategoryVATStandardRate, line1.Item.TaxCategory.ID) - assert.Equal(d(D(25.0)), d(line1.Item.TaxCategory.Percent)) + assert.Equal(d(types.D(25.0)), d(line1.Item.TaxCategory.Percent)) line2 := invoice.InvoiceLines[1] - assert.Equal(a(D(24.0)), a(line2.LineExtensionAmount.Amount)) + assert.Equal(a(types.D(24.0)), a(line2.LineExtensionAmount.Amount)) assert.Equal(TaxCategoryVATStandardRate, line2.Item.TaxCategory.ID) - assert.Equal(d(D(10.0)), d(line2.Item.TaxCategory.Percent)) + assert.Equal(d(types.D(10.0)), d(line2.Item.TaxCategory.Percent)) line3 := invoice.InvoiceLines[2] - assert.Equal(a(D(136.0)), a(line3.LineExtensionAmount.Amount)) + assert.Equal(a(types.D(136.0)), a(line3.LineExtensionAmount.Amount)) assert.Equal(TaxCategoryVATStandardRate, line3.Item.TaxCategory.ID) - assert.Equal(d(D(25.0)), d(line3.Item.TaxCategory.Percent)) + assert.Equal(d(types.D(25.0)), d(line3.Item.TaxCategory.Percent)) line4 := invoice.InvoiceLines[3] - assert.Equal(a(D(95.0)), a(line4.LineExtensionAmount.Amount)) + assert.Equal(a(types.D(95.0)), a(line4.LineExtensionAmount.Amount)) assert.Equal(TaxCategoryVATExempt, line4.Item.TaxCategory.ID) - assert.Equal(d(D(0.0)), d(line4.Item.TaxCategory.Percent)) + assert.Equal(d(types.D(0.0)), d(line4.Item.TaxCategory.Percent)) line5 := invoice.InvoiceLines[4] - assert.Equal(a(D(53.0)), a(line5.LineExtensionAmount.Amount)) + assert.Equal(a(types.D(53.0)), a(line5.LineExtensionAmount.Amount)) assert.Equal(TaxCategoryVATExempt, line5.Item.TaxCategory.ID) - assert.Equal(d(D(0.0)), d(line5.Item.TaxCategory.Percent)) + assert.Equal(d(types.D(0.0)), d(line5.Item.TaxCategory.Percent)) } // Document totals (BG-22) // BT-106 - assert.Equal(a(D(433.0)), a(invoice.LegalMonetaryTotal.LineExtensionAmount.Amount), "BT-106 incorrect value") + assert.Equal(a(types.D(433.0)), a(invoice.LegalMonetaryTotal.LineExtensionAmount.Amount), "BT-106 incorrect value") // BT-107 if allowanceTotalAmount := invoice.LegalMonetaryTotal.AllowanceTotalAmount; assert.NotNil(allowanceTotalAmount, "BT-107 must be non-nil") { - assert.Equal(a(D(15)), a(allowanceTotalAmount.Amount), "BT-107 incorrect value") + assert.Equal(a(types.D(15)), a(allowanceTotalAmount.Amount), "BT-107 incorrect value") } // BT-108 if chargeTotalAmount := invoice.LegalMonetaryTotal.ChargeTotalAmount; assert.NotNil(chargeTotalAmount, "BT-108 must be non-nil") { - assert.Equal(a(D(35)), a(chargeTotalAmount.Amount), "BT-108 incorrect value") + assert.Equal(a(types.D(35)), a(chargeTotalAmount.Amount), "BT-108 incorrect value") } // BT-109 - assert.Equal(a(D(453.0)), a(invoice.LegalMonetaryTotal.TaxExclusiveAmount.Amount), "BT-109 incorrect value") + assert.Equal(a(types.D(453.0)), a(invoice.LegalMonetaryTotal.TaxExclusiveAmount.Amount), "BT-109 incorrect value") // BT-110 if assert.Equal(2, len(invoice.TaxTotal), "Must have a TaxTotal for each currency") { taxTotalID := findTaxTotalByCurrency(invoice.TaxTotal, documentCurrencyID) if assert.True(taxTotalID >= 0, "TaxTotal for document currency code(BT-5) not set") && assert.NotNil(invoice.TaxTotal[taxTotalID].TaxAmount, "BT-110 must be non-nil") { - assert.Equal(a(D(72.65)), a(invoice.TaxTotal[taxTotalID].TaxAmount.Amount), "BT-110 incorrect value") + assert.Equal(a(types.D(72.65)), a(invoice.TaxTotal[taxTotalID].TaxAmount.Amount), "BT-110 incorrect value") } taxTotalTaxCurrencyID := findTaxTotalByCurrency(invoice.TaxTotal, CurrencyRON) if assert.True(taxTotalTaxCurrencyID >= 0, "TaxTotal for tax currency code(BT-6) not set") && assert.NotNil(invoice.TaxTotal[taxTotalTaxCurrencyID].TaxAmount, "BT-111 must be non-nil") { - assert.Equal(a(D(361.01)), a(invoice.TaxTotal[taxTotalTaxCurrencyID].TaxAmount.Amount), "BT-110 incorrect value") + assert.Equal(a(types.D(361.01)), a(invoice.TaxTotal[taxTotalTaxCurrencyID].TaxAmount.Amount), "BT-110 incorrect value") } } // BT-112 - assert.Equal(a(D(525.65)), a(invoice.LegalMonetaryTotal.TaxInclusiveAmount.Amount), "BT-112 incorrect value") + assert.Equal(a(types.D(525.65)), a(invoice.LegalMonetaryTotal.TaxInclusiveAmount.Amount), "BT-112 incorrect value") // BT-115 - assert.Equal(a(D(525.65)), a(invoice.LegalMonetaryTotal.PayableAmount.Amount), "BT-115 incorrect value") + assert.Equal(a(types.D(525.65)), a(invoice.LegalMonetaryTotal.PayableAmount.Amount), "BT-115 incorrect value") } } } diff --git a/efactura/invoice.go b/efactura/invoice.go index 62dad90..6d5ff90 100644 --- a/efactura/invoice.go +++ b/efactura/invoice.go @@ -17,6 +17,7 @@ package efactura import ( "fmt" + "github.com/printesoi/e-factura-go/types" ixml "github.com/printesoi/e-factura-go/xml" "github.com/printesoi/xml-go" ) @@ -54,12 +55,12 @@ type Invoice struct { // Term: Data emiterii facturii // Description: Data la care a fost emisă factura. // Cardinality: 1..1 - IssueDate Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 IssueDate"` + IssueDate types.Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 IssueDate"` // ID: BT-9 // Term: Data scadenţei // Description: Data până la care trebuie făcută plata. // Cardinality: 0..1 - DueDate *Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 DueDate,omitempty"` + DueDate *types.Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 DueDate,omitempty"` // ID: BT-3 // Term: Codul tipului facturii // Description: Un cod care specifică tipul funcţional al facturii. @@ -248,7 +249,7 @@ type InvoiceDocumentReference struct { // Description: Data emiterii facturii anterioare trebuie furnizată în // cazul în care identificatorul facturii anterioare nu este unic. // Cardinality: 0..1 - IssueDate *Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 IssueDate,omitempty"` + IssueDate *types.Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 IssueDate,omitempty"` } type InvoiceSupplier struct { @@ -604,7 +605,7 @@ type InvoiceDelivery struct { // ID: BT-72 // Term: Data reală a livrării // Cardinality: 0..1 - ActualDeliveryDate *Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 ActualDeliveryDate,omitempty"` + ActualDeliveryDate *types.Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 ActualDeliveryDate,omitempty"` } type InvoiceDeliveryLocation struct { @@ -662,12 +663,12 @@ type InvoicePeriod struct { // Term: Data de început a perioadei de facturare // Description: Data la care începe perioada de facturare. // Cardinality: 0..1 - StartDate *Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 StartDate,omitempty"` + StartDate *types.Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 StartDate,omitempty"` // ID: BT-74 // Term: Data de sfârșit a perioadei de facturare // Description: Data la care sfârșește perioada de facturare. // Cardinality: 0..1 - EndDate *Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 EndDate,omitempty"` + EndDate *types.Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 EndDate,omitempty"` } type InvoicePaymentMeans struct { @@ -784,7 +785,7 @@ type InvoiceDocumentAllowanceCharge struct { // taxei suplimentare la nivelul documentului, pentru a calcula // valoarea taxei suplimentare la nivelul documentului. // Cardinality: 0..1 - Percent *Decimal `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 MultiplierFactorNumeric,omitempty"` + Percent *types.Decimal `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 MultiplierFactorNumeric,omitempty"` // Field: TaxCategory.ID // ID: BT-102 // Term: Codul categoriei de TVA pentru taxe suplimentare la nivelul @@ -855,7 +856,7 @@ type InvoiceTaxSubtotal struct { // InvoiceTaxCategory is a struct that encodes a cac:TaxCategory node. type InvoiceTaxCategory struct { ID TaxCategoryCodeType `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 ID"` - Percent Decimal `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 Percent"` + Percent types.Decimal `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 Percent"` TaxExemptionReason string `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 TaxExemptionReason,omitempty"` TaxExemptionReasonCode TaxExemptionReasonCodeType `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 TaxExemptionReasonCode,omitempty"` TaxScheme TaxScheme `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2 TaxScheme"` @@ -868,7 +869,7 @@ type InvoiceTaxCategory struct { func (c InvoiceTaxCategory) MarshalXML(e *xml.Encoder, start xml.StartElement) error { type invoiceTaxCategory struct { ID TaxCategoryCodeType `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 ID"` - Percent *Decimal `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 Percent,omitempty"` + Percent *types.Decimal `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 Percent,omitempty"` TaxExemptionReason string `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 TaxExemptionReason,omitempty"` TaxExemptionReasonCode TaxExemptionReasonCodeType `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 TaxExemptionReasonCode,omitempty"` TaxScheme TaxScheme `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2 TaxScheme"` @@ -970,7 +971,7 @@ type InvoiceLine struct { // InvoicedQuantity represents the quantity (of items) on an invoice line. type InvoicedQuantity struct { - Quantity Decimal `xml:",chardata"` + Quantity types.Decimal `xml:",chardata"` // The unit of the quantity. UnitCode UnitCodeType `xml:"unitCode,attr"` // The quantity unit code list. @@ -986,11 +987,11 @@ type InvoiceLinePeriod struct { // ID: BT-134 // Term: Data de început a perioadei de facturare a liniei facturii // Cardinality: 0..1 - StartDate *Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 StartDate,omitempty"` + StartDate *types.Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 StartDate,omitempty"` // ID: BT-135 // Term: Data de sfârșit a perioadei de facturare // Cardinality: 0..1 - EndDate *Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 EndDate,omitempty"` + EndDate *types.Date `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 EndDate,omitempty"` } // InvoiceLineAllowanceCharge is a struct that encodes the cbc:AllowanceCharge @@ -1121,8 +1122,8 @@ type InvoiceLineTaxCategory struct { // ID: BT-152 // Term: Cota TVA pentru articolul facturat // Cardinality: 0..1 - Percent Decimal `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 Percent"` - TaxScheme TaxScheme `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2 TaxScheme"` + Percent types.Decimal `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 Percent"` + TaxScheme TaxScheme `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2 TaxScheme"` } // MarshalXML implements the xml.Marshaler interface. We use a custom @@ -1132,7 +1133,7 @@ type InvoiceLineTaxCategory struct { func (c InvoiceLineTaxCategory) MarshalXML(e *xml.Encoder, start xml.StartElement) error { type invoiceLineTaxCategory struct { ID TaxCategoryCodeType `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 ID"` - Percent *Decimal `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 Percent,omitempty"` + Percent *types.Decimal `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 Percent,omitempty"` TaxScheme TaxScheme `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2 TaxScheme"` } xmlCat := invoiceLineTaxCategory{ diff --git a/efactura/xml_types.go b/efactura/xml_types.go index 8ef6c0f..e02e7ab 100644 --- a/efactura/xml_types.go +++ b/efactura/xml_types.go @@ -15,99 +15,15 @@ package efactura import ( - "time" - + "github.com/printesoi/e-factura-go/types" "github.com/printesoi/xml-go" - - itime "github.com/printesoi/e-factura-go/time" ) -// Date is a wrapper of the time.Time type which marshals to XML in the -// YYYY-MM-DD format and is assumed to be in the Romanian timezone location. -type Date struct { - time.Time -} - -// MakeDate creates a date with the provided year, month and day in the -// Romanian time zone location. -func MakeDate(year int, month time.Month, day int) Date { - return Date{itime.Date(year, month, day, 0, 0, 0, 0)} -} - -// NewDate same as MakeDate, but returns a pointer to Date. -func NewDate(year int, month time.Month, day int) *Date { - return MakeDate(year, month, day).Ptr() -} - -// MakeDateFromTime creates a Date in Romanian time zone location from the -// given time.Time. -func MakeDateFromTime(t time.Time) Date { - return MakeDate(itime.TimeInRomania(t).Date()) -} - -// NewDateFromTime same as MakeDateFromTime, but returns a pointer to Date. -func NewDateFromTime(t time.Time) *Date { - return MakeDate(itime.TimeInRomania(t).Date()).Ptr() -} - -// MakeDateFromString creates a Date in Romanian time zone from a string in the -// YYYY-MM-DD format. -func MakeDateFromString(str string) (Date, error) { - t, err := itime.ParseInRomania(time.DateOnly, str) - if err != nil { - return Date{}, err - } - return MakeDate(t.Date()), nil -} - -// NewDateFromString same as MakeDateFromString, but returns a pointer to Date. -func NewDateFromString(str string) (*Date, error) { - d, err := MakeDateFromString(str) - if err != nil { - return nil, err - } - return d.Ptr(), nil -} - -// MarshalXML implements the xml.Marshaler interface. -func (d Date) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - v := d.Format(time.DateOnly) - return e.EncodeElement(v, start) -} - -// UnmarshalXML implements the xml.Unmarshaler interface. -func (dt *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - var sd string - if err := d.DecodeElement(&sd, &start); err != nil { - return err - } - - t, err := itime.ParseInRomania(time.DateOnly, sd) - if err != nil { - return err - } - - *dt = Date{Time: t} - return nil -} - -// Ptr is a helper to return a *Date in contexts where a pointer is needed. -func (d Date) Ptr() *Date { - return &d -} - -// IsInitialized checks if the Date is initialized (ie is created explicitly -// with a constructor or initialized by setting the Time, not implicitly via -// var declaration with no initialization). -func (d Date) IsInitialized() bool { - return d != Date{} -} - // AmountWithCurrency represents an embeddable type that stores an amount as // chardata and the currency ID as the currencyID attribute. The name of the // node must be controlled by the parent type. type AmountWithCurrency struct { - Amount Decimal `xml:",chardata"` + Amount types.Decimal `xml:",chardata"` CurrencyID CurrencyCodeType `xml:"currencyID,attr,omitempty"` } diff --git a/etransport/declaration.go b/etransport/declaration.go index 2023a7a..fcae28e 100644 --- a/etransport/declaration.go +++ b/etransport/declaration.go @@ -17,8 +17,7 @@ package etransport import ( "errors" - "github.com/shopspring/decimal" - + "github.com/printesoi/e-factura-go/types" ixml "github.com/printesoi/e-factura-go/xml" "github.com/printesoi/xml-go" ) @@ -177,11 +176,11 @@ type PostingDeclarationNotificationTransportedGood struct { OpPurposeCode OpPurposeCodeType `xml:"codScopOperatiune,attr"` TariffCode string `xml:"codTarifar,attr,omitempty"` GoodName string `xml:"denumireMarfa,attr"` - Quantity decimal.Decimal `xml:"cantitate,attr"` + Quantity types.Decimal `xml:"cantitate,attr"` UnitMeasureCode UnitMeasureCodeType `xml:"codUnitateMasura,attr"` - NetWeight *decimal.Decimal `xml:"greutateNeta,attr,omitempty"` - GrossWeight decimal.Decimal `xml:"greutateBruta,attr"` - LeiValueNoVAT *decimal.Decimal `xml:"valoareLeiFaraTva,attr,omitempty"` + NetWeight *types.Decimal `xml:"greutateNeta,attr,omitempty"` + GrossWeight types.Decimal `xml:"greutateBruta,attr"` + LeiValueNoVAT *types.Decimal `xml:"valoareLeiFaraTva,attr,omitempty"` DeclarantRef string `xml:"refDeclaranti,attr,omitempty"` } diff --git a/types/date.go b/types/date.go new file mode 100644 index 0000000..920daff --- /dev/null +++ b/types/date.go @@ -0,0 +1,104 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package types + +import ( + "time" + + "github.com/printesoi/xml-go" + + itime "github.com/printesoi/e-factura-go/time" +) + +// Date is a wrapper of the time.Time type which marshals to XML in the +// YYYY-MM-DD format and is assumed to be in the Romanian timezone location. +type Date struct { + time.Time +} + +// MakeDate creates a date with the provided year, month and day in the +// Romanian time zone location. +func MakeDate(year int, month time.Month, day int) Date { + return Date{itime.Date(year, month, day, 0, 0, 0, 0)} +} + +// NewDate same as MakeDate, but returns a pointer to Date. +func NewDate(year int, month time.Month, day int) *Date { + return MakeDate(year, month, day).Ptr() +} + +// MakeDateFromTime creates a Date in Romanian time zone location from the +// given time.Time. +func MakeDateFromTime(t time.Time) Date { + return MakeDate(itime.TimeInRomania(t).Date()) +} + +// NewDateFromTime same as MakeDateFromTime, but returns a pointer to Date. +func NewDateFromTime(t time.Time) *Date { + return MakeDate(itime.TimeInRomania(t).Date()).Ptr() +} + +// MakeDateFromString creates a Date in Romanian time zone from a string in the +// YYYY-MM-DD format. +func MakeDateFromString(str string) (Date, error) { + t, err := itime.ParseInRomania(time.DateOnly, str) + if err != nil { + return Date{}, err + } + return MakeDate(t.Date()), nil +} + +// NewDateFromString same as MakeDateFromString, but returns a pointer to Date. +func NewDateFromString(str string) (*Date, error) { + d, err := MakeDateFromString(str) + if err != nil { + return nil, err + } + return d.Ptr(), nil +} + +// MarshalXML implements the xml.Marshaler interface. +func (d Date) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + v := d.Format(time.DateOnly) + return e.EncodeElement(v, start) +} + +// UnmarshalXML implements the xml.Unmarshaler interface. +func (dt *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var sd string + if err := d.DecodeElement(&sd, &start); err != nil { + return err + } + + t, err := itime.ParseInRomania(time.DateOnly, sd) + if err != nil { + return err + } + + *dt = Date{Time: t} + return nil +} + +// Ptr is a helper to return a *Date in contexts where a pointer is needed. +func (d Date) Ptr() *Date { + return &d +} + +// IsInitialized checks if the Date is initialized (ie is created explicitly +// with a constructor or initialized by setting the Time, not implicitly via +// var declaration with no initialization). +func (d Date) IsInitialized() bool { + return d != Date{} +} diff --git a/efactura/decimal.go b/types/decimal.go similarity index 98% rename from efactura/decimal.go rename to types/decimal.go index e182160..d8df155 100644 --- a/efactura/decimal.go +++ b/types/decimal.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License -package efactura +package types import ( "github.com/printesoi/xml-go" @@ -179,9 +179,9 @@ func (d Decimal) AsAmount() Decimal { // Cmp compares the numbers represented by d and d2 and returns: // -// -1 if d < d2 -// 0 if d == d2 -// +1 if d > d2 +// -1 if d < d2 +// 0 if d == d2 +// +1 if d > d2 func (d Decimal) Cmp(d2 Decimal) int { return d.Decimal.Cmp(d2.Decimal) } From a71c51e8f472ae718ad162255083b886a350354e Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sun, 4 Aug 2024 22:16:36 +0300 Subject: [PATCH 06/22] Use types.Date for dates in etranport.PostingDeclarationV2 --- etransport/declaration.go | 16 ++++++++-------- types/date.go | 37 +++++++++++++++++++++++++++++++++++++ types/decimal.go | 8 ++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/etransport/declaration.go b/etransport/declaration.go index fcae28e..9cc8091 100644 --- a/etransport/declaration.go +++ b/etransport/declaration.go @@ -197,7 +197,7 @@ type PostingDeclarationNotificationTransportData struct { TransportOrgCountryCode CountryCodeType `xml:"codTaraOrgTransport,attr"` TransportOrgCode string `xml:"codOrgTransport,attr,omitempty"` TransportOrgName string `xml:"denumireOrgTransport,attr"` - TransportDate string `xml:"dataTransport,attr"` + TransportDate types.Date `xml:"dataTransport,attr"` } type PostingDeclationPlace struct { @@ -222,7 +222,7 @@ type PostingDeclationLocation struct { type PostingDeclarationTransportDocument struct { DocumentType DocumentType `xml:"tipDocument,attr"` DocumentNo string `xml:"numarDocument,attr,omitempty"` - DocumentDate string `xml:"dataDocument,attr"` + DocumentDate types.Date `xml:"dataDocument,attr"` Remarks string `xml:"observatii,attr,omitempty"` } @@ -243,10 +243,10 @@ type PostingDeclarationConfirmation struct { } type PostingDeclarationVehicleChange struct { - UIT UITType `xml:"uit,attr"` - LicensePlate string `xml:"nrVehicul,attr"` - Trailer1LicensePlate string `xml:"nrRemorca1,attr,omitempty"` - Trailer2LicensePlate string `xml:"nrRemorca2,attr,omitempty"` - ChangeDate string `xml:"dataModificare,attr"` - Remarks string `xml:"observatii,attr,omitempty"` + UIT UITType `xml:"uit,attr"` + LicensePlate string `xml:"nrVehicul,attr"` + Trailer1LicensePlate string `xml:"nrRemorca1,attr,omitempty"` + Trailer2LicensePlate string `xml:"nrRemorca2,attr,omitempty"` + ChangeDate types.DateTime `xml:"dataModificare,attr"` + Remarks string `xml:"observatii,attr,omitempty"` } diff --git a/types/date.go b/types/date.go index 920daff..792caa9 100644 --- a/types/date.go +++ b/types/date.go @@ -75,6 +75,15 @@ func (d Date) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(v, start) } +// MarshalXMLAttr implements the xml.MarshalerAttr interface. +func (d Date) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + v := d.Format(time.DateOnly) + return xml.Attr{ + Name: name, + Value: v, + }, nil +} + // UnmarshalXML implements the xml.Unmarshaler interface. func (dt *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var sd string @@ -102,3 +111,31 @@ func (d Date) Ptr() *Date { func (d Date) IsInitialized() bool { return d != Date{} } + +const ( + xsDateTimeFmt = "2006-01-02T15:04:05" +) + +type DateTime struct { + time.Time +} + +// MakeDateTime creates a DateTime in RoZoneLocation. +func MakeDateTime(year int, month time.Month, day, hour, min, sec, nsec int) DateTime { + return DateTime{Time: itime.Date(year, month, day, hour, min, sec, nsec)} +} + +// MarshalXML implements the xml.Marshaler interface. +func (d DateTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + v := d.Format(xsDateTimeFmt) + return e.EncodeElement(v, start) +} + +// MarshalXMLAttr implements the xml.MarshalerAttr interface. +func (d DateTime) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + v := d.Format(xsDateTimeFmt) + return xml.Attr{ + Name: name, + Value: v, + }, nil +} diff --git a/types/decimal.go b/types/decimal.go index d8df155..4d85c1e 100644 --- a/types/decimal.go +++ b/types/decimal.go @@ -91,6 +91,14 @@ func (d *Decimal) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(d.String(), start) } +// MarshalXMLAttr implements the xml.MarshalerAttr interface. +func (d *Decimal) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + return xml.Attr{ + Name: name, + Value: d.String(), + }, nil +} + // MarshalText implements the encoding.TextMarshaler interface. This is needed // so we can use Decimal as chardata. func (d Decimal) MarshalText() (text []byte, err error) { From f10033c085682bf554e293918e518c69c0484c63 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sun, 4 Aug 2024 23:58:27 +0300 Subject: [PATCH 07/22] Add etransport-cli command, implement auth and get-message-state --- cmd/etransport-cli/cmd/api.go | 42 +++++++ .../cmd/api_get_message_state.go | 71 +++++++++++ cmd/etransport-cli/cmd/auth.go | 36 ++++++ cmd/etransport-cli/cmd/auth_exchange_code.go | 69 +++++++++++ .../cmd/auth_get_authorize_link.go | 73 +++++++++++ cmd/etransport-cli/cmd/helpers.go | 76 ++++++++++++ cmd/etransport-cli/cmd/root.go | 113 ++++++++++++++++++ cmd/etransport-cli/main.go | 21 ++++ 8 files changed, 501 insertions(+) create mode 100644 cmd/etransport-cli/cmd/api.go create mode 100644 cmd/etransport-cli/cmd/api_get_message_state.go create mode 100644 cmd/etransport-cli/cmd/auth.go create mode 100644 cmd/etransport-cli/cmd/auth_exchange_code.go create mode 100644 cmd/etransport-cli/cmd/auth_get_authorize_link.go create mode 100644 cmd/etransport-cli/cmd/helpers.go create mode 100644 cmd/etransport-cli/cmd/root.go create mode 100644 cmd/etransport-cli/main.go diff --git a/cmd/etransport-cli/cmd/api.go b/cmd/etransport-cli/cmd/api.go new file mode 100644 index 0000000..444dd85 --- /dev/null +++ b/cmd/etransport-cli/cmd/api.go @@ -0,0 +1,42 @@ +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package cmd + +import ( + "github.com/spf13/cobra" +) + +// apiCmd represents the api command +var apiCmd = &cobra.Command{ + Use: "api", + Short: "e-transport API calls", +} + +const ( + flagNameOauthClientID = "oauth-client-id" + flagNameOauthClientSecret = "oauth-client-secret" + flagNameOAuthRedirectURL = "oauth-redirect-url" + flagNameOAuthToken = "oauth-token" +) + +func init() { + apiCmd.PersistentFlags().String(flagNameOauthClientID, "", "OAuth2 client ID") + _ = apiCmd.MarkPersistentFlagRequired(flagNameOauthClientID) + apiCmd.PersistentFlags().String(flagNameOauthClientSecret, "", "OAuth2 client secret") + _ = apiCmd.MarkPersistentFlagRequired(flagNameOauthClientSecret) + apiCmd.PersistentFlags().String(flagNameOAuthRedirectURL, "", "OAuth2 redirect URL. This needs to match one of the URLs for the OAuth2 app in SPV.") + _ = apiCmd.MarkPersistentFlagRequired(flagNameOAuthRedirectURL) + apiCmd.PersistentFlags().String(flagNameOAuthToken, "", "JSON-encoded OAuth2 token. The access token should not necessarily be valid, but rather only the refresh token.") + _ = apiCmd.MarkPersistentFlagRequired(flagNameOAuthToken) + + rootCmd.AddCommand(apiCmd) +} diff --git a/cmd/etransport-cli/cmd/api_get_message_state.go b/cmd/etransport-cli/cmd/api_get_message_state.go new file mode 100644 index 0000000..c63366c --- /dev/null +++ b/cmd/etransport-cli/cmd/api_get_message_state.go @@ -0,0 +1,71 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package cmd + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" +) + +const ( + flagNameGetMessageStateUploadIndex = "upload-index" +) + +// apiGetMessageStateCmd represents the `api download` command +var apiGetMessageStateCmd = &cobra.Command{ + Use: "get-message-state", + Short: "Get message state for an upload index", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := newEtransportClient(ctx, cmd) + if err != nil { + cmd.SilenceUsage = true + return err + } + + fvUploadIndex, err := cmd.Flags().GetInt64(flagNameGetMessageStateUploadIndex) + if err != nil { + return err + } + + res, err := client.GetMessageState(ctx, fvUploadIndex) + if err != nil { + cmd.SilenceUsage = true + return fmt.Errorf("get message state failed: %w", err) + } + if res.IsOk() { + fmt.Printf("Message for upload index %d: ok\n", fvUploadIndex) + } else if res.IsProcessing() { + fmt.Printf("Message for upload index %d: processing\n", fvUploadIndex) + } else if res.IsInvalidXML() { + fmt.Printf("Message for upload index %d: invalid XML, message: '%s',\n", fvUploadIndex, res.GetFirstErrorMessage()) + } else if res.IsNok() { + fmt.Printf("Message for upload index %d: nok, message: '%s'\n", fvUploadIndex, res.GetFirstErrorMessage()) + } else { + fmt.Printf("Message for upload index %d: unknown state '%s', message: '%s'\n", fvUploadIndex, res.State, res.GetFirstErrorMessage()) + } + + return nil + }, +} + +func init() { + apiGetMessageStateCmd.Flags().Int64(flagNameGetMessageStateUploadIndex, 0, "Upload index") + _ = apiGetMessageStateCmd.MarkFlagRequired(flagNameGetMessageStateUploadIndex) + + apiCmd.AddCommand(apiGetMessageStateCmd) +} diff --git a/cmd/etransport-cli/cmd/auth.go b/cmd/etransport-cli/cmd/auth.go new file mode 100644 index 0000000..23db46f --- /dev/null +++ b/cmd/etransport-cli/cmd/auth.go @@ -0,0 +1,36 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package cmd + +import ( + "github.com/spf13/cobra" +) + +// authCmd represents the auth command +var authCmd = &cobra.Command{ + Use: "auth", + Short: "OAuth2 protocol for ANAF APIs", +} + +func init() { + authCmd.PersistentFlags().String(flagNameOauthClientID, "", "OAuth2 client ID") + _ = authCmd.MarkPersistentFlagRequired(flagNameOauthClientID) + authCmd.PersistentFlags().String(flagNameOauthClientSecret, "", "OAuth2 client secret") + _ = authCmd.MarkPersistentFlagRequired(flagNameOauthClientSecret) + authCmd.PersistentFlags().String(flagNameOAuthRedirectURL, "", "OAuth2 redirect URL. This needs to match one of the URLs for the OAuth2 app in SPV.") + _ = authCmd.MarkPersistentFlagRequired(flagNameOAuthRedirectURL) + + rootCmd.AddCommand(authCmd) +} diff --git a/cmd/etransport-cli/cmd/auth_exchange_code.go b/cmd/etransport-cli/cmd/auth_exchange_code.go new file mode 100644 index 0000000..a6acd40 --- /dev/null +++ b/cmd/etransport-cli/cmd/auth_exchange_code.go @@ -0,0 +1,69 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" +) + +const ( + flagNameOAuthDeviceCode = "oauth-device-code" +) + +// authExchangeCodeCmd represents the `accounts get-list` command +var authExchangeCodeCmd = &cobra.Command{ + Use: "exchange-code", + Short: "Exchange an OAuth device code for a token", + Long: `Exchange an OAuth device code for a token`, + RunE: func(cmd *cobra.Command, args []string) error { + oauth2Cfg, err := newOAuth2Config(cmd) + if err != nil { + cmd.SilenceUsage = true + return err + } + + fvCode, err := cmd.Flags().GetString(flagNameOAuthDeviceCode) + if err != nil { + return err + } + + token, err := oauth2Cfg.Exchange(context.Background(), fvCode) + if err != nil { + cmd.SilenceUsage = true + return err + } + + tokenJSON, err := json.Marshal(token) + if err != nil { + cmd.SilenceUsage = true + return err + } + + fmt.Println(string(tokenJSON)) + return nil + }, +} + +func init() { + authExchangeCodeCmd.Flags().String(flagNameOAuthDeviceCode, "", + "OAuth2 device code to be exchanged for a token") + _ = authExchangeCodeCmd.MarkFlagRequired(flagNameOAuthDeviceCode) + + authCmd.AddCommand(authExchangeCodeCmd) +} diff --git a/cmd/etransport-cli/cmd/auth_get_authorize_link.go b/cmd/etransport-cli/cmd/auth_get_authorize_link.go new file mode 100644 index 0000000..62b642d --- /dev/null +++ b/cmd/etransport-cli/cmd/auth_get_authorize_link.go @@ -0,0 +1,73 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package cmd + +import ( + "fmt" + "io" + + "github.com/pkg/browser" + "github.com/spf13/cobra" +) + +const ( + flagNameAuthGetAuthorizeLinkState = "oauth-state" + flagNameAuthGetAuthorizeLinkOpen = "open" +) + +// authGetAuthorizeLinkCmd represents the `accounts get-list` command +var authGetAuthorizeLinkCmd = &cobra.Command{ + Use: "get-authorize-link", + Short: "Get OAuth2 authorize link", + Long: `Get OAuth2 authorize link`, + RunE: func(cmd *cobra.Command, args []string) error { + oauth2Cfg, err := newOAuth2Config(cmd) + if err != nil { + cmd.SilenceUsage = true + return err + } + + fvState, err := cmd.Flags().GetString(flagNameAuthGetAuthorizeLinkState) + if err != nil { + return err + } + + fvOpen, err := cmd.Flags().GetBool(flagNameAuthGetAuthorizeLinkOpen) + if err != nil { + return err + } + + authURL := oauth2Cfg.AuthCodeURL(fvState) + fmt.Printf("%s\n", authURL) + + if fvOpen { + // Discard info messages from browser package printed to Stdout. + browser.Stdout = io.Discard + if err := browser.OpenURL(authURL); err != nil { + cmd.SilenceUsage = true + return err + } + } + + return nil + }, +} + +func init() { + authGetAuthorizeLinkCmd.Flags().String(flagNameAuthGetAuthorizeLinkState, "", "OAuth2 state param") + authGetAuthorizeLinkCmd.Flags().Bool(flagNameAuthGetAuthorizeLinkOpen, false, "Open the authorize link in the default browser") + + authCmd.AddCommand(authGetAuthorizeLinkCmd) +} diff --git a/cmd/etransport-cli/cmd/helpers.go b/cmd/etransport-cli/cmd/helpers.go new file mode 100644 index 0000000..390dbe8 --- /dev/null +++ b/cmd/etransport-cli/cmd/helpers.go @@ -0,0 +1,76 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package cmd + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/printesoi/e-factura-go/etransport" + "github.com/printesoi/e-factura-go/oauth2" +) + +func newOAuth2Config(cmd *cobra.Command) (cfg oauth2.Config, err error) { + fvClientID, err := cmd.InheritedFlags().GetString(flagNameOauthClientID) + if err != nil { + return cfg, err + } + fvClientSecret, err := cmd.InheritedFlags().GetString(flagNameOauthClientSecret) + if err != nil { + return cfg, err + } + fvRedirectURL, err := cmd.Flags().GetString(flagNameOAuthRedirectURL) + if err != nil { + return cfg, err + } + + cfg, err = oauth2.MakeConfig( + oauth2.ConfigCredentials(fvClientID, fvClientSecret), + oauth2.ConfigRedirectURL(fvRedirectURL), + ) + return +} + +func newEtransportClient(ctx context.Context, cmd *cobra.Command) (client *etransport.Client, err error) { + fvProduction, err := cmd.InheritedFlags().GetBool(flagNameProduction) + if err != nil { + return nil, err + } + + fvToken, err := cmd.InheritedFlags().GetString(flagNameOAuthToken) + if err != nil { + return nil, err + } + + token, err := oauth2.TokenFromJSON([]byte(fvToken)) + if err != nil { + return nil, fmt.Errorf("error loading token from JSON: %w", err) + } + + oauth2Cfg, err := newOAuth2Config(cmd) + if err != nil { + return nil, fmt.Errorf("error creating oauth2 config: %w", err) + } + + tokenSource := oauth2Cfg.TokenSource(ctx, token) + if fvProduction { + client, err = etransport.NewProductionClient(ctx, tokenSource) + } else { + client, err = etransport.NewSandboxClient(ctx, tokenSource) + } + return +} diff --git a/cmd/etransport-cli/cmd/root.go b/cmd/etransport-cli/cmd/root.go new file mode 100644 index 0000000..34798a4 --- /dev/null +++ b/cmd/etransport-cli/cmd/root.go @@ -0,0 +1,113 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "etransport-cli", + Short: "A CLI client for the e-transport APIs", + Run: func(cmd *cobra.Command, args []string) { + cmd.Usage() + }, +} + +const ( + flagNameProduction = "production" +) + +var ( + etransportCfgFile string +) + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + rootCmd.PersistentFlags().StringVar(&etransportCfgFile, "config", "", "config file (default is $HOME/.e-transport.yaml)") + rootCmd.PersistentFlags().Bool(flagNameProduction, false, "Production mode (default sandbox)") + + bindViperFlag := func(name string) { + viper.BindPFlag(name, rootCmd.PersistentFlags().Lookup(name)) + viper.BindEnv(name) + } + viper.SetEnvPrefix("EFACTURA") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + for _, flagName := range []string{ + flagNameProduction, + } { + bindViperFlag(flagName) + } +} + +func initConfig() { + if etransportCfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(etransportCfgFile) + } else { + // Find home directory. + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // Search config in home directory with name ".e-transport.yaml". + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".e-transport") + } + + viper.AutomaticEnv() + if err := viper.ReadInConfig(); err == nil { + fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } + + // This is a hack to make cobra required flags work with viper + // https://github.com/spf13/viper/issues/397#issuecomment-544272457 + postInitCommands(rootCmd.Commands()) +} + +func postInitCommands(commands []*cobra.Command) { + for _, cmd := range commands { + presetRequiredFlags(cmd) + if cmd.HasSubCommands() { + postInitCommands(cmd.Commands()) + } + } +} + +func presetRequiredFlags(cmd *cobra.Command) { + viper.BindPFlags(cmd.Flags()) + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if viper.IsSet(f.Name) && viper.GetString(f.Name) != "" { + cmd.Flags().Set(f.Name, viper.GetString(f.Name)) + } + }) +} diff --git a/cmd/etransport-cli/main.go b/cmd/etransport-cli/main.go new file mode 100644 index 0000000..cdcb1c8 --- /dev/null +++ b/cmd/etransport-cli/main.go @@ -0,0 +1,21 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package main + +import "github.com/printesoi/e-factura-go/cmd/etransport-cli/cmd" + +func main() { + cmd.Execute() +} From 0a1b8f2d4d963cec44e4277a8de69d39f70e0723 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Mon, 5 Aug 2024 20:56:06 +0300 Subject: [PATCH 08/22] Add uuid in go.mod --- go.mod | 1 + go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/go.mod b/go.mod index 1f77c15..7750a6c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.1 require ( github.com/alexsergivan/transliterator v1.0.0 + github.com/google/uuid v1.4.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/printesoi/xml-go v1.0.1 github.com/shopspring/decimal v1.3.1 diff --git a/go.sum b/go.sum index 544f412..60a99cc 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= From 28a155c7e5f7190c1c2895ade9e893d91e3b3973 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Mon, 5 Aug 2024 23:00:36 +0300 Subject: [PATCH 09/22] Implement get-messages-list api subcommand for efactura-cli --- cmd/efactura-cli/cmd/api_get_messages_list.go | 122 ++++++++++++++++++ cmd/efactura-cli/cmd/root.go | 2 +- 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 cmd/efactura-cli/cmd/api_get_messages_list.go diff --git a/cmd/efactura-cli/cmd/api_get_messages_list.go b/cmd/efactura-cli/cmd/api_get_messages_list.go new file mode 100644 index 0000000..140ccee --- /dev/null +++ b/cmd/efactura-cli/cmd/api_get_messages_list.go @@ -0,0 +1,122 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/printesoi/e-factura-go/efactura" + "github.com/spf13/cobra" +) + +const ( + flagNameMessageFilter = "filter" + flagNameMessageNumDays = "num-days" + flagNameMessageCIF = "cif" + flagNameMessageJSON = "json" +) + +// apiGetMessagesCmd represents the `api get-messages` command +var apiGetMessagesCmd = &cobra.Command{ + Use: "get-messages", + Short: "Get messages list", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := newEfacturaClient(ctx, cmd) + if err != nil { + cmd.SilenceUsage = true + return err + } + + fvCIF, err := cmd.Flags().GetString(flagNameMessageCIF) + if err != nil { + return err + } + + fvNumDays, err := cmd.Flags().GetInt(flagNameMessageNumDays) + if err != nil { + return err + } + + fvFilter, err := cmd.Flags().GetString(flagNameMessageFilter) + if err != nil { + return err + } + + messageFilter := efactura.MessageFilterAll + switch fvFilter { + case efactura.MessageFilterErrors.String(): + messageFilter = efactura.MessageFilterErrors + case efactura.MessageFilterSent.String(): + messageFilter = efactura.MessageFilterSent + case efactura.MessageFilterReceived.String(): + messageFilter = efactura.MessageFilterReceived + case efactura.MessageFilterBuyerMessage.String(): + messageFilter = efactura.MessageFilterBuyerMessage + case "A", "": + messageFilter = efactura.MessageFilterAll + default: + return fmt.Errorf("invalid filter '%s'", fvFilter) + } + res, err := client.GetMessagesList(ctx, fvCIF, fvNumDays, messageFilter) + if err != nil { + cmd.SilenceUsage = true + return err + } + if !res.IsOk() { + cmd.SilenceUsage = true + return fmt.Errorf("error getting messages list: %s", res.Error) + } + + asJson, err := cmd.Flags().GetBool(flagNameMessageJSON) + if err != nil { + return err + } + if asJson { + messagesData, err := json.Marshal(res) + if err != nil { + cmd.SilenceUsage = true + return err + } + fmt.Printf("%s\n", string(messagesData)) + } else { + fmt.Printf("Got %d messages:\n", len(res.Messages)) + for i, m := range res.Messages { + t, _ := time.Parse("200601021504", m.CreationDate) + fmt.Printf("%d. [%s] CIF=%s, Type=%s, Upload_index=%d, ID=%d, Details=%s\n", + i, t.Format(time.DateTime), m.CIF, m.Type, m.GetUploadIndex(), m.GetID(), m.Details) + } + } + + return nil + }, +} + +func init() { + apiGetMessagesCmd.Flags().Int(flagNameMessageNumDays, 1, "The number of days to fetch messages (must be >=1, <= 60)") + _ = apiGetMessagesCmd.MarkFlagRequired(flagNameDownloadID) + + apiGetMessagesCmd.Flags().String(flagNameMessageFilter, "A", "Message filter") + + apiGetMessagesCmd.Flags().String(flagNameMessageCIF, "", "CIF/CUI") + _ = apiGetMessagesCmd.MarkFlagRequired(flagNameMessageCIF) + + apiGetMessagesCmd.Flags().Bool(flagNameMessageJSON, false, "Output RAW json") + + apiCmd.AddCommand(apiGetMessagesCmd) +} diff --git a/cmd/efactura-cli/cmd/root.go b/cmd/efactura-cli/cmd/root.go index a86f247..fa5c464 100644 --- a/cmd/efactura-cli/cmd/root.go +++ b/cmd/efactura-cli/cmd/root.go @@ -26,7 +26,7 @@ import ( // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "client", + Use: "efactura-cli", Short: "A CLI client for the ANAF e-factura APIs", Run: func(cmd *cobra.Command, args []string) { cmd.Usage() From 8ee555df0b4e21e5a682240c019279ff98fbd778 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Tue, 6 Aug 2024 09:49:15 +0300 Subject: [PATCH 10/22] Add missing GetFirstErrorMessage for GetMessageStateResponse --- etransport/rest.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/etransport/rest.go b/etransport/rest.go index 9e80e9f..aedfb29 100644 --- a/etransport/rest.go +++ b/etransport/rest.go @@ -170,6 +170,15 @@ func (r *GetMessageStateResponse) IsInvalidXML() bool { return r != nil && r.State == GetMessageStateCodeInvalidXML } +// GetFirstErrorMessage returns the first error message. If no error messages +// are set for the response, empty string is returned. +func (r *GetMessageStateResponse) GetFirstErrorMessage() string { + if r == nil || len(r.Errors) == 0 { + return "" + } + return r.Errors[0].ErrorMessage +} + // GetMessageState fetch the state of a message. The uploadIndex must a result // from an upload operation. func (c *Client) GetMessageState( From 1e2c4abdbb96a053743e014e73076cebc66da919 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Tue, 6 Aug 2024 23:18:01 +0300 Subject: [PATCH 11/22] Add xml2pdf command for efactura-cli --- cmd/efactura-cli/cmd/api_get_message_state.go | 1 + cmd/efactura-cli/cmd/api_xml2pdf.go | 104 ++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 cmd/efactura-cli/cmd/api_xml2pdf.go diff --git a/cmd/efactura-cli/cmd/api_get_message_state.go b/cmd/efactura-cli/cmd/api_get_message_state.go index 5d88a93..5a8bcc8 100644 --- a/cmd/efactura-cli/cmd/api_get_message_state.go +++ b/cmd/efactura-cli/cmd/api_get_message_state.go @@ -17,6 +17,7 @@ package cmd import ( "context" "fmt" + "github.com/spf13/cobra" ) diff --git a/cmd/efactura-cli/cmd/api_xml2pdf.go b/cmd/efactura-cli/cmd/api_xml2pdf.go new file mode 100644 index 0000000..c1b505f --- /dev/null +++ b/cmd/efactura-cli/cmd/api_xml2pdf.go @@ -0,0 +1,104 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/printesoi/e-factura-go/efactura" + "github.com/spf13/cobra" +) + +const ( + flagNameXML2PDFInvoiceXML = "xml" + flagNameXML2PDFOutFile = "out" + flagNameXML2PDFNoValidate = "no-validate" +) + +// apiXML2PDFCmd represents the `api download` command +var apiXML2PDFCmd = &cobra.Command{ + Use: "xml2pdf", + Short: "Convert XML to PDF", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + client, err := newEfacturaClient(ctx, cmd) + if err != nil { + cmd.SilenceUsage = true + return err + } + + fvInvoiceXML, err := cmd.Flags().GetString(flagNameXML2PDFInvoiceXML) + if err != nil { + return err + } + + xmlFile, err := os.Open(fvInvoiceXML) + if err != nil { + cmd.SilenceUsage = true + return err + } + defer xmlFile.Close() + + fvNoValidate, err := cmd.Flags().GetBool(flagNameXML2PDFNoValidate) + if err != nil { + return err + } + + fvOutFile, err := cmd.Flags().GetString(flagNameXML2PDFOutFile) + if err != nil { + return err + } + + res, err := client.XMLToPDF(ctx, xmlFile, efactura.ValidateStandardFACT1, fvNoValidate) + if err != nil { + cmd.SilenceUsage = true + return err + } + + out := os.Stdout + if fvOutFile != "" { + out, err = os.OpenFile(fvOutFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + cmd.SilenceUsage = true + return err + } + defer func() { + if err := out.Close(); err != nil { + fmt.Fprintf(os.Stderr, "error closing file: %v\n", err) + } + }() + } + if _, err := out.Write(res.PDF); err != nil { + cmd.SilenceUsage = true + fmt.Fprintf(os.Stderr, "error writing PDF data: %v\n", err) + return err + } + + return nil + }, +} + +func init() { + apiXML2PDFCmd.Flags().String(flagNameXML2PDFInvoiceXML, "", "Input Invoice XML file path") + _ = apiXML2PDFCmd.MarkFlagRequired(flagNameXML2PDFInvoiceXML) + + apiXML2PDFCmd.Flags().String(flagNameXML2PDFOutFile, "", "Write output to this file instead of stdout") + + apiXML2PDFCmd.Flags().Bool(flagNameXML2PDFNoValidate, false, "Skip validation of XML file") + + apiCmd.AddCommand(apiXML2PDFCmd) +} From 2b59dc79ab115213dbc984a1f30f14cdd190290a Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sat, 14 Sep 2024 22:17:21 +0300 Subject: [PATCH 12/22] Add GetFirstErrorMessage helper for UploadV2Response --- efactura/rest.go | 10 +++++----- etransport/rest.go | 9 +++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/efactura/rest.go b/efactura/rest.go index 5465c57..fdd1314 100644 --- a/efactura/rest.go +++ b/efactura/rest.go @@ -120,11 +120,11 @@ type ( // MessagesListResponse is the parsed response from the list messages // endpoint. MessagesListResponse struct { - Error string `json:"eroare"` - Title string `json:"titlu"` - Serial string `json:"serial"` - CUI string `json:"cui"` - Messages []Message `json:"mesaje"` + Error string `json:"eroare,omitempty"` + Title string `json:"titlu,omitempty"` + Serial string `json:"serial,omitempty"` + CUI string `json:"cui,omitempty"` + Messages []Message `json:"mesaje,omitempty"` } // MessagesListPaginationResponse is the parsed response from the list diff --git a/etransport/rest.go b/etransport/rest.go index aedfb29..740b2ff 100644 --- a/etransport/rest.go +++ b/etransport/rest.go @@ -240,6 +240,15 @@ func (r *UploadV2Response) GetUIT() UITType { return r.UIT } +// GetFirstErrorMessage returns the first error message. If no error messages +// are set for the response, empty string is returned. +func (r *UploadV2Response) GetFirstErrorMessage() string { + if r == nil || len(r.Errors) == 0 { + return "" + } + return r.Errors[0].ErrorMessage +} + type uploadStandard string const ( From ad61cb05f0b6e57ad1d956bc369c1ef3408bd164 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sun, 15 Sep 2024 00:15:20 +0300 Subject: [PATCH 13/22] Fix typo --- etransport/codes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etransport/codes.go b/etransport/codes.go index 5858dd1..985e873 100644 --- a/etransport/codes.go +++ b/etransport/codes.go @@ -772,7 +772,7 @@ const ( // "40" Municipiul Bucureşti CountyCodeB CountyCodeType = "40" // "1" Alba - CountryCodeAB CountyCodeType = "1" + CountyCodeAB CountyCodeType = "1" // "2" Arad CountyCodeAR CountyCodeType = "2" // "3" Argeş From 18b5eed185c41a8f0e426815685411d356d34450 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sun, 15 Sep 2024 00:32:16 +0300 Subject: [PATCH 14/22] Remove duplicate comment --- etransport/codes.go | 172 ++++++++++++++++---------------------------- 1 file changed, 63 insertions(+), 109 deletions(-) diff --git a/etransport/codes.go b/etransport/codes.go index 985e873..97ee7b4 100644 --- a/etransport/codes.go +++ b/etransport/codes.go @@ -53,73 +53,6 @@ const ( OpTypeDIE OpType = "70" ) -// pentru codTipOperatiune = "10" AIC - Achiziţie intracomunitară: -// -// "101" Comercializare -// "201" Producție -// "301" Gratuități -// "401" Echipament comercial -// "501" Mijloace fixe -// "601" Consum propriu -// "703" Operațiuni de livrare cu instalare -// "801" Leasing financiar/operațional -// "802" Bunuri în garanție -// "901" Operațiuni scutite -// "1001" Investiție in curs -// "1101" Donații, ajutoare -// "9901" Altele -// -// pentru codTipOperatiune = "12" LHI - Operațiuni în sistem lohn (UE) - intrare: -// -// "9999" Același cu operațiunea -// -// pentru codTipOperatiune = "14" SCI - Stocuri la dispoziția clientului (Call-off stock) - intrare: -// -// "9999" Același cu operațiunea -// -// pentru codTipOperatiune = "20" LIC - Livrare intracomunitară: -// -// "101" Comercializare -// "301" Gratuități -// "703" Operațiuni de livrare cu instalare -// "801" Leasing financiar/operațional -// "802" Bunuri în garanție -// "9901" Altele -// -// pentru codTipOperatiune = "22" LHE - Operațiuni în sistem lohn (UE) - ieşire: -// -// "9999" Același cu operațiunea -// -// pentru codTipOperatiune = "24" SCE - Stocuri la dispoziția clientului (Call-off stock) - ieşire: -// -// "9999" Același cu operațiunea -// -// pentru codTipOperatiune = "30" TTN - Transport pe teritoriul naţional: -// -// "101" Comercializare -// "704" Transfer între gestiuni -// "705" Bunuri puse la dispoziția clientului -// "9901" Altele -// -// pentru codTipOperatiune = "40" IMP - Import: -// -// "9999" Același cu operațiunea -// -// pentru codTipOperatiune = "50" EXP - Export: -// -// "9999" Același cu operațiunea -// -// pentru codTipOperatiune = "60" DIN - Tranzactie intracomunitara - Intrare pentru depozitare/formare nou transport: -// -// "9999" Același cu operațiunea -// -// pentru codTipOperatiune = "70" DIE - Tranzactie intracomunitara - Iesire dupa depozitare/formare nou transport: -// -// "9999" Același cu operațiunea -// -// TODO: enum -type OpReason string - type CountryCodeType string const ( @@ -859,48 +792,69 @@ type OpPurposeCodeType string const ( // Câmpul codScopOperatiune ia valori diferite, în funcţie de valoarea câmpului codTipOperatiune, astfel: -// - pentru codTipOperatiune = "10" AIC - Achiziţie intracomunitară: -// "101" Comercializare -// "201" Producție -// "301" Gratuități -// "401" Echipament comercial -// "501" Mijloace fixe -// "601" Consum propriu -// "703" Operațiuni de livrare cu instalare -// "801" Leasing financiar/operațional -// "802" Bunuri în garanție -// "901" Operațiuni scutite -// "1001" Investiție in curs -// "1101" Donații, ajutoare -// "9901" Altele -// - pentru codTipOperatiune = "12" LHI - Operațiuni în sistem lohn (UE) - intrare: -// "9999" Același cu operațiunea -// - pentru codTipOperatiune = "14" SCI - Stocuri la dispoziția clientului (Call-off stock) - intrare: -// "9999" Același cu operațiunea -// - pentru codTipOperatiune = "20" LIC - Livrare intracomunitară: -// "101" Comercializare -// "301" Gratuități -// "703" Operațiuni de livrare cu instalare -// "801" Leasing financiar/operațional -// "802" Bunuri în garanție -// "9901" Altele -// - pentru codTipOperatiune = "22" LHE - Operațiuni în sistem lohn (UE) - ieşire: -// "9999" Același cu operațiunea -// - pentru codTipOperatiune = "24" SCE - Stocuri la dispoziția clientului (Call-off stock) - ieşire: -// "9999" Același cu operațiunea -// - pentru codTipOperatiune = "30" TTN - Transport pe teritoriul naţional: -// "101" Comercializare -// "704" Transfer între gestiuni -// "705" Bunuri puse la dispoziția clientului -// "9901" Altele -// - pentru codTipOperatiune = "40" IMP - Import: -// "9999" Același cu operațiunea -// - pentru codTipOperatiune = "50" EXP - Export: -// "9999" Același cu operațiunea -// - pentru codTipOperatiune = "60" DIN - Tranzactie intracomunitara - Intrare pentru depozitare/formare nou transport: -// "9999" Același cu operațiunea -// - pentru codTipOperatiune = "70" DIE - Tranzactie intracomunitara - Iesire dupa depozitare/formare nou transport: -// "9999" Același cu operațiunea +// pentru codTipOperatiune = "10" AIC - Achiziţie intracomunitară: +// +// "101" Comercializare +// "201" Producție +// "301" Gratuități +// "401" Echipament comercial +// "501" Mijloace fixe +// "601" Consum propriu +// "703" Operațiuni de livrare cu instalare +// "801" Leasing financiar/operațional +// "802" Bunuri în garanție +// "901" Operațiuni scutite +// "1001" Investiție in curs +// "1101" Donații, ajutoare +// "9901" Altele +// +// pentru codTipOperatiune = "12" LHI - Operațiuni în sistem lohn (UE) - intrare: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "14" SCI - Stocuri la dispoziția clientului (Call-off stock) - intrare: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "20" LIC - Livrare intracomunitară: +// +// "101" Comercializare +// "301" Gratuități +// "703" Operațiuni de livrare cu instalare +// "801" Leasing financiar/operațional +// "802" Bunuri în garanție +// "9901" Altele +// +// pentru codTipOperatiune = "22" LHE - Operațiuni în sistem lohn (UE) - ieşire: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "24" SCE - Stocuri la dispoziția clientului (Call-off stock) - ieşire: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "30" TTN - Transport pe teritoriul naţional: +// +// "101" Comercializare +// "704" Transfer între gestiuni +// "705" Bunuri puse la dispoziția clientului +// "9901" Altele +// +// pentru codTipOperatiune = "40" IMP - Import: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "50" EXP - Export: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "60" DIN - Tranzactie intracomunitara - Intrare pentru depozitare/formare nou transport: +// +// "9999" Același cu operațiunea +// +// pentru codTipOperatiune = "70" DIE - Tranzactie intracomunitara - Iesire dupa depozitare/formare nou transport: +// +// "9999" Același cu operațiunea ) // Valori posibile pentru câmpul codUnitateMasura: UN/ECE Recommendation N°20 and UN/ECE Recommendation N°21 — Unit codes From 40e0884a0b74111aab9d4c5337537d7907bc2558 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sat, 5 Oct 2024 21:25:21 +0300 Subject: [PATCH 15/22] Fix unmarshaling for InvoiceNote --- efactura/invoice.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/efactura/invoice.go b/efactura/invoice.go index 6d5ff90..f6caee9 100644 --- a/efactura/invoice.go +++ b/efactura/invoice.go @@ -1193,12 +1193,12 @@ func (n InvoiceNote) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(xmlNote, start) } -func (n InvoiceNote) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (n *InvoiceNote) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var xmlNote struct { Note string `xml:",chardata"` } if err := d.DecodeElement(&xmlNote, &start); err != nil { - return nil + return err } // TODO: implement parsing the code return nil From 588498faacf9d6a339c180a549be7a71bbea7d57 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sat, 5 Oct 2024 21:26:33 +0300 Subject: [PATCH 16/22] Add more(all) field in the etransport Message object --- etransport/rest.go | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/etransport/rest.go b/etransport/rest.go index 740b2ff..0b6ab32 100644 --- a/etransport/rest.go +++ b/etransport/rest.go @@ -23,6 +23,7 @@ import ( "strings" ierrors "github.com/printesoi/e-factura-go/internal/errors" + "github.com/printesoi/e-factura-go/types" ixml "github.com/printesoi/e-factura-go/xml" ) @@ -34,14 +35,23 @@ const ( apiPathInfo = apiBase + "info" ) +type MessageState string + +const ( + MessageStateOK MessageState = "OK" + MessageStateERR MessageState = "ERR" +) + type Message struct { UIT UITType `json:"uit"` - DeclarantCode string `json:"cod_decl"` + DeclarantCode int64 `json:"cod_decl"` DeclarantRef string `json:"ref_decl"` Source string `json:"sursa"` - UploadID string `json:"id_incarcare"` + UploadID int64 `json:"id_incarcare"` CreatedDate string `json:"data_creare"` - OpType string `json:"tip_op"` + State MessageState `json:"stare"` + Op string `json:"tip"` + OpType int `json:"tip_op,omitempty"` TransportDate string `json:"data_transp"` CommercialPartnerCountryCode CountryCodeType `json:"pc_tara,omitempty"` CommercialPartnerCode string `json:"pc_cod,omitempty"` @@ -53,6 +63,11 @@ type Message struct { Trailer1LicensePlate string `json:"nr_rem1,omitempty"` Trailer2LicensePlate string `json:"nr_rem2,omitempty"` Messages []MessageError `json:"mesaje,omitempty"` + GrossTotalWeight types.Decimal `json:"gr_tot_bruta,omitempty"` + NetTotalWeight types.Decimal `json:"gr_tot_neta,omitempty"` + TotalValue types.Decimal `json:"val_tot,omitempty"` + LineCount int64 `json:"nr_linii,omitempty"` + PostIncident string `json:"post_avarie,omitempty"` } type MessageErrorType string @@ -100,6 +115,15 @@ func (r *MessagesListResponse) IsOk() bool { return r != nil && (len(r.Errors) == 0 || len(r.Errors) == 1 && strings.HasPrefix(r.Errors[0].ErrorMessage, "Nu exista mesaje in ")) } +// GetFirstErrorMessage returns the first error message. If no error messages +// are set for the response, empty string is returned. +func (r *MessagesListResponse) GetFirstErrorMessage() string { + if r == nil || len(r.Errors) == 0 { + return "" + } + return r.Errors[0].ErrorMessage +} + // GetMessagesList fetches the list of messages for a provided cif and number // of days. // NOTE: If there are no messages for the given interval, ANAF APIs From 79e086e70f87eb7a2fd75e03f77df7a0ca94ebc2 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sat, 5 Oct 2024 21:53:28 +0300 Subject: [PATCH 17/22] Add country code for Kosovo and enum values for OpPurposeCodeType --- etransport/codes.go | 50 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/etransport/codes.go b/etransport/codes.go index 97ee7b4..9df5b79 100644 --- a/etransport/codes.go +++ b/etransport/codes.go @@ -548,6 +548,8 @@ const ( CountryCodeYE CountryCodeType = "YE" // YT - Mayotte CountryCodeYT CountryCodeType = "YT" + // XK - Kosovo + CountryCodeXK CountryCodeType = "XK" // ZA - South Africa CountryCodeZA CountryCodeType = "ZA" // ZM - Zambia @@ -558,9 +560,6 @@ const ( CountryCode1A CountryCodeType = "1A" ) -type BCPCodeType string - -const ( // "1" Petea (HU) // "2" Borș (HU) // "3" Vărșand (HU) @@ -600,11 +599,8 @@ const ( // "37" Nădlac 2 - A1 (HU) // "38" Borș 2 - A3 (HU) // TODO: enum values -) - -type CustomsOfficeCodeType string +type BCPCodeType string -const ( // Valori posibile pentru câmpul codBirouVamal (BVI/F - Birou Vamal de Interior/Frontieră): // "12801" BVI Alba Iulia (ROBV0300) // "22801" BVI Arad (ROTM0200) @@ -697,7 +693,7 @@ const ( // "522801" BVI Giurgiu (ROBU3910) // "522901" BVF Zona Liberă Giurgiu (ROBU3980) // TODO: enum values -) +type CustomsOfficeCodeType string type CountyCodeType string @@ -788,9 +784,6 @@ const ( CountyCodeVN CountyCodeType = "39" ) -type OpPurposeCodeType string - -const ( // Câmpul codScopOperatiune ia valori diferite, în funcţie de valoarea câmpului codTipOperatiune, astfel: // pentru codTipOperatiune = "10" AIC - Achiziţie intracomunitară: // @@ -855,6 +848,41 @@ const ( // pentru codTipOperatiune = "70" DIE - Tranzactie intracomunitara - Iesire dupa depozitare/formare nou transport: // // "9999" Același cu operațiunea +type OpPurposeCodeType string + +const ( + // "101" Comercializare + OpPurposeCodeTypeCommercialization OpPurposeCodeType = "101" + // "201" Producție + OpPurposeCodeTypeProduction OpPurposeCodeType = "201" + // "301" Gratuități + OpPurposeCodeTypeGratuities OpPurposeCodeType = "301" + // "401" Echipament comercial + OpPurposeCodeTypeCommercialEquipment OpPurposeCodeType = "401" + // "501" Mijloace fixe + OpPurposeCodeTypeFixedAssets OpPurposeCodeType = "501" + // "601" Consum propriu + OpPurposeCodeOwnConsumption OpPurposeCodeType = "601" + // "703" Operațiuni de livrare cu instalare + OpPurposeCodeDeliveryWithInstallation OpPurposeCodeType = "703" + // "704" Transfer între gestiuni + OpPurposeCodeTransfer OpPurposeCodeType = "704" + // "705" Bunuri puse la dispoziția clientului + OpPurposeGoodsAsCustomersDisposal OpPurposeCodeType = "705" + // "801" Leasing financiar/operațional + OpPurposeCodeLeasing OpPurposeCodeType = "801" + // "802" Bunuri în garanție + OpPurposeCodeGoodsUnderWarranty OpPurposeCodeType = "802" + // "901" Operațiuni scutite + OpPurposeCodeExemptOperations OpPurposeCodeType = "901" + // "1001" Investiție in curs + OpPurposeCodeOngoingInvestment OpPurposeCodeType = "1001" + // "1101" Donații, ajutoare + OpPurposeCodeDonations OpPurposeCodeType = "1101" + // "9901" Altele + OpPurposeCodeOthers OpPurposeCodeType = "9901" + // 9999" Același cu operațiunea + OpPurposeSameAsOperation OpPurposeCodeType = "9999" ) // Valori posibile pentru câmpul codUnitateMasura: UN/ECE Recommendation N°20 and UN/ECE Recommendation N°21 — Unit codes From 9c591f2fc9376ff2ca455149aa0c6d5ff82622f8 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sat, 5 Oct 2024 23:29:05 +0300 Subject: [PATCH 18/22] Add util package - convert efactura.CountrySubentityType <-> etransport.CountyCodeType --- util/conv.go | 207 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 util/conv.go diff --git a/util/conv.go b/util/conv.go new file mode 100644 index 0000000..43d7bac --- /dev/null +++ b/util/conv.go @@ -0,0 +1,207 @@ +// Copyright 2024 Victor Dodon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package util + +import ( + "github.com/printesoi/e-factura-go/efactura" + "github.com/printesoi/e-factura-go/etransport" +) + +// EfacturaRoCountrySubentityToEtransportCountyCode convert the given +// efactura.CountrySubentityType to an etransport.CountyCode. If +// efacturaSubentity is a valid subentity code, the second returned value will +// be true. +func EfacturaRoCountrySubentityToEtransportCountyCode(efacturaSubentity efactura.CountrySubentityType) (_ etransport.CountyCodeType, ok bool) { + switch efacturaSubentity { + case efactura.CountrySubentityRO_B: + return etransport.CountyCodeB, true + case efactura.CountrySubentityRO_AB: + return etransport.CountyCodeAB, true + case efactura.CountrySubentityRO_AR: + return etransport.CountyCodeAR, true + case efactura.CountrySubentityRO_AG: + return etransport.CountyCodeAG, true + case efactura.CountrySubentityRO_BC: + return etransport.CountyCodeBC, true + case efactura.CountrySubentityRO_BH: + return etransport.CountyCodeBH, true + case efactura.CountrySubentityRO_BN: + return etransport.CountyCodeBN, true + case efactura.CountrySubentityRO_BT: + return etransport.CountyCodeBT, true + case efactura.CountrySubentityRO_BR: + return etransport.CountyCodeBR, true + case efactura.CountrySubentityRO_BV: + return etransport.CountyCodeBV, true + case efactura.CountrySubentityRO_BZ: + return etransport.CountyCodeBZ, true + case efactura.CountrySubentityRO_CL: + return etransport.CountyCodeCL, true + case efactura.CountrySubentityRO_CS: + return etransport.CountyCodeCS, true + case efactura.CountrySubentityRO_CJ: + return etransport.CountyCodeCJ, true + case efactura.CountrySubentityRO_CT: + return etransport.CountyCodeCT, true + case efactura.CountrySubentityRO_CV: + return etransport.CountyCodeCV, true + case efactura.CountrySubentityRO_DB: + return etransport.CountyCodeDB, true + case efactura.CountrySubentityRO_DJ: + return etransport.CountyCodeDJ, true + case efactura.CountrySubentityRO_GL: + return etransport.CountyCodeGL, true + case efactura.CountrySubentityRO_GR: + return etransport.CountyCodeGR, true + case efactura.CountrySubentityRO_GJ: + return etransport.CountyCodeGJ, true + case efactura.CountrySubentityRO_HR: + return etransport.CountyCodeHR, true + case efactura.CountrySubentityRO_HD: + return etransport.CountyCodeHD, true + case efactura.CountrySubentityRO_IL: + return etransport.CountyCodeIL, true + case efactura.CountrySubentityRO_IS: + return etransport.CountyCodeIS, true + case efactura.CountrySubentityRO_IF: + return etransport.CountyCodeIF, true + case efactura.CountrySubentityRO_MM: + return etransport.CountyCodeMM, true + case efactura.CountrySubentityRO_MH: + return etransport.CountyCodeMH, true + case efactura.CountrySubentityRO_MS: + return etransport.CountyCodeMS, true + case efactura.CountrySubentityRO_NT: + return etransport.CountyCodeNT, true + case efactura.CountrySubentityRO_OT: + return etransport.CountyCodeOT, true + case efactura.CountrySubentityRO_PH: + return etransport.CountyCodePH, true + case efactura.CountrySubentityRO_SJ: + return etransport.CountyCodeSJ, true + case efactura.CountrySubentityRO_SM: + return etransport.CountyCodeSM, true + case efactura.CountrySubentityRO_SB: + return etransport.CountyCodeSB, true + case efactura.CountrySubentityRO_SV: + return etransport.CountyCodeSV, true + case efactura.CountrySubentityRO_TR: + return etransport.CountyCodeTR, true + case efactura.CountrySubentityRO_TM: + return etransport.CountyCodeTM, true + case efactura.CountrySubentityRO_TL: + return etransport.CountyCodeTL, true + case efactura.CountrySubentityRO_VS: + return etransport.CountyCodeVS, true + case efactura.CountrySubentityRO_VL: + return etransport.CountyCodeVL, true + case efactura.CountrySubentityRO_VN: + return etransport.CountyCodeVN, true + } + return "", false +} + +// EtransportRoCountyCodeToEfacturaCountrySubentity convert the given +// etransport.CountyCodeType to an efactura.CountrySubentityType. If the given +// county code is valid, the second returned value will be true. +func EtransportRoCountyCodeToEfacturaCountrySubentity(etransportCountyCode etransport.CountyCodeType) (_ efactura.CountrySubentityType, ok bool) { + switch etransportCountyCode { + case etransport.CountyCodeB: + return efactura.CountrySubentityRO_B, true + case etransport.CountyCodeAB: + return efactura.CountrySubentityRO_AB, true + case etransport.CountyCodeAR: + return efactura.CountrySubentityRO_AR, true + case etransport.CountyCodeAG: + return efactura.CountrySubentityRO_AG, true + case etransport.CountyCodeBC: + return efactura.CountrySubentityRO_BC, true + case etransport.CountyCodeBH: + return efactura.CountrySubentityRO_BH, true + case etransport.CountyCodeBN: + return efactura.CountrySubentityRO_BN, true + case etransport.CountyCodeBT: + return efactura.CountrySubentityRO_BT, true + case etransport.CountyCodeBR: + return efactura.CountrySubentityRO_BR, true + case etransport.CountyCodeBV: + return efactura.CountrySubentityRO_BV, true + case etransport.CountyCodeBZ: + return efactura.CountrySubentityRO_BZ, true + case etransport.CountyCodeCL: + return efactura.CountrySubentityRO_CL, true + case etransport.CountyCodeCS: + return efactura.CountrySubentityRO_CS, true + case etransport.CountyCodeCJ: + return efactura.CountrySubentityRO_CJ, true + case etransport.CountyCodeCT: + return efactura.CountrySubentityRO_CT, true + case etransport.CountyCodeCV: + return efactura.CountrySubentityRO_CV, true + case etransport.CountyCodeDB: + return efactura.CountrySubentityRO_DB, true + case etransport.CountyCodeDJ: + return efactura.CountrySubentityRO_DJ, true + case etransport.CountyCodeGL: + return efactura.CountrySubentityRO_GL, true + case etransport.CountyCodeGR: + return efactura.CountrySubentityRO_GR, true + case etransport.CountyCodeGJ: + return efactura.CountrySubentityRO_GJ, true + case etransport.CountyCodeHR: + return efactura.CountrySubentityRO_HR, true + case etransport.CountyCodeHD: + return efactura.CountrySubentityRO_HD, true + case etransport.CountyCodeIL: + return efactura.CountrySubentityRO_IL, true + case etransport.CountyCodeIS: + return efactura.CountrySubentityRO_IS, true + case etransport.CountyCodeIF: + return efactura.CountrySubentityRO_IF, true + case etransport.CountyCodeMM: + return efactura.CountrySubentityRO_MM, true + case etransport.CountyCodeMH: + return efactura.CountrySubentityRO_MH, true + case etransport.CountyCodeMS: + return efactura.CountrySubentityRO_MS, true + case etransport.CountyCodeNT: + return efactura.CountrySubentityRO_NT, true + case etransport.CountyCodeOT: + return efactura.CountrySubentityRO_OT, true + case etransport.CountyCodePH: + return efactura.CountrySubentityRO_PH, true + case etransport.CountyCodeSJ: + return efactura.CountrySubentityRO_SJ, true + case etransport.CountyCodeSM: + return efactura.CountrySubentityRO_SM, true + case etransport.CountyCodeSB: + return efactura.CountrySubentityRO_SB, true + case etransport.CountyCodeSV: + return efactura.CountrySubentityRO_SV, true + case etransport.CountyCodeTR: + return efactura.CountrySubentityRO_TR, true + case etransport.CountyCodeTM: + return efactura.CountrySubentityRO_TM, true + case etransport.CountyCodeTL: + return efactura.CountrySubentityRO_TL, true + case etransport.CountyCodeVS: + return efactura.CountrySubentityRO_VS, true + case etransport.CountyCodeVL: + return efactura.CountrySubentityRO_VL, true + case etransport.CountyCodeVN: + return efactura.CountrySubentityRO_VN, true + } + return "", false +} From d29ed610cd5e671a86a55f9ea1335c97e10c0f2c Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sat, 5 Oct 2024 23:33:15 +0300 Subject: [PATCH 19/22] Add etransport.RoCountyNameToCountyCode helper method --- etransport/codes.go | 98 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/etransport/codes.go b/etransport/codes.go index 9df5b79..9f2781a 100644 --- a/etransport/codes.go +++ b/etransport/codes.go @@ -14,6 +14,12 @@ package etransport +import ( + "strings" + + "github.com/printesoi/e-factura-go/text" +) + type DeclPostIncidentType string const ( @@ -784,6 +790,98 @@ const ( CountyCodeVN CountyCodeType = "39" ) +// RoCountyNameToCountyCode returns the county code for a Romanian county name. +// eg. "bucurești" -> "40" +func RoCountyNameToCountyCode(name string) (_ CountyCodeType, ok bool) { + switch strings.ToLower(text.Transliterate(name)) { + case "bucuresti", "municipiul bucuresti": + return CountyCodeB, true + case "alba": + return CountyCodeAB, true + case "arad": + return CountyCodeAR, true + case "arges": + return CountyCodeAG, true + case "bacau": + return CountyCodeBC, true + case "bihor": + return CountyCodeBH, true + case "bistrita-nasaud": + return CountyCodeBN, true + case "botosani": + return CountyCodeBT, true + case "braila": + return CountyCodeBR, true + case "brasov": + return CountyCodeBV, true + case "buzau": + return CountyCodeBZ, true + case "calarasi": + return CountyCodeCL, true + case "caras-severin": + return CountyCodeCS, true + case "cluj": + return CountyCodeCJ, true + case "constanta": + return CountyCodeCT, true + case "covasna": + return CountyCodeCV, true + case "dambovita": + return CountyCodeDB, true + case "dolj": + return CountyCodeDJ, true + case "galati": + return CountyCodeGL, true + case "giurgiu": + return CountyCodeGR, true + case "gorj": + return CountyCodeGJ, true + case "harghita": + return CountyCodeHR, true + case "hunedoara": + return CountyCodeHD, true + case "ialomita": + return CountyCodeIL, true + case "iasi": + return CountyCodeIS, true + case "ilfov": + return CountyCodeIF, true + case "maramures": + return CountyCodeMM, true + case "mehedinti": + return CountyCodeMH, true + case "mures": + return CountyCodeMS, true + case "neamt": + return CountyCodeNT, true + case "olt": + return CountyCodeOT, true + case "prahova": + return CountyCodePH, true + case "salaj": + return CountyCodeSJ, true + case "satu mare": + return CountyCodeSM, true + case "sibiu": + return CountyCodeSB, true + case "suceava": + return CountyCodeSV, true + case "teleorman": + return CountyCodeTR, true + case "timis": + return CountyCodeTM, true + case "tulcea": + return CountyCodeTL, true + case "vaslui": + return CountyCodeVS, true + case "valcea": + return CountyCodeVL, true + case "vrancea": + return CountyCodeVN, true + } + return +} + // Câmpul codScopOperatiune ia valori diferite, în funcţie de valoarea câmpului codTipOperatiune, astfel: // pentru codTipOperatiune = "10" AIC - Achiziţie intracomunitară: // From bf82052a916fbea6d57cb42b8a53ffc26c37a2cd Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sat, 12 Oct 2024 14:57:53 +0300 Subject: [PATCH 20/22] Add helper TokenRefresher for oauth2.Config and improve docs for oauth2 --- oauth2/config.go | 2 +- oauth2/token_source.go | 36 +++++++++++++++++++++++------------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/oauth2/config.go b/oauth2/config.go index c2e3984..9b91e24 100644 --- a/oauth2/config.go +++ b/oauth2/config.go @@ -74,7 +74,7 @@ func ConfigEndpoint(endpoint xoauth2.Endpoint) ConfigOption { // MakeConfig creates a Config using provided options. At least // ConfigCredentials must be provided, otherwise -// ErrInvoiceOAuth2Credentials will be returned. If an invalid endpoint if +// ErrInvalidOAuth2Credentials will be returned. If an invalid endpoint if // provided using ConfigEndpoint, then ErrInvalidOAuth2Endpoint is // returned. func MakeConfig(opts ...ConfigOption) (cfg Config, err error) { diff --git a/oauth2/token_source.go b/oauth2/token_source.go index e334261..f040de3 100644 --- a/oauth2/token_source.go +++ b/oauth2/token_source.go @@ -29,6 +29,9 @@ import ( ioauth2 "github.com/printesoi/e-factura-go/internal/oauth2" ) +// TokenChangedHandler is a handler provided to +// Config.TokenSourceWithChangedHandler or Config.TokenRefresher that is called +// when the token changes (is refreshed). type TokenChangedHandler func(ctx context.Context, t *xoauth2.Token) error // tokenFromInternal maps an *ioauth2.Token struct into @@ -125,27 +128,34 @@ func (s *reuseTokenSource) Token() (*xoauth2.Token, error) { return t, nil } -// TokenSource returns a TokenSource that returns t until t expires, -// automatically refreshing it as necessary using the provided context. -func (c *Config) TokenSource(ctx context.Context, t *xoauth2.Token) xoauth2.TokenSource { - tkr := &tokenRefresher{ - ctx: ctx, - conf: &c.Config, - } - if t != nil { - tkr.refreshToken = t.RefreshToken +// TokenRefresher returns a TokenSource that makes "grant_type"=="refresh_token" +// HTTP requests to renew a token using a RefreshToken. +// WARNING: the returned TokenSource is not safe for concurrent access, so you +// need to protect it with a mutex. It's recommended to use TokenSource instead. +func (c *Config) TokenRefresher(ctx context.Context, t *xoauth2.Token, onTokenChanged TokenChangedHandler) xoauth2.TokenSource { + if t == nil || t.RefreshToken == "" { + return nil } - return &reuseTokenSource{ - t: t, - new: tkr, + return &tokenRefresher{ + ctx: ctx, + conf: &c.Config, + refreshToken: t.RefreshToken, + onTokenChanged: onTokenChanged, } } +// TokenSource returns a TokenSource that returns t until t expires, +// automatically refreshing it as necessary using the provided context. The +// returned TokenSource is safe for concurrent access. +func (c *Config) TokenSource(ctx context.Context, t *xoauth2.Token) xoauth2.TokenSource { + return c.TokenSourceWithChangedHandler(ctx, t, nil) +} + // TokenSourceWithChangedHandler returns a TokenSource that returns t until t // expires, automatically refreshing it as necessary using the provided // context. Every time the access token is refreshed, the onTokenChanged // handler is called. This is useful if you need to update the token in a -// store/db. +// store/db. The returned TokenSource is safe for concurrent access. func (c *Config) TokenSourceWithChangedHandler(ctx context.Context, t *xoauth2.Token, onTokenChanged TokenChangedHandler) xoauth2.TokenSource { tkr := &tokenRefresher{ ctx: ctx, From c3ab05619de9d0d89f709e544a33c9f652c6a926 Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sat, 12 Oct 2024 15:36:13 +0300 Subject: [PATCH 21/22] README improvements and fix code snippets --- README.md | 53 +++++++++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 3411030..f36aad0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # e-factura-go [![Tests](https://github.com/printesoi/e-factura-go/actions/workflows/tests.yml/badge.svg)](https://github.com/printesoi/e-factura-go/actions/workflows/test.yml) [![Coverage Status](https://coveralls.io/repos/github/printesoi/e-factura-go/badge.svg)](https://coveralls.io/github/printesoi/e-factura-go) [![Go Report Card](https://goreportcard.com/badge/github.com/printesoi/e-factura-go)](https://goreportcard.com/report/github.com/printesoi/e-factura-go) -Package e-factura-go provides a client for using the e-factura and e-transport APIs. +Package e-factura-go provides a client for using the RO e-factura and RO e-transport APIs. ## NOTICE ## @@ -27,7 +27,7 @@ import ( ) oauth2Cfg, err := efactura_oauth2.MakeConfig( - efactura_oauth2.ConfigCredentials(anafAppClientID, anafApplientSecret), + efactura_oauth2.ConfigCredentials(anafAppClientID, anafAppClientSecret), efactura_oauth2.ConfigRedirectURL(anafAppRedirectURL), ) if err != nil { @@ -67,8 +67,9 @@ if err != nil { ## e-factura ## -This package can be use both for interacting with (calling) the ANAF e-factura -API via the Client object and for generating an Invoice UBL XML. +This package can be use both for interacting with (calling) the +[RO e-factura API](https://mfinante.gov.ro/ro/web/efactura/informatii-tehnice) +via the Client object and for generating an Invoice-2 UBL XML. Construct a new simple client for production environment: @@ -77,10 +78,8 @@ import ( "github.com/printesoi/e-factura-go/efactura" ) -client, err := efactura.NewProductionClient( - context.Background(), - efactura_oauth2.TokenSource(token), -) +ctx := context.TODO() +client, err := efactura.NewProductionClient(ctx, efactura_oauth2.TokenSource(ctx, token)) if err != nil { // Handle error } @@ -89,10 +88,8 @@ if err != nil { Construct a new simple client for sandbox (test) environment: ```go -client, err := efactura.NewSandboxClient( - context.Background(), - efactura_oauth2.TokenSource(token), -) +ctx := context.TODO() +client, err := efactura.NewSandboxClient(ctx, efactura_oauth2.TokenSource(ctx, token)) if err != nil { // Handle error } @@ -102,14 +99,13 @@ If you want to store the token in a store/db and update it everytime it refreshes use `efactura_oauth2.TokenSourceWithChangedHandler`: ```go +ctx := context.TODO() onTokenChanged := func(ctx context.Context, token *xoauth.Token) error { - fmt.Printf("Token changed...") + // Token changed, maybe update/store it in a database/persistent storage. return nil } -client, err := efactura.NewSandboxClient( - context.Background(), - efactura_oauth2.TokenSourceWithChangedHandler(token, onTokenChanged), -) +client, err := efactura.NewSandboxClient(ctx, + efactura_oauth2.TokenSourceWithChangedHandler(ctx, token, onTokenChanged)) if err != nil { // Handle error } @@ -319,10 +315,11 @@ if err := efactura.UnmarshalInvoice(data, &invoice); err != nil { **NOTE** Only use efactura.UnmarshalInvoice, because `encoding/xml` package cannot unmarshal a struct like efactura.Invoice due to namespace prefixes! -## e-Transport ## +## RO e-Transport ## The `etransport` package can be used for interacting with (calling) the -e-transport API via the Client object. +[RO e-Transport v2](https://mfinante.gov.ro/ro/web/etransport/informatii-tehnice) API +via the Client object or to build a declaration v2 XML using the PostingDeclarationV2 objects. Construct a new simple client for production environment: @@ -331,10 +328,8 @@ import ( "github.com/printesoi/e-factura-go/etransport" ) -client, err := etransport.NewProductionClient( - context.Background(), - efactura_oauth2.TokenSource(token), -) +ctx := context.TODO() +client, err := etransport.NewProductionClient(ctx, efactura_oauth2.TokenSource(ctx, token)) if err != nil { // Handle error } @@ -343,10 +338,8 @@ if err != nil { Construct a new simple client for sandbox (test) environment: ```go -client, err := etransport.NewSandboxClient( - context.Background(), - efactura_oauth2.TokenSource(token), -) +ctx := context.TODO() +client, err := etransport.NewSandboxClient(ctx, efactura_oauth2.TokenSource(ctx, token)) if err != nil { // Handle error } @@ -366,12 +359,13 @@ if uploadRes.IsOk() { fmt.Printf("Upload index: %d\n", uploadRes.GetUploadIndex()) } else { // The upload was not successful, check uploadRes.Errors + fmt.Printf("Upload failed: %s\n", uploadRes.GetFirstErrorMessage()) } ``` ### Get message state ### -you can check the message state for an upload index resulted from an upload: +Check the message state for an upload index resulted from an upload: ```go resp, err := client.GetMessageState(ctx, uploadIndex) @@ -401,6 +395,9 @@ if resp.IsOk() { for _, message := range resp.Messages { // Process message } +} else { + // Handle error + fmt.Printf("GetMessagesList failed: %s\n", resp.GetFirstErrorMessage()) } ``` From 5346d27a4ed01028df3c7ccc7c092aa08f9e5c8f Mon Sep 17 00:00:00 2001 From: Victor Dodon Date: Sat, 12 Oct 2024 15:37:36 +0300 Subject: [PATCH 22/22] Test Go version 1.23.x in github actions --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 26f46db..3152dd3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,6 +15,7 @@ jobs: go-version: - '1.21.x' - '1.22.x' + - '1.23.x' steps: - uses: actions/checkout@v4