其實,我原本這一篇心裡是想製作第三方金流當插件,然後實作,但是有太多東西可以說明可以介紹,如果依照製作插件寫下去,剩下幾天可能不太夠吧!!
所以沒關係,我就先介紹一下怎麼製作。
先來說明一下,第三方付款模組是什麼,還記得當初 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
每一個第三方支付類別都會有一個唯一識別碼,例如 台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 事件時,會執行此方法。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)
}
}
}
}
// ...
}
以上這一些事件就是與要將第三方金流提供商做成模組時,一定會需要的函式,限定需要函式就是為了要功能更加完整。