NestJSを使ってStripe APIを叩く実装を一通り書いてみる

なにこれ?

今回はNestJSでStripeを実装するためのサンプルコードを書いてみました。

意外とNestJSを使ってStripe APIを叩きにいくようなコードがネット上で探せなかったので、誰かの手助けになると幸いです。

この記事のゴール

NestJSを使ってStripeのAPIを叩きにいけるようになることがゴールです。
細かい調整はご自身でお願いします。

今回はとにかく「apiを叩けるようになったー」っていうのがゴールとしているので!

本題

前提

  • NestJSはすでに触ったことがある人向け記事なので、文法とかの説明はありません。
  • すでにNestJSのプロジェクトは準備されている想定です。

Stripeアカウントを作成する

まずはStripeは検証用アカウント作成できるので以下からアカウント作成をしましょう

では、開発を進めていきます

ライブラリを利用する準備

ではStripeのapiを叩く準備をしていきますね。
今回Stripeのapiを叩くためには以下のライブラリを利用させていただきます。

npm install stripe

src配下にstripeディレクトリを作成して、以下ファイルを作成

src – stripe.module.ts
  L constants.ts

stripe.module.ts

import { DynamicModule, Module, Provider } from '@nestjs/common';
import Stripe from 'stripe';
import { STRIPE_CLIENT } from './constants';

@Module({})
export class StripeModule {
  static forRoot(apiKey: string, config: Stripe.StripeConfig): DynamicModule {
    const stripe = new Stripe(apiKey, config);
    const stripeProvider: Provider = {
      provide: STRIPE_CLIENT,
      useValue: stripe,
    };

    return {
      module: StripeModule,
      providers: [stripeProvider],
      exports: [stripeProvider],
      global: true,
    };
  }
}Code language: JavaScript (javascript)

constants.ts

export const STRIPE_CLIENT = 'STRIPE_CLIENT';Code language: JavaScript (javascript)

app.module.tsに以下を追記します。
また、ここで環境変数を呼び出す必要があるので以下ライブラリをインストール

npm i --save @nestjs/config

Stripeのモジュールより上部にConfigModuleがないと.envを読み込んでくれないので注意してください。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HttpModule } from '@nestjs/axios';
import { StripeModule } from './stripe/stripe.module';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: ['.env'],
    }),
    StripeModule.forRoot(process.env.STRIPE_SECRET_API_KEY, {
      apiVersion: '2023-10-16',
    }),
    HttpModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}Code language: JavaScript (javascript)

そしたら.envにはSTRIPE_SECRET_API_KEYという環境変数を準備してあげます

STRIPE_SECRET_API_KEY="**************************"Code language: JavaScript (javascript)

ここのapi keyはストライブのダッシュボードに開発者ページのシークレットキーがあるのでそれをコピペして貼り付けましょう。

Stripeのapiを叩きにいくAPIを実装

では次にStripeのapiを叩きにいくAPIを実装します。
商品の作成から、支払いリンクの発行、返金まで一連の流れで必要そうなapi全て叩きにいけるように準備しました。(API仕様書のサンプルコードとほとんど同じです)

他にも必要なapiがあれば、公式のapi仕様書を参考にして、いろいろ書いてみてください。

app.controller.ts

import {
  Body,
  Controller,
  Get,
  Headers,
  Inject,
  Param,
  Post,
} from '@nestjs/common';
import { AppService } from './app.service';
import { STRIPE_CLIENT } from './stripe/constants';
import Stripe from 'stripe';

@Controller('v1')
export class AppController {
  constructor(
    private readonly appService: AppService,
    @Inject(STRIPE_CLIENT) private stripe: Stripe,
  ) {}

  // 顧客作成(https://stripe.com/docs/api/customers/create)
  @Post('customers')
  createCustomer(
    @Body('description') description: string,
    @Body('email') email: string,
    @Body('name') name: string,
  ) {
    const customer = this.stripe.customers.create({
      description: description,
      email: email,
      name: name,
    });
    return customer;
  }

  @Get('customers')
  listCustomers() {
    return this.stripe.customers.list();
  }

  // 顧客取得(https://stripe.com/docs/api/customers/retrieve)
  @Get('customers/:id')
  customerDetails(@Param('id') id: string) {
    return this.stripe.customers.retrieve(id);
  }

  // アカウント一覧(https://stripe.com/docs/api/accounts/list)
  @Get('accounts')
  listAccounts() {
    return this.stripe.accounts.list({
      limit: 100,
    });
  }

  // アカウント登録(https://stripe.com/docs/api/accounts/create)
  @Post('accounts')
  createAccount(
    @Body('email') email: string,
    @Body('business_url') business_url: string,
    @Body('name') name: string,
  ) {
    return this.stripe.accounts.create({
      country: 'JP',
      type: 'custom',
      email: email,
      capabilities: {
        card_payments: { requested: true },
        transfers: { requested: true },
      },
      business_profile: { url: business_url, name: name }, // アカウント登録の時にurlがなかったら支払いリンクが発行されないっぽい
    });
  }

  // アカウント更新(https://stripe.com/docs/api/accounts/update)
  // 必要なパラメータは適宜追加
  @Post('accounts/:id')
  async updateAccount(
    @Param('id') id: string,
    @Body('email') email: string,
    @Body('business_url') business_url: string,
    @Body('name') name: string,
  ) {
    const account = await this.stripe.accounts.update(id, {
      email: email,
      business_profile: { url: business_url, name: name }, // アカウント登録の時にurlがなかったら支払いリンクが発行されないっぽい
    });
    return account;
  }

  // 商品を登録(https://stripe.com/docs/api/products/create)
  @Post('products')
  createProduct(
    @Body('name') name: string,
    @Body('stripe_account') stripe_account: any,
  ) {
    return this.stripe.products.create(
      {
        name: name,
      },
      {
        stripeAccount: stripe_account,
      },
    );
  }

  // 商品一覧(https://stripe.com/docs/api/products/list)
  @Get('products')
  listProducts() {
    return this.stripe.products.list({
      limit: 10,
    });
  }

  // 価格一覧(https://stripe.com/docs/api/prices/list)
  @Get('prices')
  listPrices() {
    return this.stripe.prices.list({
      limit: 100,
    });
  }

  // 価格を登録(https://stripe.com/docs/api/prices/create)
  @Post('prices')
  createPrice(
    @Body('unit_amount') unit_amount: number,
    @Body('product') product: string,
    @Body('stripe_account') stripe_account: any,
  ) {
    return this.stripe.prices.create(
      {
        unit_amount: unit_amount,
        currency: 'jpy',
        recurring: { interval: 'month' },
        product: product, // productのid
      },
      {
        stripeAccount: stripe_account,
      },
    );
  }

  // 支払いリンクの発行(https://stripe.com/docs/api/payment_links/payment_links/create)
  @Post('payment-links')
  createPaymentLink(
    @Body('price_id') price_id: string,
    @Body('quantity') quantity: number,
    @Body('stripe_account') stripe_account: any,
  ) {
    return this.stripe.paymentLinks.create(
      {
        line_items: [
          {
            price: price_id,
            quantity: quantity,
          },
        ],
      },
      {
        stripeAccount: stripe_account,
      },
    );
  }

  // 以下コマンドを叩いてwebhookを待つ(ローカルでのテスト)
  // stripe listen --forward-to localhost:3000/webhook
  @Post('webhook')
  handleStripeWebhook(
    @Headers('stripe-signature') signature: string,
    @Body() body: any,
  ) {
    // 以下条件のレスポンスが購入成功のデータを受け取っているはずなので、customerを見てどの顧客がいくらの支払いをしたのか確認できる
    //支払い情報をダッシュボードから確認(https://dashboard.stripe.com/test/payments)
    if (
      body.data.object.object == 'payment_intent' &&
      body.data.object.status == 'succeeded'
    ) {
      console.log(body.data.object);
    }
  }

  // 返金(https://stripe.com/docs/refunds?locale=ja-JP&dashboard-or-api=api)
  // api(https://stripe.com/docs/api/refunds/create)
  // api仕様書にはchargeかpayment_intentどちらかをリクエストすると記載されているが、payment_intentを採用(特に理由はない)
  @Post('refunds')
  createRefunds(@Body('payment_intent') payment_intent: any) {
    return this.stripe.refunds.create({
      payment_intent: payment_intent,
    });
  }

  // 請求書の作成
  @Post('invoice')
  async createInvoice(
    @Body('customerId') customerId: any,
    @Body('price') price: any,
    @Body('daysUntilDue') daysUntilDue: any,
  ) {
    // Create an Invoice
    const invoice = await this.stripe.invoices.create({
      customer: customerId,
      collection_method: 'send_invoice',
      days_until_due: daysUntilDue,
    });

    // Create an Invoice Item with the Price, and Customer you want to charge
    const invoiceItem = await this.stripe.invoiceItems.create({
      customer: customerId,
      price: price,
      invoice: invoice.id,
    });

    // 請求書を作成して送信する(https://stripe.com/docs/invoicing/integration/quickstart?locale=ja-JP)
    // 本番ならメールを飛ばせるけど、テスト環境ではapi経由でメールが飛ばない
    const sendInvoice = await this.stripe.invoices.sendInvoice(invoice.id);

    // 請求書とPDFのダウンロードリンク
    console.log(sendInvoice.hosted_invoice_url);
    console.log(sendInvoice.invoice_pdf);

    return sendInvoice;
  }

  // 不足データを更新する必要がある(https://stripe.com/docs/api/account_links/create)
  @Post('account_links')
  async createAccountLinks(
    @Body('account') account: any,
    @Body('refresh_url') refresh_url: any,
    @Body('return_url') return_url: any,
  ) {
    const accountLink = await this.stripe.accountLinks.create({
      account: account,
      refresh_url: refresh_url,
      return_url: return_url,
      type: 'account_onboarding',
    });
    return accountLink;
  }
}Code language: JavaScript (javascript)

こんな感じでapiを作っていきます。

webhookの利用

上記でもwebhookを利用した実装は記載していますが、もう少し詳しく記載しますね。
webhookを利用するにはstripe cliのインストールが必要です。(個人の環境に合わせてインストールしてください。)

brew install stripe/stripe-cli/stripe

stripe login

これでログインして認証してください。

そして、自分のローカルホストを叩きに来るように設定します(向き先は個人の環境に合わせてください)
stripe listen --forward-to localhost:3000/webhook

ここまでできたら、上記のコードにも記載していますが、以下のようなコードを書きます。
webhookで取得したいデータはいろいろあると思うのでconsole.log見ていろいろ調べてみてください

  // 以下コマンドを叩いてwebhookを待つ(ローカルでのテスト)
  // stripe listen --forward-to localhost:3000/webhook
  @Post('webhook')
  handleStripeWebhook(
    @Headers('stripe-signature') signature: string,
    @Body() body: any,
  ) {
    // 以下条件のレスポンスが購入成功のデータを受け取っているはずなので、customerを見てどの顧客がいくらの支払いをしたのか確認できる
    //支払い情報をダッシュボードから確認(https://dashboard.stripe.com/test/payments)
    if (
      body.data.object.object == 'payment_intent' &&
      body.data.object.status == 'succeeded'
    ) {
      console.log(body.data.object);
    }
  }Code language: JavaScript (javascript)

これで一通りのStripeを利用した実装は完了かと思います!

その他

開発環境でテストで支払いを実施するには?

開発環境で仮で支払いをするなら以下リンク先のテストカード番号を利用しましょう。

ここで紹介されているカード番号を利用するとよいでしょう。

開発環境で支払いリンクのメールが飛ばないんだが?

開発環境では、支払いリンクのメール送信がapiを通じて飛ばす事ができないようです。
もし試したいのであれば、ダッシュボードから手動で支払いを作成して、メールを飛ばしてみましょう。

まとめ

NestJSあまり触ったことない+Stripe全然知らんって時に調査したんですが、あまり記事がなくて困りました。

ただ、Stripeは公式YouTubeや、api referenceがしっかり準備されているのでとても優しいです。さすがだ。
でも、そのapiを叩くまでにどんな準備が必要なのかなどがなくて困ったんすよね。(NestJSほぼ書いたことないし)

自分自身が結構何しているのか分からんくて困ったので、この記事で誰かの役に立つと嬉しいす。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA