From e11fc8c4829b1c3eff3f6102e8b13e905842ec37 Mon Sep 17 00:00:00 2001 From: Grzegorz Lisowski Date: Fri, 2 Feb 2024 15:06:43 +0100 Subject: [PATCH] Add posting invoice into the CLI We want to be able to post invoices into the KSeF API and get back the UPO that proves that the invoice has been accepted by the system. Here we get the parameters from the CLI and use the requests we prepared to post the invoice and save the UPO. The example command would look like go run ./cmd/gobl.ksef send ./test/data/out/invoice-pl-pl.xml 1234567788 624A48824F01935DADE66C83D4874C0EF7AF0529CB5F0F412E6932F189D3864A ./api/keys/test.pem --- cmd/gobl.ksef/main.go | 21 ++++++ cmd/gobl.ksef/root.go | 1 + cmd/gobl.ksef/send.go | 148 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 cmd/gobl.ksef/send.go diff --git a/cmd/gobl.ksef/main.go b/cmd/gobl.ksef/main.go index 79729de..8e40324 100644 --- a/cmd/gobl.ksef/main.go +++ b/cmd/gobl.ksef/main.go @@ -45,3 +45,24 @@ func inputFilename(args []string) string { } return "" } + +func inputNip(args []string) string { + if len(args) > 1 && args[1] != "-" { + return args[1] + } + return "" +} + +func inputToken(args []string) string { + if len(args) > 2 && args[2] != "-" { + return args[2] + } + return "" +} + +func inputKeyPath(args []string) string { + if len(args) > 3 && args[3] != "-" { + return args[3] + } + return "" +} diff --git a/cmd/gobl.ksef/root.go b/cmd/gobl.ksef/root.go index 003c10a..30f1418 100644 --- a/cmd/gobl.ksef/root.go +++ b/cmd/gobl.ksef/root.go @@ -22,6 +22,7 @@ func (o *rootOpts) cmd() *cobra.Command { } cmd.AddCommand(versionCmd()) + cmd.AddCommand(send(o).cmd()) cmd.AddCommand(convert(o).cmd()) return cmd diff --git a/cmd/gobl.ksef/send.go b/cmd/gobl.ksef/send.go new file mode 100644 index 0000000..c4e5e40 --- /dev/null +++ b/cmd/gobl.ksef/send.go @@ -0,0 +1,148 @@ +package main + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "os" + "time" + + ksef_api "github.com/invopop/gobl.ksef/api" + "github.com/spf13/cobra" +) + +type sendOpts struct { + *rootOpts +} + +func send(o *rootOpts) *sendOpts { + return &sendOpts{rootOpts: o} +} + +func (c *sendOpts) cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "send [infile] [nip] [token] [keyPath]", + Short: "Send a GOBL JSON to the KSeF API", + RunE: c.runE, + } + + return cmd +} + +func (c *sendOpts) runE(cmd *cobra.Command, args []string) error { + // ctx := commandContext(cmd) + nip := inputNip(args) + token := inputToken(args) + keyPath := inputKeyPath(args) + + input, err := openInput(cmd, args) + if err != nil { + return err + } + defer func() { + err = input.Close() + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + }() + + data, err := io.ReadAll(input) + if err != nil { + return fmt.Errorf("reading input: %w", err) + } + + client := ksef_api.NewClient( + ksef_api.WithID(nip), + ksef_api.WithToken(token), + ksef_api.WithKeyPath(keyPath), + ) + + _, err = SendInvoice(client, data) + if err != nil { + return fmt.Errorf("sending invoices: %w", err) + } + return nil +} + +// SendInvoice sends invoices to KSeF +func SendInvoice(c *ksef_api.Client, data []byte) (string, error) { + ctx := context.Background() + + err := ksef_api.FetchSessionToken(ctx, c) + if err != nil { + return "", err + } + + sendInvoiceResponse, err := ksef_api.SendInvoice(ctx, c, data) + if err != nil { + return "", err + } + + _, err = waitUntilInvoiceIsProcessed(ctx, c, sendInvoiceResponse.ElementReferenceNumber) + if err != nil { + return "", err + } + + res, err := waitUntilSessionIsTerminated(ctx, c) + if err != nil { + return "", err + } + upoBytes, err := base64.StdEncoding.DecodeString(res.Upo) + if err != nil { + return "", err + } + file, err := os.Create(res.ReferenceNumber + ".xml") + if err != nil { + return "", err + } + defer func() { + if err := file.Close(); err != nil { + fmt.Println("Error when closing:", err) + } + }() + _, err = file.Write(upoBytes) + if err != nil { + return "", err + } + + return string(upoBytes), nil +} + +func waitUntilInvoiceIsProcessed(ctx context.Context, c *ksef_api.Client, referenceNumber string) (*ksef_api.InvoiceStatusResponse, error) { + for { + status, err := ksef_api.FetchInvoiceStatus(ctx, c, referenceNumber) + if err != nil { + return nil, err + } + if status.ProcessingCode == 200 || status.ProcessingCode == 404 || status.ProcessingCode == 400 { + return status, nil + } + sleepContext(ctx, 5*time.Second) + } +} + +func waitUntilSessionIsTerminated(ctx context.Context, c *ksef_api.Client) (*ksef_api.SessionStatusByReferenceResponse, error) { + _, err := ksef_api.TerminateSession(ctx, c) + if err != nil { + return nil, err + } + for { + status, err := ksef_api.GetSessionStatusByReference(ctx, c) + + if err != nil { + return nil, err + } + if status.ProcessingCode == 200 { + return status, nil + } + sleepContext(ctx, 5*time.Second) + } +} + +func sleepContext(ctx context.Context, delay time.Duration) { + select { + case <-ctx.Done(): + case <-time.After(delay): + } +}