Skip to content

Conversation

@toddkazakov
Copy link
Contributor

@toddkazakov toddkazakov commented Dec 16, 2025

  • BACK-4162
  • BACK-4164
  • BACK-4165
  • BACK-4041
  • BACK-3959
  • BACK-4166
sequenceDiagram
    actor User
    participant JotForm
    participant Platform
    participant Customer.io
    participant Shopify

    User->>JotForm: Submits survey
    JotForm->>Platform: Webhook call with submission ID
    Platform->>JotForm: Pull submission details
    JotForm-->>Platform: Return submission data
    
    Platform->>Platform: Check eligibility criteria
    Platform->>Customer.io: Check if participant ID exists
    Customer.io-->>Platform: Return participant status
    
    Platform->>Shopify: Generate discount code
    Shopify-->>Platform: Return discount code
    
    Platform->>Customer.io: Send oura_eligibility_survey_completed event
    Customer.io->>Customer.io: Trigger Sizing Kit Campaign
    
    User->>Shopify: Place Sizing Kit Order
    Shopify->>Platform: Send orders/create notification
    Platform->>Customer.io: Send oura_sizing_kit_ordered event
    Shopify->>Platform: Send fulfillments/update or fulfillments/create notification
    Platform->>Shopify: Generate discount code
    Shopify-->>Platform: Return discount code
    Platform->>Shopify: Send oura_sizing_kit_delivered event
    Customer.io->>Customer.io: Trigger Ring Campaign

    User->>Shopify: Place Ring Order
    Shopify->>Platform: Send orders/create notification
    Platform->>Customer.io: Send oura_ring_ordered event
    Shopify->>Platform: Send fulfillments/update or fulfillments/create notification
    Platform->>Shopify: Send oura_sizing_kit_delivered event
Loading

@toddkazakov toddkazakov force-pushed the tk-oura-webhooks-shopify branch from 8e9edd8 to 3935ecd Compare December 16, 2025 14:31
@toddkazakov toddkazakov force-pushed the tk-oura-webhooks-shopify branch from 3935ecd to 9a99e16 Compare December 16, 2025 14:49
@toddkazakov
Copy link
Contributor Author

/deploy dev1

@toddkazakov
Copy link
Contributor Author

/deploy dev

@tidebot
Copy link
Collaborator

tidebot commented Dec 16, 2025

toddkazakov updated values.yaml file in dev1

@tidebot
Copy link
Collaborator

tidebot commented Dec 16, 2025

toddkazakov updated flux policies file in dev1

@tidebot
Copy link
Collaborator

tidebot commented Dec 16, 2025

toddkazakov deployed platform tk-oura-webhooks-shopify branch to dev1 namespace

@toddkazakov toddkazakov force-pushed the tk-oura-webhooks-shopify branch from 37d1eac to 27ec01d Compare December 17, 2025 15:03
@toddkazakov toddkazakov force-pushed the tk-oura-webhooks-shopify branch 6 times, most recently from 91c0bc2 to 6c322c7 Compare December 18, 2025 20:34
@toddkazakov toddkazakov force-pushed the tk-oura-webhooks-shopify branch from 6c322c7 to a3aa538 Compare December 18, 2025 21:19
@toddkazakov toddkazakov changed the base branch from tk-oura-webhooks to master December 23, 2025 09:50
@toddkazakov toddkazakov force-pushed the tk-oura-webhooks-shopify branch 3 times, most recently from 3997ec3 to 22aae96 Compare December 23, 2025 10:21
@toddkazakov toddkazakov force-pushed the tk-oura-webhooks-shopify branch from 22aae96 to 9995641 Compare December 23, 2025 14:44
@toddkazakov toddkazakov requested a review from ewollesen January 7, 2026 13:00
@toddkazakov toddkazakov changed the title Add shopify webhooks for OURA Add webhooks for OURA Jan 8, 2026
Copy link
Contributor

@ewollesen ewollesen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things small things that I think can be quickly fixed. Nothing majorly blocking, though the use of the platform's error package might take a fair bit of cut and paste, if it's decided that it should be done at all.

In general, while there are tests, they seem to focus on successful completion, i.e. testing that things work, not that things that shouldn't work don't work, or that things that don't work fail in the expected manner. Given the relatively temporary nature of this code, that's not too concerning, but as always, today's hot fix is tomorrow production code. So... Shrug, something to think about maybe.

func (c *Client) FindCustomers(ctx context.Context, filter map[string]any) (*FindCustomersResponse, error) {
url := fmt.Sprintf("%s/v1/customers", c.appAPIBaseURL)

body, _ := json.Marshal(filter)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check for and handle errors. It's very unlikely here, but better than panicking.

body, _ := json.Marshal(filter)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of platform, so errors.Wrap is probably a better choice.

This occurs several times throughout, so I'll leave it to you to find the rest.

if err := json.NewDecoder(resp.Body).Decode(&errResp); err == nil && len(errResp.Errors) > 0 {
return fmt.Errorf("API error (status %d): %s", resp.StatusCode, errResp.Errors[0].Message)
}
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I often find it more useful to return resp.Status, which includes both the code, and the basic name of the status code as a string. It's sometimes more helpful in the logs.


type segmentMembershipResponse struct {
Identifiers []Identifiers `json:"identifiers"`
IDs []string `json:"ids"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why IDs and Identifiers? Isn't IDs an abbreviation of Identifiers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the response that we get from customer io - we have no control of it.

ctx := req.Context()
responder := request.MustNewResponder(res, req)

if err := req.ParseMultipartForm(multipartMaxMemory); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be simpler I think to call to PostFormValue. It will call the appropriate ParseForm method as needed, and return the first matching field.

OuraRingProductID = "15496765964675" // Todd's test store
OuraRingDiscountCodeTitle = "Oura Ring Discount Code"

DiscountCodeLength = 12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these aren't being manually input, and I think they're not... is there any reason not to use the full output of rand.Text()?

The result contains at least 128 bits of randomness, enough to prevent brute force guessing attacks and to make the likelihood of collisions vanishingly small.

I don't have a problem with 12 characters, but better safe than sorry, especially if no one needs to copy the codes, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are sending those in emails. I'd prefer to keep them short. The worst that can happen is to generate a duplicate value the shopify rejects. The reconciliation process should retry this.

},
}

return f.customerIOClient.SendEvent(ctx, identifiers.ID, sizingKitDelivered)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any sort of retry logic here, maybe that'll be covered by the task / job that gets run 1x / day to reconcile?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The job will be run more frequently (every hour or so) and will reprocessed failures.

return nil
}

func (w *WebhookProcessor) ensureConsentRecordExists(ctx context.Context, userID string, submission *SubmissionResponse) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question just to confirm:

The fact that we've received a jotform submission means that they've consented, right?

I seem to remember that was the case, but wanted to be sure.

In theory we're not emailing anyone < 18 years of age, but in the event that we did, do we want to perform any double checks here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the submission is marked as eligible, then we must upsert a consent record. This is the only value that we check. If we don't want to execute this logic, jotform ought to mark the submission is ineligible.

}
case OuraRingProductID:
if err := f.onRingDelivered(ctx, customers.Identifiers[0], event, deliveredProducts.DiscountCode); err != nil {
logger.WithError(err).Warn("unable to send ring delivered event")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically it's preferable to either log the error, or return it, but not both. When you log then return the error, it tends to get logged multiple times which clutters the log output. Consider using errors.Wrap instead of logging.

return err
}
default:
logger.Warn("ignoring fulfillment event for unknown product")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What effect will returning nil here have later down the line?

I don't see a test case covering this. Seems like it would be easier to debug this if an error were returned. The error would get logged and the user would likely get some useful message as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a fulfilment that we can't process, so returning a nil will prevent shopify from resending the event. Returning an error will make shopify retry the webhook delivery.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants