今回は有効期限が迫っているカードの更新を顧客に促すフローを自動化する方法を紹介する。使うのはLambdaとSESだけなのでとてもお手軽にインフラが揃う。必要なのはOmiseとAWSのアカウントだけ。
準備
お知らせテンプレートの作成
以下のようなテンプレートを作成
{
"Template": {
"TemplateName": "ExpiredCardNotification",
"SubjectPart": "【重要】 カードの更新をお願いします",
"TextPart": "お客様、\n お世話になります。\n あなたのカード (名義:{{name}}, 有効期限: {{month}}/{{year}}) はもうすぐ使用できなくなります。\n 更新する場合は弊社のフォームにて再登録をお願いします",
"HtmlPart": "お客様、</br> お世話になります。</br> あなたのカード (名義:{{name}}, 有効期限: {{month}}/{{year}}) はもうすぐ使用できなくなります。</br> 更新する場合は弊社のフォームにて再登録をお願いします""
}
}
お知らせテンプレートの登録
AWS SES に作成したテンプレートを登録。
aws ses create-template --cli-input-json file://<テンプレートのファイル名>
上のコマンドを叩くとテンプレート名ExpiredCardNotification
で登録される
AWS Lambda にコードをアップロード
今回は async
とrequest
のライブラリが必要だったので基本ロジックのコード以外にもこれらをアップロードする必要があった。以下の方法でアップロードする:
-
ディレクトリを作る
-
request
、request-promise
そしてlodash
をインストールnpm install --save request request-promise lodash
レポジトリの
package.json
を使う場合はnpm install --production
-
カード有効期限モニタリングコードの作成。名前は
index.js
にすること -
コードの圧縮
zip -r ../lambda_code.zip *
-
圧縮ファイルのアップロード
-
以下の環境変数を登録
-
OMISE_SECRET_KEY
(Omise の秘密鍵) -
SES_SOURCE
(送信元) -
SES_AWS_REGION
(AWS SES のリージョン) -
REMAINING_MONTHS_THRESHOLD
(カードの有効期限何ヶ月前に通知するか)
-
ちなみにこれがカードモニタリングコード
const rp = require('request-promise')
const aws = require('aws-sdk')
const _ = require('lodash')
const Config = {
AWSRegion: process.env.SES_AWS_REGION || 'us-east-1',
RemainingMonthsThreshold: parseInt(process.env.REMAINING_MONTHS_THRESHOLD || 2),
OmiseSecretKey: process.env.OMISE_SECRET_KEY || '',
OmiseCustomerApiUrl: process.env.OMISE_CUSTOMER_API_URL || 'https://api.omise.co/customers',
SesSource: process.env.SES_SOURCE || ''
}
const ses = new aws.SES({
region: Config.AWSRegion
})
const monthDiff = (month, year) => {
const today = new Date()
return (year - today.getFullYear()) * 12 + month - today.getMonth()
}
const selectUnnotifiedCards = (customer, cards) => {
const alreadyNotified = _.get(customer, 'metadata.notified')
if (alreadyNotified) {
return _.filter(cards, c => !alreadyNotified.includes(c.id))
} else {
return cards
}
}
const makeEmailParamsPerCard = (customerEmail, card) => {
const templateParams = {
card_id: card.id,
name: card.name,
month: card.expiration_month,
year: card.expiration_year,
months_til_expiration: card.months_til_expiration
}
return {
Destination: { ToAddresses: [customerEmail] },
ReplacementTemplateData: JSON.stringify(templateParams)
}
}
const customerWithEmailParams = (customer) => {
const unnotifiedCards = selectUnnotifiedCards(customer, customer.cards.data)
const cardsWithExpiration = _.map(unnotifiedCards, c => _.merge(c, Object({ months_til_expiration: monthDiff(c.expiration_month, c.expiration_year) })))
const cardsToNotify = _.filter(cardsWithExpiration, c => c.months_til_expiration <= Config.RemainingMonthsThreshold)
return {
emailParams: _.map(cardsToNotify, c => makeEmailParamsPerCard(customer.email, c)),
customer_id: customer.id,
card_ids: _.map(cardsToNotify, c => c.id),
notified: _.get(customer, 'metadata.notified', [])
}
}
const getCustomers = async () => {
const getCustomersParams = {
url: Config.OmiseCustomerApiUrl,
auth: {
user: Config.OmiseSecretKey,
pass: ''
},
transform: function (body) {
return _.map(JSON.parse(body).data, c => customerWithEmailParams(c))
}
}
return rp(getCustomersParams)
}
const createBulkEmailParams = (destinations) => {
return {
Destinations: destinations,
Source: Config.SesSource,
Template: 'ExpiredCardNotification',
DefaultTemplateData: '{"name":"","month":"","year":"","months_til_expiration":"","card_id":""}'
}
}
const registerNotified = async (customerId, cardIds) => {
const data = {
metadata: {
notified: cardIds
}
}
const patchCustomersParams = {
url: Config.OmiseCustomerApiUrl + '/' + customerId,
method: 'PATCH',
body: data,
json: true,
auth: {
'user': process.env.OMISE_SECRET_KEY,
'pass': ''
}
}
return rp(patchCustomersParams)
}
const markAsNotifiedPromises = (cardStatus, customers) => {
return _.map(customers, function (customer) {
const notified = customer.notified
const cardIds = _.filter(customer.card_ids, c => cardStatus[c] === true)
const newNotified = notified.concat(cardIds)
const customerId = customer.customer_id
return registerNotified(customerId, newNotified)
})
}
const main = async () => {
const customers = await getCustomers()
const toSend = _.filter(customers, c => c.card_ids.length > 0)
const destinations = _.flatMap(toSend, c => c.emailParams)
const cards = _.flatMap(toSend, c => c.card_ids)
if (cards.length === 0) {
return { cardStatus: [], customers: [] }
}
console.log('===SENDING EMAIL===')
const bulkEmailParams = createBulkEmailParams(destinations)
const sendPromise = ses.sendBulkTemplatedEmail(bulkEmailParams).promise()
return sendPromise.then((data) => {
const statuses = {}
_.zip(cards, data.Status).forEach(function ([cardId, status]) {
statuses[cardId] = status.Status === 'Success'
})
return { cardStatus: statuses, customers: toSend }
})
}
exports.handler = async (event, context) => {
console.log('Incoming: ', event)
const result = await main()
.catch(err => {
console.log('error', err)
return context.fail(err)
})
if (result.customers.length === 0) {
return context.succeed(event)
}
console.log(result.cardStatus)
console.log('===EMAIL SENT===')
const reports = await markAsNotifiedPromises(result.cardStatus, result.customers)
await Promise.all(reports)
context.succeed(event)
}
カード更新後
カードの更新通知がされるとマーチャントのメタデータに更新したという記録が残される。具体的にはnotified
のアレイに通知されたカードが追加される。
const registerNotified = async (customerId, cardIds) => {
const data = {
metadata: {
notified: cardIds
}
}
const patchCustomersParams = {
url: Config.OmiseCustomerApiUrl + '/' + customerId,
method: 'PATCH',
body: data,
json: true,
auth: {
'user': process.env.OMISE_SECRET_KEY,
'pass': ''
}
}
return rp(patchCustomersParams)
}
そこで、更新が済んだらこの notified
からカードId を削除すれば有効期限の監視を再開しなければいけない。
API だと以下のようにできる
curl -H "Content-Type:application/json" -X PATCH https://api.omise.co/customers/<customer id> -u <secret key>: -d '{ "metadata": {} }'
最後に
Omise には omise-node や他の言語のクライアントライブラリがあり、これらを駆使して AWS Lambda のコードを書くこともできる。しかし、AWS Lambda に積められるコードの量に限度があったり、コード量が 3MB を超えると付属のナウいエディタが使えなかったりするので今回はフルスクラッチで軽量なものを作ってみた。