Skip to content

Commit 91c0bc2

Browse files
committed
Add webhook for orders create events to notify customer.io of placed orders
1 parent d0d7ec6 commit 91c0bc2

File tree

8 files changed

+238
-21
lines changed

8 files changed

+238
-21
lines changed

auth/service/service/service.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,12 @@ func (s *Service) initializeRouter() error {
333333
return errors.Wrap(err, "unable to create fulfillment event processor")
334334
}
335335

336-
shopifyRouter, err := shopifyAPI.NewRouter(fulfillmentEventProcessor)
336+
ordersCreateEventProcessor, err := shopify.NewOrdersCreateEventProcessor(s.Logger(), customerIOClient, shopifyClnt)
337+
if err != nil {
338+
return errors.Wrap(err, "unable to create orders create event processor")
339+
}
340+
341+
shopifyRouter, err := shopifyAPI.NewRouter(fulfillmentEventProcessor, ordersCreateEventProcessor)
337342
if err != nil {
338343
return errors.Wrap(err, "unable to create shopify router")
339344
}

oura/customerio/events.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package customerio
22

33
const (
44
OuraEligibilitySurveyCompletedEventType = "oura_eligibility_survey_completed"
5+
OuraSizingKitOrderedEventType = "oura_sizing_kit_ordered"
56
OuraSizingKitDeliveredEventType = "oura_sizing_kit_delivered"
7+
OuraRingOrderedEventType = "oura_ring_ordered"
68
OuraRingDeliveredEventType = "oura_ring_delivered"
79
)
810

@@ -12,8 +14,18 @@ type OuraEligibilitySurveyCompletedData struct {
1214
OuraSizingKitDiscountCode string `json:"oura_sizing_kit_discount_code,omitempty"`
1315
}
1416

17+
type OuraSizingKitOrderedData struct {
18+
OuraSizingKitDiscountCode string `json:"oura_sizing_kit_discount_code"`
19+
}
20+
1521
type OuraSizingKitDeliveredData struct {
16-
OuraRingDiscountCode string `json:"oura_ring_discount_code"`
22+
OuraSizingKitDiscountCode string `json:"oura_sizing_kit_discount_code"`
23+
OuraRingDiscountCode string `json:"oura_ring_discount_code"`
1724
}
1825

19-
type OuraRingDeliveredData struct{}
26+
type OuraRingOrderedData struct {
27+
OuraRingDiscountCode string `json:"oura_ring_discount_code"`
28+
}
29+
type OuraRingDeliveredData struct {
30+
OuraRingDiscountCode string `json:"oura_ring_discount_code"`
31+
}

oura/shopify/api/router.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,21 @@ import (
1010
)
1111

1212
type Router struct {
13-
fulfillmentEventProcessor *shopify.FulfillmentEventProcessor
13+
fulfillmentEventProcessor *shopify.FulfillmentEventProcessor
14+
ordersCreateEventProcessor *shopify.OrdersCreateEventProcessor
1415
}
1516

16-
func NewRouter(fulfillmentEventProcessor *shopify.FulfillmentEventProcessor) (*Router, error) {
17+
func NewRouter(fulfillmentEventProcessor *shopify.FulfillmentEventProcessor, ordersCreateEventProcessor *shopify.OrdersCreateEventProcessor) (*Router, error) {
1718
return &Router{
18-
fulfillmentEventProcessor: fulfillmentEventProcessor,
19+
fulfillmentEventProcessor: fulfillmentEventProcessor,
20+
ordersCreateEventProcessor: ordersCreateEventProcessor,
1921
}, nil
2022
}
2123

2224
func (r *Router) Routes() []*rest.Route {
2325
return []*rest.Route{
2426
rest.Post("/v1/partners/shopify/fulfillment", r.HandleFulfillmentEvent),
27+
rest.Post("/v1/partners/shopify/orders/create", r.HandleOrdersCreateEvent),
2528
}
2629
}
2730

@@ -30,7 +33,7 @@ func (r *Router) HandleFulfillmentEvent(res rest.ResponseWriter, req *rest.Reque
3033
responder := request.MustNewResponder(res, req)
3134

3235
event := shopify.FulfillmentEvent{}
33-
if err := request.DecodeRequestBody(req.Request, event); err != nil {
36+
if err := request.DecodeRequestBody(req.Request, &event); err != nil {
3437
responder.Error(http.StatusBadRequest, err)
3538
return
3639
}
@@ -43,3 +46,22 @@ func (r *Router) HandleFulfillmentEvent(res rest.ResponseWriter, req *rest.Reque
4346
responder.Empty(http.StatusOK)
4447
return
4548
}
49+
50+
func (r *Router) HandleOrdersCreateEvent(res rest.ResponseWriter, req *rest.Request) {
51+
ctx := req.Context()
52+
responder := request.MustNewResponder(res, req)
53+
54+
event := shopify.OrdersCreateEvent{}
55+
if err := request.DecodeRequestBody(req.Request, &event); err != nil {
56+
responder.Error(http.StatusBadRequest, err)
57+
return
58+
}
59+
60+
if err := r.ordersCreateEventProcessor.Process(ctx, event); err != nil {
61+
responder.Error(http.StatusInternalServerError, err)
62+
return
63+
}
64+
65+
responder.Empty(http.StatusOK)
66+
return
67+
}

oura/shopify/client.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ type DiscountCodeInput struct {
2121
}
2222

2323
type DeliveredProducts struct {
24-
OrderID string `json:"order_id"`
2524
IDs []string `json:"products"`
2625
DiscountCode string `json:"discount_code"`
2726
}

oura/shopify/client/order.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,12 @@ func (c *defaultClient) GetDeliveredProducts(ctx context.Context, orderID string
5959
}
6060
}
6161

62-
return &shopify.DeliveredProducts{IDs: ids}, nil
62+
var discountCode string
63+
if resp.GetOrderByIdentifier().GetDiscountCode() != nil {
64+
discountCode = *resp.GetOrderByIdentifier().GetDiscountCode()
65+
}
66+
return &shopify.DeliveredProducts{
67+
IDs: ids,
68+
DiscountCode: discountCode,
69+
}, nil
6370
}

oura/shopify/fulfillment.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
)
1313

1414
var (
15-
deliveredProductIDToOuraDiscountAttribute = map[string]string{
15+
productIDToOuraDiscountAttribute = map[string]string{
1616
OuraSizingKitProductID: "oura_sizing_kit_discount_code",
1717
OuraRingProductID: "oura_ring_discount_code",
1818
}
@@ -75,7 +75,7 @@ func (f *FulfillmentEventProcessor) Process(ctx context.Context, event Fulfillme
7575
deliveredProductID := deliveredProducts.IDs[0]
7676
logger = logger.WithField("orderId", orderId).WithField("productId", deliveredProductID)
7777

78-
attribute, ok := deliveredProductIDToOuraDiscountAttribute[deliveredProductID]
78+
attribute, ok := productIDToOuraDiscountAttribute[deliveredProductID]
7979
if !ok {
8080
logger.Warn("unable to find discount attribute for delivered product")
8181
return nil
@@ -111,12 +111,12 @@ func (f *FulfillmentEventProcessor) Process(ctx context.Context, event Fulfillme
111111

112112
switch deliveredProductID {
113113
case OuraSizingKitProductID:
114-
if err := f.onSizingKitDelivered(ctx, customers.Identifiers[0], event); err != nil {
114+
if err := f.onSizingKitDelivered(ctx, customers.Identifiers[0], event, deliveredProducts.DiscountCode); err != nil {
115115
logger.WithError(err).Warn("unable to send sizing kit delivered event")
116116
return err
117117
}
118118
case OuraRingProductID:
119-
if err := f.onRingDelivered(ctx, customers.Identifiers[0], event); err != nil {
119+
if err := f.onRingDelivered(ctx, customers.Identifiers[0], event, deliveredProducts.DiscountCode); err != nil {
120120
logger.WithError(err).Warn("unable to send ring delivered event")
121121
return err
122122
}
@@ -127,7 +127,7 @@ func (f *FulfillmentEventProcessor) Process(ctx context.Context, event Fulfillme
127127
return nil
128128
}
129129

130-
func (f *FulfillmentEventProcessor) onSizingKitDelivered(ctx context.Context, identifiers customerio.Identifiers, event FulfillmentEvent) error {
130+
func (f *FulfillmentEventProcessor) onSizingKitDelivered(ctx context.Context, identifiers customerio.Identifiers, event FulfillmentEvent, sizingKitDiscountCode string) error {
131131
discountCode := RandomDiscountCode()
132132
err := f.shopifyClient.CreateDiscountCode(ctx, DiscountCodeInput{
133133
Title: OuraRingDiscountCodeTitle,
@@ -142,18 +142,21 @@ func (f *FulfillmentEventProcessor) onSizingKitDelivered(ctx context.Context, id
142142
Name: customerio.OuraSizingKitDeliveredEventType,
143143
ID: fmt.Sprintf("%d", event.ID),
144144
Data: customerio.OuraSizingKitDeliveredData{
145-
OuraRingDiscountCode: discountCode,
145+
OuraRingDiscountCode: discountCode,
146+
OuraSizingKitDiscountCode: sizingKitDiscountCode,
146147
},
147148
}
148149

149150
return f.customerIOClient.SendEvent(ctx, identifiers.ID, sizingKitDelivered)
150151
}
151152

152-
func (f *FulfillmentEventProcessor) onRingDelivered(ctx context.Context, identifiers customerio.Identifiers, event FulfillmentEvent) error {
153+
func (f *FulfillmentEventProcessor) onRingDelivered(ctx context.Context, identifiers customerio.Identifiers, event FulfillmentEvent, ringDiscountCode string) error {
153154
ringDelivered := customerio.Event{
154155
Name: customerio.OuraRingDeliveredEventType,
155156
ID: fmt.Sprintf("%d", event.ID),
156-
Data: customerio.OuraRingDeliveredData{},
157+
Data: customerio.OuraRingDeliveredData{
158+
OuraRingDiscountCode: ringDiscountCode,
159+
},
157160
}
158161

159162
return f.customerIOClient.SendEvent(ctx, identifiers.ID, ringDelivered)

oura/shopify/fulfillment_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ var _ = Describe("FulfillmentEventProcessor", func() {
8686
shopifyClnt.EXPECT().
8787
GetDeliveredProducts(gomock.Any(), fmt.Sprintf("gid://shopify/Order/%d", event.OrderID)).
8888
Return(&shopify.DeliveredProducts{
89-
OrderID: fmt.Sprintf("%d", event.OrderID),
9089
IDs: []string{shopify.OuraSizingKitProductID},
9190
DiscountCode: sizingKitDiscountCode,
9291
}, nil)
@@ -125,7 +124,8 @@ var _ = Describe("FulfillmentEventProcessor", func() {
125124
"name": "oura_sizing_kit_delivered",
126125
"id": "` + fmt.Sprintf("%d", event.ID) + `",
127126
"data": {
128-
"oura_ring_discount_code": "` + input.Code + `"
127+
"oura_ring_discount_code": "` + input.Code + `",
128+
"oura_sizing_kit_discount_code": "` + sizingKitDiscountCode + `"
129129
}
130130
}`),
131131
},
@@ -153,7 +153,6 @@ var _ = Describe("FulfillmentEventProcessor", func() {
153153
shopifyClnt.EXPECT().
154154
GetDeliveredProducts(gomock.Any(), fmt.Sprintf("gid://shopify/Order/%d", event.OrderID)).
155155
Return(&shopify.DeliveredProducts{
156-
OrderID: fmt.Sprintf("%d", event.OrderID),
157156
IDs: []string{shopify.OuraRingProductID},
158157
DiscountCode: discountCode,
159158
}, nil)
@@ -184,7 +183,9 @@ var _ = Describe("FulfillmentEventProcessor", func() {
184183
ouraTest.NewRequestJSONBodyMatcher(`{
185184
"name": "oura_ring_delivered",
186185
"id": "` + fmt.Sprintf("%d", event.ID) + `",
187-
"data": {}
186+
"data": {
187+
"oura_ring_discount_code": "` + discountCode + `"
188+
}
188189
}`),
189190
},
190191
ouraTest.Response{StatusCode: http.StatusOK, Body: "{}"},

oura/shopify/order.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package shopify
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/tidepool-org/platform/log"
9+
"github.com/tidepool-org/platform/oura/customerio"
10+
)
11+
12+
type OrdersCreateEvent struct {
13+
ID int64 `json:"id"`
14+
AdminGraphQLAPIID string `json:"admin_graphql_api_id"`
15+
ConfirmationNumber interface{} `json:"confirmation_number"`
16+
Confirmed bool `json:"confirmed"`
17+
ContactEmail string `json:"contact_email"`
18+
CreatedAt time.Time `json:"created_at"`
19+
DiscountCodes []DiscountCode `json:"discount_codes"`
20+
Email string `json:"email"`
21+
LineItems []LineItem `json:"line_items"`
22+
Name string `json:"name"`
23+
Note interface{} `json:"note"`
24+
NoteAttributes []interface{} `json:"note_attributes"`
25+
OrderNumber int `json:"order_number"`
26+
OrderStatusUrl string `json:"order_status_url"`
27+
ProcessedAt time.Time `json:"processed_at"`
28+
Reference interface{} `json:"reference"`
29+
UpdatedAt time.Time `json:"updated_at"`
30+
UserId interface{} `json:"user_id"`
31+
Returns []interface{} `json:"returns"`
32+
}
33+
34+
type DiscountCode struct {
35+
Code string `json:"code"`
36+
Type string `json:"type"`
37+
Amount string `json:"amount"`
38+
}
39+
40+
type LineItem struct {
41+
ID int64 `json:"id"`
42+
AdminGraphQLAPIID string `json:"admin_graphql_api_id"`
43+
ProductID int64 `json:"product_id"`
44+
}
45+
46+
type OrdersCreateEventProcessor struct {
47+
logger log.Logger
48+
49+
customerIOClient *customerio.Client
50+
shopifyClient Client
51+
}
52+
53+
func NewOrdersCreateEventProcessor(logger log.Logger, customerIOClient *customerio.Client, shopifyClient Client) (*OrdersCreateEventProcessor, error) {
54+
return &OrdersCreateEventProcessor{
55+
logger: logger,
56+
customerIOClient: customerIOClient,
57+
shopifyClient: shopifyClient,
58+
}, nil
59+
}
60+
61+
func (f *OrdersCreateEventProcessor) Process(ctx context.Context, event OrdersCreateEvent) error {
62+
logger := f.logger.WithField("orderId", event.ID)
63+
64+
var products []string
65+
for _, lineItem := range event.LineItems {
66+
products = append(products, fmt.Sprintf("%d", lineItem.ProductID))
67+
}
68+
69+
if len(products) == 0 {
70+
logger.Info("ignoring orders create event with no products")
71+
return nil
72+
} else if len(products) > 1 {
73+
logger.Warn("ignoring orders create event with multiple products")
74+
return nil
75+
}
76+
77+
productID := products[0]
78+
logger = logger.WithField("productId", productID)
79+
80+
attribute, ok := productIDToOuraDiscountAttribute[productID]
81+
if !ok {
82+
logger.Warn("unable to find discount attribute for product")
83+
return nil
84+
}
85+
86+
var discountCodes []string
87+
for _, discountCode := range event.DiscountCodes {
88+
discountCodes = append(discountCodes, discountCode.Code)
89+
}
90+
91+
if len(discountCodes) == 0 {
92+
logger.Warn("ignoring orders create event with no discount codes")
93+
return nil
94+
} else if len(discountCodes) > 1 {
95+
logger.Warn("ignoring orders create event with multiple discount codes")
96+
return nil
97+
}
98+
99+
discountCode := discountCodes[0]
100+
customers, err := f.customerIOClient.FindCustomers(ctx, map[string]any{
101+
"filter": map[string]any{
102+
"and": []any{
103+
map[string]any{
104+
"field": attribute,
105+
"operator": "eq",
106+
"value": discountCode,
107+
},
108+
},
109+
},
110+
})
111+
if err != nil {
112+
logger.WithError(err).Warn("unable to find customers")
113+
return nil
114+
}
115+
116+
if len(customers.Identifiers) == 0 {
117+
logger.Warn("no customers found for delivered products")
118+
return nil
119+
} else if len(customers.Identifiers) > 1 {
120+
userIds := make([]string, 0, len(customers.Identifiers))
121+
for _, id := range customers.Identifiers {
122+
userIds = append(userIds, id.ID)
123+
}
124+
logger.WithField("userIds", userIds).Warn("multiple customers found for delivered products")
125+
return nil
126+
}
127+
128+
switch productID {
129+
case OuraSizingKitProductID:
130+
if err := f.onSizingKitOrdered(ctx, customers.Identifiers[0], discountCode); err != nil {
131+
logger.WithError(err).Warn("unable to send sizing kit ordered event")
132+
return err
133+
}
134+
case OuraRingProductID:
135+
if err := f.onRingOrdered(ctx, customers.Identifiers[0], discountCode); err != nil {
136+
logger.WithError(err).Warn("unable to send ring ordered event")
137+
return err
138+
}
139+
default:
140+
logger.Warn("ignoring orders create event for unknown product")
141+
}
142+
143+
return nil
144+
}
145+
146+
func (f *OrdersCreateEventProcessor) onSizingKitOrdered(ctx context.Context, identifiers customerio.Identifiers, discountCode string) error {
147+
sizingKitOrdered := customerio.Event{
148+
Name: customerio.OuraSizingKitOrderedEventType,
149+
ID: discountCode,
150+
Data: customerio.OuraSizingKitOrderedData{
151+
OuraSizingKitDiscountCode: discountCode,
152+
},
153+
}
154+
155+
return f.customerIOClient.SendEvent(ctx, identifiers.ID, sizingKitOrdered)
156+
}
157+
158+
func (f *OrdersCreateEventProcessor) onRingOrdered(ctx context.Context, identifiers customerio.Identifiers, discountCode string) error {
159+
ringOrdered := customerio.Event{
160+
Name: customerio.OuraRingOrderedEventType,
161+
ID: discountCode,
162+
Data: customerio.OuraRingOrderedData{
163+
OuraRingDiscountCode: discountCode,
164+
},
165+
}
166+
167+
return f.customerIOClient.SendEvent(ctx, identifiers.ID, ringOrdered)
168+
}

0 commit comments

Comments
 (0)