Skip to content

Commit

Permalink
Add efactura-cli command
Browse files Browse the repository at this point in the history
- The command does not yet implement all api methods, just generate
  authorize URL, exchange device code, download invoice, validate
  invoice.
  • Loading branch information
printesoi committed May 4, 2024
1 parent 9de760e commit 4738bf9
Show file tree
Hide file tree
Showing 12 changed files with 705 additions and 19 deletions.
38 changes: 38 additions & 0 deletions cmd/efactura-cli/cmd/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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"
)

// apiCmd represents the auth command
var apiCmd = &cobra.Command{
Use: "api",
Short: "e-factura API calls",
}

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)
}
94 changes: 94 additions & 0 deletions cmd/efactura-cli/cmd/api_download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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 (
"bytes"
"context"
"fmt"
"io"
"os"

"github.com/spf13/cobra"
)

const (
flagNameDownloadID = "id"
flagNameDownloadOutFile = "out"
)

// apiDownloadCmd represents the `api download` command
var apiDownloadCmd = &cobra.Command{
Use: "download",
Short: "Download an Invoice zip",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := newEfacturaClient(ctx, cmd)
if err != nil {
cmd.SilenceUsage = true
return err
}

fvDownloadID, err := cmd.Flags().GetInt64(flagNameDownloadID)
if err != nil {
return err
}

res, err := client.DownloadInvoice(ctx, fvDownloadID)
if err != nil {
cmd.SilenceUsage = true
return err
}
if !res.IsOk() {
cmd.SilenceUsage = true
return fmt.Errorf("error downloading invoice %d: %s", fvDownloadID, res.Error.Error)
}

fvOutFile, err := cmd.Flags().GetString(flagNameDownloadOutFile)
if err != nil {
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 := io.Copy(out, bytes.NewReader(res.Zip)); err != nil {
cmd.SilenceUsage = true
fmt.Fprintf(os.Stderr, "error copying data for invoice %d: %v\n", fvDownloadID, err)
return err
}

return nil
},
}

func init() {
apiDownloadCmd.Flags().Int64(flagNameDownloadID, 0, "Download ID for the Invoice zip")
_ = apiDownloadCmd.MarkFlagRequired(flagNameDownloadID)

apiDownloadCmd.Flags().String(flagNameDownloadOutFile, "", "Write output to this file instead of stdout")

apiCmd.AddCommand(apiDownloadCmd)
}
75 changes: 75 additions & 0 deletions cmd/efactura-cli/cmd/api_validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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 (
"bytes"
"context"
"fmt"
"github.com/printesoi/e-factura-go/efactura"
"github.com/spf13/cobra"
"os"
)

const (
flagNameUploadInvoiceXML = "invoice-xml"
)

// apiValidateInvoiceCmd represents the `api download` command
var apiValidateInvoiceCmd = &cobra.Command{
Use: "validate-invoice",
Short: "Validate Invoice UBL XML",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()

fvInvoiceXML, err := cmd.Flags().GetString(flagNameUploadInvoiceXML)
if err != nil {
return err
}

xmlData, err := os.ReadFile(fvInvoiceXML)
if err != nil {
cmd.SilenceUsage = true
return fmt.Errorf("failed to read file: %w", err)
}

client, err := newEfacturaClient(ctx, cmd)
if err != nil {
cmd.SilenceUsage = true
return err
}

validateRes, err := client.ValidateXML(ctx, bytes.NewReader(xmlData), efactura.ValidateStandardFACT1)
if err != nil {
cmd.SilenceUsage = true
return fmt.Errorf("failed to validate Invoice UBL XML: %w", err)
}
if validateRes.IsOk() {
fmt.Println("successfully validated Invoice UBL XML")
} else {
cmd.SilenceUsage = true
return fmt.Errorf("validate Invoice UBL XML failed: %s", validateRes.GetFirstMessage())
}

return nil
},
}

func init() {
apiValidateInvoiceCmd.Flags().String(flagNameUploadInvoiceXML, "", "Path of the Invoice XML file to validate")
_ = apiValidateInvoiceCmd.MarkFlagRequired(flagNameUploadInvoiceXML)

apiCmd.AddCommand(apiValidateInvoiceCmd)
}
36 changes: 36 additions & 0 deletions cmd/efactura-cli/cmd/auth.go
Original file line number Diff line number Diff line change
@@ -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)
}
68 changes: 68 additions & 0 deletions cmd/efactura-cli/cmd/auth_exchange_code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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)
}
73 changes: 73 additions & 0 deletions cmd/efactura-cli/cmd/auth_get_authorize_link.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 4738bf9

Please sign in to comment.