-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PFS-165 moto payment line failed email (#17)
* 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
1 parent
b70c86d
commit e4bf85e
Showing
19 changed files
with
770 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.