kintoneを使うことで、さまざまなバックオフィス業務を効率化・自動化することができます。さらにデータ(レコード)やUIのカスタマイズ方法が用意されているため、企業それぞれに合ったカスタマイズを行うことができることも、企業がバックオフィス業務の効率化のためにkintoneを採用するメリットとなっています。
そんなkintoneには、請求業務や売上・契約情報の管理・分析などのアプリも公開されています。これらのアプリを使うことで、請求書の発行や契約情報の管理を、ブラウザ上で自身のビジネスに合った形で行うことができます。
kintoneとStripeを連携して、請求業務をさらに効率化しよう
kintoneのアプリストアに公開されている、多くの請求業務系のアプリには、「発行した請求書の支払いがいまどんなステータスか」を自動で確認する機能がありません。そのため請求業務に携わるスタッフは、ネットバンクや請求管理のSaaSなどを利用して支払い状況を確認し、kintoneに連携させています。
今回は、この請求管理・消し込み業務を自動化するヒントとして、「発行した見積書から、Stripeの請求書を作成する」方法を紹介します。この方法を行うことで、kintone側で作成した見積内容から、クレジットカード・銀行振込で支払いを受け付けることができる請求書をStripeで発行することができます。
完成イメージ
作成するプラグインは、商品見積書パックアプリで作成した見積書を利用します。
[請求書を発行する]ボタンをクリックすることで、Stripe APIを利用して請求書を発行し、クレジットカードや銀行振込での支払いを行うことができるページのURLを見積書のレコードに登録します。実運用で利用できるプラグインにする際は、発行した請求書支払いURLをメールワイズから送信したり、Stripe Webhook APIを用意してkintoneに売上情報や決済状況などを同期させるなどの開発を行うことになります。
「見積書からStripeで請求書を発行する」プラグインを作ろう
ここからは、簡単に手順やコードを紹介していきます。
「商品見積書パック」アプリを追加する
まずはプラグインを追加するために、kintoneアプリを追加しましょう。
kintoneアプリストアから「商品見積書パック」でアプリを検索し、詳細ページに移動しましょう。
アプリの追加に成功すると、kintoneのポータル画面に[商品リスト]アプリと[見積書]アプリが追加されます。
見積書アプリをカスタマイズする
追加されたアプリを、Stripeの請求書管理システム( Stripe Invoicing )と連携するためには、いくつかのフィールドを追加する必要があります。
アプリのカスタマイズ画面を開いて、3つのフィールドを追加しましょう。
1: メールアドレスを追加
まずは請求書を登録する際に利用する、メールアドレスを登録できるようにしましょう。
[文字列(1行)]フィールドを追加し、フィールド名をメールアドレス
として保存します。
このレコードを利用して、Stripeに請求書メールの送付を行わせたり、未払い時の通知などを行うことができます。
2: Stripeで発行した請求書のIDを保存する
Stripeで発行した請求書のIDを、kintoneのレコードに紐づけるためのフィールドを追加しましょう。これによって、「どの見積書と請求書が対になっているか」を確認しやすくなります。
今回はStripe請求書ID
という名前と、stripe_invoice_id
というフィールドコードを使用して、フィールドを追加しました。
3: Stripeで発行した請求書の支払いURLを保存する
「顧客が支払いを行うためのURL」も、レコードに保存しましょう。これによって、kintoneアプリを見るだけで、顧客に共有すべきURLを見つけることができます。
今回は請求書PDF
という名前と、stripe_invoice_pdf_url
というフィールドコードを使用して、フィールドを追加しました。
create-pluginでkintoneプラグインをセットアップ
続いてアプリに適用するプラグインを作成しましょう。今回はcreate-pluginコマンドを利用します。
% npx @kintone/create-plugin kintone-stripe-invoice-plugin --lang ja --template modern
コマンドを実行すると、対話形式でプラグインの設定を尋ねられます。入力例を以下に掲載しますので、コピーアンドペーストして使うか、独自の設定を登録してください。
? プラグインの英語名を入力してください [1-64文字] Stripe Invoicing plugin
? プラグインの説明を入力してください [1-200文字] Manage invoice by using Stripe API
? 日本語をサポートしますか? Yes
? プラグインの日本語名を入力してください [1-64文字] (省略可) Stripe請求書管理プラグイン
? プラグインの日本語の説明を入力してください [1-200文字] (省略可) 発行した請求書を、Stripeと連携します
? 中国語をサポートしますか? No
? プラグインの英語のWebサイトURLを入力してください (省略可)
? プラグインの日本語のWebサイトURLを入力してください (省略可)
? モバイルページをサポートしますか? No
? @kintone/plugin-uploaderを使いますか? Yes
プラグインのセットアップに成功すると、次のようなメッセージが表示されます。
Success! Created Stripe Invoicing plugin at kintone-stripe-invoice-plugin
npm start
ファイルの変更を監視してプラグインのzipを自動的に作成するプロセスを起動します
その後、@kintone/plugin-uploaderにより、プラグインのzipは自動的にアップロードされます
npm run build
プラグインのzipを作成します
npm run lint
ESLintを使ってJavaScriptのソースコードをチェックします
まずは次のコマンドを実行してください
その後、あなたのkintone環境の情報を入力してください
cd kintone-stripe-invoice-plugin
npm start
プラグインをkintoneアカウントと連携する
セットアップしたプラグインを、kintoneアカウントと連携させましょう。成功メッセージの手順に従って、npm start
を実行します。
% cd kintone-stripe-invoice-plugin
% npm start
npm start
を実行すると、まずkintoneにアップロードするプラグインのZIPファイルが生成されます。
----------------------
Success to create a plugin zip!
Plugin ID: xxxxxxxxx
Path: ./dist/plugin.zip
----------------------
その後kintoneアカウントに接続するため、「ベースURL」と「ログイン情報」の入力を求められます。
? kintoneのベースURLを入力してください (https://example.cybozu.com): ()
? ログイン名を入力してください: ()
? パスワードを入力してください: [hidden] [input is hidden]
入力が完了すると、数秒から十数秒ほど時間をおいて、ZIPファイルのアップロードが始まります。
Open https://example.cybozu.com/login?saml=off
Trying to log in...
Navigate to https://dk1hbsx7brpq.cybozu.com/k/admin/system/plugin/
Trying to upload dist/plugin.zip
ここまでのステップで、エラーメッセージが表示されなかった場合は、アップロードに成功しています。
アップロードしたプラグインをkintoneアプリと連携する
kintoneアカウントにアップロードしたプラグインは、任意のアプリに追加できます。今回は見積書のデータからStripe APIを呼び出しますので、[見積書]アプリを開きましょう。
[アプリ内検索]左側にある歯車アイコンをクリックしましょう。
表示されたメニューのうち、[アプリ管理]を選択します。
アカウントにアップロードされたアプリの一覧が表示されます。[見積書]の列にある歯車アイコンをクリックして、見積書アプリの設定画面に移動しましょう。
見積書アプリの設定画面に到達しました。プラグインを登録するには、[カスタマイズ・サービス連携]セクションにある[プラグイン]をクリックします。
アプリに登録・連携されているプラグインの一覧が表示されますが、初期状態ではどのプラグインも登録されていない状態が表示されています。アップロードしたプラグインを追加するには、[追加する]テキストをクリックしましょう。
アップロードしたプラグインが表示されますので、チェックボックスをオンにしましょう。
[追加]ボタンをクリックすると、アプリへの追加が完了します。
成功メッセージが画面上部に表示され、一覧にプラグインが追加されていれば成功です。
StripeのAPIキーを、kintoneで安全に利用できるようにする
Stripeは、サーバ側で利用する「シークレットキー」とフロントエンドで利用する「公開可能キー」、そして「制限付きキー」の3種類をAPIキーとして提供しています。このうち、請求書などの操作を行うためには、「シークレットキー」または「制限付きキー」を利用します。ただしこれらのAPIキーは、ソースコードに直接記入するなど、第三者が入手できてしまう状況を作ると、第三者によるStripeアカウントへの不正アクセスに繋がります。
そこでkintoneの[APIプロキシー機能]を利用し、安全にAPIキーを管理できるようにしましょう。
手順は以下の記事にて紹介していますので、この手順を完了させてください。
プラグインのUIを追加しよう
いよいよプラグインのコードを追加していきます。今回はkintoneチームが提供しているUIコンポーネントライブラリを利用し、[請求書を発行する]ボタンをアプリに追加します。
UIコンポーネントライブラリをインストールする
まずはライブラリをインストールしましょう。
% npm i kintone-ui-component
アプリの見積書詳細画面にボタンを追加する
アプリのソースコードを変更して、ボタンを追加しましょう。Spinner
要素を利用して、ボタンをクリックした際のローディング画面も設定しておきます。
import { Button, Spinner } from "kintone-ui-component";
const PLUGIN_ID = kintone.$PLUGIN_ID;
kintone.events.on("app.record.detail.show", (e) => {
const spaceElement = document.getElementById("record-gaia");
if (spaceElement === null) {
throw new Error("The header element is unavailable on this page");
}
const fragment = document.createDocumentFragment();
const rowElement = document.createElement("div");
rowElement.classList.add("row-gaia");
const buttonElement = new Button({
text: "請求書を発行する",
type: "submit",
});
buttonElement.addEventListener("click", async (event) => {
const spinnerElement = new Spinner({
text: "作成中...",
});
spinnerElement.open();
// ここに請求書発行処理を追加する
spinnerElement.close();
});
rowElement.appendChild(buttonElement);
fragment.appendChild(rowElement);
spaceElement.appendChild(fragment);
});
プラグインの変更をアップロードする
プラグインの変更をkintoneにアップロードしましょう。ビルドした後、アップロードを行います。
% npm run build
% npm run upload -- --base-url https://<kintoneのベースURL> --username <ログインユーザー名> --password <パスワード>
アップロード成功後、[請求書を発行する]ボタンが追加されていれば成功です。
Stripe APIを利用して、請求書を発行する
最後にStripe APIを利用した請求書発行フローを実装しましょう。
実装コードは、先ほど追加したボタンの「クリックイベント」に対して追加していきます。
buttonElement.addEventListener("click", async (event) => {
const spinnerElement = new Spinner({
text: "作成中...",
});
spinnerElement.open();
// ここに請求書発行処理を追加する
spinnerElement.close();
});
Stripe APIを呼び出す関数を作成しよう
まずはStripe APIをkintoneのAPIプロキシー経由で呼び出すための関数を用意しましょう。上で紹介しているkintoneプラグインのAPIプロキシー機能を使って、プラグインからStripe APIを呼び出す方法の記事を完了している場合、次のような関数を作ります。
async function callStripeAPI(path, method, body) {
const apiResult = await new Promise((resolve, reject) => {
kintone.plugin.app.proxy(
PLUGIN_ID,
`https://api.stripe.com/v1/${path}`,
method,
{
"Content-Type": "application/x-www-form-urlencoded",
},
body || {},
(response) => {
const result = JSON.parse(response);
resolve(result);
},
(e) => {
console.log(e);
reject(e);
}
);
});
return apiResult;
}
TypeScriptの場合
async function callStripeAPI<Body = any, Result = any>(
path: string,
method: "GET" | "POST" | "PUT" | "DELETE",
body?: Body
) {
const apiResult = await new Promise<Result>((resolve, reject) => {
kintone.plugin.app.proxy(
PLUGIN_ID,
`https://api.stripe.com/v1/${path}`,
method,
{
"Content-Type": "application/x-www-form-urlencoded",
},
body || {},
(response) => {
const result = JSON.parse(response);
resolve(result);
},
(e) => {
console.log(e);
reject(e);
}
);
});
return apiResult;
}
kintoneの商品レコードをStripeに同期する関数を追加する
kintoneでは、見積書に登録する商品データは[商品リスト]アプリで管理しています。
このデータをStripe側で利用できるようにするための関数を作りましょう。ここでは、kintone側の[型番]フィールドの情報を、Stripeの商品IDとして登録することで、同じ商品が二重三重に登録されることを防止しています。
/**
* Stripe上の商品データ・料金データを作成する
*/
async function createStripeProductAndPriceIfNotExists(data) {
/**
* Stripe上に、商品・料金データを作成する。
* kintoneの「商品リスト」アプリの「型番」を商品データのIDに設定し、
* データがStripe上にない場合のみ作成する。
*/
const productId = data.型番.value;
const productExpantionQuery = new URLSearchParams("");
productExpantionQuery.set("expand[]", "default_price");
let product = await callStripeAPI(
`products/${productId}?${productExpantionQuery.toString()}`,
"GET"
).catch(() => ({}));
if (!product || !product.id) {
const newProductData = new URLSearchParams("");
newProductData.set("id", productId);
newProductData.set("name", data.商品名.value);
newProductData.set("default_price_data[currency]", "jpy");
newProductData.set("default_price_data[unit_amount]", data.単価.value);
newProductData.set("expand[]", "default_price");
product = await callStripeAPI(
`products`,
"POST",
newProductData.toString()
);
}
if (product.default_price.id) {
return {
priceId: product.default_price.id,
};
}
/**
* 料金データだけStripe上に存在しない場合の処理。
* 料金データを新規に作成し、デフォルト料金として登録する。
*/
const newPriceData = new URLSearchParams("");
newPriceData.set("currency", "jpy");
newPriceData.set("unit_amount", data.単価.value);
newPriceData.set("product", product.id);
const newPrice = await callStripeAPI(
`prices`,
"POST",
newPriceData.toString()
);
const updateProductData = new URLSearchParams("");
updateProductData.set("default_price", newPrice.id);
updateProductData.set("expand[]", "default_price");
product = await callStripeAPI(
`products/${product.id}`,
"POST",
updateProductData.toString()
);
return {
priceId: newPrice.id,
};
}
請求先の顧客情報を、Stripeに作成する関数を用意する
続いて支払い状況の管理やカード情報などを安全に管理することを目的に、Stripe側に顧客データを作成します。ここでは、アプリに追加した[メールアドレス]フィールドの情報をキーに、データが重複作成されないように作りましょう。
/**
* Stripe上にあるCustomerデータを取得・生成する。
* メールアドレスをキーとし、重複がある場合は最新のものを利用する
*/
async function createStripeCustomerIfNotExists(data) {
const searchCustomerData = new URLSearchParams("");
searchCustomerData.set("email", data.email);
const { data: customers } = await callStripeAPI(
"customers?" + searchCustomerData.toString(),
"GET",
{}
);
if (customers && customers.length > 0) {
return customers[0].id;
}
const createCustomerData = new URLSearchParams("");
createCustomerData.set("name", data.name);
createCustomerData.set("email", data.email);
const { id: customerId } = await callStripeAPI(
"customers",
"POST",
createCustomerData.toString()
);
return customerId;
}
請求書を発行する処理を追加する
顧客と商品情報の連携ができましたので、請求書を作成します。
このコードの中でゃ次の手順を順番に実行しています。
-
kintone.app.record.get()
でレコード情報を取得します - 見積もり内容を取得するため
recordData.record.見積明細.value
をループで処理する - 請求書の明細項目(StripeのInvoice LineItems)を、ループの中で1行ごとに登録する
- 請求明細の登録が終わり次第、請求書を発行する
- 請求内容を確定させ、支払いができる状態にする
buttonElement.addEventListener("click", async (event) => {
const spinnerElement = new Spinner({
text: "作成中...",
});
spinnerElement.open();
try {
const recordData = kintone.app.record.get();
const customerId = await createStripeCustomerIfNotExists({
name: recordData.record.宛名.value,
email: recordData.record.メールアドレス.value,
});
for await (const item of recordData.record.見積明細.value) {
const data = item.value;
const { priceId } = await createStripeProductAndPriceIfNotExists(data);
/**
* 請求書の明細を登録する処理
*/
const newInvoiceLineItemData = new URLSearchParams("");
newInvoiceLineItemData.set("customer", customerId);
newInvoiceLineItemData.set("price", priceId);
newInvoiceLineItemData.set("quantity", data.数量.value);
await callStripeAPI(
"invoiceitems",
"POST",
newInvoiceLineItemData.toString()
);
}
const newInvoice = new URLSearchParams("");
newInvoice.set("customer", customerId);
newInvoice.set("collection_method", "send_invoice");
newInvoice.set("days_until_due", "30");
const invoice = await callStripeAPI(
"invoices",
"POST",
newInvoice.toString()
);
const finalizedInvoiceData = await callStripeAPI(
`invoices/${invoice.id}/finalize`,
"POST"
);
console.log(finalizedInvoiceData);
} catch (error) {
console.log(error);
} finally {
spinnerElement.close();
}
});
作成した請求書情報を、kintoneのレコードに追加する
最後に、Stripeに作成した請求書情報を、kintoneのレコードに登録しましょう。kintoneのAPIを利用して、Stripe APIから取得したデータを登録し、最後に画面を再読み込みさせています。
const finalizedInvoiceData = await callStripeAPI(
`invoices/${invoice.id}/finalize`,
"POST"
);
- console.log(finalizedInvoiceData);
+ kintone.api(kintone.api.url("/k/v1/record.json", true), "PUT", {
+ app: kintone.app.getId(),
+ id: recordData.record.$id.value,
+ record: {
+ stripe_invoice_id: {
+ value: invoice.id,
+ },
+ stripe_invoice_pdf_url: {
+ value: finalizedInvoiceData.hosted_invoice_url,
+ },
+ },
+ });
+ location.reload();
} catch (error) {
console.log(error);
} finally {
これでプラグインの実装が完了しました。アプリをアップロードして、ボタンを操作すると、作成した見積書にStripeの請求書IDとURLが追加されています。
おわりに
kintoneプラグインでは、APIキーを安全に取り扱うため、Stripe SDKを使わずにAPI連携を行うことになります。そのため、APIドキュメントはNode
ではなくcURL
を確認する必要がある点にご注意ください。
また、このようなkintoneプラグインを開発する際は、レコードコメントを利用して、「いつ
プラグインがレコードを操作したか」の記録を残すと、より便利に使うことができるかもしれません。
Stripe APIを呼び出す実装に慣れることができれば、請求書だけでなく売上データの取り込みやサブスクリプションの管理などにも応用することができます。また、コードを書くことに慣れていない方でも、kintoneとStripeをノーコードで連携する機能を提供するサービスやkintoneとStripe両方をサポートするIPaaSなどを活用することもできます。
[appendix]プラグインの全コード
// You can use the ESModules syntax and @kintone/rest-api-client without additional settings.
// import { KintoneRestAPIClient } from "@kintone/rest-api-client";
import { Button, Spinner } from "kintone-ui-component";
const PLUGIN_ID = kintone.$PLUGIN_ID;
async function callStripeAPI<Body = any, Result = any>(
path: string,
method: "GET" | "POST" | "PUT" | "DELETE",
body?: Body
) {
const apiResult = await new Promise<Result>((resolve, reject) => {
kintone.plugin.app.proxy(
PLUGIN_ID,
`https://api.stripe.com/v1/${path}`,
method,
{
"Content-Type": "application/x-www-form-urlencoded",
},
body || {},
(response) => {
const result = JSON.parse(response);
resolve(result);
},
(e) => {
console.log(e);
reject(e);
}
);
});
return apiResult;
}
/**
* Stripe上にあるCustomerデータを取得・生成する。
* メールアドレスをキーとし、重複がある場合は最新のものを利用する
*/
async function createStripeCustomerIfNotExists(data: {
name: string;
email: string;
}) {
const searchCustomerData = new URLSearchParams("");
searchCustomerData.set("email", data.email);
const { data: customers } = await callStripeAPI(
"customers?" + searchCustomerData.toString(),
"GET",
{}
);
if (customers && customers.length > 0) {
return customers[0].id;
}
const createCustomerData = new URLSearchParams("");
createCustomerData.set("name", data.name);
createCustomerData.set("email", data.email);
const { id: customerId } = await callStripeAPI(
"customers",
"POST",
createCustomerData.toString()
);
return customerId;
}
/**
* Stripe上の商品データ・料金データを作成する
*/
async function createStripeProductAndPriceIfNotExists(data: {
型番: {
value: string;
};
商品名: {
value: string;
};
単価: {
value: string;
};
}) {
/**
* Stripe上に、商品・料金データを作成する。
* kintoneの「商品リスト」アプリの「型番」を商品データのIDに設定し、
* データがStripe上にない場合のみ作成する。
*/
const productId = data.型番.value;
const productExpantionQuery = new URLSearchParams("");
productExpantionQuery.set("expand[]", "default_price");
let product = await callStripeAPI(
`products/${productId}?${productExpantionQuery.toString()}`,
"GET"
).catch(() => ({}));
if (!product || !product.id) {
const newProductData = new URLSearchParams("");
newProductData.set("id", productId);
newProductData.set("name", data.商品名.value);
newProductData.set("default_price_data[currency]", "jpy");
newProductData.set("default_price_data[unit_amount]", data.単価.value);
newProductData.set("expand[]", "default_price");
product = await callStripeAPI(
`products`,
"POST",
newProductData.toString()
);
}
if (product.default_price.id) {
return {
priceId: product.default_price.id,
};
}
/**
* 料金データだけStripe上に存在しない場合の処理。
* 料金データを新規に作成し、デフォルト料金として登録する。
*/
const newPriceData = new URLSearchParams("");
newPriceData.set("currency", "jpy");
newPriceData.set("unit_amount", data.単価.value);
newPriceData.set("product", product.id);
const newPrice = await callStripeAPI(
`prices`,
"POST",
newPriceData.toString()
);
const updateProductData = new URLSearchParams("");
updateProductData.set("default_price", newPrice.id);
updateProductData.set("expand[]", "default_price");
product = await callStripeAPI(
`products/${product.id}`,
"POST",
updateProductData.toString()
);
return {
priceId: newPrice.id,
};
}
kintone.events.on("app.record.detail.show", (e) => {
const spaceElement = document.getElementById("record-gaia");
if (spaceElement === null) {
throw new Error("The header element is unavailable on this page");
}
const fragment = document.createDocumentFragment();
const rowElement = document.createElement("div");
rowElement.classList.add("row-gaia");
const buttonElement = new Button({
text: "請求書を発行する",
type: "submit",
});
buttonElement.addEventListener("click", async (event) => {
const spinnerElement = new Spinner({
text: "作成中...",
});
spinnerElement.open();
try {
const recordData = kintone.app.record.get();
const customerId = await createStripeCustomerIfNotExists({
name: recordData.record.宛名.value,
email: recordData.record.メールアドレス.value,
});
for await (const item of recordData.record.見積明細.value) {
const data = item.value;
const { priceId } = await createStripeProductAndPriceIfNotExists(data);
/**
* 請求書の明細を登録する処理
*/
const newInvoiceLineItemData = new URLSearchParams("");
newInvoiceLineItemData.set("customer", customerId);
newInvoiceLineItemData.set("price", priceId);
newInvoiceLineItemData.set("quantity", data.数量.value);
await callStripeAPI(
"invoiceitems",
"POST",
newInvoiceLineItemData.toString()
);
}
const newInvoice = new URLSearchParams("");
newInvoice.set("customer", customerId);
newInvoice.set("collection_method", "send_invoice");
newInvoice.set("days_until_due", "30");
const invoice = await callStripeAPI(
"invoices",
"POST",
newInvoice.toString()
);
const finalizedInvoiceData = await callStripeAPI(
`invoices/${invoice.id}/finalize`,
"POST"
);
console.log(finalizedInvoiceData);
kintone.api(kintone.api.url("/k/v1/record.json", true), "PUT", {
app: kintone.app.getId(),
id: recordData.record.$id.value,
record: {
stripe_invoice_id: {
value: invoice.id,
},
stripe_invoice_pdf_url: {
value: finalizedInvoiceData.hosted_invoice_url,
},
},
});
location.reload();
} catch (error) {
console.log(error);
} finally {
spinnerElement.close();
}
});
rowElement.appendChild(buttonElement);
fragment.appendChild(rowElement);
spaceElement.appendChild(fragment);
});