LoginSignup
12
15

More than 1 year has passed since last update.

LINE Pay APIを使った決済機能を作ってみた Node.js版 (Version3対応)

Posted at

概要

LINE Pay APIを利用したいと考えていましたが、version3の記事が少なかったと感じたので使い方について紹介していければと思います。
また、Node.js版のSDKがversion3ではなさそうだったので、直接APIを実行しています。
(現在はPython版SDKしかない模様?)
今回はLINE botを利用した決済機能という想定で作成しています。

LINE Pay APIについて

LINEユーザーがLINE pay加盟店のサイトで利用できる決済システムを提供しています。
LINE Pay APIを利用するためには加盟店登録を行う必要がありますが、検証目的でのSandbox環境も提供されているので、試してみたい方はこちらの環境を利用します。
登録すればChannel IdChannel SecretKeyが渡されますので、こちらを利用していきます。

Sandbox環境の利用方法はこちら

LINE Pay API Version3の主な変更点

・Request API
以下抜粋
version2ではIDとパスワードによる認証方法を使用していましたが、version3ではHMACを利用して認証とメッセージ検証を行う方法に変わります。version2でパスワードを渡していたX-LINE-ChannelSecretは、加盟店側で保存してください。HMAC署名を作成するときは、SecretKeyを使用します。X-LINE-Authorization-Nounceにランダムで生成される値を指定することで、署名のセキュリティを強化できます。

認証周りでの変更があったような感じですね。
この辺りについても後ほど説明します。

LINE Pay APIを使うための準備

・LINE PayのSandbox環境
実際にLINEPayを利用するためには、加盟店登録をする必要がありますが、お試しで利用する場合はSandbox環境で機能検証することができます。
・MessageAPIのChannel
LINEのBotを利用するためにMessageAPIのChannelを作成します。
Messaging APIについてはこちらの記事を参考にすると良いかと思います

決済機能のデモ

こちらのようなイメージで決済機能を実装していきます。
qiita-linepay.gif

  1. ユーザーのアクションに対してbotが応答し、「商品明細を確認し、決済をしてください。」というメッセージを応答
  2. ユーザーが確認ボタンを押下
  3. botから「こちらから決済を進めてください。」というメッセージを応答
  4. ユーザーがLINE Payで決済ボタンを押下
  5. LINE Payでの決済画面が立ち上がり、決済を実行

解説

大きく2つに分けて解説します。
・Messaging API
1. ユーザーのアクションに対してbotが応答し、「商品明細を確認し、決済をしてください。」というメッセージを応答
2. ユーザーが確認ボタンを押下
3. botから「こちらから決済を進めてください。」というメッセージを応答

・LINE Pay API
4. ユーザーがLINE Payで決済ボタンを押下
5. LINE Payでの決済画面が立ち上がり、決済を実行

Messaging APIの処理

1. ユーザーのアクションに対してbotが応答し、「商品明細を確認し、決済をしてください。」というメッセージを応答
2. ユーザーが確認ボタンを押下
3. botから「こちらから決済を進めてください。」というメッセージを応答

'use strict'

const line = require('@line/bot-sdk')
const express = require('express')
const LinePay = require('./linePay')
const payment = require('./payment')
require('dotenv').config()

const config = {
    channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
    channelSecret: process.env.CHANNEL_SECRET,
};

const client = new line.Client(config);
line.middleware(config);

const app = express();
app.post('/webhook', line.middleware(config), (req, res) => {
    Promise
        .all(req.body.events.map(handleEvent))
        .then((result) => res.json(result));
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
    console.log(`listening on ${port}`);
});


// 1. ユーザーのイベント実行時(メッセージ送信時)に実行
async function handleEvent(event) {
    switch(event.type) {
// 2. 「確認」ボタンはpostbackイベントとして処理
        case 'postback':
            handlePostbackEvent(event)
            return
        default:
            break;
    }

    return client.replyMessage(event.replyToken, {
        type: 'template',
        altText: '商品明細を確認し、決済をしてください。',
        template: {
            type: 'confirm',
            text: '商品明細を確認し、決済をしてください。',
            actions: [
                { type: 'postback', label: '確認', data: 'yes'},
                { type: 'postback', label: '確認しない', data: 'no'}
            ]
        }
    })
}
// 3. ポストバックイベントを実行
const handlePostbackEvent = async (event) => {
    switch(event.postback.data) {
        case 'yes':
            const linePay = new LinePay()
            const result = await linePay.request(payment)
            console.log(result)

            return client.replyMessage(event.replyToken,{
                type: 'template',
                altText: 'こちらから決済を進めてください。',
                template: {
                    type: 'buttons',
                    text: 'こちらから決済を進めてください。',
                    actions: [
                        { type: 'uri', label: 'LINE Payで決済', uri: result.info.paymentUrl.web },
                    ]
                }
            })


        case 'no':
            return client.replyMessage(event.replyToken, {
                type: 'text',
                text: 'またのご利用お待ちしております'
            })
    }
}

まずは、ユーザーのメッセージ送信時にhandleEventメソッドが実行されbotから応答メッセージとして2つのpostbackアクションを持ったテンプレートが返されます。
返却されたメッセージに対して、「確認」ボタンを押下すると、handlePostbackEventメソッドが実行されます。

LINE Pay APIの処理

4. ユーザーがLINE Payで決済ボタンを押下
5. LINE Payでの決済画面が立ち上がり、決済を実行

const fetch = require('node-fetch')
require('dotenv').config()
const uuid = require('uuid')
const crypto = require('crypto')
const jsonBigInt = require('json-bigint')({ storeAsString: true })

class linePay {
     async _headers(path, body = '') {
        const channelId = process.env.LINE_PAY_CHANNEL_ID //あなたのchannelIdをセット
        const channelSecret = process.env.LINE_PAY_CHANNEL_SECRETKEY //あなたのsecretkeyをセット 
        if(channelId === undefined || channelSecret === undefined) {
            throw new Error('line-pay secret is not found.')
        }

        const nonce = uuid.v4()
        const message = channelSecret + path + body + nonce
        const encrypt = crypto
            .createHmac('sha256', channelSecret)
            .update(message)
        const signature = encrypt.digest('base64')
        return {
            'Content-Type': 'application/json',
            'X-LINE-ChannelId': channelId,
            'X-LINE-Authorization-Nonce': nonce,
            'X-LINE-Authorization': signature,
        }
    }
    async _post(path, payload) {
        const body = JSON.stringify(payload)
        const headers = await this._headers(path, body)
        const url = process.env.LINE_PAY_END_POINT + path
        const res = await fetch(url, { method: 'POST', body, headers })
        const resText = await res.text()
        console.log(resText)
        const resJson = jsonBigInt.parse(resText)
        return resJson
    }

    async request(payment) {
        const products = payment.items.map((item) => {
            return {
                name: item.item_name,
                price: item.tax_included_price,
                quantity: item.quantity,
                originalPrice: item.raw_price,
                imageUrl: item.image_url,
            }
        })
        const packages = [
            {
                id: payment.store_code,
                name: payment.store_name,
                amount: payment.amount,
                products,
            },
        ]
        const options = {
            amount: payment.amount,
            currency: 'JPY',
            orderId: payment.payment_id,
            packages,
            options: { payment: { capture: true } },
            redirectUrls: {
                confirmUrl: process.env.CONFIRM_URL, // 確認画面のURL
                confirmUrlType: 'CLIENT',
                cancelUrl: process.env.CANCEL_URL, // キャンセル画面のURL
            }
        }

        const result = await this._post('/v3/payments/request', options)
        if(!result.info || result.returnCode !== '0000') {
            console.log(result)
        }
        return result
    }
}

module.exports = linePay

Request Header

Key Type Requirement Description
Content-Type String Y application/json
X-LINE-ChannelId String Y Payment Integration Information - Channel ID
X-LINE-MerchantDeviceProfileId String N Offline Support - Device Type
X-LINE-Authorization-Nonce String Y UUID or Request timestamp
X-LINE-Authorization String Y HMAC Base64 Signature

公式リファレンスにある通り、上記の内容でheaderを作成しています。
version3のアップデートにもあったUUIDを利用しハッシュを組み合わせたHMAC署名にしています。

Request Body
amount:決済金額
currency:決済通貨
orderId:加盟店の注文番号(加盟店が管理するユニークなID)
packages:配送単位(一つのパッケージの中に複数の商品を入れることが可能)
capture:自動売上確定の有無
confirmUrl:ユーザーが決済要求を承認した後、confirmUrlを自動的に呼出
confirmUrlType:CLIENTは決済完了のためにユーザー画面を加盟店のconfirmUrlに遷移
cancelUrl:LINE Pay決済画面でユーザーが決済を途中で取り消せば、決済取消画面へ遷移

{
  amount: payment.amount,
  currency: 'JPY',
  orderId: payment.payment_id,
  packages,
  options: { payment: { capture: true } },
  redirectUrls: {
       confirmUrl: process.env.CONFIRM_URL,
       confirmUrlType: 'CLIENT',
       cancelUrl: process.env.CANCEL_URL,
 }
}

paymentpackagesにはそれぞれ以下が格納されています。

const items = [
    {
        image_url: 'https://picsum.photos/200',
        item_id: 'item_1ifetpyzm1',
        item_name: 'テスト アイテム',
        tax_included_price: 330,
        raw_price: 300,
        quantity: 1
    }
]

const payment = {
    payment_id: 'abc123',
    amount: 330,
    items,
    store_code: 'abc123',
    store_name: 'abc123'
}

const products = payment.items.map((item) => {
 return {
          name: item.item_name,
          price: item.tax_included_price,
          quantity: item.quantity,
          originalPrice: item.raw_price,
          imageUrl: item.image_url,
        }
})

const packages = [
 {
   id: payment.store_code,
   name: payment.store_name,
   amount: payment.amount,
   products,
 },
]

Responce Body
以下の内容でresponceが返されます。成功すれば、returnCode000で返却され、決済画面に遷移するためにinfo.paymentUrl.webをポストバックアクションのuriに設定してあげると完了です。

Item type Length Description
returnCode String 4 結果コード
returnMessage String 300 結果メッセージ
info.transactionId Number 19 取引番号
info.paymentAccessToken String 12 LINE PayでScannerを利用する代わりにコードを入力する場合、そのコード値
info.paymentUrl.app String 300 決済画面に遷移するアプリのURL
info.paymentUrl.web String 300 決済画面に遷移するWeb URL

最後に

最後まで読んでいただきありがとうございました。
Sandbox環境ということもあり、本番環境とは少し動作が異なることは仕方ないですが、簡単に試せることはいいですね。
LINE Pay APIは個人事業主でも加盟店登録すれば利用可能なのでぜひ導入してみてはいかがでしょうか。

宣伝

パーソルプロセス&テクノロジー株式会社(以下パーソルP&T)、システムソリューション(SSOL)事業部所属の髙井です。

私はモビリティソリューションデザインチームに所属しており、モビリティ(ここでは移動手段全般)に関するサービスを考えたり、アプリを構築したりしております。

いわゆる「MaaS」に取り組んでおります。

私たちが「MaaS」に取り組む中で、現在活用している、もしくは活用する予定の技術やサービスやとりあえず発信したいことなどなど、幅広くチームメンバーと共に執筆していきたいと思います。
メンバーごとに違った内容を発信していきますので、お楽しみに!

また、「MaaS」について詳しく知りたい方は、チームメンバーの吉田が記事を掲載しておりますので、
ぜひそちらをご覧ください。

「MaaSとは」でたどり着いて欲しい記事 (1/3 前編)
「MaaSとは」でたどり着いて欲しい記事 (2/3 中編)
「MaaSとは」でたどり着いて欲しい記事 (3/3 後編)

最後まで読んでいただきありがとうございました!

12
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
15