Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple Order and Shipment Workflows #2

Merged
merged 13 commits into from
Mar 25, 2024
6 changes: 1 addition & 5 deletions activities/activities.go
robholland marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
package activities

import "go.temporal.io/sdk/client"

type Activities struct {
client client.Client
}
type Activities struct{}
34 changes: 34 additions & 0 deletions activities/notification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package activities
Copy link
Member

Choose a reason for hiding this comment

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

@cretz Any preferences on the question of whether notifications like these should be defined by the source (pubsub-style) or by the receiver (which I would call master/slave style, which I know is now considered very politically incorrect but I don't know any good equivalent).

I tend to prefer the latter, which would imply (here, I think) that the OP App defines the information it needs and treats other systems as auxiliary.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Let's discuss on Slack.


import (
"context"

"github.com/temporalio/orders-reference-app-go/pkg/ordersapi"
)

type ShipmentCreatedNotificationInput struct {
OrderID ordersapi.OrderID
}
type ShipmentCreatedNotificationResult struct{}
robholland marked this conversation as resolved.
Show resolved Hide resolved

func (a *Activities) ShipmentCreatedNotification(ctx context.Context, input ShipmentCreatedNotificationInput) (ShipmentCreatedNotificationResult, error) {
return ShipmentCreatedNotificationResult{}, nil
}

type ShipmentDispatchedNotificationInput struct {
OrderID ordersapi.OrderID
}
type ShipmentDispatchedNotificationResult struct{}

func (a *Activities) ShipmentDispatchedNotification(ctx context.Context, input ShipmentDispatchedNotificationInput) (ShipmentDispatchedNotificationResult, error) {
return ShipmentDispatchedNotificationResult{}, nil
}

type ShipmentDeliveredNotificationInput struct {
OrderID ordersapi.OrderID
}
type ShipmentDeliveredNotificationResult struct{}

func (a *Activities) ShipmentDeliveredNotification(ctx context.Context, input ShipmentDeliveredNotificationInput) (ShipmentDeliveredNotificationResult, error) {
return ShipmentDeliveredNotificationResult{}, nil
}
18 changes: 18 additions & 0 deletions activities/shipment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package activities

import (
"context"

"github.com/temporalio/orders-reference-app-go/pkg/ordersapi"
)

type RegisterShipmentInput struct {
OrderID ordersapi.OrderID
Items []ordersapi.Item
}

type RegisterShipmentResult struct{}

func (a *Activities) RegisterShipment(ctx context.Context, input RegisterShipmentInput) (RegisterShipmentResult, error) {
return RegisterShipmentResult{}, nil
}
20 changes: 20 additions & 0 deletions docs/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# System Overview

```mermaid
sequenceDiagram
participant Customer
participant Order
participant Shipment
participant Courier

Customer->>Order: place order
Order->>Shipment: create shipment
Shipment->>Courier: register shipment
Courier->>Shipment: shipment registered
Shipment->>Customer: shipment created
Courier->>Shipment: shipment dispatched
Shipment->>Customer: shipment dispatched
Courier->>Shipment: shipment delivered
Shipment->>Customer: shipment delivered
Order->>Customer: order complete
```
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module github.com/temporalio/orders-reference-app-go

go 1.22

require go.temporal.io/sdk v1.26.0
require (
github.com/stretchr/testify v1.9.0
go.temporal.io/sdk v1.26.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
Expand All @@ -17,7 +20,6 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
go.temporal.io/api v1.29.1 // indirect
golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
golang.org/x/net v0.22.0 // indirect
Expand Down
37 changes: 37 additions & 0 deletions internal/shipmentapi/shipmentapi.go
robholland marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package shipmentapi

import (
"fmt"

"github.com/temporalio/orders-reference-app-go/pkg/ordersapi"
)

// The Shipment interfaces are only used internally.

// Signals will be sent by the courier via a local API service,
robholland marked this conversation as resolved.
Show resolved Hide resolved
// so we don't need to expose these.

func ShipmentWorkflowID(orderID ordersapi.OrderID) string {
return fmt.Sprintf("shipment:%s", orderID)
}

type ShipmentInput struct {
OrderID ordersapi.OrderID
Items []ordersapi.Item
}

const ShipmentUpdateSignalName = "ShipmentUpdate"

type ShipmentStatus int

const (
ShipmentStatusRegistered ShipmentStatus = iota
ShipmentStatusDispatched
ShipmentStatusDelivered
)

type ShipmentUpdateSignal struct {
Status ShipmentStatus
}

type ShipmentResult struct{}
20 changes: 20 additions & 0 deletions pkg/ordersapi/ordersapi.go
robholland marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ordersapi

// The Orders API is exposed as the JSON equivalents will be used to start Orders via the local API.

// TODO: Do we want to do pass-through like this or separate and translate API definitions to Workflow types?
robholland marked this conversation as resolved.
Show resolved Hide resolved
// TODO: Protobufs?

type OrderID string

type Item struct {
SKU string
Quantity int32
}

type OrderInput struct {
ID OrderID
Items []Item
}

type OrderResult struct{}
30 changes: 30 additions & 0 deletions workflows/order.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package workflows

import (
"github.com/temporalio/orders-reference-app-go/internal/shipmentapi"
"github.com/temporalio/orders-reference-app-go/pkg/ordersapi"
"go.temporal.io/sdk/workflow"
)

func Order(ctx workflow.Context, order ordersapi.OrderInput) (ordersapi.OrderResult, error) {
var result ordersapi.OrderResult

ctx = workflow.WithChildOptions(ctx,
workflow.ChildWorkflowOptions{
WorkflowID: shipmentapi.ShipmentWorkflowID(order.ID),
},
)

err := workflow.ExecuteChildWorkflow(ctx,
Shipment,
shipmentapi.ShipmentInput{
OrderID: order.ID,
Items: order.Items,
},
).Get(ctx, nil)
if err != nil {
return result, err
}

return result, nil
}
40 changes: 40 additions & 0 deletions workflows/order_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package workflows_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/temporalio/orders-reference-app-go/internal/shipmentapi"
"github.com/temporalio/orders-reference-app-go/pkg/ordersapi"
"github.com/temporalio/orders-reference-app-go/workflows"
"go.temporal.io/sdk/testsuite"
"go.temporal.io/sdk/workflow"
)

func TestOrderWorkflow(t *testing.T) {
s := testsuite.WorkflowTestSuite{}
env := s.NewTestWorkflowEnvironment()

env.RegisterWorkflow(workflows.Order)

orderInput := ordersapi.OrderInput{
Items: []ordersapi.Item{
{SKU: "test1", Quantity: 1},
{SKU: "test2", Quantity: 3},
},
}

env.OnWorkflow(workflows.Shipment, mock.Anything, mock.Anything).Return(func(ctx workflow.Context, input shipmentapi.ShipmentInput) (shipmentapi.ShipmentResult, error) {
return shipmentapi.ShipmentResult{}, nil
})

env.ExecuteWorkflow(
workflows.Order,
orderInput,
)

var result ordersapi.OrderResult
err := env.GetWorkflowResult(&result)
assert.NoError(t, err)
}
92 changes: 92 additions & 0 deletions workflows/shipment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package workflows

import (
"time"

"github.com/temporalio/orders-reference-app-go/activities"
"github.com/temporalio/orders-reference-app-go/internal/shipmentapi"
"go.temporal.io/sdk/workflow"
)

type shipment struct {
status shipmentapi.ShipmentStatus
}

func Shipment(ctx workflow.Context, input shipmentapi.ShipmentInput) (shipmentapi.ShipmentResult, error) {
return new(shipment).run(ctx, input)
}

func (s *shipment) run(ctx workflow.Context, input shipmentapi.ShipmentInput) (shipmentapi.ShipmentResult, error) {
workflow.Go(ctx, s.statusUpdater)

var result shipmentapi.ShipmentResult

ctx = workflow.WithActivityOptions(ctx,
workflow.ActivityOptions{
StartToCloseTimeout: 5 * time.Second,
},
)

err := workflow.ExecuteActivity(ctx,
a.RegisterShipment,
activities.RegisterShipmentInput{
OrderID: input.OrderID,
Items: input.Items,
},
).Get(ctx, nil)
if err != nil {
return result, err
}

err = workflow.ExecuteActivity(ctx,
a.ShipmentCreatedNotification,
activities.ShipmentCreatedNotificationInput{
OrderID: input.OrderID,
},
).Get(ctx, nil)
if err != nil {
return result, err
}

s.waitForStatus(ctx, shipmentapi.ShipmentStatusDispatched)

err = workflow.ExecuteActivity(ctx,
a.ShipmentDispatchedNotification,
activities.ShipmentDispatchedNotificationInput{
OrderID: input.OrderID,
},
).Get(ctx, nil)
if err != nil {
return result, err
}

s.waitForStatus(ctx, shipmentapi.ShipmentStatusDelivered)

err = workflow.ExecuteActivity(ctx,
a.ShipmentDeliveredNotification,
activities.ShipmentDeliveredNotificationInput{
OrderID: input.OrderID,
},
).Get(ctx, nil)
if err != nil {
return result, err
}

return result, nil
}

func (s *shipment) statusUpdater(ctx workflow.Context) {
var signal shipmentapi.ShipmentUpdateSignal

ch := workflow.GetSignalChannel(ctx, shipmentapi.ShipmentUpdateSignalName)
for {
ch.Receive(ctx, &signal)
s.status = signal.Status
}
}

func (s *shipment) waitForStatus(ctx workflow.Context, status shipmentapi.ShipmentStatus) {
workflow.Await(ctx, func() bool {
return s.status == status
})
}
67 changes: 67 additions & 0 deletions workflows/shipment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package workflows_test

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/temporalio/orders-reference-app-go/activities"
"github.com/temporalio/orders-reference-app-go/internal/shipmentapi"
"github.com/temporalio/orders-reference-app-go/pkg/ordersapi"
"github.com/temporalio/orders-reference-app-go/workflows"
"go.temporal.io/sdk/testsuite"
)

func TestShipmentWorkflow(t *testing.T) {
s := testsuite.WorkflowTestSuite{}
env := s.NewTestWorkflowEnvironment()
var a *activities.Activities

shipmentInput := shipmentapi.ShipmentInput{
OrderID: "test",
Items: []ordersapi.Item{
{SKU: "test1", Quantity: 1},
{SKU: "test2", Quantity: 3},
},
}

env.RegisterActivity(a.RegisterShipment)

env.OnActivity(a.ShipmentCreatedNotification, mock.Anything, mock.Anything).Return(
func(ctx context.Context, input activities.ShipmentCreatedNotificationInput) (activities.ShipmentCreatedNotificationResult, error) {
env.SignalWorkflow(
shipmentapi.ShipmentUpdateSignalName,
shipmentapi.ShipmentUpdateSignal{
Status: shipmentapi.ShipmentStatusDispatched,
},
)

return activities.ShipmentCreatedNotificationResult{}, nil
},
)

env.OnActivity(a.ShipmentDispatchedNotification, mock.Anything, mock.Anything).Return(
func(ctx context.Context, input activities.ShipmentDispatchedNotificationInput) (activities.ShipmentDispatchedNotificationResult, error) {
env.SignalWorkflow(
shipmentapi.ShipmentUpdateSignalName,
shipmentapi.ShipmentUpdateSignal{
Status: shipmentapi.ShipmentStatusDelivered,
},
)

return activities.ShipmentDispatchedNotificationResult{}, nil
},
)

env.RegisterActivity(a.ShipmentDeliveredNotification)

env.ExecuteWorkflow(
workflows.Shipment,
shipmentInput,
)

var result shipmentapi.ShipmentResult
err := env.GetWorkflowResult(&result)
assert.NoError(t, err)
}