From b622807deee11d52c2ded2ca9cf9260969899589 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Thu, 15 Aug 2024 14:11:48 +0900 Subject: [PATCH] Initial commit --- .github/dependabot.yml | 15 ++++ .github/workflows/go.yml | 89 +++++++++++++++++++++ .gitignore | 3 + README.md | 3 +- date.go | 38 +++++++++ epostbusiness.go | 17 ++++ go.mod | 3 + letter.go | 162 +++++++++++++++++++++++++++++++++++++++ login.go | 82 ++++++++++++++++++++ 9 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/go.yml create mode 100644 date.go create mode 100644 epostbusiness.go create mode 100644 go.mod create mode 100644 letter.go create mode 100644 login.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2710e24 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + reviewers: + - "Fank" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + reviewers: + - "Fank" diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..385eb7e --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,89 @@ +name: Go + +on: + push: + branches: + - main + - master + pull_request: + +permissions: + contents: read + pull-requests: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Set up Golang build cache + uses: actions/cache@v4 + with: + path: ~/.cache/go-build + key: ${{ runner.os }}-golang-golangci-lint-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-golang- + + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Set up Golang build cache + uses: actions/cache@v4 + with: + path: ~/.cache/go-build + key: ${{ runner.os }}-golang-test-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-golang- + + - name: Download go modules + run: go mod download + + - name: Test + uses: robherley/go-test-action@v0 + with: + testArguments: ./... + + license-check: + runs-on: ubuntu-latest + name: License Check + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Install go-licenses + run: go install github.com/google/go-licenses@latest + shell: bash + + - name: Check licenses + run: go-licenses check ./... + shell: bash + + - name: Get licenses list + run: go-licenses csv ./... + shell: bash diff --git a/.gitignore b/.gitignore index 6f72f89..2852701 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ go.work.sum # env file .env + +# IDE +.idea \ No newline at end of file diff --git a/README.md b/README.md index d729276..eccf826 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # epostbusiness -E-POSTBUSINESS Box golang SDK + +[E-POSTBUSINESS Box](https://www.deutschepost.de/de/e/epost/geschaeftskunden/epost-business-box.html) golang SDK diff --git a/date.go b/date.go new file mode 100644 index 0000000..2a3d0ab --- /dev/null +++ b/date.go @@ -0,0 +1,38 @@ +package epostbusiness + +import ( + "errors" + "time" +) + +const TimeFormat = "2006-01-02T15:04:05.999" + +type Time struct { + time.Time +} + +func (t Time) MarshalJSON() ([]byte, error) { + if y := t.Time.Year(); y < 0 || y >= 10000 { + // RFC 3339 is clear that years are 4 digits exactly. + // See golang.org/issue/4556#c15 for more discussion. + return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]") + } + + b := make([]byte, 0, len(TimeFormat)+2) + b = append(b, '"') + b = t.Time.AppendFormat(b, TimeFormat) + b = append(b, '"') + + return b, nil +} + +func (t *Time) UnmarshalJSON(data []byte) error { + // Ignore null, like in the main JSON package. + if string(data) == "null" { + return nil + } + // Fractional seconds are handled implicitly by Parse. + var err error + t.Time, err = time.ParseInLocation(`"`+TimeFormat+`"`, string(data), time.Local) + return err +} diff --git a/epostbusiness.go b/epostbusiness.go new file mode 100644 index 0000000..66d5e55 --- /dev/null +++ b/epostbusiness.go @@ -0,0 +1,17 @@ +package epostbusiness + +import "net/http" + +const url = "https://api.epost.docuguide.com" + +type API struct { + Client *http.Client + + jwt string +} + +func New() *API { + return &API{ + Client: http.DefaultClient, + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3d2e6af --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/enthus-golang/epostbusiness + +go 1.22.6 diff --git a/letter.go b/letter.go new file mode 100644 index 0000000..0a371a7 --- /dev/null +++ b/letter.go @@ -0,0 +1,162 @@ +package epostbusiness + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" +) + +type Letter struct { + FileName string `json:"fileName" validate:"min=5,max=200"` + Data string `json:"data"` + IsColor bool `json:"isColor"` + IsDuplex bool `json:"isDuplex"` + BatchID int `json:"batchID"` + TestFlag bool `json:"testFlag"` + TestShowRestrictedArea bool `json:"testShowRestrictedArea"` + TestEMail string `json:"testEMail"` + AddressLine1 string `json:"addressLine1"` + AddressLine2 string `json:"addressLine2"` + AddressLine3 string `json:"addressLine3"` + ZipCode string `json:"zipCode"` + City string `json:"city"` + Country string `json:"country"` + SenderAdressLine1 string `json:"senderAdressLine1"` + SenderStreet string `json:"senderStreet"` + SenderZipCode string `json:"senderZipCode"` + SenderCity string `json:"senderCity"` + Custom1 string `json:"custom1"` + Custom2 string `json:"custom2"` +} + +type LetterIdentifier struct { + FileName string `json:"fileName"` + LetterID int `json:"letterID"` +} + +func (a API) CreateLetters(ctx context.Context, letters []Letter) ([]LetterIdentifier, error) { + var letterIdentifiers []LetterIdentifier + + body, err := json.Marshal(letters) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url+"/api/Letter", bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+a.jwt) + + res, err := a.Client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + switch res.StatusCode { + case http.StatusOK: + err = json.NewDecoder(res.Body).Decode(&letterIdentifiers) + if err != nil { + return nil, err + } + + return letterIdentifiers, nil + + case http.StatusBadRequest: + fallthrough + case http.StatusUnauthorized: + fallthrough + case http.StatusTooManyRequests: + var loginErr loginError + + err = json.NewDecoder(res.Body).Decode(&loginErr) + if err != nil { + return nil, err + } + + return nil, fmt.Errorf("%s: %s", loginErr.Code, loginErr.Description) + + default: + return nil, errors.New(res.Status) + } +} + +type LetterStatus struct { + LetterID int `json:"letterID"` + FileName string `json:"fileName"` + StatusID int `json:"statusID"` + StatusDetails *string `json:"statusDetails"` + CreatedDate Time `json:"createdDate"` + ProcessedDate *Time `json:"processedDate"` + PrintUploadDate *Time `json:"printUploadDate"` + PrintFeedbackDate *Time `json:"printFeedbackDate"` + TestFlag *bool `json:"testFlag"` + TestEMail *string `json:"testEMail"` + TestShowRestrictedArea bool `json:"testShowRestrictedArea"` + RegisteredLetter *string `json:"registeredLetter"` + RegisteredLetterID *string `json:"registeredLetterID"` + BatchID int `json:"batchID"` + CoverLetter bool `json:"coverLetter"` + NoOfPages int `json:"noOfPages"` + SubVendorID *string `json:"subVendorID"` + Custom1 *string `json:"custom1"` + Custom2 *string `json:"custom2"` + Custom3 *string `json:"custom3"` + Custom4 *string `json:"custom4"` + Custom5 *string `json:"custom5"` + ZipCode *string `json:"zipCode"` + City *string `json:"city"` + Country *string `json:"country"` + IsColor bool `json:"isColor"` + IsDuplex bool `json:"isDuplex"` + RegisteredLetterStatus *string `json:"registeredLetterStatus"` + RegisteredLetterStatusDate *Time `json:"registeredLetterStatusDate"` + ErrorList []LetterError `json:"errorList"` +} + +type LetterError struct { + Description string `json:"description"` + Level string `json:"level"` + Code string `json:"code"` + Date *Time `json:"date"` +} + +func (a API) GetLettersStatusList(ctx context.Context, letterIDs []int) ([]LetterStatus, error) { + var statusList []LetterStatus + + body, err := json.Marshal(letterIDs) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url+"/api/Letter/StatusQuery", bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+a.jwt) + + res, err := a.Client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%d: %s", res.StatusCode, res.Status) + } + + err = json.NewDecoder(res.Body).Decode(&statusList) + if err != nil { + return nil, err + } + + return statusList, nil +} diff --git a/login.go b/login.go new file mode 100644 index 0000000..242ee75 --- /dev/null +++ b/login.go @@ -0,0 +1,82 @@ +package epostbusiness + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" +) + +type login struct { + VendorID string `json:"vendorID"` + EKP string `json:"ekp"` + Secret string `json:"secret"` + Password string `json:"password"` +} + +type loginToken struct { + Token string `json:"token"` +} + +type loginError struct { + Level string `json:"level"` + Code string `json:"code"` + Description string `json:"description"` + Date time.Time `json:"date"` +} + +func (a *API) Login(ctx context.Context, vendorID, ekp, secret, password string) (bool, error) { + body, err := json.Marshal(login{ + VendorID: vendorID, + EKP: ekp, + Secret: secret, + Password: password, + }) + if err != nil { + return false, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url+"/api/Login", bytes.NewReader(body)) + if err != nil { + return false, err + } + + req.Header.Set("Content-Type", "application/json") + + res, err := a.Client.Do(req) + if err != nil { + return false, err + } + defer res.Body.Close() + + switch res.StatusCode { + case http.StatusOK: + var token loginToken + err = json.NewDecoder(res.Body).Decode(&token) + if err != nil { + return false, err + } + + a.jwt = token.Token + return true, nil + + case http.StatusBadRequest: + fallthrough + case http.StatusUnauthorized: + fallthrough + case http.StatusTooManyRequests: + var loginErr loginError + err = json.NewDecoder(res.Body).Decode(&loginErr) + if err != nil { + return false, err + } + + return false, fmt.Errorf("%s: %s", loginErr.Code, loginErr.Description) + + default: + return false, errors.New(res.Status) + } +}