kintoneのAPIプロキシーを活用して、見積書アプリの情報からStripeで請求書を発行するプラグインを作る方法

kintoneを使うことで、さまざまなバックオフィス業務を効率化・自動化することができます。さらにデータ（レコード）やUIのカスタマイズ方法が用意されているため、企業それぞれに合ったカスタマイズを行うことができることも、企業がバックオフィス業務の効率化のためにkintoneを採用するメリットとなっています。

そんなkintoneには、請求業務や売上・契約情報の管理・分析などのアプリも公開されています。これらのアプリを使うことで、請求書の発行や契約情報の管理を、ブラウザ上で自身のビジネスに合った形で行うことができます。

## kintoneとStripeを連携して、請求業務をさらに効率化しよう

kintoneのアプリストアに公開されている、多くの請求業務系のアプリには、「発行した請求書の支払いがいまどんなステータスか」を自動で確認する機能がありません。そのため請求業務に携わるスタッフは、ネットバンクや請求管理のSaaSなどを利用して支払い状況を確認し、kintoneに連携させています。

今回は、この請求管理・消し込み業務を自動化するヒントとして、「発行した見積書から、Stripeの請求書を作成する」方法を紹介します。この方法を行うことで、kintone側で作成した見積内容から、クレジットカード・銀行振込で支払いを受け付けることができる請求書をStripeで発行することができます。

## 完成イメージ

作成するプラグインは、[商品見積書パック](https://kintone-sol.cybozu.co.jp/apps/054-mitsumori.html)アプリで作成した見積書を利用します。

![スクリーンショット 2023-10-17 14.55.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/ded22436-ca91-2163-2795-533677db4472.png)

[請求書を発行する]ボタンをクリックすることで、Stripe APIを利用して請求書を発行し、クレジットカードや銀行振込での支払いを行うことができるページのURLを見積書のレコードに登録します。実運用で利用できるプラグインにする際は、発行した請求書支払いURLを[メールワイズ](https://mailwise.cybozu.co.jp/)から送信したり、[Stripe Webhook API](https://stripe.com/docs/webhooks?locale=ja-JP)を用意してkintoneに売上情報や決済状況などを同期させるなどの開発を行うことになります。

## 「見積書からStripeで請求書を発行する」プラグインを作ろう

ここからは、簡単に手順やコードを紹介していきます。

### 「商品見積書パック」アプリを追加する

まずはプラグインを追加するために、kintoneアプリを追加しましょう。

[kintoneアプリストア](https://kintone-sol.cybozu.co.jp/apps/)から「商品見積書パック」でアプリを検索し、詳細ページに移動しましょう。

![スクリーンショット 2023-09-19 15.37.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/f98f68c0-687b-6323-0fb8-c3ba4a6d1d04.png)

アプリの追加に成功すると、kintoneのポータル画面に[商品リスト]アプリと[見積書]アプリが追加されます。

![スクリーンショット 2023-09-19 15.38.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/c519d1ac-f4cd-2879-8320-cbb2651911d0.png)

### 見積書アプリをカスタマイズする

追加されたアプリを、Stripeの請求書管理システム（ [Stripe Invoicing](https://stripe.com/jp/invoicing) ）と連携するためには、いくつかのフィールドを追加する必要があります。
アプリのカスタマイズ画面を開いて、3つのフィールドを追加しましょう。

#### 1: メールアドレスを追加
まずは請求書を登録する際に利用する、メールアドレスを登録できるようにしましょう。

[文字列（1行）]フィールドを追加し、フィールド名を`メールアドレス`として保存します。

![スクリーンショット 2023-09-20 9.44.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/0f01e673-70f4-a50b-569d-7b651b62eee5.png)

このレコードを利用して、Stripeに請求書メールの送付を行わせたり、未払い時の通知などを行うことができます。

#### 2: Stripeで発行した請求書のIDを保存する

Stripeで発行した請求書のIDを、kintoneのレコードに紐づけるためのフィールドを追加しましょう。これによって、「どの見積書と請求書が対になっているか」を確認しやすくなります。

今回は`Stripe請求書ID`という名前と、`stripe_invoice_id`というフィールドコードを使用して、フィールドを追加しました。

![スクリーンショット 2023-09-20 9.45.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/0beafc87-1183-e3c0-93c7-46ef767fe802.png)

#### 3: Stripeで発行した請求書の支払いURLを保存する

「顧客が支払いを行うためのURL」も、レコードに保存しましょう。これによって、kintoneアプリを見るだけで、顧客に共有すべきURLを見つけることができます。

![スクリーンショット 2023-09-20 9.44.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/1eff286e-0add-9cca-c940-4eb240f4ef5b.png)

今回は`請求書PDF`という名前と、`stripe_invoice_pdf_url`というフィールドコードを使用して、フィールドを追加しました。

### create-pluginでkintoneプラグインをセットアップ

続いてアプリに適用するプラグインを作成しましょう。今回は[create-plugin](https://www.npmjs.com/package/@kintone/create-plugin)コマンドを利用します。

```bash
% npx @kintone/create-plugin kintone-stripe-invoice-plugin --lang ja --template modern
```

コマンドを実行すると、対話形式でプラグインの設定を尋ねられます。入力例を以下に掲載しますので、コピーアンドペーストして使うか、独自の設定を登録してください。

```bash
? プラグインの英語名を入力してください [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
```

プラグインのセットアップに成功すると、次のようなメッセージが表示されます。

```bash

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`を実行します。

```bash
% cd kintone-stripe-invoice-plugin
% npm start
```

`npm start`を実行すると、まずkintoneにアップロードするプラグインのZIPファイルが生成されます。


```bash

----------------------
Success to create a plugin zip!
Plugin ID: xxxxxxxxx
Path: ./dist/plugin.zip
----------------------
```

その後kintoneアカウントに接続するため、「**ベースURL**」と「**ログイン情報**」の入力を求められます。

```bash
? kintoneのベースURLを入力してください (https://example.cybozu.com): () 
? ログイン名を入力してください: () 
? パスワードを入力してください: [hidden] [input is hidden] 

```

入力が完了すると、数秒から十数秒ほど時間をおいて、ZIPファイルのアップロードが始まります。

```bash
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を呼び出しますので、[見積書]アプリを開きましょう。

![スクリーンショット 2023-09-19 15.39.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/5ad7761d-8779-1685-d833-7fd11c8ac230.png)

[アプリ内検索]左側にある歯車アイコンをクリックしましょう。

![スクリーンショット 2023-09-19 15.39.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/02402207-f001-34bb-eeff-f3a80a9929c6.png)

表示されたメニューのうち、[アプリ管理]を選択します。

![スクリーンショット 2023-09-19 15.39.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/04aedd73-7e2a-6c8a-dfdc-fa727481fbbc.png)

アカウントにアップロードされたアプリの一覧が表示されます。[見積書]の列にある歯車アイコンをクリックして、見積書アプリの設定画面に移動しましょう。

![スクリーンショット 2023-09-19 15.39.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/d22ecdeb-63cb-7eb8-9a13-9f0ffea4b86f.png)

見積書アプリの設定画面に到達しました。プラグインを登録するには、[カスタマイズ・サービス連携]セクションにある[プラグイン]をクリックします。

![スクリーンショット 2023-09-19 15.39.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/d89b961b-fade-735e-d4d3-b9c0b2016e7c.png)

アプリに登録・連携されているプラグインの一覧が表示されますが、初期状態ではどのプラグインも登録されていない状態が表示されています。アップロードしたプラグインを追加するには、[追加する]テキストをクリックしましょう。


![スクリーンショット 2023-09-19 15.39.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/9a841ec6-0833-6c86-0058-72c12346f375.png)

アップロードしたプラグインが表示されますので、チェックボックスをオンにしましょう。

![スクリーンショット 2023-09-19 15.40.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/99b1929d-f57b-8b3c-691c-24d4e55e32b2.png)

[追加]ボタンをクリックすると、アプリへの追加が完了します。

![スクリーンショット 2023-09-19 15.40.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/d247a47b-1405-139d-aef5-e5abb103baed.png)

成功メッセージが画面上部に表示され、一覧にプラグインが追加されていれば成功です。

![スクリーンショット 2023-09-19 15.40.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/32ee379a-5fb5-283c-e500-ee1e2e7aa756.png)

### StripeのAPIキーを、kintoneで安全に利用できるようにする

Stripeは、サーバ側で利用する「シークレットキー」とフロントエンドで利用する「公開可能キー」、そして「制限付きキー」の３種類をAPIキーとして提供しています。このうち、請求書などの操作を行うためには、「シークレットキー」または「制限付きキー」を利用します。ただしこれらのAPIキーは、ソースコードに直接記入するなど、第三者が入手できてしまう状況を作ると、第三者によるStripeアカウントへの不正アクセスに繋がります。

そこでkintoneの[APIプロキシー機能]を利用し、安全にAPIキーを管理できるようにしましょう。

手順は以下の記事にて紹介していますので、この手順を完了させてください。

https://qiita.com/hideokamoto/items/ba5dff7a0ec1cd91c648

### プラグインのUIを追加しよう

いよいよプラグインのコードを追加していきます。今回はkintoneチームが提供しているUIコンポーネントライブラリを利用し、[請求書を発行する]ボタンをアプリに追加します。

#### UIコンポーネントライブラリをインストールする

まずはライブラリをインストールしましょう。

```bash
% npm i kintone-ui-component
```

#### アプリの見積書詳細画面にボタンを追加する

アプリのソースコードを変更して、ボタンを追加しましょう。`Spinner`要素を利用して、ボタンをクリックした際のローディング画面も設定しておきます。

```javascript
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にアップロードしましょう。ビルドした後、アップロードを行います。


```bash
% npm run build
% npm run upload -- --base-url https://<kintoneのベースURL> --username <ログインユーザー名> --password <パスワード>
```

アップロード成功後、[請求書を発行する]ボタンが追加されていれば成功です。
![スクリーンショット 2023-10-17 15.38.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/70c059d1-4e21-3eea-2710-32bb967e8b92.png)

## Stripe APIを利用して、請求書を発行する

最後にStripe APIを利用した請求書発行フローを実装しましょう。

実装コードは、先ほど追加したボタンの「クリックイベント」に対して追加していきます。

```js
  buttonElement.addEventListener("click", async (event) => {
    const spinnerElement = new Spinner({
      text: "作成中...",
    });
    spinnerElement.open();

    // ここに請求書発行処理を追加する

    spinnerElement.close();
  });
```

### Stripe APIを呼び出す関数を作成しよう

まずはStripe APIをkintoneのAPIプロキシー経由で呼び出すための関数を用意しましょう。上で紹介している[kintoneプラグインのAPIプロキシー機能を使って、プラグインからStripe APIを呼び出す方法](https://qiita.com/hideokamoto/items/ba5dff7a0ec1cd91c648)の記事を完了している場合、次のような関数を作ります。

```js

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;
}

```
:::note
**TypeScriptの場合**

```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では、見積書に登録する商品データは[商品リスト]アプリで管理しています。

![スクリーンショット 2023-10-17 15.47.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/45849457-e819-9a3b-8db4-fdaf58fd3e1f.png)

このデータをStripe側で利用できるようにするための関数を作りましょう。ここでは、kintone側の[**型番**]フィールドの情報を、Stripeの**商品ID**として登録することで、同じ商品が二重三重に登録されることを防止しています。


```js
/**
 * 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側に顧客データを作成します。ここでは、アプリに追加した[メールアドレス]フィールドの情報をキーに、データが重複作成されないように作りましょう。

```js

/**
 * 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行ごとに登録する
+ 請求明細の登録が終わり次第、請求書を発行する
+ 請求内容を確定させ、支払いができる状態にする

```js
  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から取得したデータを登録し、最後に画面を再読み込みさせています。

```diff_javascript

      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が追加されています。

![スクリーンショット 2023-10-17 14.55.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/8bc256c3-7eed-0c9f-024b-e9d1a86e3c62.png)

## おわりに

kintoneプラグインでは、APIキーを安全に取り扱うため、Stripe SDKを使わずにAPI連携を行うことになります。そのため、APIドキュメントは`Node`ではなく`cURL`を確認する必要がある点にご注意ください。

また、このようなkintoneプラグインを開発する際は、[レコードコメント](https://cybozu.dev/ja/kintone/docs/rest-api/records/add-comment/)を利用して、「いつ
プラグインがレコードを操作したか」の記録を残すと、より便利に使うことができるかもしれません。

Stripe APIを呼び出す実装に慣れることができれば、請求書だけでなく売上データの取り込みやサブスクリプションの管理などにも応用することができます。また、コードを書くことに慣れていない方でも、[kintoneとStripeをノーコードで連携する機能を提供するサービス](https://www.r3it.com/info/stripe-customine-relation)や[kintoneとStripe両方をサポートするIPaaS](https://intercom.help/yoom/ja/collections/3041779-%E3%82%A2%E3%83%97%E3%83%AA%E9%80%A3%E6%90%BA)などを活用することもできます。


https://www.r3it.com/info/stripe-customine-relation

https://lp.yoom.fun/apps

https://zapier.com/




## [appendix]プラグインの全コード

```typescript
// 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);
});
```