AWS Lambda と AWS SESを使ってたった150行でカード有効期限お知らせサービスを書いた話
AWS Lambda と AWS SESを使ってたった150行でカード有効期限お知らせサービスを書いた話:
今回は有効期限が迫っているカードの更新を顧客に促すフローを自動化する方法を紹介する。使うのはLambdaとSESだけなのでとてもお手軽にインフラが揃う。必要なのはOmiseとAWSのアカウントだけ。
レポジトリ
以下のようなテンプレートを作成
AWS SES に作成したテンプレートを登録。
上のコマンドを叩くとテンプレート名
今回は
カードの更新通知がされるとマーチャントのメタデータに更新したという記録が残される。具体的には
そこで、更新が済んだらこの
API だと以下のようにできる
Omise には omise-node や他の言語のクライアントライブラリがあり、これらを駆使して AWS Lambda のコードを書くこともできる。しかし、AWS Lambda に積められるコードの量に限度があったり、コード量が 3MB を超えると付属のナウいエディタが使えなかったりするので今回はフルスクラッチで軽量なものを作ってみた。
Lambda といえば、Lisp。LispといえばLisp Alien。
今回は有効期限が迫っているカードの更新を顧客に促すフローを自動化する方法を紹介する。使うのは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 を超えると付属のナウいエディタが使えなかったりするので今回はフルスクラッチで軽量なものを作ってみた。Lambda といえば、Lisp。LispといえばLisp Alien。
コメント
コメントを投稿