Package e-factura-go provides a client for using the RO e-factura and RO e-transport APIs.
!!! This project is still in alpha stage, use it at you own risk !!!
e-factura-go requires Go version >= 1.21. With Go installed:
go get github.com/printesoi/e-factura-go
will resolve and add the package to the current development module, along with its dependencies.
Construct the required OAuth2 config needed for an e-factura or e-transport Client:
import (
efactura_oauth2 "github.com/printesoi/e-factura-go/pkg/oauth2"
)
oauth2Cfg, err := efactura_oauth2.MakeConfig(
efactura_oauth2.ConfigCredentials(anafAppClientID, anafAppClientSecret),
efactura_oauth2.ConfigRedirectURL(anafAppRedirectURL),
)
if err != nil {
// Handle error
}
Generate an authorization link for certificate authorization:
authorizeURL := oauth2Cfg.AuthCodeURL(state)
// Redirect the user to authorizeURL
Getting a token from an authorization code (the parameter code
sent via GET
to the redirect URL):
// Assuming the oauth2Cfg is built as above
token, err := oauth2Cfg.Exchange(ctx, authorizationCode)
if err != nil {
// Handle error
}
If you specified a non-empty state when building the authorization URL, you
will also receive the state
parameter with code
.
Parse the initial token from JSON:
token, err := efactura_oauth2.TokenFromJSON([]byte(tokenJSON))
if err != nil {
// Handle error
}
This package can be use both for interacting with (calling) the RO e-factura API via the Client object and for generating an Invoice-2 UBL XML.
Construct a new simple client for production environment:
import (
"github.com/printesoi/e-factura-go/pkg/efactura"
)
ctx := context.TODO()
client, err := efactura.NewProductionClient(ctx, efactura_oauth2.TokenSource(ctx, token))
if err != nil {
// Handle error
}
Construct a new simple client for sandbox (test) environment:
ctx := context.TODO()
client, err := efactura.NewSandboxClient(ctx, efactura_oauth2.TokenSource(ctx, token))
if err != nil {
// Handle error
}
If you want to store the token in a store/db and update it everytime it
refreshes use efactura_oauth2.TokenSourceWithChangedHandler
:
ctx := context.TODO()
onTokenChanged := func(ctx context.Context, token *xoauth.Token) error {
// Token changed, maybe update/store it in a database/persistent storage.
return nil
}
client, err := efactura.NewSandboxClient(ctx,
efactura_oauth2.TokenSourceWithChangedHandler(ctx, token, onTokenChanged))
if err != nil {
// Handle error
}
E-factura APIs expect dates to be in Romanian timezone and will return dates
and times in Romanian timezone. This library tries to load the
Europe/Bucharest
timezone location on init so that creating and parsing dates
will work as expected. The user of this library is responsible to ensure the
Europe/Bucharest
location is available. If you are not sure that the target
system will have system timezone database, you can use in you main
package:
import _ "time/tzdata"
to load the Go embedded copy of the timezone database.
var invoice efactura.Invoice
// Build invoice (manually, or with the InvoiceBuilder)
uploadRes, err := client.UploadInvoice(ctx, invoice, "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
}
For self-billed invoices, and/or if the buyer in not a Romanian entity, you can use
the UploadOptionSelfBilled()
, UploadOptionForeign()
upload options:
uploadRes, err := client.UploadInvoice(ctx, invoice, "123456789",
efactura.UploadOptionSelfBilled(), efactura.UploadOptionForeign())
If you have already the raw XML to upload (maybe you generated it by other means), you can use the UploadXML method.
To upload an Invoice XML:
uploadRes, err := client.UploadXML(ctx, xml, UploadStandardUBL, "123456789")
msg := efactura.RaspMessage{
UploadIndex: 5008787839,
Message: "test",
}
uploadRes, err := client.UploadRaspMessage(ctx, msg, "123456789")
if err != nil {
// Handle error
}
resp, err := client.GetMessageState(ctx, uploadIndex)
if err != nil {
// Handle error
}
switch {
case resp.IsOk():
// Uploaded invoice was processed
fmt.Printf("Download ID: %d\n", resp.GetDownloadID())
case resp.IsNok():
// Processing failed
case resp.IsProcessing():
// The message/invoice is still processing
case resp.IsInvalidXML():
// The uploaded XML is invalid
numDays := 7
resp, err := client.GetMessagesList(ctx, "123456789", numDays, MessageFilterAll)
if err != nil {
// Handle error
}
if resp.IsOk() {
for _, message := range resp.Messages {
switch {
case message.IsError():
// The message is an error for an upload
case message.IsSentInvoice():
// The message is a sent invoice
case message.IsReceivedInvoice():
// The message is a received invoice
case message.IsBuyerMessage():
// The message is a message from the buyer
}
}
}
downloadID := 3013004158
resp, err := client.DownloadInvoice(ctx, downloadID)
if err != nil {
// Handle error
}
if resp.IsOk() {
// The contents of the ZIP file is found in the resp.Zip byte slice.
}
var invoice Invoice
// Build invoice (manually, or with the InvoiceBuilder)
validateRes, err := client.ValidateInvoice(ctx, invoice)
if err != nil {
// Handle error
}
if validateRes.IsOk() {
// Validation successful
}
This library tries its best to overcome the not so clever API implementation and to detect limits exceeded errors. To check if the error is cause by a limit:
import (
efactura_errors "github.com/printesoi/e-factura-go/pkg/errors"
)
resp, err := client.GetMessageState(ctx, uploadIndex)
if err != nil {
var limitsErr *efactura_errors.LimitExceededError
var responseErr *efactura_errors.ErrorResponse
if errors.As(err, &limitsErr) {
// The limits were exceeded. limitsErr.ErrorResponse contains more
// information about the HTTP response, and the limitsErr.Limit field
// contains the limit for the day.
} else if errors.As(err, &responseErr) {
// ErrorResponse means we got the HTTP response but we failed to parse
// it or some other error like invalid response content type.
}
}
TODO: See TestInvoiceBuilder() from builders_test.go for an example of using InvoiceBuilder for creating an Invoice.
In case you need to get the XML encoding of the invoice (eg. you need to store it somewhere before the upload):
var invoice Invoice
// Build invoice (manually, or with the InvoiceBuilder)
xmlData, err := invoice.XML()
if err != nil {
// Handle error
}
To get the XML with indentation:
xmlData, err := invoice.XMLIndent("", " ")
if err != nil {
// Handle error
}
NOTE Don't use the standard encoding/xml
package for generating the XML
encoding, since it does not produce Canonical XML [XML-C14N]!
var invoice efactura.Invoice
if err := efactura.UnmarshalInvoice(data, &invoice); err != nil {
// Handle error
}
NOTE Only use efactura.UnmarshalInvoice, because encoding/xml
package
cannot unmarshal a struct like efactura.Invoice due to namespace prefixes!
The etransport
package can be used for interacting with (calling) the
RO e-Transport v2 API
via the Client object or to build a declaration v2 XML using the PostingDeclarationV2 objects.
Construct a new simple client for production environment:
import (
"github.com/printesoi/e-factura-go/pkg/etransport"
)
ctx := context.TODO()
client, err := etransport.NewProductionClient(ctx, efactura_oauth2.TokenSource(ctx, token))
if err != nil {
// Handle error
}
Construct a new simple client for sandbox (test) environment:
ctx := context.TODO()
client, err := etransport.NewSandboxClient(ctx, efactura_oauth2.TokenSource(ctx, token))
if err != nil {
// Handle error
}
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
fmt.Printf("Upload failed: %s\n", uploadRes.GetFirstErrorMessage())
}
Check the message state for an upload index resulted from an upload:
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
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
}
} else {
// Handle error
fmt.Printf("GetMessagesList failed: %s\n", resp.GetFirstErrorMessage())
}
Pull requests are more than welcome :)
This library is distributed under the Apache License version 2.0 found in the LICENSE file.
If you need help integrating this library in your software or you need consulting services regarding e-factura APIs contact me (contact email in my Github profile).