├── .nvmrc ├── .gitignore ├── .eslintignore ├── .github └── CODEOWNERS ├── tsconfig.json ├── src ├── shared.ts ├── worker.ts ├── activities.ts ├── scripts │ ├── cancel-subscription.ts │ ├── update-chargeamt.ts │ ├── execute-workflow.ts │ └── query-billinginfo.ts ├── mocha │ ├── workflows.test.ts │ └── activities.test.ts └── workflows.ts ├── .eslintrc.js ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .eslintrc.js -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This repository is maintained by the Temporal Education team 2 | * @temporalio/education 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "version": "4.9.5", 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "rootDir": "./src", 9 | "outDir": "./lib" 10 | }, 11 | "include": ["src/**/*.ts"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /src/shared.ts: -------------------------------------------------------------------------------- 1 | // @@@SNIPSTART subscription-ts-shared-file 2 | export const TASK_QUEUE_NAME = "subscriptions-task-queue"; 3 | 4 | export interface Customer { 5 | firstName: string; 6 | lastName: string; 7 | email: string; 8 | subscription: { 9 | trialPeriod: number; 10 | billingPeriod: number; 11 | maxBillingPeriods: number; 12 | initialBillingPeriodCharge: number; 13 | }; 14 | id: string; 15 | } 16 | // @@@SNIPEND 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | project: './tsconfig.json', 6 | tsconfigRootDir: __dirname, 7 | }, 8 | plugins: ['@typescript-eslint', 'deprecation'], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/eslint-recommended', 12 | 'plugin:@typescript-eslint/recommended', 13 | 'prettier', 14 | ], 15 | rules: { 16 | '@typescript-eslint/no-unused-vars': [ 17 | 'warn', 18 | { 19 | argsIgnorePattern: '^_', 20 | varsIgnorePattern: '^_', 21 | }, 22 | ], 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | 'object-shorthand': ['error', 'always'], 25 | 'deprecation/deprecation': 'warn', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/worker.ts: -------------------------------------------------------------------------------- 1 | // @@@SNIPSTART subscription-ts-worker-start 2 | import { TASK_QUEUE_NAME } from "./shared"; 3 | import { NativeConnection, Worker } from "@temporalio/worker"; 4 | import * as activities from "./activities"; 5 | 6 | async function run() { 7 | const connection = await NativeConnection.connect({ 8 | address: "localhost:7233", 9 | }); 10 | 11 | // Step 1: Register Workflows and Activities with the Worker and connect to 12 | // the Temporal server. 13 | const worker = await Worker.create({ 14 | connection, 15 | workflowsPath: require.resolve("./workflows"), 16 | activities, 17 | taskQueue: TASK_QUEUE_NAME, 18 | }); 19 | 20 | // Step 2: Start accepting tasks on the `subscriptions-task-queue` queue 21 | await worker.run(); 22 | } 23 | 24 | run().catch((err) => { 25 | console.error(err); 26 | process.exit(1); 27 | }); 28 | // @@@SNIPEND 29 | -------------------------------------------------------------------------------- /src/activities.ts: -------------------------------------------------------------------------------- 1 | // @@@SNIPSTART subscription-ts-activities 2 | import { log } from "@temporalio/activity"; 3 | 4 | import { Customer } from "./shared"; 5 | 6 | export async function sendWelcomeEmail(customer: Customer) { 7 | log.info(`Sending welcome email to ${customer.email}`); 8 | } 9 | export async function sendCancellationEmailDuringTrialPeriod( 10 | customer: Customer 11 | ) { 12 | log.info(`Sending trial cancellation email to ${customer.email}`); 13 | } 14 | export async function chargeCustomerForBillingPeriod( 15 | customer: Customer, 16 | chargeAmount: number 17 | ) { 18 | log.info( 19 | `Charging ${customer.email} amount ${chargeAmount} for their billing period` 20 | ); 21 | } 22 | export async function sendSubscriptionFinishedEmail( 23 | customer: Customer 24 | ) { 25 | log.info(`Sending subscription completed email to ${customer.email}`); 26 | } 27 | export async function sendSubscriptionOverEmail(customer: Customer) { 28 | log.info(`Sending subscription over email to ${customer.email}`); 29 | } 30 | // @@@SNIPEND 31 | -------------------------------------------------------------------------------- /src/scripts/cancel-subscription.ts: -------------------------------------------------------------------------------- 1 | // @@@SNIPSTART subscription-ts-cancel-subscription-signal 2 | import { Connection, Client } from "@temporalio/client"; 3 | import { cancelSubscription, subscriptionWorkflow } from "../workflows"; 4 | import { TASK_QUEUE_NAME, Customer } from "../shared"; 5 | 6 | async function run() { 7 | const connection = await Connection.connect({ address: "localhost:7233" }); 8 | const client = new Client({ 9 | connection, 10 | }); 11 | const subscriptionWorkflowExecution = await client.workflow.start( 12 | subscriptionWorkflow, 13 | { 14 | args: [customer], 15 | taskQueue: TASK_QUEUE_NAME, 16 | workflowId: `subscription-${customer.id}`, 17 | } 18 | ); 19 | const handle = await client.workflow.getHandle(`subscription-${customer.id}`); 20 | 21 | await handle.signal(cancelSubscription); 22 | console.log(await subscriptionWorkflowExecution.result()); 23 | } 24 | 25 | run().catch((err) => { 26 | console.error(err); 27 | process.exit(1); 28 | }); 29 | 30 | const customer: Customer = { 31 | firstName: "Grant", 32 | lastName: "Fleming", 33 | email: "email-1@customer.com", 34 | subscription: { 35 | trialPeriod: 2000, 36 | billingPeriod: 2000, 37 | maxBillingPeriods: 12, 38 | initialBillingPeriodCharge: 100, 39 | }, 40 | id: "ABC123", 41 | }; 42 | // @@@SNIPEND 43 | -------------------------------------------------------------------------------- /src/scripts/update-chargeamt.ts: -------------------------------------------------------------------------------- 1 | // @@@SNIPSTART subscription-ts-updatechargeamount-signal 2 | import { Connection, Client } from "@temporalio/client"; 3 | import { subscriptionWorkflow, updateBillingChargeAmount } from "../workflows"; 4 | import { TASK_QUEUE_NAME, Customer } from "../shared"; 5 | 6 | async function run() { 7 | const connection = await Connection.connect({ address: "localhost:7233" }); 8 | const client = new Client({ 9 | connection, 10 | }); 11 | const subscriptionWorkflowExecution = await client.workflow.start( 12 | subscriptionWorkflow, 13 | { 14 | args: [customer], 15 | taskQueue: TASK_QUEUE_NAME, 16 | workflowId: `subscription-${customer.id}`, 17 | } 18 | ); 19 | const handle = await client.workflow.getHandle(`subscription-${customer.id}`); 20 | 21 | // Signal workflow and update charge amount to 300 for next billing period 22 | try { 23 | await handle.signal(updateBillingChargeAmount, 300); 24 | console.log( 25 | `subscription-${customer.id} updating BillingPeriodChargeAmount to 300` 26 | ); 27 | } catch (err) { 28 | console.error("Cant signal workflow", err); 29 | } 30 | } 31 | 32 | run().catch((err) => { 33 | console.error(err); 34 | process.exit(1); 35 | }); 36 | 37 | const customer: Customer = { 38 | firstName: "Grant", 39 | lastName: "Fleming", 40 | email: "email-1@customer.com", 41 | subscription: { 42 | trialPeriod: 2000, 43 | billingPeriod: 2000, 44 | maxBillingPeriods: 12, 45 | initialBillingPeriodCharge: 100, 46 | }, 47 | id: "ABC123", 48 | }; 49 | // @@@SNIPEND 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "temporal-subscription-workflow-project", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "tsc --build", 7 | "build.watch": "tsc --build --watch", 8 | "start": "ts-node src/worker.ts", 9 | "start.watch": "nodemon src/worker.ts", 10 | "workflow": "ts-node src/scripts/execute-workflow.ts", 11 | "querybillinginfo": "ts-node src/scripts/query-billinginfo.ts", 12 | "cancelsubscription": "ts-node src/scripts/cancel-subscription.ts", 13 | "updatechargeamount": "ts-node src/scripts/update-chargeamt.ts", 14 | "lint": "eslint .", 15 | "test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts" 16 | }, 17 | "nodemonConfig": { 18 | "execMap": { 19 | "ts": "ts-node" 20 | }, 21 | "ext": "ts", 22 | "watch": [ 23 | "src" 24 | ] 25 | }, 26 | "dependencies": { 27 | "@temporalio/activity": "^1.10.0", 28 | "@temporalio/client": "^1.10.0", 29 | "@temporalio/worker": "^1.10.0", 30 | "@temporalio/workflow": "^1.10.0", 31 | "@types/node": "^20.14.2" 32 | }, 33 | "devDependencies": { 34 | "@temporalio/testing": "^1.10.0", 35 | "@types/mocha": "8.x", 36 | "@tsconfig/node20": "^1.0.2", 37 | "@typescript-eslint/eslint-plugin": "^5.0.0", 38 | "@typescript-eslint/parser": "^5.0.0", 39 | "eslint": "^7.32.0", 40 | "eslint-config-prettier": "^8.3.0", 41 | "eslint-plugin-deprecation": "^1.2.1", 42 | "nodemon": "^3.1.3", 43 | "mocha": "8.x", 44 | "prettier": "^2.4.1", 45 | "ts-node": "^10.9.2", 46 | "typescript": "^5.4.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/scripts/execute-workflow.ts: -------------------------------------------------------------------------------- 1 | // @@@SNIPSTART subscription-ts-workflow-execution-starter 2 | import { Connection, Client } from "@temporalio/client"; 3 | import { subscriptionWorkflow } from "../workflows"; 4 | import { Customer, TASK_QUEUE_NAME } from "../shared"; 5 | 6 | async function run() { 7 | const connection = await Connection.connect({ address: "localhost:7233" }); 8 | const client = new Client({ 9 | connection, 10 | }); 11 | 12 | const custArray: Customer[] = [1, 2, 3, 4, 5].map((i) => ({ 13 | firstName: "First Name" + i, 14 | lastName: "Last Name" + i, 15 | email: "email-" + i + "@customer.com", 16 | subscription: { 17 | trialPeriod: 3 + i * 1000, // 3 seconds 18 | billingPeriod: 3 + i, // 3 seconds 19 | maxBillingPeriods: 3, 20 | initialBillingPeriodCharge: 120 + i * 10, 21 | }, 22 | id: "Id-" + i, 23 | })); 24 | const resultArr = await Promise.all( 25 | custArray.map(async (cust) => { 26 | try { 27 | const execution = await client.workflow.start(subscriptionWorkflow, { 28 | args: [cust], 29 | taskQueue: TASK_QUEUE_NAME, 30 | workflowId: "SubscriptionsWorkflow" + cust.id, 31 | workflowRunTimeout: "10 mins", 32 | }); 33 | const result = await execution.result(); 34 | return result; 35 | } catch (err) { 36 | console.error("Unable to execute workflow for customer:", cust.id, err); 37 | return `Workflow failed for: ${cust.id}`; 38 | } 39 | }) 40 | ); 41 | resultArr.forEach((result) => { 42 | console.log("Workflow result", result); 43 | }); 44 | } 45 | 46 | run().catch((err) => { 47 | console.error(err); 48 | process.exit(1); 49 | }); 50 | // @@@SNIPEND 51 | -------------------------------------------------------------------------------- /src/mocha/workflows.test.ts: -------------------------------------------------------------------------------- 1 | import { TestWorkflowEnvironment } from "@temporalio/testing"; 2 | import { after, before, describe, it } from "mocha"; 3 | import { Worker } from "@temporalio/worker"; 4 | import assert from "assert"; 5 | import { subscriptionWorkflow } from "../workflows"; 6 | import * as activities from "../activities"; 7 | import { Customer } from "../shared"; 8 | 9 | describe("Subscription Workflow", function () { 10 | this.timeout(10000); 11 | let worker: Worker; 12 | let testEnv: TestWorkflowEnvironment; 13 | 14 | before(async () => { 15 | this.timeout(10000); 16 | testEnv = await TestWorkflowEnvironment.createTimeSkipping(); 17 | }); 18 | 19 | beforeEach(async function () { 20 | const { nativeConnection } = testEnv; 21 | worker = await Worker.create({ 22 | connection: nativeConnection, 23 | taskQueue: "test", 24 | workflowsPath: require.resolve("../workflows"), 25 | activities, 26 | }); 27 | }); 28 | 29 | after(async () => { 30 | await testEnv?.teardown(); 31 | }); 32 | 33 | const customer: Customer = { 34 | firstName: "John", 35 | lastName: "Doe", 36 | email: "john.doe@example.com", 37 | subscription: { 38 | trialPeriod: 1000, 39 | billingPeriod: 1000, 40 | maxBillingPeriods: 3, 41 | initialBillingPeriodCharge: 100, 42 | }, 43 | id: "customer-id-123", 44 | }; 45 | 46 | it("completes the workflow with trial cancellation", async () => { 47 | const { client } = testEnv; 48 | 49 | const result = await worker.runUntil( 50 | client.workflow.execute(subscriptionWorkflow, { 51 | args: [customer], 52 | workflowId: "trial-cancel-test", 53 | taskQueue: "test", 54 | }) 55 | ); 56 | 57 | assert.equal(result, `Completed trial-cancel-test, Total Charged: 300`); 58 | }); 59 | }); -------------------------------------------------------------------------------- /src/scripts/query-billinginfo.ts: -------------------------------------------------------------------------------- 1 | // @@@SNIPSTART subscription-ts-querybillinginfo-query 2 | import { Connection, Client } from "@temporalio/client"; 3 | import { subscriptionWorkflow } from "../workflows"; 4 | import { TASK_QUEUE_NAME, Customer } from "../shared"; 5 | 6 | async function run() { 7 | const connection = await Connection.connect({ address: "localhost:7233" }); 8 | const client = new Client({ 9 | connection, 10 | }); 11 | 12 | const subscriptionWorkflowExecution = await client.workflow.start( 13 | subscriptionWorkflow, 14 | { 15 | args: [customer], 16 | taskQueue: TASK_QUEUE_NAME, 17 | workflowId: `subscription-${customer.id}`, 18 | } 19 | ); 20 | 21 | // Wait for some time before querying to allow the workflow to progress 22 | for (let i = 1; i <= 5; i++) { 23 | // Loop for 5 billing periods 24 | await new Promise((resolve) => setTimeout(resolve, 2500)); // Adjust the wait time to match billing period plus buffer 25 | try { 26 | const billingPeriodNumber = 27 | await subscriptionWorkflowExecution.query( 28 | "billingPeriodNumber" 29 | ); 30 | const totalChargedAmount = 31 | await subscriptionWorkflowExecution.query("totalChargedAmount"); 32 | 33 | console.log("Workflow Id", subscriptionWorkflowExecution.workflowId); 34 | console.log("Billing Period", billingPeriodNumber); 35 | console.log("Total Charged Amount", totalChargedAmount); 36 | } catch (err) { 37 | console.error( 38 | `Error querying workflow with ID ${subscriptionWorkflowExecution.workflowId}:`, 39 | err 40 | ); 41 | } 42 | } 43 | } 44 | 45 | run().catch((err) => { 46 | console.error(err); 47 | process.exit(1); 48 | }); 49 | 50 | const customer: Customer = { 51 | firstName: "Grant", 52 | lastName: "Fleming", 53 | email: "email-1@customer.com", 54 | subscription: { 55 | trialPeriod: 2000, 56 | billingPeriod: 2000, 57 | maxBillingPeriods: 12, 58 | initialBillingPeriodCharge: 100, 59 | }, 60 | id: "ABC123", 61 | }; 62 | // @@@SNIPEND 63 | -------------------------------------------------------------------------------- /src/mocha/activities.test.ts: -------------------------------------------------------------------------------- 1 | import { MockActivityEnvironment } from '@temporalio/testing'; 2 | import { describe, it } from 'mocha'; 3 | import * as activities from '../activities'; 4 | import assert from 'assert'; 5 | import { Customer } from '../shared'; 6 | 7 | describe('Subscription Activities', () => { 8 | const customer: Customer = { 9 | firstName: 'John', 10 | lastName: 'Doe', 11 | email: 'john.doe@example.com', 12 | subscription: { 13 | trialPeriod: 30, 14 | billingPeriod: 30, 15 | maxBillingPeriods: 12, 16 | initialBillingPeriodCharge: 100, 17 | }, 18 | id: 'customer-id-123', 19 | }; 20 | 21 | it('successfully sends welcome email', async () => { 22 | const env = new MockActivityEnvironment(); 23 | const result = await env.run(activities.sendWelcomeEmail, customer); 24 | assert.equal(result, undefined); // No return value, just log 25 | }); 26 | 27 | it('successfully sends trial cancellation email', async () => { 28 | const env = new MockActivityEnvironment(); 29 | const result = await env.run(activities.sendCancellationEmailDuringTrialPeriod, customer); 30 | assert.equal(result, undefined); // No return value, just log 31 | }); 32 | 33 | it('successfully charges customer for billing period', async () => { 34 | const env = new MockActivityEnvironment(); 35 | const result = await env.run(activities.chargeCustomerForBillingPeriod, customer, 150); 36 | assert.equal(result, undefined); // No return value, just log 37 | }); 38 | 39 | it('successfully sends active subscription cancellation email', async () => { 40 | const env = new MockActivityEnvironment(); 41 | const result = await env.run(activities.sendCancellationEmailDuringActiveSubscription, customer); 42 | assert.equal(result, undefined); // No return value, just log 43 | }); 44 | 45 | it('successfully sends subscription over email', async () => { 46 | const env = new MockActivityEnvironment(); 47 | const result = await env.run(activities.sendSubscriptionOverEmail, customer); 48 | assert.equal(result, undefined); // No return value, just log 49 | }); 50 | }); -------------------------------------------------------------------------------- /src/workflows.ts: -------------------------------------------------------------------------------- 1 | // @@@SNIPSTART subscription-ts-workflow-definition 2 | import { 3 | proxyActivities, 4 | log, 5 | defineSignal, 6 | defineQuery, 7 | setHandler, 8 | condition, 9 | workflowInfo, 10 | sleep, 11 | } from "@temporalio/workflow"; 12 | import type * as activities from "./activities"; 13 | import { Customer } from "./shared"; 14 | 15 | const { 16 | sendWelcomeEmail, 17 | sendSubscriptionFinishedEmail, 18 | chargeCustomerForBillingPeriod, 19 | sendCancellationEmailDuringTrialPeriod, 20 | sendSubscriptionOverEmail, 21 | } = proxyActivities({ 22 | startToCloseTimeout: "5 seconds", 23 | }); 24 | 25 | export const cancelSubscription = defineSignal("cancelSubscription"); 26 | export const customerIdNameQuery = defineQuery("customerIdName"); 27 | export const billingPeriodNumberQuery = defineQuery( 28 | "billingPeriodNumber" 29 | ); 30 | export const updateBillingChargeAmount = defineSignal<[number]>( 31 | "updateBillingChargeAmount" 32 | ); 33 | export const totalChargedAmountQuery = 34 | defineQuery("totalChargedAmount"); 35 | 36 | export async function subscriptionWorkflow( 37 | customer: Customer 38 | ): Promise { 39 | let subscriptionCancelled = false; 40 | let totalCharged = 0; 41 | let billingPeriodNumber = 1; 42 | let billingPeriodChargeAmount = 43 | customer.subscription.initialBillingPeriodCharge; 44 | 45 | setHandler(customerIdNameQuery, () => customer.id); 46 | setHandler(cancelSubscription, () => { 47 | subscriptionCancelled = true; 48 | }); 49 | setHandler(updateBillingChargeAmount, (newAmount: number) => { 50 | billingPeriodChargeAmount = newAmount; 51 | log.info( 52 | `Updating BillingPeriodChargeAmount to ${billingPeriodChargeAmount}` 53 | ); 54 | }); 55 | setHandler(billingPeriodNumberQuery, () => billingPeriodNumber); 56 | setHandler(totalChargedAmountQuery, () => totalCharged); 57 | 58 | // Send welcome email to customer 59 | await sendWelcomeEmail(customer); 60 | 61 | // Used to wait for the subscription to be cancelled or for a trial period timeout to elapse 62 | if ( 63 | await condition( 64 | () => subscriptionCancelled, 65 | customer.subscription.trialPeriod 66 | ) 67 | ) { 68 | await sendCancellationEmailDuringTrialPeriod(customer); 69 | return `Subscription finished for: ${customer.id}`; 70 | } else { 71 | // Trial period is over, start billing until we reach the max billing periods for the subscription or subscription has been cancelled 72 | while (true) { 73 | if (billingPeriodNumber > customer.subscription.maxBillingPeriods) break; 74 | 75 | if (subscriptionCancelled) { 76 | await sendSubscriptionFinishedEmail(customer); 77 | return `Subscription finished for: ${customer.id}, Total Charged: ${totalCharged}`; 78 | } 79 | 80 | log.info(`Charging ${customer.id} amount ${billingPeriodChargeAmount}`); 81 | 82 | await chargeCustomerForBillingPeriod(customer, billingPeriodChargeAmount); 83 | totalCharged += billingPeriodChargeAmount; 84 | billingPeriodNumber++; 85 | 86 | // Wait for the next billing period or until the subscription is cancelled 87 | await sleep(customer.subscription.billingPeriod); 88 | } 89 | 90 | // If the subscription period is over and not cancelled, notify the customer to buy a new subscription 91 | await sendSubscriptionOverEmail(customer); 92 | return `Completed ${ 93 | workflowInfo().workflowId 94 | }, Total Charged: ${totalCharged}`; 95 | } 96 | } 97 | // @@@SNIPEND 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project template illustrates the design pattern for subscription style business logic. 2 | 3 | You might compare this to similar projects in our [Go](https://github.com/temporalio/subscription-workflow-project-template-go), [Java](https://github.com/temporalio/subscription-workflow-project-template-java), [Python](https://github.com/temporalio/email-subscription-project-python/) and [PHP](https://github.com/temporalio/subscription-workflow-project-template-php) SDKs. 4 | 5 | ## Project Specifications 6 | 7 | Your task is to write a Workflow for a limited time Subscription (eg a 36 month Phone plan) that satisfies these conditions: 8 | 9 | 1. When the user signs up, **send a welcome email** and start a free trial for `trialPeriod`. 10 | 2. If the user cancels during the trial, **send a trial cancellation email** and complete the Workflow. Liekewise, if the `trialPeriod` expires, start the billing process. 11 | 3. Billing Process: 12 | - As long as you have not exceeded `maxBillingPeriods`, 13 | - **Charge the customer** for the `billingPeriodChargeAmount`. 14 | - Then wait for the next `billingPeriod`. 15 | - If the customer cancels during a billing period, **send a subscription cancellation email**. 16 | - If Subscription has ended normally (exceeded `maxBillingPeriods` without cancellation), **send a subscription ended email** and complete the Workflow. 17 | 4. At any point while subscriptions are ongoing, be able to look up and change any customer's: 18 | - Amount Charged 19 | - Period number (for manual adjustments e.g. refunds) 20 | 21 | Of course, this all has to be fault tolerant, scalable to millions of customers, testable, maintainable! 22 | 23 | ## Tutorial 24 | 25 | The guided tutorial for writing this on your own can be found [here](https://learn.temporal.io/tutorials/typescript/subscriptions/). 26 | 27 | ## Setup 28 | 29 | 1. Start by running the Temporal Server in one terminal window: `temporal server start-dev --ui-port 8080 --db-filename clusterdata.db`. For more details on this command, please refer to the `Set up a local development environment for Temporal and TypeScript` tutorial [here](https://learn.temporal.io/getting_started/typescript/dev_environment/). 30 | 31 | 2. In another terminal window, run `npm install` to install dependencies. Then, start the Worker by running `npm run start.watch`. 32 | 33 | 3. In another terminal window, run the Workflow Execution with `npm run workflow`. 34 | 35 | This will start the Workflow Executions for 5 customers in parallel. You will see the result of the Activity calls in the terminal Window your Worker is running: 36 | 37 | ```bash 38 | Sending welcome email to email-1@customer.com 39 | Sending welcome email to email-2@customer.com 40 | Sending welcome email to email-4@customer.com 41 | Sending welcome email to email-5@customer.com 42 | Sending welcome email to email-3@customer.com 43 | ``` 44 | 45 | Each of their periods and charges are varied on purpose to simulate real life variation. 46 | After their "trial" period, each customer will be charged: 47 | 48 | ```bash 49 | Charging email-1@customer.com amount 130 for their billing period 50 | Charging email-2@customer.com amount 140 for their billing period 51 | Charging email-3@customer.com amount 150 for their billing period 52 | Charging email-4@customer.com amount 160 for their billing period 53 | Charging email-5@customer.com amount 170 for their billing period 54 | ``` 55 | 56 | If you let this run to completion, the Workflow Executions complete and report their results back: 57 | 58 | ```bash 59 | Workflow result Completed SubscriptionsWorkflowId-1, Total Charged: 390 60 | Workflow result Completed SubscriptionsWorkflowId-2, Total Charged: 420 61 | Workflow result Completed SubscriptionsWorkflowId-3, Total Charged: 450 62 | Workflow result Completed SubscriptionsWorkflowId-4, Total Charged: 580 63 | Workflow result Completed SubscriptionsWorkflowId-5, Total Charged: 510 64 | ``` 65 | 66 | **Get billing info** 67 | 68 | You can Query the Workflow Executions and get the billing information for each customer. Do this by running the following command: 69 | 70 | ```bash 71 | npm run querybillinginfo 72 | ``` 73 | 74 | You can run this after execution completes, or during execution to see the billing period number increase during the executions or see the total charged amount thus far. 75 | 76 | ```bash 77 | Workflow Id subscription-ABC123 78 | Billing Period 2 79 | Total Charged Amount 100 80 | Workflow Id subscription-ABC123 81 | Billing Period 3 82 | Total Charged Amount 200 83 | # etc. 84 | ``` 85 | 86 | **Update billing** 87 | 88 | You can send a Signal a Workflow Execution to update the billing cycle cost to 300 for all customers. Do this by running the following command: 89 | 90 | ```bash 91 | npm run updatechargeamount 92 | ``` 93 | 94 | This will update all customer's charge amount to 300: 95 | 96 | ```bash 97 | subscription-ABC123 updating BillingPeriodChargeAmount to 300 98 | ``` 99 | 100 | **Cancel subscription** 101 | 102 | You can send a Signal to all Workflow Executions to cancel the subscription for all customers. The Workflow Executions will complete after the currently executing billing period. Do this by running the command: 103 | 104 | ```bash 105 | npm run cancelsubscription 106 | ``` 107 | 108 | This will cancel the subscription to the user supplied in the Client: 109 | 110 | ```bash 111 | Subscription finished for: ABC123 112 | ``` 113 | 114 | After running this, check out the [Temporal Web UI](localhost://8088) and see that all subscription Workflow Executions have a "Completed" status. 115 | 116 | --------------------------------------------------------------------------------