TemporalでSubscriptionのWorkflowを組む(課金処理編)
前回からの続きとなります。
本記事ではいよいよ3番の「課金処理」を実装しましょう。
- ✅ユーザーがサインアップしたらウェルカムメッセージ送信し、無料トライアル期間を
TrialPeriod
の間有効化する - ✅
TrialPeriod
が終わったら、課金処理を開始する- ✅無料トライアル期間内にユーザーがキャンセルしたらキャンセルメールを送信する
- 課金処理
-
MaxBillingPeriods
を超えていない限り -
BillingPeriodChargeAmound
を顧客に請求する - 2が終わったら
BillingPeriod
待機する - 待機中に顧客がキャンセルをしたら、サブスクリプションキャンセルメールを送信する
- サブスクリプションが完了(
MaxBillingPeriods
を超えた)したら、サブスクリプション終了メールを送信する
-
- サブスクリプション購読中の任意のタイミングで顧客に関する以下の情報が取得可能とする
- 請求金額
- 現在のサブスクリプション期間(手動での調整用 e.g. 返金など)
データモデルの作成
課金処理を実装する前に顧客と対応するCustomer型を作っておきます。
export type Customer = {
firstName: string;
lastName: string;
email: string;
subscription: {
trialPeriod: number;
billingPeriod: number;
maxBillingPeriods: number;
initialBillingPeriodCharge: number;
};
id: string;
};
Workflowとactivityのリファクタ
Workflowではこの Customer
を使うようにします。あわせてactivitiesもいままで email
を受け取るようにしてましたが、 Customer
を受けるように修正しておきます。
export const cancelSubscription = wf.defineSignal('cancelSignal');
-export async function subscriptionWorkflow(email: string, trialPeriod: string | number): Promise<void> {
+export async function subscriptionWorkflow(customer: Customer): Promise<void> {
let isCanceled = false;
wf.setHandler(cancelSubscription, () => void (isCanceled = true));
- await acts.sendWelcomeEmail(email);
+ await acts.sendWelcomeEmail(customer);
if (await wf.condition(() => isCanceled, customer.subscription.trialPeriod)) {
- await acts.sendCancellationEmailDuringTrialPeriod(email);
+ await acts.sendCancellationEmailDuringTrialPeriod(customer);
} else {
- await acts.sendSubscriptionOverEmail(email);
+ await acts.sendSubscriptionOverEmail(customer);
}
}
Clientの改修
Workflowが受け取る情報が email
から Customer
に変わったので、呼び出し側のClientも改修します。ついでに複数のWorkflowをいっきに実行するようにしてみましょう。5人の顧客がサインアップしたことにします。
コード詳細
import { Connection, Client } from '@temporalio/client';
import { subscriptionWorkflow } from '../workflows';
import { nanoid } from 'nanoid';
import { Customer } from '../types';
async function run() {
const connection = await Connection.connect({ address: 'temporal:7233' });
const client = new Client({ connection });
const customers = [1, 2, 3, 4, 5].map(
(i) =>
({
firstName: 'First Name' + i,
lastName: 'Last Name' + i,
email: `email-${i}@example.com`,
subscription: {
trialPeriod: 3000 + i * 10000, // 3+ seconds,
billingPeriod: 3000 + i * 10000, // 3+ seconds,
maxBillingPeriods: 3,
initialBillingPeriodCharge: 120 + i * 10,
},
id: `id-${i}`,
} as Customer)
);
const results = await Promise.all(
customers.map((customer) =>
client.workflow
.start(subscriptionWorkflow, {
args: [customer],
taskQueue: 'subscription-tutorial',
// in practice, use a meaningful business ID, like customerId or transactionId
workflowId: 'workflow-' + nanoid(),
})
.catch((err) => console.error('Unable to execute workflow', err))
)
);
results.forEach((result) => {
console.log('Workflow result', result);
});
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
テスト
リファクタが終わったところで、ワークフローを実行してみましょう。
npm run start.watch
npm run workflow
# Workerのログ
Sending welcome email to email-4@example.com
Sending welcome email to email-2@example.com
Sending welcome email to email-5@example.com
Sending welcome email to email-3@example.com
Sending welcome email to email-1@example.com
# しばらくすると↓
Sending subscription over email to email-1@example.com
Sending subscription over email to email-2@example.com
Sending subscription over email to email-3@example.com
Sending subscription over email to email-4@example.com
Sending subscription over email to email-5@example.com
ちゃんと動いてそうですね。
課金処理
実装
課金処理の実装には特に新しい概念は登場しません。今までの知識の応用になります。
以下2つのActivityを追加します。(例によって中身はただの console.log
)
- 顧客に請求する(chargeCustomerForBillingPeriod)
- サブスクリプションキャンセルメールを送信する(sendCancellationEmailDuringActiveSubscription)
そして、Workflow側に「課金処理」を実装します。ここは普通に要件に則ってプログラムを組む感じになります。内容が気になる方はは「コードの詳細」を見てください。
要件
- 課金処理
MaxBillingPeriods
を超えていない限りBillingPeriodChargeAmound
を顧客に請求する- 2が終わったら
BillingPeriod
待機する- 待機中に顧客がキャンセルをしたら、サブスクリプションキャンセルメールを送信する
- サブスクリプションが完了(
MaxBillingPeriods
を超えた)したら、サブスクリプション終了メールを送信する
コードの詳細
import * as wf from '@temporalio/workflow';
import type * as activities from './activities';
import { Customer } from './types';
const acts = wf.proxyActivities<typeof activities>({
startToCloseTimeout: '1 minute',
});
export const cancelSubscription = wf.defineSignal('cancelSignal');
export async function subscriptionWorkflow(customer: Customer, trialPeriod: string | number): Promise<void> {
let isCanceled = false;
wf.setHandler(cancelSubscription, () => void (isCanceled = true));
await acts.sendWelcomeEmail(customer);
if (await wf.condition(() => isCanceled, trialPeriod)) {
await acts.sendCancellationEmailDuringTrialPeriod(customer);
} else {
await billingCycle(customer);
}
}
async function billingCycle(customer: Customer) {
let isCanceled = false;
wf.setHandler(cancelSubscription, () => void (isCanceled = true)); // reuse signals
await acts.chargeCustomerForBillingPeriod(customer);
for (let num = 0; num < customer.subscription.maxBillingPeriods; num++) {
// Wait 1 billing period to charge customer or if they cancel subscription
// whichever comes first
if (await wf.condition(() => isCanceled, customer.subscription.billingPeriod)) {
// If customer cancelled their subscription send notification email
await acts.sendCancellationEmailDuringActiveSubscription(customer);
break;
}
await acts.chargeCustomerForBillingPeriod(customer);
}
// if we get here the subscription period is over
if (!isCanceled) {
await acts.sendSubscriptionOverEmail(customer);
}
}
テスト
実装が終わったのでテストしてみます。
npm run start.watch
npm run workflow
まずは顧客5人全員にウェルカムメッセージが送信されます。
Sending welcome email to email-1@example.com
Sending welcome email to email-2@example.com
Sending welcome email to email-4@example.com
Sending welcome email to email-3@example.com
Sending welcome email to email-5@example.com
しばらくすると最速で無料トライアルが終わる一人目の顧客への課金が開始されます。
Charging email-1@example.com amount 130 for their billing period
ここで、まだ無料トライアル期間中の4人目の顧客のトライアル期間をキャンセルしましょう(workflowIdは実行時の内容から取得してくる)。
$ npm run workflow:cancel -- workflow-q9qI6Ny3rfefhiicGwxx7
> temporal-hello-world@0.1.0 workflow:cancel
> ts-node src/scripts/cancelSubscription.ts workflow-q9qI6Ny3rfefhiicGwxx7
Workerのログにトライアルがキャンセルされた旨がログされます。
Sending trial cancellation email to email-4@example.com
しばらく放置していると、それぞれのサブスクリプションの期間に基づいて課金が実行されていくのが確認できます。
Charging email-2@example.com amount 140 for their billing period
Charging email-1@example.com amount 130 for their billing period
Charging email-3@example.com amount 150 for their billing period
Charging email-1@example.com amount 130 for their billing period
Charging email-2@example.com amount 140 for their billing period
では、このあたりで3人目と5人目の顧客のサブスクリプションをキャンセルします。
Sending active subscriber cancellation email to email-3@example.com
Charging email-1@example.com amount 130 for their billing period
Sending subscription over email to email-1@example.com
Charging email-5@example.com amount 170 for their billing period
Charging email-2@example.com amount 140 for their billing period
Sending active subscriber cancellation email to email-5@example.com
Charging email-2@example.com amount 140 for their billing period
Sending subscription over email to email-2@example.com
3人目がキャンセルになり、その間もアクティブな顧客への課金は実行されています。そして、一人目の顧客の課金期間が完了しているのも確認できます。そして最終的に3人がキャンセルをして、二人が最後まで課金されることが確認できます。
おわりに
ということで本記事で要件3の「課金処理」が実装できました。Workflowを馴染みのある言語で実装していけるのはかなり良いですね。ちゃんと単体テストも書いていくことができるのでTemporalのDevXの良さを実感しています。最後はQueryについて見ていこうと思います。
現在の実装状況
- ✅ユーザーがサインアップしたらウェルカムメッセージ送信し、無料トライアル期間を
TrialPeriod
の間有効化する - ✅
TrialPeriod
が終わったら、課金処理を開始する
- ✅無料トライアル期間内にユーザーがキャンセルしたらキャンセルメールを送信する - ✅課金処理
1. ✅MaxBillingPeriods
を超えていない限り
2. ✅BillingPeriodChargeAmound
を顧客に請求する
3. ✅2が終わったらBillingPeriod
待機する
4. ✅待機中に顧客がキャンセルをしたら、サブスクリプションキャンセルメールを送信する
5. ✅サブスクリプションが完了(MaxBillingPeriods
を超えた)したら、サブスクリプション終了メールを送信する - サブスクリプション購読中の任意のタイミングで顧客に関する以下の情報が取得可能とする
- 請求金額
- 現在のサブスクリプション期間(手動での調整用 e.g. 返金など)
このデモを試したい人は以下のリポジトリでVSCodeのdevcontainerを用意しているので、うまくいけばそのまま動作確認できます。
Discussion