{"id":28506427,"url":"https://github.com/temporalio/temporal-ecommerce","last_synced_at":"2025-07-05T03:31:48.388Z","repository":{"id":40557786,"uuid":"349811739","full_name":"temporalio/temporal-ecommerce","owner":"temporalio","description":null,"archived":false,"fork":false,"pushed_at":"2025-07-03T02:07:01.000Z","size":325,"stargazers_count":64,"open_issues_count":11,"forks_count":29,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-07-03T03:23:10.945Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/temporalio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-03-20T18:59:46.000Z","updated_at":"2025-07-03T02:07:04.000Z","dependencies_parsed_at":"2024-12-02T19:27:37.663Z","dependency_job_id":"32f20cab-8f66-4bca-aeee-641f1aaaa023","html_url":"https://github.com/temporalio/temporal-ecommerce","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/temporalio/temporal-ecommerce","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/temporalio%2Ftemporal-ecommerce","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/temporalio%2Ftemporal-ecommerce/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/temporalio%2Ftemporal-ecommerce/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/temporalio%2Ftemporal-ecommerce/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/temporalio","download_url":"https://codeload.github.com/temporalio/temporal-ecommerce/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/temporalio%2Ftemporal-ecommerce/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263676471,"owners_count":23494615,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-08T20:05:41.948Z","updated_at":"2025-07-05T03:31:48.382Z","avatar_url":"https://github.com/temporalio.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# temporal-ecommerce\n\nThis is a demo app for a tutorial showing the process of developing a [Temporal eCommerce application in Go](https://learn.temporal.io/tutorials/go/build-an-ecommerce-app/), using the Stripe and Mailgun APIs.\n\n## Instructions\n\nTo run the worker and server, you must set the `STRIPE_PRIVATE_KEY`, `MAILGUN_DOMAIN`, and `MAILGUN_PRIVATE_KEY` environment variables.\nYou can set the values to \"test\", which will allow you to add and remove elements from your cart.\nBut you won't be able to checkout or receive abandoned cart notifications if these values aren't set.\n\nTo run the worker, make sure you have a local instance of Temporal Server running (e.g. with [the Temporal CLI](https://github.com/temporalio/cli)), then run:\n\n```bash\nenv STRIPE_PRIVATE_KEY=stripe-key-here env MAILGUN_DOMAIN=mailgun-domain-here env MAILGUN_PRIVATE_KEY=mailgun-private-key-here go run worker/main.go\n```\n\nTo run the API server, you must also set the `PORT` environment variable as follows.\n\n```bash\nenv STRIPE_PRIVATE_KEY=stripe-key-here env MAILGUN_DOMAIN=mailgun-domain-here env MAILGUN_PRIVATE_KEY=mailgun-private-key-here env PORT=3001 go run api/main.go\n```\n\nYou can then run the UI on port 8080:\n\n```\ncd frontend\nnpm install\nnpm start\n```\n\n## Interacting with the API server with cURL\n\nHere is a guide to the basic routes that you can see and what they expect:\n\n```bash\n# get items\ncurl http://localhost:3001/products\n\n# response:\n# {\"products\":[\n    # {\"Id\":0,\"Name\":\"iPhone 12 Pro\",\"Description\":\"Test\",\"Image\":\"https://images.unsplash.com/photo-1603921326210-6edd2d60ca68\",\"Price\":999},\n    # {\"Id\":1,\"Name\":\"iPhone 12\",\"Description\":\"Test\",\"Image\":\"https://images.unsplash.com/photo-1611472173362-3f53dbd65d80\",\"Price\":699},\n    # {\"Id\":2,\"Name\":\"iPhone SE\",\"Description\":\"399\",\"Image\":\"https://images.unsplash.com/photo-1529618160092-2f8ccc8e087b\",\"Price\":399},\n    # {\"Id\":3,\"Name\":\"iPhone 11\",\"Description\":\"599\",\"Image\":\"https://images.unsplash.com/photo-1574755393849-623942496936\",\"Price\":599}\n# ]}\n\n# create cart\ncurl -X POST http://localhost:3001/cart\n\n# response:\n# {\"cart\":{\"Items\":[],\"Email\":\"\"},\n#  \"workflowID\":\"CART-1619483151\"}\n\n# add item\ncurl -X PUT -d '{\"ProductId\":3,\"Quantity\":1}' -H 'Content-Type: application/json' http://localhost:3001/cart/CART-1619483151/4a4436be-3307-42ea-a9ab-3b63f5520bee/add\n\n# response: {\"ok\":1}\n\n# get cart\ncurl http://localhost:3001/cart/CART-1619483151/4a4436be-3307-42ea-a9ab-3b63f5520bee\n\n# response:\n# {\"Email\":\"\",\"Items\":[{\"ProductId\":3,\"Quantity\":1}]}\n```\n\n## Interacting with the API server with Node.js\n\nBelow is a Node.js script that creates a new cart, adds/removes some items, and checks out.\n\n```javascript\n'use strict';\n\nconst assert = require('assert');\nconst axios = require('axios');\n\nvoid async function main() {\n  let { data } = await axios.post('http://localhost:3001/cart');\n\n  const { workflowID } = data;\n  console.log(workflowID)\n\n  await axios.put(`http://localhost:3001/cart/${workflowID}/add`, { ProductID: 1, Quantity: 2 });\n\n  ({ data } = await axios.get(`http://localhost:3001/cart/${workflowID}`));\n  console.log(data);\n  assert.deepEqual(data.Items, [ { ProductId: 1, Quantity: 2 } ]);\n\n  await axios.put(`http://localhost:3001/cart/${workflowID}/remove`, { ProductID: 1, Quantity: 1 });\n\n  ({ data } = await axios.get(`http://localhost:3001/cart/${workflowID}`));\n  console.log(data);\n  assert.deepEqual(data.Items, [ { ProductId: 1, Quantity: 1 } ]);\n\n  await axios.put(`http://localhost:3001/cart/${workflowID}/checkout`, { Email: 'val@temporal.io' });\n\n  ({ data } = await axios.get(`http://localhost:3001/cart/${workflowID}`));\n  console.log(data);\n}();\n```\n\n## Notes on Testing\n\nThe following is a basic setup for testing a Temporal Workflow using `go test` and [Testify](https://github.com/stretchr/testify) based on [Temporal's Go testing docs](https://docs.temporal.io/dev-guide/go/testing).\n\nYou can find the full source code for the test suite in the `workflow_test.go` file.\n\n```go\npackage app\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/mock\"\n\t\"github.com/stretchr/testify/suite\"\n\n\t\"go.temporal.io/sdk/activity\"\n\t\"go.temporal.io/sdk/testsuite\"\n\t\"go.temporal.io/sdk/client\"\n\n\t\"time\"\n)\n\ntype UnitTestSuite struct {\n\tsuite.Suite\n\ttestsuite.WorkflowTestSuite\n\n\tenv *testsuite.TestWorkflowEnvironment\n}\n\nfunc (s *UnitTestSuite) SetupTest() {\n\t// You are responsible for calling `NewTestWorkflowEnvironment()` to initialize\n\t// Temporal's testing utilities, but you can also add any other setup you need\n\t// in this function.\n\ts.env = s.NewTestWorkflowEnvironment()\n}\n\nfunc (s *UnitTestSuite) AfterTest(suiteName, testName string) {\n\ts.env.AssertExpectations(s.T())\n}\n\nfunc TestUnitTestSuite(t *testing.T) {\n\tsuite.Run(t, new(UnitTestSuite))\n}\n```\n\nThe most important property is the `env` property, which is an instance of [Temporal's `TestWorkflowEnvironment` struct](https://pkg.go.dev/go.temporal.io/temporal/internal#TestWorkflowEnvironment).\nA `TestWorkflowEnvironment` provides utilities for testing Workflows, including executing Workflows, mocking Activities, and Signaling and Querying test Workflows.\n\nThe [testify package](https://github.com/stretchr/testify) also provides utilities for organizing tests, including setting up and tearing down test suites using `SetupTest()` and `AfterTest()`.\nFor example, you can define multiple test suites as shown below.\n\n```go\ntype UnitTestSuite struct {\n\tsuite.Suite\n\ttestsuite.WorkflowTestSuite\n\n\tenv *testsuite.TestWorkflowEnvironment\n}\n\nfunc (s *UnitTestSuite) SetupTest() {\n\ts.env = s.NewTestWorkflowEnvironment()\n}\n\nfunc (s *UnitTestSuite) AfterTest(suiteName, testName string) {\n\ts.env.AssertExpectations(s.T())\n}\n\ntype IntegrationTestSuite struct {\n\tsuite.Suite\n\ttestsuite.WorkflowTestSuite\n\n\tenv *testsuite.TestWorkflowEnvironment\n}\n\nfunc (s *IntegrationTestSuite) SetupTest() {\n\ts.env = s.NewTestWorkflowEnvironment()\n}\n\nfunc (s *IntegrationTestSuite) AfterTest(suiteName, testName string) {\n\ts.env.AssertExpectations(s.T())\n}\n```\n\n### Querying Workflows in tests\n\nRemember that, in this app, a shopping cart is a Workflow.\nTo get the current state of the shopping cart, you send a Query to the Workflow, and to update the cart you send a Signal to the Workflow.\n\nThe below code shows how you can use `env.QueryWorkflow()` to send a Query to the shopping cart Workflow.\n\n```go\nfunc (s *UnitTestSuite) Test_QueryCart() {\n\tcart := CartState{Items: make([]CartItem, 0)}\n\n\ts.env.ExecuteWorkflow(CartWorkflow, cart)\n\n  // Note that `ExecuteWorkflow()` is blocking: the Workflow is done by the time\n  // the test gets to this line.\n\ts.True(s.env.IsWorkflowCompleted())\n\n  // Send a query to the Workflow and assert that the shopping cart is still empty\n\tres, err := s.env.QueryWorkflow(\"getCart\")\n\ts.NoError(err)\n\terr = res.Get(\u0026cart)\n\ts.NoError(err)\n\ts.Equal(0, len(cart.Items))\n}\n```\n\nNote that the above code Queries the Workflow _after the Workflow is done_.\nIn order to interact with the Workflow via Queries and Signals while the Workflow is running, you should use the test environment's `RegisterDelayedCallback()` function as shown below.\nMake sure you call `RegisterDelayedCallback()` _before_ `ExecuteWorkflow()`, otherwise Temporal will execute the entire Workflow without executing the callback.\n\n```go\nfunc (s *UnitTestSuite) Test_IntermediateQuery() {\n\tcart := CartState{Items: make([]CartItem, 0)}\n\n  // Register a callback to execute after 1 millisecond elapses in the Workflow.\n\ts.env.RegisterDelayedCallback(func() {\n\t\tres, err := s.env.QueryWorkflow(\"getCart\")\n\t\ts.NoError(err)\n\t\terr = res.Get(\u0026cart)\n\t\ts.NoError(err)\n\t\ts.Equal(len(cart.Items), 0)\n\t}, time.Millisecond*1)\n\n\ts.env.ExecuteWorkflow(CartWorkflow, cart)\n\n\ts.True(s.env.IsWorkflowCompleted())\n}\n```\n\nYou can Query a Workflow after it is completed, but you can't Signal a Workflow after it is completed.\n\n### Signaling Workflows in tests\n\nSo in order to Signal a Workflow from your tests, you need to use `RegisterDelayedCallback()`.\nJust remember that Signaling is asynchronous, so you need to add a separate `RegisterDelayedCallback()` to read the result of your Signal using a Query.\nFor example, below is a test case for the `AddToCart()` method.\n\n```go\nfunc (s *UnitTestSuite) Test_AddToCart() {\n\tcart := CartState{Items: make([]CartItem, 0)}\n\n  // First callback at 1ms: query to make sure the cart is empty, and signal to add an item.\n\ts.env.RegisterDelayedCallback(func() {\n\t\tres, err := s.env.QueryWorkflow(\"getCart\")\n\t\ts.NoError(err)\n\t\terr = res.Get(\u0026cart)\n\t\ts.NoError(err)\n\t\ts.Equal(len(cart.Items), 0)\n\n\t\tupdate := AddToCartSignal{\n\t\t\tRoute: RouteTypes.ADD_TO_CART,\n\t\t\tItem: CartItem{ProductId: 1, Quantity: 1},\n\t\t}\n\t\ts.env.SignalWorkflow(\"cartMessages\", update)\n\t}, time.Millisecond*1)\n\n  // Second callback at 2ms: query to make sure the item is in the cart\n  // This needs to be a separate callback, `s.Equal(1, len(cart.Items))` would\n  // fail if it were in the 1ms callback.\n\ts.env.RegisterDelayedCallback(func() {\n\t\tres, err := s.env.QueryWorkflow(\"getCart\")\n\t\ts.NoError(err)\n\t\terr = res.Get(\u0026cart)\n\t\ts.NoError(err)\n\n    s.Equal(1, len(cart.Items))\n    s.Equal(1, cart.Items[0].Quantity)\n\t}, time.Millisecond*2)\n\n\ts.env.ExecuteWorkflow(CartWorkflow, cart)\n\n\ts.True(s.env.IsWorkflowCompleted())\n}\n```\n\n### Sending multiple Signals to Workflows in tests\n\nSimilarly, if you want to send a Query to check the state of the Workflow between Signals, you should put the Query in a separate `RegisterDelayedCallback()` call.\nYou can move any Queries that don't have any Signals after them to after the `ExecuteWorkflow()` call.\nFor example, below is a test case for the `RemoveFromCart()` method.\n\n```go\nfunc (s *UnitTestSuite) Test_RemoveFromCart() {\n\tcart := CartState{Items: make([]CartItem, 0)}\n\n\t// Add 2 items to the cart\n\ts.env.RegisterDelayedCallback(func() {\n\t\tupdate := AddToCartSignal{\n\t\t\tRoute: RouteTypes.ADD_TO_CART,\n\t\t\tItem: CartItem{ProductId: 1, Quantity: 2},\n\t\t}\n\t\ts.env.SignalWorkflow(\"cartMessages\", update)\n\t}, time.Millisecond*1)\n\n\t// Query the current state and then remove 1 item from the cart\n\ts.env.RegisterDelayedCallback(func() {\n\t\tres, err := s.env.QueryWorkflow(\"getCart\")\n\t\ts.NoError(err)\n\t\terr = res.Get(\u0026cart)\n\t\ts.NoError(err)\n\t\ts.Equal(len(cart.Items), 1)\n\t\ts.Equal(cart.Items[0].Quantity, 2)\n\n\t\tupdate := AddToCartSignal{\n\t\t\tRoute: RouteTypes.REMOVE_FROM_CART,\n\t\t\tItem: CartItem{ProductId: 1, Quantity: 1},\n\t\t}\n\t\ts.env.SignalWorkflow(\"cartMessages\", update)\n\t}, time.Millisecond*2)\n\n\ts.env.ExecuteWorkflow(CartWorkflow, cart)\n\n\ts.True(s.env.IsWorkflowCompleted())\n\n\t// Since there's no more Signals, no need to put this Query in a\n\t// `RegisterDelayedCallback()` call.\n\tres, err := s.env.QueryWorkflow(\"getCart\")\n\ts.NoError(err)\n\terr = res.Get(\u0026cart)\n\ts.NoError(err)\n\ts.Equal(1, len(cart.Items))\n\ts.Equal(cart.Items[0].Quantity, 1)\n}\n```\n\nThis covers testing the basic functionality of adding items to and removing items from the shopping cart.\nBut what about testing more sophisticated features, like testing that the Workflow sends an abandoned cart email after 10 minutes?\n\n### Mocking Activities and Controlling Time\n\nTemporal's test environment makes it easy to mock Activities, replacing them with a stubbed out function.\nFor example, the below test asserts that sending a checkout Signal calls the `CreateStripeCharge` Activity with the correct receipt email using the test environment's `OnActivity()` function.\n\n```go\nfunc (s *UnitTestSuite) Test_Checkout() {\n\tcart := CartState{Items: make([]CartItem, 0)}\n\n\tvar a *Activities\n\tsendTo := \"\"\n\n\ts.env.OnActivity(a.CreateStripeCharge, mock.Anything, mock.Anything).Return(\n\t\tfunc(_ context.Context, cart CartState) (error) {\n\t\t\tsendTo = cart.Email\n\t\t\treturn nil\n\t\t})\n\n\t// Add a product to the cart\n\ts.env.RegisterDelayedCallback(func() {\n\t\tupdate := AddToCartSignal{\n\t\t\tRoute: RouteTypes.ADD_TO_CART,\n\t\t\tItem: CartItem{ProductId: 1, Quantity: 1},\n\t\t}\n\t\ts.env.SignalWorkflow(\"cartMessages\", update)\n\t}, time.Millisecond*1)\n\n\t// Check out\n\ts.env.RegisterDelayedCallback(func() {\n\t\tres, err := s.env.QueryWorkflow(\"getCart\")\n\t\ts.NoError(err)\n\t\terr = res.Get(\u0026cart)\n\t\ts.NoError(err)\n\t\ts.Equal(len(cart.Items), 1)\n\t\ts.Equal(cart.Items[0].Quantity, 1)\n\n\t\tupdate := CheckoutSignal{\n\t\t\tRoute: RouteTypes.CHECKOUT,\n\t\t\tEmail: \"test@temporal.io\",\n\t\t}\n\t\ts.env.SignalWorkflow(\"cartMessages\", update)\n\t}, time.Millisecond*2)\n\n\t// Workflow should be completed after checking out\n\ts.env.RegisterDelayedCallback(func() {\n\t\ts.True(s.env.IsWorkflowCompleted())\n\t}, time.Millisecond*3)\n\n\ts.env.ExecuteWorkflow(CartWorkflow, cart)\n\n\ts.Equal(sendTo, \"test@temporal.io\")\n}\n```\n\nWhat about testing the abandoned cart email?\nNormally, testing the abandoned cart email is tricky because it involves waiting for 10 minutes.\nThe key insight is that Temporal's test environment advances time internally, and time in the test environment is **not** [wall-clock time](https://en.wikipedia.org/wiki/Elapsed_real_time).\n\nThe `RegisterDelayedCallback()` function ties into the test environment's internal notion of time.\nCalling `RegisterDelayedCallback(fn, time.Minute*5)` does **not** tell the test environment to wait for 5 minutes of wall-clock time.\nThat means testing the abandoned cart email is easy: mock out the `SendAbandonedCartEmail()` activity and use `RegisterDelayedCallback()` with the `abandonedCartTimeout` as shown below.\n\n```go\nfunc (s *UnitTestSuite) Test_AbandonedCart() {\n\tcart := CartState{Items: make([]CartItem, 0)}\n\n\tvar a *Activities\n\n\tsendTo := \"\"\n\ts.env.OnActivity(a.SendAbandonedCartEmail, mock.Anything, mock.Anything).Return(\n\t\tfunc(_ context.Context, _sendTo string) (error) {\n\t\t\tsendTo = _sendTo\n\t\t\treturn nil\n\t\t})\n\n\t// Add a product to the cart\n\ts.env.RegisterDelayedCallback(func() {\n\t\tupdate := AddToCartSignal{\n\t\t\tRoute: RouteTypes.ADD_TO_CART,\n\t\t\tItem: CartItem{ProductId: 1, Quantity: 1},\n\t\t}\n\t\ts.env.SignalWorkflow(\"cartMessages\", update)\n\n\t\tupdateEmail := UpdateEmailSignal{\n\t\t\tRoute: RouteTypes.UPDATE_EMAIL,\n\t\t\tEmail: \"abandoned_test@temporal.io\",\n\t\t}\n\t\ts.env.SignalWorkflow(\"cartMessages\", updateEmail)\n\t}, time.Millisecond*1)\n\n\t// Wait for 10 mins and make sure abandoned cart email has been sent. The extra\n\t// 2ms is because signals are async, so the last change to the cart happens at 2ms.\n\ts.env.RegisterDelayedCallback(func() {\n\t\ts.Equal(sendTo, \"abandoned_test@temporal.io\")\n\t}, abandonedCartTimeout + time.Millisecond*2)\n\n\ts.env.ExecuteWorkflow(CartWorkflow, cart)\n\n\ts.True(s.env.IsWorkflowCompleted())\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftemporalio%2Ftemporal-ecommerce","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftemporalio%2Ftemporal-ecommerce","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftemporalio%2Ftemporal-ecommerce/lists"}