[BugFix] fix webhook process (#5047)

This commit is contained in:
Hill-waffo
2026-05-24 16:19:27 +08:00
committed by GitHub
parent ebbe315533
commit 0354c38bef
7 changed files with 62 additions and 40 deletions
+27 -19
View File
@@ -17,14 +17,15 @@ type WaffoPancakePriceSnapshot struct {
}
// WaffoPancakeCreateSessionParams is the input to CreateWaffoPancakeCheckoutSession.
// BuyerIdentity (merchant-controlled, stable per user) is what survives the
// buyer editing email at checkout — see WaffoPancakeBuyerIdentityFromUserID.
// BuyerIdentity must be stable per user (see WaffoPancakeBuyerIdentityFromUserID).
// OrderMerchantExternalID = our trade_no; Pancake echoes it back in webhooks.
type WaffoPancakeCreateSessionParams struct {
ProductID string
BuyerIdentity string
PriceSnapshot *WaffoPancakePriceSnapshot
BuyerEmail string
ExpiresInSeconds *int
ProductID string
BuyerIdentity string
PriceSnapshot *WaffoPancakePriceSnapshot
BuyerEmail string
ExpiresInSeconds *int
OrderMerchantExternalID string
}
// WaffoPancakeCheckoutSession is the response of CreateWaffoPancakeCheckoutSession.
@@ -52,7 +53,9 @@ type WaffoPancakeWebhookEvent struct {
}
type WaffoPancakeWebhookData struct {
// OrderID = Pancake ORD_* (logs); OrderMerchantExternalID = our trade_no (lookup).
OrderID string
OrderMerchantExternalID string
BuyerEmail string
Currency string
Amount string
@@ -107,10 +110,11 @@ func CreateWaffoPancakeCheckoutSession(ctx context.Context, params *WaffoPancake
sdkParams := pancake.AuthenticatedCheckoutParams{
CreateCheckoutSessionParams: pancake.CreateCheckoutSessionParams{
ProductID: params.ProductID,
Currency: "USD",
BuyerEmail: optionalString(params.BuyerEmail),
ExpiresInSeconds: params.ExpiresInSeconds,
ProductID: params.ProductID,
Currency: "USD",
BuyerEmail: optionalString(params.BuyerEmail),
ExpiresInSeconds: params.ExpiresInSeconds,
OrderMerchantExternalID: optionalString(params.OrderMerchantExternalID),
},
BuyerIdentity: params.BuyerIdentity,
}
@@ -163,6 +167,10 @@ func VerifyConfiguredWaffoPancakeWebhook(payload string, signatureHeader string)
if evt.Data.MerchantProvidedBuyerIdentity != nil {
identity = *evt.Data.MerchantProvidedBuyerIdentity
}
externalID := ""
if evt.Data.OrderMerchantExternalID != nil {
externalID = *evt.Data.OrderMerchantExternalID
}
return &WaffoPancakeWebhookEvent{
ID: evt.ID,
Timestamp: evt.Timestamp,
@@ -172,6 +180,7 @@ func VerifyConfiguredWaffoPancakeWebhook(payload string, signatureHeader string)
Mode: string(evt.Mode),
Data: WaffoPancakeWebhookData{
OrderID: evt.Data.OrderID,
OrderMerchantExternalID: externalID,
BuyerEmail: evt.Data.BuyerEmail,
Currency: evt.Data.Currency,
Amount: evt.Data.Amount,
@@ -183,19 +192,18 @@ func VerifyConfiguredWaffoPancakeWebhook(payload string, signatureHeader string)
}
// ResolveWaffoPancakeTradeNo maps a verified webhook event to a local TopUp
// trade_no, rejecting any payload whose buyer identity doesn't match the one
// we recorded at checkout — defence-in-depth on top of signature verification.
// trade_no via OrderMerchantExternalID, and rejects buyer-identity mismatches.
func ResolveWaffoPancakeTradeNo(event *WaffoPancakeWebhookEvent) (string, error) {
if event == nil {
return "", fmt.Errorf("missing webhook event")
}
tradeNo := strings.TrimSpace(event.Data.OrderID)
tradeNo := strings.TrimSpace(event.Data.OrderMerchantExternalID)
if tradeNo == "" {
return "", fmt.Errorf("missing webhook orderId")
return "", fmt.Errorf("missing webhook orderMerchantExternalId")
}
topUp := model.GetTopUpByTradeNo(tradeNo)
if topUp == nil || topUp.PaymentProvider != model.PaymentProviderWaffoPancake {
return "", fmt.Errorf("waffo pancake order not found for webhook orderId=%s", tradeNo)
return "", fmt.Errorf("waffo pancake order not found for tradeNo=%s", tradeNo)
}
expectedIdentity := WaffoPancakeBuyerIdentityFromUserID(topUp.UserId)
actualIdentity := strings.TrimSpace(event.Data.MerchantProvidedBuyerIdentity)
@@ -216,13 +224,13 @@ func ResolveWaffoPancakeSubscriptionTradeNo(event *WaffoPancakeWebhookEvent) (st
if event == nil {
return "", fmt.Errorf("missing webhook event")
}
tradeNo := strings.TrimSpace(event.Data.OrderID)
tradeNo := strings.TrimSpace(event.Data.OrderMerchantExternalID)
if tradeNo == "" {
return "", fmt.Errorf("missing webhook orderId")
return "", fmt.Errorf("missing webhook orderMerchantExternalId")
}
order := model.GetSubscriptionOrderByTradeNo(tradeNo)
if order == nil || order.PaymentProvider != model.PaymentProviderWaffoPancake {
return "", fmt.Errorf("waffo pancake subscription order not found for webhook orderId=%s", tradeNo)
return "", fmt.Errorf("waffo pancake subscription order not found for tradeNo=%s", tradeNo)
}
expectedIdentity := WaffoPancakeBuyerIdentityFromUserID(order.UserId)
actualIdentity := strings.TrimSpace(event.Data.MerchantProvidedBuyerIdentity)
+16 -10
View File
@@ -57,7 +57,8 @@ func TestResolveWaffoPancakeTradeNo_UsesWebhookOrderIDWhenLocalOrderExists(t *te
tradeNo, err := ResolveWaffoPancakeTradeNo(&WaffoPancakeWebhookEvent{
Data: WaffoPancakeWebhookData{
OrderID: "ORD_5dXBtmF2HLlHfbPNm0Wcnz",
OrderID: "ORD_internal_pancake_id",
OrderMerchantExternalID: "ORD_5dXBtmF2HLlHfbPNm0Wcnz",
MerchantProvidedBuyerIdentity: WaffoPancakeBuyerIdentityFromUserID(topUp.UserId),
},
})
@@ -84,7 +85,8 @@ func TestResolveWaffoPancakeTradeNo_RejectsBuyerIdentityMismatch(t *testing.T) {
// crossed-wires bug or a tampered payload. Either way: reject.
tradeNo, err := ResolveWaffoPancakeTradeNo(&WaffoPancakeWebhookEvent{
Data: WaffoPancakeWebhookData{
OrderID: "ORD_identity_mismatch_case",
OrderID: "ORD_internal_pancake_id",
OrderMerchantExternalID: "ORD_identity_mismatch_case",
MerchantProvidedBuyerIdentity: WaffoPancakeBuyerIdentityFromUserID(99), // wrong user
},
})
@@ -113,7 +115,8 @@ func TestResolveWaffoPancakeTradeNo_RejectsMissingBuyerIdentity(t *testing.T) {
// reject so that we never credit anonymous orders to a specific user.
tradeNo, err := ResolveWaffoPancakeTradeNo(&WaffoPancakeWebhookEvent{
Data: WaffoPancakeWebhookData{
OrderID: "ORD_missing_identity",
OrderID: "ORD_internal_pancake_id",
OrderMerchantExternalID: "ORD_missing_identity",
},
})
require.Error(t, err)
@@ -146,9 +149,10 @@ func TestResolveWaffoPancakeTradeNo_FailsWhenWebhookOrderIDIsUnknown(t *testing.
tradeNo, err := ResolveWaffoPancakeTradeNo(&WaffoPancakeWebhookEvent{
Data: WaffoPancakeWebhookData{
OrderID: "ORD_unknown",
BuyerEmail: user.Email,
Amount: "29.00",
OrderID: "ORD_internal_pancake_id",
OrderMerchantExternalID: "WAFFO_PANCAKE-unknown",
BuyerEmail: user.Email,
Amount: "29.00",
},
})
require.Error(t, err)
@@ -177,7 +181,8 @@ func TestResolveWaffoPancakeSubscriptionTradeNo_UsesWebhookOrderIDWhenLocalOrder
tradeNo, err := ResolveWaffoPancakeSubscriptionTradeNo(&WaffoPancakeWebhookEvent{
Data: WaffoPancakeWebhookData{
OrderID: "WAFFO_PANCAKE_SUB-1-1700000000-abc123",
OrderID: "ORD_internal_pancake_id",
OrderMerchantExternalID: "WAFFO_PANCAKE_SUB-1-1700000000-abc123",
MerchantProvidedBuyerIdentity: WaffoPancakeBuyerIdentityFromUserID(order.UserId),
},
})
@@ -202,7 +207,8 @@ func TestResolveWaffoPancakeSubscriptionTradeNo_RejectsBuyerIdentityMismatch(t *
tradeNo, err := ResolveWaffoPancakeSubscriptionTradeNo(&WaffoPancakeWebhookEvent{
Data: WaffoPancakeWebhookData{
OrderID: "WAFFO_PANCAKE_SUB-42-mismatch",
OrderID: "ORD_internal_pancake_id",
OrderMerchantExternalID: "WAFFO_PANCAKE_SUB-42-mismatch",
MerchantProvidedBuyerIdentity: WaffoPancakeBuyerIdentityFromUserID(99), // wrong user
},
})
@@ -228,7 +234,7 @@ func TestResolveWaffoPancakeSubscriptionTradeNo_RejectsMissingBuyerIdentity(t *t
tradeNo, err := ResolveWaffoPancakeSubscriptionTradeNo(&WaffoPancakeWebhookEvent{
Data: WaffoPancakeWebhookData{
OrderID: "WAFFO_PANCAKE_SUB-7-missing-identity",
OrderMerchantExternalID: "WAFFO_PANCAKE_SUB-7-missing-identity",
},
})
require.Error(t, err)
@@ -253,7 +259,7 @@ func TestResolveWaffoPancakeSubscriptionTradeNo_FailsWhenWebhookOrderIDIsUnknown
tradeNo, err := ResolveWaffoPancakeSubscriptionTradeNo(&WaffoPancakeWebhookEvent{
Data: WaffoPancakeWebhookData{
OrderID: "WAFFO_PANCAKE_SUB-unknown",
OrderMerchantExternalID: "WAFFO_PANCAKE_SUB-unknown",
},
})
require.Error(t, err)