Mail api for sending order confirmation to planetposen customers

This commit is contained in:
2022-12-02 18:26:07 +01:00
commit 2812f6bcbf
13 changed files with 782 additions and 0 deletions

135
client/http.go Normal file
View File

@@ -0,0 +1,135 @@
// Package client contains a HTTP client.
package client
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
opentracing "github.com/opentracing/opentracing-go"
)
// Parameters provides the parameters used when creating a new HTTP client.
type Parameters struct {
Timeout *time.Duration
}
// NewHTTPClient instantiates a new HTTPClient based on provided parameters.
func NewHTTPClient(parameters Parameters) HTTPClient {
if parameters.Timeout == nil {
timeout := 1 * time.Second
parameters.Timeout = &timeout
}
client := &http.Client{
Timeout: *parameters.Timeout,
}
return HTTPClient{client}
}
// HTTPRequestData contains the request data.
type HTTPRequestData struct {
Method string
URL string
Headers map[string]string
PostPayload []byte
GetPayload *url.Values
}
// HTTPClient contains the HTTP client.
type HTTPClient struct {
*http.Client
}
// HTTPStatusCodeError is an error that occurs when receiving an unexpected status code (>= 400).
type HTTPStatusCodeError struct {
URL string
StatusCode int
Message string
}
// Error return an error string.
func (e HTTPStatusCodeError) Error() string {
return fmt.Sprintf("Error response from %s, got status: %d", e.URL, e.StatusCode)
}
// RequestBytes does the actual HTTP request.
// Returns a slice of bytes or an error.
func (client *HTTPClient) RequestBytes(ctx context.Context, reqData HTTPRequestData) ([]byte, error) {
span, ctx := opentracing.StartSpanFromContext(ctx, "RequestBytes")
defer span.Finish()
r, err := client.request(ctx, reqData)
if err != nil {
return nil, err
}
defer r.Body.Close()
if r.StatusCode >= 400 {
resp, _ := ioutil.ReadAll(r.Body)
message := string(resp)
span.SetTag("error", true)
span.LogKV("message", fmt.Errorf("error making request to %s, got error: %s", reqData.URL, message))
return nil, HTTPStatusCodeError{
URL: reqData.URL,
StatusCode: r.StatusCode,
Message: message,
}
}
return ioutil.ReadAll(r.Body)
}
func (client *HTTPClient) request(ctx context.Context, reqData HTTPRequestData) (*http.Response, error) {
var req *http.Request
var err error
if reqData.Method == http.MethodPost {
req, err = http.NewRequest(reqData.Method, reqData.URL, bytes.NewBuffer(reqData.PostPayload))
} else {
req, err = http.NewRequest(reqData.Method, reqData.URL, nil)
}
if err != nil {
return nil, err
}
if reqData.GetPayload != nil {
req.URL.RawQuery = reqData.GetPayload.Encode()
}
span := opentracing.SpanFromContext(ctx)
if span != nil {
opentracing.GlobalTracer().Inject(
span.Context(),
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(req.Header),
)
}
for k, v := range reqData.Headers {
req.Header.Set(k, v)
}
req.Header.Set("User-Agent", "planetposen-mail")
resp, err := client.Do(req)
if err != nil {
if reqData.Method == http.MethodPost {
return resp, fmt.Errorf("Error making request: %v. Body: %s", err, reqData.PostPayload)
}
return resp, fmt.Errorf("Error making request: %v. Query: %v", err, req.URL.RawQuery)
}
return resp, nil
}

View File

@@ -0,0 +1,56 @@
package sendgrid
import (
"context"
"encoding/json"
"fmt"
"github.com/kevinmidboe/planetposen-mail/client"
"github.com/kevinmidboe/planetposen-mail/mail"
"net/http"
)
// SendOrderConfirmation sends an order confirmation.
func (c *Client) SendOrderConfirmation(ctx context.Context, record mail.OrderConfirmationEmailData) error {
reqBody := sendEmailPayload{
Personalizations: []personalization{
{
To: []email{
{
Email: record.ToEmail,
},
},
Subject: record.Subject,
},
},
From: email{
Email: record.FromEmail,
Name: record.FromName,
},
Content: []content{
{
Type: "text/html",
Value: record.Markup,
},
},
}
jsonPayload, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("error marshalling sendEmailPayload: %w", err)
}
reqData := client.HTTPRequestData{
Method: http.MethodPost,
URL: fmt.Sprintf("%s/v3/mail/send", c.Endpoint),
Headers: map[string]string{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", c.APIKey),
},
PostPayload: jsonPayload,
}
_, err = c.HTTPClient.RequestBytes(ctx, reqData)
if err != nil {
return fmt.Errorf("error making request to sendgrid to send email: %w", err)
}
return nil
}

View File

@@ -0,0 +1,45 @@
package sendgrid
import (
"time"
"github.com/kevinmidboe/planetposen-mail/client"
"github.com/kevinmidboe/planetposen-mail/config"
)
// Client holds the HTTP client and endpoint information.
type Client struct {
Endpoint string
APIKey string
HTTPClient client.HTTPClient
}
// Init sets up a new sendgrid client.
func (c *Client) Init(config *config.Config) error {
timeout := 5 * time.Second
c.Endpoint = config.SendGridAPIEndpoint
c.APIKey = config.SendGridAPIKey
c.HTTPClient = client.NewHTTPClient(client.Parameters{Timeout: &timeout})
return nil
}
type sendEmailPayload struct {
Personalizations []personalization `json:"personalizations"`
From email `json:"from"`
Content []content `json:"content"`
}
type personalization struct {
To []email `json:"to"`
Subject string `json:"subject"`
}
type email struct {
Email string `json:"email"`
Name string `json:"name"`
}
type content struct {
Type string `json:"type"`
Value string `json:"value"`
}

32
cmd/server/main.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
"context"
"github.com/kevinmidboe/planetposen-mail/config"
"github.com/kevinmidboe/planetposen-mail/server"
log "github.com/sirupsen/logrus"
)
func main() {
// log.SetFormatter(logrustic.NewFormatter("planetposen-mail"))
log.Info("Starting ...")
ctx := context.Background()
config, err := config.LoadConfig()
if err != nil {
log.Fatal(err.Error())
}
var s server.Server
if err := s.Create(ctx, config); err != nil {
log.Fatal(err.Error())
}
if err := s.Serve(ctx); err != nil {
log.Fatal(err.Error())
}
}

28
config/config.go Normal file
View File

@@ -0,0 +1,28 @@
// Package config handles environment variables.
package config
import (
"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"
log "github.com/sirupsen/logrus"
)
// Config contains environment variables.
type Config struct {
Port string `envconfig:"PORT" default:"8000"`
SendGridAPIEndpoint string `envconfig:"SEND_GRID_API_ENDPOINT" required:"true"`
SendGridAPIKey string `envconfig:"SEND_GRID_API_KEY" required:"true"`
}
// LoadConfig reads environment variables, populates and returns Config.
func LoadConfig() (*Config, error) {
if err := godotenv.Load(); err != nil {
log.Info("No .env file found")
}
var c Config
err := envconfig.Process("", &c)
return &c, err
}

View File

@@ -0,0 +1,208 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" style="background: #ffffff;margin: 0 auto;padding: 0;width: 100%;height: 100%;background-color: #ffffff;">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{.PageTitle}}</title>
<style type="text/css">@media only screen and (max-width:575px){.fs-sm-16px{font-size:16px !important}.fs-sm-20px{font-size:20px !important}.pb-sm-56px{padding-bottom:56px !important}.pt-sm-56px{padding-top:56px !important}}@media (max-width:575px){.table td,.table th{padding:12px 5px !important}.table *{font-size:14px !important}.container .container-content{border-radius:0 !important}.btn.btn-lg .btn-content{padding:15px 34px !important;font-size:18px !important}}</style>
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<style type="text/css">
a {
text-decoration: none;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<style type="text/css">
.col, div.col { width:100% !important; max-width:100% !important; }
.hidden-outlook, .hidden-outlook table {
display:none !important;
mso-hide:all !important;
}
* {
font-family: sans-serif !important;
-ms-text-size-adjust: 100%;
}
img {
-ms-interpolation-mode:bicubic;
}
td.body-content {
width: 680px;
}
td.row-content {
font-size:0;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<style type="text/css">
.visible-outlook {
display: none !important;
}
a[x-apple-data-detectors],
.unstyle-auto-detected-links a {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
body {
background: #f1f1f2;
margin: 0 auto;
padding: 0;
width: 100%;
height: 100%;
background-color: #f1f1f2;
}
body > table {
background: #f1f1f2;
border-collapse: collapse;
color: #373737;
background-color: #f1f1f2;
}
.btn:hover tbody tr td {
background-color: red !important;
}
</style>
<!--<![endif]-->
<!--[if !mso]><!-->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:400,700">
<!--<![endif]-->
<!--[if mso]><style type="text/css">.ms-t1{line-height:1.45;font-family:Roboto,RobotoDraft,Helvetica,Arial,sans-serif;color:#373737;font-size:16px;text-align:left;font-weight:400}</style><![endif]-->
</head>
<body>
<table role="presentation" cellspacing="0" cellpadding="0" width="100%">
<tbody>
<tr>
<!--[if mso]><td></td><![endif]-->
<td class="body-content" style="font-size: 16px;font-family: Roboto, RobotoDraft, Helvetica, Arial, sans-serif;font-weight: normal;line-height: 1.45;text-align: left;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #373737;">
<h1 class="text-center fs-sm-20px" style="mso-line-height-rule: exactly;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;font-weight: 700;margin: 32px;color: black;font-size: 24px;text-align: center;">{{.PageTitle}}</h1>
<table class="ms-t1 container" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse: collapse;width: 100%;max-width: 680px;margin: 0 auto;">
<tbody>
<tr>
<td class="container-content pt-sm-56px pb-sm-56px" style="background: #fff;padding: 0px;vertical-align: top;max-width: 680px;background-color: #fff;border-radius: 8px;padding-top: 56px;padding-bottom: 56px;">
<center><svg class="icon bi bi-shop shop-icon fs-sm-70px" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" preserveAspectRatio="none" style="font-size: 86px;color: var(--primary); width: 80px;">
<path d="M2.97 1.35A1 1 0 0 1 3.73 1h8.54a1 1 0 0 1 .76.35l2.609 3.044A1.5 1.5 0 0 1 16 5.37v.255a2.375 2.375 0 0 1-4.25 1.458A2.371 2.371 0 0 1 9.875 8 2.37 2.37 0 0 1 8 7.083 2.37 2.37 0 0 1 6.125 8a2.37 2.37 0 0 1-1.875-.917A2.375 2.375 0 0 1 0 5.625V5.37a1.5 1.5 0 0 1 .361-.976l2.61-3.045zm1.78 4.275a1.375 1.375 0 0 0 2.75 0 .5.5 0 0 1 1 0 1.375 1.375 0 0 0 2.75 0 .5.5 0 0 1 1 0 1.375 1.375 0 1 0 2.75 0V5.37a.5.5 0 0 0-.12-.325L12.27 2H3.73L1.12 5.045A.5.5 0 0 0 1 5.37v.255a1.375 1.375 0 0 0 2.75 0 .5.5 0 0 1 1 0zM1.5 8.5A.5.5 0 0 1 2 9v6h1v-5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v5h6V9a.5.5 0 0 1 1 0v6h.5a.5.5 0 0 1 0 1H.5a.5.5 0 0 1 0-1H1V9a.5.5 0 0 1 .5-.5zM4 15h3v-5H4v5zm5-5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3zm3 0h-2v3h2v-3z"></path>
</svg>
<h2 style="margin: 0 0 10px 0;mso-line-height-rule: exactly;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;font-weight: 700;font-size: 24px;line-height: 1.8;margin-top: 18px;margin-bottom: 56px;margin-left: 18px;margin-right: 18px;" class="fs-sm-20px"><strong>Thank You For Your Order,</strong><br><strong>David Smith</strong></h2>
<table class="ms-t1 layout-table" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: collapse;width: 90%;">
<tbody>
<tr>
<td style="color: #606060;font-size: 18px;padding-bottom: 25px;" class="fs-sm-16px">July 22, 2025</td>
<td class="text-right fs-sm-16px" style="color: #606060;font-size: 18px;padding-bottom: 25px;text-align: right;">Order id: <strong>{{.OrderId}}</strong></td>
</tr>
</tbody>
</table>
<table class="ms-t1 table" border="0" cellpadding="0" cellspacing="0" style="border-collapse: collapse;width: 100%;margin-bottom: 16px;">
<thead>
<tr style="background: ghostwhite;background-color: ghostwhite;">
<th class="text-left" style="border-top: 1px solid #e9e9e9;padding: 20px 30px;vertical-align: bottom;border-bottom: 2px solid #e9e9e9;font-weight: 700;font-size: 14px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;text-align: left;" colspan="4">ITEM</th>
<th style="border-top: 1px solid #e9e9e9;padding: 20px 30px;vertical-align: bottom;border-bottom: 2px solid #e9e9e9;font-weight: 700;font-size: 14px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;">QTY</th>
<th style="border-top: 1px solid #e9e9e9;padding: 20px 30px;vertical-align: bottom;border-bottom: 2px solid #e9e9e9;font-weight: 700;font-size: 14px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;">PRICE</th>
</tr>
</thead>
<tbody>
{{ range .Products }}
<tr>
<td class="text-left" colspan="1" style="vertical-align: middle;border-top: 1px solid #e9e9e9;padding: 20px 30px;padding-right: 0px;text-align: left;"><img class="w-sm-50px" src={{.Image}} style="width: 120px;" alt="Item 1" /></td>
<td class="text-left" colspan="3" style="vertical-align: middle;border-top: 1px solid #e9e9e9;padding: 20px 30px;padding-left: 0px;padding-right: 0px;text-align: left;">
<h2 style="margin: 0 0 10px 0;mso-line-height-rule: exactly;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;font-weight: 700;font-size: 18px;margin-bottom: 5px;"><strong>{{.Name}}</strong></h2><span style="color: #9a9a9a;font-size: 16px;">{{.Description}}</span>
</td>
<td class="text-center" style="vertical-align: middle;border-top: 1px solid #e9e9e9;padding: 20px 30px;font-weight: 400;font-size: 18px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;text-align: center;">{{.Quantity}}</td>
<td class="text-center" style="vertical-align: middle;border-top: 1px solid #e9e9e9;padding: 20px 30px;font-weight: 400;font-size: 18px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;text-align: center;">{{.Currency}} {{.Price}}</td>
</tr>
{{ end }}
<!-- Tax & Total -->
<tr>
<td class="text-left" style="vertical-align: middle;border-top: 1px solid #e9e9e9;padding: 20px 30px;font-size: 18px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;padding-right: 0px;text-align: left;" colspan="3">TAX</td>
<td class="text-right" style="vertical-align: middle;border-top: 1px solid #e9e9e9;padding: 20px 30px;font-size: 18px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;text-align: right;" colspan="3">$0.00</td>
</tr>
<tr>
<td class="text-left" style="vertical-align: middle;border-top: 1px solid #e9e9e9;padding: 20px 30px;font-size: 18px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;padding-right: 0px;text-align: left;" colspan="3">Total</td>
<td class="text-right" style="vertical-align: middle;border-top: 1px solid #e9e9e9;padding: 20px 30px;font-size: 18px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;font-weight: 700;text-align: right;" colspan="3">$349.97</td>
</tr>
<!-- End tax & Total -->
<!-- Payment method & status -->
<tr style="background: ghostwhite;background-color: ghostwhite;text-transform: uppercase;">
<td class="text-left" style="vertical-align: middle;border-top: 1px solid #e9e9e9;padding: 20px 30px;font-weight: 700;font-size: 14px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;width: 50%;text-align: left;" colspan="3">payment method</td>
<td class="text-left" style="vertical-align: middle;border-top: 1px solid #e9e9e9;padding: 20px 30px;font-weight: 700;font-size: 14px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;text-align: left;" colspan="3">status</td>
</tr>
<tr>
<td class="text-left" style="border-top: 1px solid #e9e9e9;padding: 20px 30px;font-size: 16px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;color: #5b5959;vertical-align: top;width: 50%;padding-bottom: 32px;text-align: left;" colspan="3">
<h4 style="margin: 0 0 10px 0;mso-line-height-rule: exactly;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;font-weight: 700;font-size: 16px;line-height: 1.8;margin-bottom: 0px;">Credit/Debit Card<br></h4>
<p style="margin: 0 0 10px 0;mso-line-height-rule: exactly;line-height: 1.8;">VISA (xxxxxxxxxxxx3422)&nbsp;<br>Valid Thru 11/2024</p>
</td>
<td class="text-left" style="border-top: 1px solid #e9e9e9;padding: 20px 30px;font-size: 16px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;color: #5b5959;vertical-align: top;padding-bottom: 32px;text-align: left;" colspan="3">
<h4 style="margin: 0 0 10px 0;mso-line-height-rule: exactly;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;font-weight: 700;font-size: 16px;line-height: 1.8;margin-bottom: 0px;"><strong>Paid on July 22, 2020</strong><br></h4>
<p style="margin: 0 0 10px 0;mso-line-height-rule: exactly;line-height: 1.8;">Created, shipment pending (<a href="#" style="text-decoration: none;color: #3535e4;">track</a>)</p>
</td>
</tr>
<!-- End payment method & status -->
<!-- Shipping -->
<tr style="background: ghostwhite;background-color: ghostwhite;text-transform: uppercase;">
<td class="text-left" style="vertical-align: middle;border-top: 1px solid #e9e9e9;padding: 20px 30px;font-weight: 700;font-size: 14px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;width: 50%;text-align: left;" colspan="3">shipping address</td>
<td class="text-left" style="vertical-align: middle;border-top: 1px solid #e9e9e9;padding: 20px 30px;font-weight: 700;font-size: 14px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;text-align: left;" colspan="3">shipping method</td>
</tr>
<tr>
<td class="text-left" style="border-top: 1px solid #e9e9e9;padding: 20px 30px;font-size: 16px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;color: #5b5959;vertical-align: top;width: 50%;padding-bottom: 32px;text-align: left;" colspan="3">
<p style="margin: 0 0 10px 0;mso-line-height-rule: exactly;line-height: 1.6;"><strong>David Smith</strong><br>1234 Street Name<br>City, State 12345<br>(123) 555-32234</p>
</td>
<td class="text-left" style="border-top: 1px solid #e9e9e9;padding: 20px 30px;font-size: 16px;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;color: #5b5959;vertical-align: top;padding-bottom: 32px;text-align: left;" colspan="3">
<h4 style="margin: 0 0 10px 0;mso-line-height-rule: exactly;font-family: &quot;Open Sans&quot;,Roboto,RobotoDraft,Helvetica,Arial,sans-serif;font-weight: 700;font-size: 16px;line-height: 1.8;margin-bottom: 0px;"><strong>Express Shipping</strong><br></h4>
<p style="margin: 0 0 10px 0;mso-line-height-rule: exactly;line-height: 1.8;">4-7 dager</p>
</td>
</tr>
<!-- End shipping -->
</tbody>
</table>
<!-- View order button -->
<table class="ms-t1 btn btn-primary btn-lg btn-pill" border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse: separate;">
<tbody>
<tr>
<td class="btn-content" style="background: #3535e4;border: none;cursor: pointer;text-align: center;vertical-align: middle;font-weight: 800;background-color: #3535e4;border-radius: 100px;padding: 18px 40px;"><a href="#" style="text-decoration: none;color: #fff;font-size: 125%;">View My Order</a></td>
</tr>
</tbody>
</table>
<!-- End view order button -->
</center>
</td>
</tr>
</tbody>
</table>
<div class="text-center" style="color: #323233;text-align: center;">
<p style="margin: 0 0 10px 0;mso-line-height-rule: exactly;line-height: 1.5;margin-top: 50px;margin-bottom: 25px;"><a href="https://planetposen.no">planetposen.no</a></p>
</div>
</td><!--[if mso]>
<td></td> <![endif]--></tr>
</tbody>
</table>
</body>
</html>

View File

@@ -0,0 +1,63 @@
package mail
import (
"context"
"fmt"
)
type OrderMailSender interface {
SendOrderConfirmation(ctx context.Context, record Record) error
}
type Product struct {
Name string
Image string
Description string
Quantity int
Price float32
Currency string
}
type OrderConfirmationData struct {
// PageTitle string
Email string
OrderId string
Products []Product
}
type EmailTemplateData struct {
PageTitle string
OrderId string
Products []Product
}
type OrderConfirmationEmailData struct {
Subject string
FromName string
FromEmail string
ToEmail string
Markup string
}
type Record struct {
Email string
// FullName string
Status string
OrderConfirmationEmailData OrderConfirmationEmailData
}
func OrderConfirmation(payload OrderConfirmationData) (*OrderConfirmationEmailData, error) {
var emailTemplate EmailTemplateData
emailTemplate.PageTitle = "Planetposen purchase"
emailTemplate.OrderId = payload.OrderId
emailTemplate.Products = payload.Products
orderConfirmationEmailData := buildOrderConfirmation(emailTemplate)
if orderConfirmationEmailData == nil {
return nil, fmt.Errorf("couldn't build order confirmation template for orderId %s", payload.OrderId)
}
orderConfirmationEmailData.ToEmail = payload.Email
return orderConfirmationEmailData, nil
}

26
mail/template.go Normal file
View File

@@ -0,0 +1,26 @@
package mail
import (
"html/template"
"strings"
)
func buildOrderConfirmation(templateData EmailTemplateData) *OrderConfirmationEmailData {
subject := "Orderbekreftelse fra planetposen.no"
data := &OrderConfirmationEmailData{
Subject: subject,
FromName: "noreply@kevm.dev",
FromEmail: "noreply@kevm.dev",
}
tmpl := template.Must(template.ParseFiles("mail/mail-template_order-confirmation.html"))
b := new(strings.Builder)
err := tmpl.Execute(b, templateData)
if err != nil {
return nil
}
data.Markup = b.String()
return data
}

View File

@@ -0,0 +1,55 @@
// Package handler contains handlers for events.
package handler
import (
"encoding/json"
"fmt"
"github.com/kevinmidboe/planetposen-mail/client/sendgrid"
"github.com/kevinmidboe/planetposen-mail/mail"
"net/http"
)
func SendOrderConfirmation(s *sendgrid.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
payload, err := getOrderConfirmationPayload(r)
if err != nil {
handleError(w, err, "unable to parse order payload", http.StatusBadRequest, true)
return
}
mailData, err := mail.OrderConfirmation(*payload)
err = s.SendOrderConfirmation(ctx, *mailData)
if err != nil {
fmt.Println(err)
handleError(w, err, "error from sendgrid ", http.StatusInternalServerError, true)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
responseJSON, _ := json.Marshal(struct {
Message string `json:"message"`
OrderId string `json:"orderId"`
Recipient string `json:"recipient"`
}{
Message: "Successfully sent email",
OrderId: payload.OrderId,
Recipient: payload.Email,
})
w.Write(responseJSON)
}
}
func getOrderConfirmationPayload(r *http.Request) (*mail.OrderConfirmationData, error) {
decoder := json.NewDecoder(r.Body)
var payload mail.OrderConfirmationData
err := decoder.Decode(&payload)
if err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
return &payload, nil
}

24
server/handler/error.go Normal file
View File

@@ -0,0 +1,24 @@
package handler
import (
"encoding/json"
"net/http"
log "github.com/sirupsen/logrus"
)
// handleError - Logs the error (if shouldLog is true), and outputs the error message (msg)
func handleError(w http.ResponseWriter, err error, msg string, statusCode int, shouldLog bool) {
if shouldLog {
log.WithField("err", err).Error(msg)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
errorJSON, _ := json.Marshal(struct {
Error string `json:"error"`
}{
Error: msg,
})
w.Write(errorJSON)
}

11
server/handler/healthz.go Normal file
View File

@@ -0,0 +1,11 @@
package handler
import "net/http"
// Healthz is used for our readiness and liveness probes.
// GET /_healthz
// Responds: 200
func Healthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(http.StatusText(http.StatusOK)))
}

14
server/router.go Normal file
View File

@@ -0,0 +1,14 @@
package server
import (
"github.com/kevinmidboe/planetposen-mail/server/handler"
)
const v1API string = "/api/v1/"
func (s *Server) setupRoutes() {
s.Router.HandleFunc("/_healthz", handler.Healthz).Methods("GET").Name("Health")
api := s.Router.PathPrefix(v1API).Subrouter()
api.HandleFunc("/send-confirmation", handler.SendOrderConfirmation(s.SendGrid)).Methods("POST").Name("SendOrderConfirmation")
}

85
server/server.go Normal file
View File

@@ -0,0 +1,85 @@
// Package server provides functionality to easily set up an HTTTP server.
//
// Clients:
// Database
package server
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/gorilla/mux"
"github.com/kevinmidboe/planetposen-mail/client/sendgrid"
"github.com/kevinmidboe/planetposen-mail/config"
log "github.com/sirupsen/logrus"
)
// Server holds the HTTP server, router, config and all clients.
type Server struct {
Config *config.Config
HTTP *http.Server
Router *mux.Router
SendGrid *sendgrid.Client
}
// Create sets up the HTTP server, router and all clients.
// Returns an error if an error occurs.
func (s *Server) Create(ctx context.Context, config *config.Config) error {
// metrics.RegisterPrometheusCollectors()
s.Config = config
s.Router = mux.NewRouter()
s.HTTP = &http.Server{
Addr: fmt.Sprintf(":%s", s.Config.Port),
Handler: s.Router,
}
var sendGridClient sendgrid.Client
if err := sendGridClient.Init(config); err != nil {
return fmt.Errorf("error initializing sendgrid client: %w", err)
}
s.SendGrid = &sendGridClient
s.setupRoutes()
return nil
}
// Serve tells the server to start listening and serve HTTP requests.
// It also makes sure that the server gracefully shuts down on exit.
// Returns an error if an error occurs.
func (s *Server) Serve(ctx context.Context) error {
// closer, err := trace.InitGlobalTracer(s.Config)
// if err != nil {
// return err
// }
// defer closer.Close()
go func(ctx context.Context, s *Server) {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
log.Info("Shutdown signal received")
if err := s.HTTP.Shutdown(ctx); err != nil {
log.Error(err.Error())
}
}(ctx, s)
log.Infof("Ready at: %s", s.Config.Port)
if err := s.HTTP.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf(err.Error())
}
return nil
}