Skip to content

Commit

Permalink
PFS-165 moto payment line failed email (#17)
Browse files Browse the repository at this point in the history
* PFS-143 Create back-end structure similar to finance-hub

* PFS-143 Create back-end structure similar to finance-hub

* PFS-143 Add test upload file code

* PFS-143 Update makefile and build yml

* PFS-143 Fix linting

* PFS-143 Add localstack for local bucket

* PFS-143 Upload file from form

* PFS-143 Fix linting

* PFS-143 Add SSE to upload

* PFS-143 Add success message, remove test upload type

* PFS-143 Migrate to aws-sdk-go-v2

* PFS-143 Attempt to fix blank endpoint

* PFS-143 Attempt to fix blank endpoint

* PFS-143 Undo previous change

* PFS-143 Move uploads to subfolder in bucket

* PFS-143 Move CSV header validation to back-end

* PFS-143 Add cypress test & api unit tests

* PFS-143 Create ADR

* PFS-143 Fix build

* PFS-143 Code cleanup, move upload type enum to shared

* PFS-143 Change case of upload type enum, undo SSE env var

* PFS-143 Fix cypress test

* PFS-143 Fix cypress test

* PFS-130 Change directory for testing

* PFS-130 Add s3 directory to enum

* PFS-130 Add MOTO card upload type headers

* PFS-130 Add MOTO card filename validation

* PFS-130 Remove validation for other reports

* PFS-130 Update SSE algorithm to use KMS

* PFS-130 Add error catching to upload type filename & add tests

* PFS-165 Update event & send to notify API

* PFS-165 Add unversioned files from previous commit

* PFS-165 Attempt renaming finance admin event source

* PFS-165 Fix cypress tests

* PFS-165 Log bucket & key to see why bucket can't be found

* PFS-165 Fix linting

* PFS-165 Add failed lines & report type to notify API call

* PFS-165 Update expected event

* PFS-165 More logging when sending notify request

* PFS-165 Update template ID

* PFS-165 Tidy code, add tests, handle notify API error

* PFS-165 Refactor & add success / errored emails

* PFS-165 Update template ID

* PFS-165 Temporarily comment out CSV validation for testing

* PFS-165 Refactor S3 client

* PFS-165 Fix make up
  • Loading branch information
josephsmith0705 authored Oct 23, 2024
1 parent b70c86d commit e4bf85e
Show file tree
Hide file tree
Showing 19 changed files with 770 additions and 47 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ compile-assets:
docker compose run --rm yarn build

cypress: setup-directories clean
docker compose up -d localstack
docker compose run --build cypress
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ services:
ASYNC_S3_BUCKET: opg-backoffice-async-uploads-local
AWS_REGION: eu-west-1
AWS_S3_ENDPOINT: http://localstack:4566
AWS_BASE_URL: http://localstack:4566
AWS_ACCESS_KEY_ID: localstack
AWS_SECRET_ACCESS_KEY: localstack
EVENT_BUS_NAME: local-main
S3_ENCRYPTION_KEY: alias/aws/s3
healthcheck:
test: wget -O /dev/null -S 'http://localhost:8080/health-check' 2>&1 | grep 'HTTP/1.1 200 OK' || exit 1
Expand All @@ -48,6 +50,8 @@ services:
volumes:
- "./scripts/localstack/init:/etc/localstack/init/ready.d"
- "./scripts/localstack/wait:/scripts/wait"
ports:
- "4566:4566"
environment:
AWS_DEFAULT_REGION: eu-west-1
healthcheck:
Expand Down
3 changes: 2 additions & 1 deletion docker/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ services:
- ./shared:/app/shared
- ./finance-admin-api/.air.toml:/app/finance-admin-api/.air.toml
- ./finance-admin-api/api:/app/finance-admin-api/api
- ./finance-admin-api/awsclient:/app/finance-admin-api/awsclient
- ./finance-admin-api/filestorage:/app/finance-admin-api/filestorage
- ./finance-admin-api/event:/app/finance-admin-api/event
- ./finance-admin-api/main.go:/app/finance-admin-api/main.go

yarn:
Expand Down
36 changes: 36 additions & 0 deletions finance-admin-api/api/handle_events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package api

import (
"encoding/json"
"errors"
"fmt"
"github.com/opg-sirius-finance-admin/apierror"
"github.com/opg-sirius-finance-admin/shared"
"net/http"
)

func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()

var event shared.Event
defer r.Body.Close()

if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
return apierror.BadRequestError("event", "unable to parse event", err)
}

if event.Source == shared.EventSourceFinanceHub && event.DetailType == shared.DetailTypeFinanceAdminUploadProcessed {
if detail, ok := event.Detail.(shared.FinanceAdminUploadProcessedEvent); ok {
err := s.SendEmailToNotify(ctx, detail, shared.ReportTypeUploadPaymentsMOTOCard.Translation())
if err != nil {
return err
}
}
} else {
return apierror.BadRequestError("event", fmt.Sprintf("could not match event: %s %s", event.Source, event.DetailType), errors.New("no match"))
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
return nil
}
72 changes: 72 additions & 0 deletions finance-admin-api/api/handle_events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package api

import (
"bytes"
"encoding/json"
"github.com/ministryofjustice/opg-go-common/telemetry"
"github.com/opg-sirius-finance-admin/apierror"
"github.com/opg-sirius-finance-admin/shared"
"github.com/stretchr/testify/assert"
"io"
"net/http"
"net/http/httptest"
"testing"
)

func TestServer_handleEvents(t *testing.T) {
var e apierror.BadRequest
tests := []struct {
name string
event shared.Event
expectedErr error
}{
{
name: "finance admin upload processed event",
event: shared.Event{
Source: "opg.supervision.finance",
DetailType: "finance-admin-upload-processed",
Detail: shared.FinanceAdminUploadProcessedEvent{
EmailAddress: "[email protected]",
FailedLines: map[int]string{
1: "DUPLICATE_PAYMENT",
}},
},
expectedErr: nil,
},
{
name: "unknown event",
event: shared.Event{
Source: "opg.supervision.sirius",
DetailType: "test",
},
expectedErr: e,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
mockHttpClient := MockHttpClient{}
server := Server{http: &mockHttpClient}

GetDoFunc = func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusCreated,
Body: io.NopCloser(bytes.NewReader([]byte{})),
}, nil
}

var body bytes.Buffer
_ = json.NewEncoder(&body).Encode(test.event)
r := httptest.NewRequest(http.MethodPost, "/events", &body)
ctx := telemetry.ContextWithLogger(r.Context(), telemetry.NewLogger("test"))
r = r.WithContext(ctx)
w := httptest.NewRecorder()

err := server.handleEvents(w, r)
if test.expectedErr != nil {
assert.ErrorAs(t, err, &test.expectedErr)
} else {
assert.Nil(t, err)
}
})
}
}
167 changes: 167 additions & 0 deletions finance-admin-api/api/send_to_notify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package api

import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/golang-jwt/jwt/v5"
"github.com/opg-sirius-finance-admin/shared"
"net/http"
"os"
"slices"
"strings"
"time"
)

const notifyUrl = "https://api.notifications.service.gov.uk"
const emailEndpoint = "v2/notifications/email"
const processingErrorTemplateId = "872d88b3-076e-495c-bf81-a2be2d3d234c"
const processingFailedTemplateId = "a8f9ab79-1489-4639-9e6c-cad1f079ebcf"
const processingSuccessTemplateId = "8c85cf6c-695f-493a-a25f-77b4fb5f6a8e"

type ProcessingFailedPersonalisation struct {
FailedLines []string `json:"failed_lines"`
ReportType string `json:"report_type"`
}

type ProcessingSuccessPersonalisation struct {
ReportType string `json:"report_type"`
}

type NotifyPayload struct {
EmailAddress string `json:"email_address"`
TemplateId string `json:"template_id"`
Personalisation interface{} `json:"personalisation"`
}

func parseNotifyApiKey(notifyApiKey string) (string, string) {
splitKey := strings.Split(notifyApiKey, "-")
if len(splitKey) != 11 {
return "", ""
}
iss := fmt.Sprintf("%s-%s-%s-%s-%s", splitKey[1], splitKey[2], splitKey[3], splitKey[4], splitKey[5])
jwtToken := fmt.Sprintf("%s-%s-%s-%s-%s", splitKey[6], splitKey[7], splitKey[8], splitKey[9], splitKey[10])
return iss, jwtToken
}

func createSignedJwtToken() (string, error) {
iss, jwtKey := parseNotifyApiKey(os.Getenv("OPG_NOTIFY_API_KEY"))

t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": iss,
"iat": time.Now().Unix(),
})

signedToken, err := t.SignedString([]byte(jwtKey))
if err != nil {
return "", err
}
return signedToken, nil
}

func formatFailedLines(failedLines map[int]string) []string {
var errorMessage string
var formattedLines []string
var keys []int
for i := range failedLines {
keys = append(keys, i)
}

slices.Sort(keys)

for _, key := range keys {
failedLine := failedLines[key]
errorMessage = ""

switch failedLine {
case "DATE_PARSE_ERROR":
errorMessage = "Unable to parse date"
case "AMOUNT_PARSE_ERROR":
errorMessage = "Unable to parse amount"
case "DUPLICATE_PAYMENT":
errorMessage = "Duplicate payment line"
case "CLIENT_NOT_FOUND":
errorMessage = "Could not find a client with this court reference"
}

formattedLines = append(formattedLines, fmt.Sprintf("Line %d: %s", key, errorMessage))
}

return formattedLines
}

func createNotifyPayload(detail shared.FinanceAdminUploadProcessedEvent, reportType string) NotifyPayload {
var payload NotifyPayload

if detail.Error != "" {
payload = NotifyPayload{
detail.EmailAddress,
processingErrorTemplateId,
struct {
Error string `json:"error"`
ReportType string `json:"report_type"`
}{
detail.Error,
reportType,
},
}
} else if len(detail.FailedLines) != 0 {
payload = NotifyPayload{
detail.EmailAddress,
processingFailedTemplateId,
struct {
FailedLines []string `json:"failed_lines"`
ReportType string `json:"report_type"`
}{
formatFailedLines(detail.FailedLines),
reportType,
},
}
} else {
payload = NotifyPayload{
detail.EmailAddress,
processingSuccessTemplateId,
struct {
ReportType string `json:"report_type"`
}{reportType},
}
}

return payload
}

func (s *Server) SendEmailToNotify(ctx context.Context, detail shared.FinanceAdminUploadProcessedEvent, reportType string) error {
signedToken, err := createSignedJwtToken()
if err != nil {
return err
}

var body bytes.Buffer

err = json.NewEncoder(&body).Encode(createNotifyPayload(detail, reportType))
if err != nil {
return err
}

r, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/%s", notifyUrl, emailEndpoint), &body)

if err != nil {
return err
}

r.Header.Add("Content-Type", "application/json")
r.Header.Add("Authorization", "Bearer "+signedToken)

resp, err := s.http.Do(r)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusCreated {
return nil
}

return newStatusError(resp)
}
Loading

0 comments on commit e4bf85e

Please sign in to comment.