Skip to content

Commit

Permalink
Add DownloadInvoiceParseZip method for Client
Browse files Browse the repository at this point in the history
The DownloadInvoiceParseZip method downloads the invoice/message, but
also parses the archive and returns the invoice/error message. The
signed document is not yet parsed and validated.
  • Loading branch information
printesoi committed Mar 18, 2024
1 parent 6c556a1 commit 4e5a4f8
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 2 deletions.
117 changes: 117 additions & 0 deletions rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package efactura

import (
"archive/zip"
"bytes"
"context"
"fmt"
"io"
Expand Down Expand Up @@ -61,6 +63,8 @@ type (
UploadIndex int64 `xml:"index_incarcare,attr"`
Message string `xml:"message,attr"`

// Hardcode the namespace here so we don't need a customer marshaling
// method.
XMLName xml.Name `xml:"mfp:anaf:dgti:spv:reqMesaj:v1 header"`
}

Expand All @@ -73,6 +77,8 @@ type (
ErrorMessage string `xml:"errorMessage,attr"`
} `xml:"Errors,omitempty"`

// Hardcode the namespace here so we don't need a customer marshaling
// method.
XMLName xml.Name `xml:"mfp:anaf:dgti:spv:respUploadFisier:v1 header"`
}

Expand All @@ -87,6 +93,8 @@ type (
ErrorMessage string `xml:"errorMessage,attr"`
} `xml:"Errors,omitempty"`

// Hardcode the namespace here so we don't need a customer marshaling
// method.
XMLName xml.Name `xml:"mfp:anaf:dgti:efactura:stareMesajFactura:v1 header"`
}

Expand Down Expand Up @@ -136,6 +144,29 @@ type (
Error *DownloadInvoiceResponseError
Zip []byte
}

// DownloadInvoiceParseZipResponse is the type returned by the
// DownloadInvoiceParseZip method. It includes the DownloadInvoiceResponse
// and also a *Invoice and a *InvoiceErrorMessage.
DownloadInvoiceParseZipResponse struct {
DownloadResponse *DownloadInvoiceResponse
Invoice *Invoice
InvoiceError *InvoiceErrorMessage
}

// DownloadInvoiceParseZip is the type corresponding to an Invoice message
// error from the download zip.
InvoiceErrorMessage struct {
UploadIndex int64 `xml:"Index_incarcare,attr,omitempty"`
CIFSeller string `xml:"Cif_emitent,attr,omitempty"`
Errors []struct {
ErrorMessage string `xml:"errorMessage,attr"`
} `xml:"Error,omitempty"`

// Hardcode the namespace here so we don't need a customer marshaling
// method.
XMLName xml.Name `xml:"mfp:anaf:dgti:efactura:mesajEroriFactuta:v1 header"`
}
)

const (
Expand Down Expand Up @@ -170,6 +201,9 @@ const (
var (
regexSellerCIF = regexp.MustCompile("\\bcif_emitent=(\\d+)")
regexBuyerCIF = regexp.MustCompile("\\bcif_beneficiar=(\\d+)")

regexZipFile = regexp.MustCompile("^\\d+.xml$")
regexZipSignatureFile = regexp.MustCompile("^semnatura_\\d+.xml$")
)

func (s ValidateStandard) String() string {
Expand Down Expand Up @@ -599,3 +633,86 @@ func (c *Client) DownloadInvoice(
}
return
}

// DownloadInvoiceParseZip same as DownloadInvoice but also parses the zip
// archive. If the response is not nil, the DownloadResponse will always be
// set. If there was an error parsing the zip archive, the response will
// contain the download response, and an error is returned.
func (c *Client) DownloadInvoiceParseZip(
ctx context.Context, downloadID int64,
) (response *DownloadInvoiceParseZipResponse, err error) {
dres, er := c.DownloadInvoice(ctx, downloadID)
if er != nil {
return nil, er
}

response = new(DownloadInvoiceParseZipResponse)
response.DownloadResponse = dres
if !dres.IsOk() {
return
}

var zr *zip.Reader
zr, err = zip.NewReader(bytes.NewReader(dres.Zip), int64(len(dres.Zip)))
if err != nil {
return
}

if len(zr.File) != 2 {
err = fmt.Errorf("Expected exactly 2 files in the archive, got %v", len(zr.File))
return
}

readAllZipFile := func(f *zip.File) ([]byte, error) {
zof, err := f.Open()
if err != nil {
return nil, err
}
defer zof.Close()
return io.ReadAll(zof)
}

for _, f := range zr.File {
if regexZipFile.MatchString(f.Name) {
data, er := readAllZipFile(f)
if err = er; err != nil {
return
}

// This is a trick for optimizing the unmarshaling: since the xml
// can be either an Invoice or an InvoiceErrorMessage, we create a
// struct with just an xml.Name, and based of the namespace we
// unmarshal one or the other.
type docName struct {
XMLName xml.Name
}
var doc docName
if err = xml.Unmarshal(data, &doc); err != nil {
return
}
switch doc.XMLName.Space {
case XMLNSInvoice2:
iv := new(Invoice)
if err = xml.Unmarshal(data, iv); err != nil {
return
}
response.Invoice = iv

case XMLNSMsgErrorV1:
ie := new(InvoiceErrorMessage)
if err = xml.Unmarshal(data, &ie); err != nil {
return
}
response.InvoiceError = ie

default:
err = fmt.Errorf("Invalid namespace for invoice/message '%q'", doc.XMLName.Space)
return
}
} else if regexZipSignatureFile.MatchString(f.Name) {
// TODO: parse the signed Invoice
}
}

return
}
6 changes: 4 additions & 2 deletions xmlns.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ const (
CIUSRO_v101 = "urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1"
// e-factura: UBL Version implemented
UBLVersionID = "2.1"

XMLNSMsgErrorV1 = "mfp:anaf:dgti:efactura:mesajEroriFactuta:v1"
)

func init() {
xml.NameSpaceBinding.Add("urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2", "cac")
xml.NameSpaceBinding.Add("urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2", "cbc")
xml.NameSpaceBinding.Add(XMLNSUBLcac, "cac")
xml.NameSpaceBinding.Add(XMLNSUBLcbc, "cbc")
}

0 comments on commit 4e5a4f8

Please sign in to comment.