Changed dockerfile, added error handling to /submit-document.
This commit is contained in:
parent
8bd5d260a1
commit
24581a4cea
|
|
@ -9,7 +9,7 @@ COPY . .
|
|||
RUN CGO_ENABLED=0 GOOS=linux go build -o iralex-einvoice main.go
|
||||
|
||||
# --- Run stage ---
|
||||
FROM gcr.io/distroless/base-debian12
|
||||
FROM gcr.io/distroless/base-debian12:debug
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/iralex-einvoice ./iralex-einvoice
|
||||
COPY --from=builder /app/.env.example ./.env.example
|
||||
|
|
@ -23,4 +23,4 @@ COPY --from=builder /app/.env.example ./.env.example
|
|||
EXPOSE 1404
|
||||
|
||||
# Entrypoint
|
||||
ENTRYPOINT ["/app/iralex-einvoice"]
|
||||
ENTRYPOINT ["/app/iralex-einvoice"]
|
||||
|
|
@ -2,9 +2,9 @@
|
|||
package config
|
||||
|
||||
const (
|
||||
PRODUCTION_URL = "https://api.myinvois.hasil.gov.my"
|
||||
SANDBOX_URL = "https://preprod-api.myinvois.hasil.gov.my"
|
||||
BASE_URL = PRODUCTION_URL
|
||||
LHDN_PRODUCTION_URL = "https://api.myinvois.hasil.gov.my"
|
||||
LHDN_SANDBOX_URL = "https://preprod-api.myinvois.hasil.gov.my"
|
||||
LHDN_BASE_URL = LHDN_PRODUCTION_URL
|
||||
|
||||
DEFAULT_PORT = "1404"
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ var (
|
|||
CERTIFICATE_PASSWORD string // the PIN of the aforementioned PKCS12 file
|
||||
PORT string // the port the program listens to
|
||||
DEBUG bool = false // whether to use the debug or production environment
|
||||
LOCAL bool = false // whether we're doing local development or deploying for production
|
||||
IRALEX_BACKEND_BASE_URL string
|
||||
IRALEX_AUTH_EMAIL string // email address used to get an access token from the Iralex backend
|
||||
IRALEX_AUTH_PASSWORD string // password used to get an access token from the Iralex backend
|
||||
|
|
@ -52,6 +53,7 @@ func SetUpEnvironmentVariables() {
|
|||
|
||||
PORT = os.Getenv("PORT")
|
||||
_DEBUG := os.Getenv("DEBUG")
|
||||
_LOCAL := os.Getenv("DEBUG")
|
||||
|
||||
IRALEX_BACKEND_BASE_URL = os.Getenv("IRALEX_BACKEND_BASE_URL") // required
|
||||
IRALEX_AUTH_EMAIL = os.Getenv("IRALEX_AUTH_EMAIL") // optional
|
||||
|
|
@ -121,6 +123,13 @@ func SetUpEnvironmentVariables() {
|
|||
// add more here...
|
||||
}
|
||||
|
||||
if _LOCAL != "" {
|
||||
LOCAL, err = strconv.ParseBool(_LOCAL)
|
||||
if err != nil {
|
||||
LOCAL = false
|
||||
}
|
||||
}
|
||||
|
||||
if mustTerminate {
|
||||
fmt.Printf("Provide the specified environment variables first in order to proceed further.\n")
|
||||
os.Exit(1)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package route
|
||||
|
||||
// This file contains error types that different routes use to communicate
|
||||
// failure states with the rest of the application.
|
||||
|
||||
type SubmitDocRejectionError struct {
|
||||
RejectedDocuments []string
|
||||
}
|
||||
|
||||
func (err SubmitDocRejectionError) Error() string {
|
||||
var result = "Some of the documents were rejected: "
|
||||
for _, value := range err.RejectedDocuments {
|
||||
result += value + ", "
|
||||
}
|
||||
return result[:len(result)-2]
|
||||
}
|
||||
|
||||
type SubmitDocBadStructureError struct {
|
||||
}
|
||||
|
||||
func (err SubmitDocBadStructureError) Error() string {
|
||||
return "the submitted document was ill-formed"
|
||||
}
|
||||
|
||||
type SubmitDocMaxSizeError struct {
|
||||
}
|
||||
|
||||
func (err SubmitDocMaxSizeError) Error() string {
|
||||
return "maximum size exceeded: 5 MB maximum submission size, 100 maximum e-Invoices per submission, and 300 KB maximum per e-Invoice"
|
||||
}
|
||||
|
||||
type SubmitDocDuplicateError struct {
|
||||
}
|
||||
|
||||
func (err SubmitDocDuplicateError) Error() string {
|
||||
return "duplicate submission was sent not so long ago"
|
||||
}
|
||||
|
|
@ -27,11 +27,12 @@ func AuthenticateFormData(ClientID string, ClientSecret string, TIN string) stri
|
|||
var diff time.Duration = now.Sub(old)
|
||||
|
||||
if diff.Seconds() <= 1 {
|
||||
core.LogInfo.Printf("INFO: Using access token from cache: Now=%s Expiration Date=%s\n Diff(seconds)=%f", now.Format("2006-01-02T15:04:05"), expirationDate, diff.Seconds())
|
||||
return accessToken
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Sending Request (Authenticate - Form Data):\n-----------------")
|
||||
core.LogInfo.Println("Sending Request /connect/token (Form Data):")
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("client_id", ClientID)
|
||||
|
|
@ -39,9 +40,9 @@ func AuthenticateFormData(ClientID string, ClientSecret string, TIN string) stri
|
|||
form.Add("grant_type", "client_credentials")
|
||||
form.Add("scope", "InvoicingAPI")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, config.BASE_URL+"/connect/token", strings.NewReader(form.Encode()))
|
||||
req, err := http.NewRequest(http.MethodPost, config.LHDN_BASE_URL+"/connect/token", strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
log.Fatalf("Error in constructing the request: %v", err)
|
||||
panic(fmt.Sprintf("Error in constructing the request: %w", err))
|
||||
}
|
||||
|
||||
req.PostForm = form
|
||||
|
|
@ -50,22 +51,25 @@ func AuthenticateFormData(ClientID string, ClientSecret string, TIN string) stri
|
|||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("onbehalfof", TIN)
|
||||
|
||||
bytes, _ := httputil.DumpRequestOut(req, true)
|
||||
fmt.Printf("%s\n", bytes)
|
||||
if core.DEBUG {
|
||||
bytes, _ := httputil.DumpRequestOut(req, true)
|
||||
core.LogInfo.Printf("INFO: Request:\n%s\n", bytes)
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * config.DEFAULT_TIMEOUT,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
// resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Fatalf("Error in sending the request: %v", err)
|
||||
panic(fmt.Sprintf("Error in sending the request: %v", err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var res map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&res)
|
||||
fmt.Printf("\n\n\nResponse:\n---------\n%v\n", res)
|
||||
if core.DEBUG {
|
||||
core.LogInfo.Printf("INFO: Response:\n%v\n", res)
|
||||
}
|
||||
|
||||
accessToken = fmt.Sprintf("%v", res["access_token"])
|
||||
expiresIn := res["expires_in"].(int)
|
||||
|
|
@ -85,7 +89,7 @@ func AuthenticateJSON(ClientID string, ClientSecret string, TIN string) {
|
|||
"scope": "InvoicingAPI",
|
||||
})
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, config.BASE_URL+"/connect/token", bytes.NewBuffer(body))
|
||||
req, err := http.NewRequest(http.MethodPost, config.LHDN_BASE_URL+"/connect/token", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
log.Fatalf("Error in constructing the request: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"iralex-einvoice/config"
|
||||
"iralex-einvoice/core"
|
||||
"iralex-einvoice/document"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
|
|
@ -26,22 +26,44 @@ type requestDocument struct {
|
|||
CodeNumber string `json:"codeNumber"`
|
||||
}
|
||||
|
||||
// LHDN standard error format, according to: https://sdk.myinvois.hasil.gov.my/standard-error-response/
|
||||
type lhdnStdError struct {
|
||||
PropertyName *string `json:"propertyName"`
|
||||
PropertyPath *string `json:"propertyPath"`
|
||||
ErrorCode *string `json:"errorCode"`
|
||||
Error *string `json:"error"`
|
||||
ErrorMS *string `json:"errorMS"`
|
||||
Target *string `json:"target"`
|
||||
InnerError []lhdnStdError `json:"innerError"`
|
||||
}
|
||||
|
||||
type responseSuccess struct {
|
||||
SubmissionID string `json:"submissionUID"`
|
||||
AcceptedDocuments []struct {
|
||||
UUID string `json:"uuid"`
|
||||
InvoiceCodeNumber string `json:"invoiceCodeNumber"`
|
||||
} `json:"acceptedDocuments"`
|
||||
RejectedDocuments []struct {
|
||||
InvoiceCodeNumber string `json:"invoiceCodeNumber"`
|
||||
Error lhdnStdError `json:"error"`
|
||||
} `json:"rejectedDocuments"`
|
||||
}
|
||||
|
||||
// Submits a document to LHDN.
|
||||
func SubmitDocument(accessToken string, doc document.UBLInvoiceRoot) {
|
||||
fmt.Println("Sending Request (/submit-document):\n-----------------------------------")
|
||||
func SubmitDocument(accessToken string, doc document.UBLInvoiceRoot) error {
|
||||
core.LogInfo.Println("INFO: Sending Request (/submit-document):")
|
||||
|
||||
// Minify and convert the document to base64.
|
||||
minified_doc, err := json.Marshal(doc)
|
||||
minifiedDoc, err := json.Marshal(doc)
|
||||
if err != nil {
|
||||
// Unexpected Error - This should never happen
|
||||
panic(fmt.Sprintf("Couldn't minify signed props - %s\n", err.Error()))
|
||||
}
|
||||
|
||||
var sEnc string = b64.StdEncoding.EncodeToString(minified_doc)
|
||||
var sEnc string = b64.StdEncoding.EncodeToString(minifiedDoc)
|
||||
|
||||
// Create a SHA256 hash of the document.
|
||||
hash := sha256.New()
|
||||
hash.Write(minified_doc)
|
||||
hash.Write(minifiedDoc)
|
||||
|
||||
body, err := json.Marshal(requestBody{
|
||||
Documents: []requestDocument{
|
||||
|
|
@ -54,20 +76,23 @@ func SubmitDocument(accessToken string, doc document.UBLInvoiceRoot) {
|
|||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Err %v\n", err.Error())
|
||||
return fmt.Errorf("Couldn't marshal the request body: %w\n", err)
|
||||
}
|
||||
|
||||
// Pretty print the document/body.
|
||||
// var out bytes.Buffer
|
||||
// err = json.Indent(&out, body, "", "\t")
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// fmt.Println(string(out.Bytes()))
|
||||
if core.DEBUG {
|
||||
var out bytes.Buffer
|
||||
err = json.Indent(&out, body, "", " ")
|
||||
if err != nil {
|
||||
// Unexpected Error - This should never happen
|
||||
panic(fmt.Sprintf("Couldn't marshal document body - %s\n", err.Error()))
|
||||
}
|
||||
core.LogInfo.Println("INFO: Request body:\n", string(out.Bytes()))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, config.BASE_URL+"/api/v1.0/documentsubmissions/", bytes.NewBuffer(body))
|
||||
req, err := http.NewRequest(http.MethodPost, config.LHDN_BASE_URL+"/api/v1.0/documentsubmissions/", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
log.Fatalf("Error in constructing the request: %v", err)
|
||||
return fmt.Errorf("Couldn't construct the /submit-document request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", "application/json")
|
||||
|
|
@ -75,23 +100,55 @@ func SubmitDocument(accessToken string, doc document.UBLInvoiceRoot) {
|
|||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("Authorization", "Bearer "+accessToken)
|
||||
|
||||
bytes, _ := httputil.DumpRequestOut(req, true)
|
||||
fmt.Printf("%s\n", bytes)
|
||||
if core.DEBUG {
|
||||
bytes, _ := httputil.DumpRequestOut(req, true)
|
||||
fmt.Printf("Request:\n%s\n", bytes)
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
Timeout: time.Second * config.DEFAULT_TIMEOUT,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
// resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Fatalf("Error in sending the request: %v", err)
|
||||
return fmt.Errorf("Couldn't send the request: %w\n", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var res map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&res)
|
||||
b, _ := json.MarshalIndent(res, "", " ")
|
||||
fmt.Printf("\n\n\nResponse:\n---------\n%d %v\n", resp.StatusCode, string(b))
|
||||
if resp.StatusCode == 202 {
|
||||
var res responseSuccess
|
||||
json.NewDecoder(resp.Body).Decode(&res)
|
||||
if core.DEBUG {
|
||||
b, _ := json.MarshalIndent(res, "", " ")
|
||||
core.LogInfo.Printf("INFO: Response (%d):\n%s\n", resp.StatusCode, string(b))
|
||||
}
|
||||
if len(res.RejectedDocuments) > 0 {
|
||||
var rejectedDocs []string
|
||||
for _, value := range res.RejectedDocuments {
|
||||
_ = append(rejectedDocs, value.InvoiceCodeNumber)
|
||||
}
|
||||
return SubmitDocRejectionError{
|
||||
RejectedDocuments: rejectedDocs,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
var res lhdnStdError
|
||||
json.NewDecoder(resp.Body).Decode(&res)
|
||||
if core.DEBUG {
|
||||
b, _ := json.MarshalIndent(res, "", " ")
|
||||
core.LogInfo.Printf("INFO: Response (%d):\n%s\n", resp.StatusCode, string(b))
|
||||
}
|
||||
|
||||
if resp.StatusCode == 400 && *res.ErrorCode == "BadStructure" {
|
||||
return SubmitDocBadStructureError{}
|
||||
} else if resp.StatusCode == 400 && *res.ErrorCode == "MaximumSizeExceeded" {
|
||||
return SubmitDocMaxSizeError{}
|
||||
} else if resp.StatusCode == 422 {
|
||||
return SubmitDocDuplicateError{}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
// var bill = database.Bill{}
|
||||
// document.SubmitDocument("aasdasd")
|
||||
|
|
|
|||
|
|
@ -191,7 +191,17 @@ func HandleSubmit(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
accessToken := route.AuthenticateFormData(core.CLIENT_ID, core.CLIENT_SECRET_1, core.TIN)
|
||||
route.SubmitDocument(accessToken, doc)
|
||||
err = route.SubmitDocument(accessToken, doc)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintln(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// If we reached here, everything worked fine.
|
||||
|
||||
fmt.Fprint(w, "success")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ func Run() {
|
|||
|
||||
http.HandleFunc("/ping", HandlePing)
|
||||
|
||||
if core.DEBUG {
|
||||
if core.LOCAL {
|
||||
http.HandleFunc("/submit", HandleSubmitDebug)
|
||||
} else {
|
||||
http.HandleFunc("/submit", HandleSubmit)
|
||||
|
|
|
|||
Loading…
Reference in New Issue