iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Modern Web

Medusa.js 石化我的心系列 第 24

Day24 進階導讀 - 了解怎麼 製作第三方付款模組

  • 分享至 

  • xImage
  •  

其實,我原本這一篇心裡是想製作第三方金流當插件,然後實作,但是有太多東西可以說明可以介紹,如果依照製作插件寫下去,剩下幾天可能不太夠吧!!

所以沒關係,我就先介紹一下怎麼製作。

前言

先來說明一下,第三方付款模組是什麼,還記得當初 Day10 有介紹到付款流程嗎?
就是當付款程序啟動的時候,Payment Session(付款工作階段)會與 第三方金流溝通,
MedusaJS官方支援有製作第三方金流插件只有stripe。所以今天我帶各位看看,製作第三方金流套件需要哪些主要函式,功能是什麼?

我會先把必要的東西寫出來,然後再往下慢慢介紹......

必要:匯入類別

如果要製作關於 第三方付款模組,第一步要先匯入AbstractPaymentProvider類別並且繼承,裡面有限制你主要的以及可選擇的功能函式。

import { AbstractPaymentProvider } from "@medusajs/framework/utils"

type Options = {
  apiKey: string
}
class MyPaymentProviderService extends AbstractPaymentProvider<
  Options
> {
  // TODO implement methods
}
export default MyPaymentProviderService

必要:identifier 識別碼

每一個第三方支付類別都會有一個唯一識別碼,例如 台X銀行支付系統,就可以設定為Taiwan-Paid-Version

class MyPaymentProviderService extends AbstractPaymentProvider<
  Options
> {
  static identifier = "Taiwan-Paid-Version"
  // ...
}

必要:初始化付款

當使用者確認購買要進行付款時,建立 Payment Session(付款工作階段)

// other imports...
import {
  InitiatePaymentInput,
  InitiatePaymentOutput,
} from "@medusajs/framework/types"

class MyPaymentProviderService extends AbstractPaymentProvider<
  Options
> {
  async initiatePayment(
    input: InitiatePaymentInput
  ): Promise<InitiatePaymentOutput> {
    const {
      amount,
      currency_code,
      context: customerDetails
    } = input

    // assuming you have a client that initializes the payment
    const response = await this.client.init(
      amount, currency_code, customerDetails
    )

    return {
      id: response.id,
      data: response,
    }
  }

  // ...
}

必要:授權付款函式

此方法就是Payment Session(付款工作階段)要求授權時,與第三方處理的階段區域。

此時就是將需要的購物資訊等等紀錄傳送給第三方金流提供授權。

當付款建立後,會自動建立一個 Payment 並且與訂單產生關聯。

// other imports...
import {
  AuthorizePaymentInput,
  AuthorizePaymentOutput,
  PaymentSessionStatus
} from "@medusajs/framework/types"

class MyPaymentProviderService extends AbstractPaymentProvider<
  Options
> {
  async authorizePayment(
    input: AuthorizePaymentInput
  ): Promise<AuthorizePaymentOutput> {
    const externalId = input.data?.id

    // assuming you have a client that authorizes the payment
    const paymentData = await this.client.authorizePayment(externalId)

    return {
      data: paymentData,
      status: "authorized"
    }
  }

  // ...
}

必要:取消付款函式

此函式是發生在授權付款後,管理者還沒有capture(捕獲),想要取消訂單發生的情況。

// other imports...
import {
  PaymentProviderError,
  PaymentProviderSessionResponse,
} from "@medusajs/framework/types"

class MyPaymentProviderService extends AbstractPaymentProvider<
  Options
> {
  async cancelPayment(
    input: CancelPaymentInput
  ): Promise<CancelPaymentOutput> {
    const externalId = input.data?.id

    // assuming you have a client that cancels the payment
    const paymentData = await this.client.cancelPayment(externalId)
    return { data: paymentData }
  }

  // ...
}

必要:捕獲付款函式

如果管理這對於客戶購買的這項商品沒問題,客戶也沒有 10s 內打來說訂錯,我們就可以 caputure(捕獲)付款

// other imports...
import {
  CapturePaymentInput,
  CapturePaymentOutput,
} from "@medusajs/framework/types"

class MyPaymentProviderService extends AbstractPaymentProvider<
  Options
> {
  async capturePayment(
    input: CapturePaymentInput
  ): Promise<CapturePaymentOutput> {
    const externalId = input.data?.id

      // assuming you have a client that captures the payment
    const newData = await this.client.capturePayment(externalId)
    return {
      data: {
        ...newData,
        id: externalId,
      }
    }
  }
  // ...
}

必要:取得付款狀態

會根據第三方整合中的狀態來取得 Payment session(付款工作階段)的狀態。

// other imports...
import {
  GetPaymentStatusInput,
  GetPaymentStatusOutput,
  PaymentSessionStatus
} from "@medusajs/framework/types"

class MyPaymentProviderService extends AbstractPaymentProvider<
  Options
> {
  async getPaymentStatus(
    input: GetPaymentStatusInput
  ): Promise<GetPaymentStatusOutput> {
    const externalId = input.data?.id

    // assuming you have a client that retrieves the payment status
    const status = await this.client.getStatus(externalId)

    switch (status) {
      case "requires_capture":
          return {status: "authorized"}
        case "success":
          return {status: "captured"}
        case "canceled":
          return {status: "canceled"}
        default:
          return {status: "pending"}
     }
  }

  // ...
}

必要:退款函式

當付款已經被Capture(捕獲),但是後續不滿意需要退貨時,或觸發此狀態。

// other imports...
import {
  RefundPaymentInput,
  RefundPaymentOutput,
} from "@medusajs/framework/types"

class MyPaymentProviderService extends AbstractPaymentProvider<
  Options
> {
  async refundPayment(
    input: RefundPaymentInput
  ): Promise<RefundPaymentOutput> {
    const externalId = input.data?.id

    // assuming you have a client that refunds the payment
    const newData = await this.client.refund(
        externalId,
        input.amount
      )

    return {
      data: input.data,
    }
  }
  // ...
}

必要:更新付款

當已經初始化Payment session(付款工作階段)後,突然有新的物品或資訊需要填入,可以觸發此函式更新付款。

// other imports...
import {
  UpdatePaymentInput,
  UpdatePaymentOutput,
} from "@medusajs/framework/types"

class MyPaymentProviderService extends AbstractPaymentProvider<
  Options
> {
  async updatePayment(
    input: UpdatePaymentInput
  ): Promise<UpdatePaymentOutput> {
    const { amount, currency_code, context } = input
    const externalId = input.data?.id

    // Validate context.customer
    if (!context || !context.customer) {
      throw new Error("Context must include a valid customer.");
    }

    // assuming you have a client that updates the payment
    const response = await this.client.update(
      externalId,
        {
          amount,
          currency_code,
          customer: context.customer
        }
      )

    return response
  }

  // ...
}

必要:檢索付款函式

此付款是直接跟第三方金流尋找付款資訊。

// other imports...
import {
  RetrievePaymentInput,
  RetrievePaymentOutput,
} from "@medusajs/framework/types"

class MyPaymentProviderService extends AbstractPaymentProvider<
  Options
> {
  async retrievePayment(
    input: RetrievePaymentInput
  ): Promise<RetrievePaymentOutput> {
    const externalId = input.data?.id

    // assuming you have a client that retrieves the payment
    return await this.client.retrieve(externalId)
  }
  // ...
}

取得 Webhook 通知

當從第三方付款金流收到 Webhook 事件時,會執行此方法。Medusa 使用此方法傳回的資料在 Medusa 應用程式中執行操作。

// other imports...
import {
  BigNumber
} from "@medusajs/framework/utils"
import {
  ProviderWebhookPayload,
  WebhookActionResult
} from "@medusajs/framework/types"

class MyPaymentProviderService extends AbstractPaymentProvider<
  Options
> {
  async getWebhookActionAndData(
    payload: ProviderWebhookPayload["payload"]
  ): Promise<WebhookActionResult> {
    const {
      data,
      rawData,
      headers
    } = payload

    try {
      switch(data.event_type) {
        case "authorized_amount":
          return {
            action: "authorized",
            data: {
              // assuming the session_id is stored in the metadata of the payment
              // in the third-party provider
              session_id: (data.metadata as Record<string, any>).session_id,
              amount: new BigNumber(data.amount as number)
            }
          }
        case "success":
          return {
            action: "captured",
            data: {
              // assuming the session_id is stored in the metadata of the payment
              // in the third-party provider
              session_id: (data.metadata as Record<string, any>).session_id,
              amount: new BigNumber(data.amount as number)
            }
          }
        default:
          return {
            action: "not_supported",
            data: {
              session_id: "",
              amount: new BigNumber(0)
            }
          }
      }
    } catch (e) {
      return {
        action: "failed",
        data: {
          // assuming the session_id is stored in the metadata of the payment
          // in the third-party provider
          session_id: (data.metadata as Record<string, any>).session_id,
          amount: new BigNumber(data.amount as number)
        }
      }
    }
  }

  // ...
}

以上這一些事件就是與要將第三方金流提供商做成模組時,一定會需要的函式,限定需要函式就是為了要功能更加完整。


上一篇
Day23 進階實作 - 使用 Notification 與 Subscribers 觸發通知
系列文
Medusa.js 石化我的心24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言