Changed dockerfile, added error handling to /submit-document.

This commit is contained in:
Ali Arya 2025-07-12 17:41:17 +03:30
parent 8bd5d260a1
commit 24581a4cea
8 changed files with 158 additions and 41 deletions

View File

@ -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"]

View File

@ -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"

View File

@ -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)

37
route/errors.go Normal file
View File

@ -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"
}

View File

@ -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)
}

View File

@ -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")

View File

@ -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")
}
// =============================================================================

View File

@ -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)