[Feature Request] Waffo Pancake gateway — full integration with subscription support + admin catalog binding flow (#4935)
This commit is contained in:
+164
-60
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
@@ -29,7 +28,7 @@ func setupWaffoPancakeTestDB(t *testing.T) *gorm.DB {
|
||||
model.DB = db
|
||||
model.LOG_DB = db
|
||||
|
||||
require.NoError(t, db.AutoMigrate(&model.User{}, &model.TopUp{}))
|
||||
require.NoError(t, db.AutoMigrate(&model.User{}, &model.TopUp{}, &model.SubscriptionOrder{}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
sqlDB, err := db.DB()
|
||||
@@ -41,21 +40,6 @@ func setupWaffoPancakeTestDB(t *testing.T) *gorm.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
func TestWaffoPancakeCreateSessionResponseParsesDocumentedPayload(t *testing.T) {
|
||||
var result waffoPancakeCreateSessionResponse
|
||||
err := common.Unmarshal([]byte(`{
|
||||
"data": {
|
||||
"sessionId": "cs_550e8400-e29b-41d4-a716-446655440000",
|
||||
"checkoutUrl": "https://checkout.waffo.ai/my-store-abc123/checkout/cs_550e8400-e29b-41d4-a716-446655440000",
|
||||
"expiresAt": "2026-01-22T10:30:00.000Z"
|
||||
}
|
||||
}`), &result)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result.Data)
|
||||
require.Equal(t, "cs_550e8400-e29b-41d4-a716-446655440000", result.Data.SessionID)
|
||||
require.Empty(t, result.Data.OrderID)
|
||||
}
|
||||
|
||||
func TestResolveWaffoPancakeTradeNo_UsesWebhookOrderIDWhenLocalOrderExists(t *testing.T) {
|
||||
db := setupWaffoPancakeTestDB(t)
|
||||
|
||||
@@ -64,21 +48,79 @@ func TestResolveWaffoPancakeTradeNo_UsesWebhookOrderIDWhenLocalOrderExists(t *te
|
||||
Amount: 10,
|
||||
Money: 29,
|
||||
TradeNo: "ORD_5dXBtmF2HLlHfbPNm0Wcnz",
|
||||
PaymentMethod: model.PaymentMethodWaffoPancake,
|
||||
PaymentMethod: model.PaymentMethodWaffoPancake,
|
||||
PaymentProvider: model.PaymentProviderWaffoPancake,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
require.NoError(t, db.Create(topUp).Error)
|
||||
|
||||
tradeNo, err := ResolveWaffoPancakeTradeNo(&waffoPancakeWebhookEvent{
|
||||
Data: waffoPancakeWebhookData{
|
||||
OrderID: "ORD_5dXBtmF2HLlHfbPNm0Wcnz",
|
||||
tradeNo, err := ResolveWaffoPancakeTradeNo(&WaffoPancakeWebhookEvent{
|
||||
Data: WaffoPancakeWebhookData{
|
||||
OrderID: "ORD_5dXBtmF2HLlHfbPNm0Wcnz",
|
||||
MerchantProvidedBuyerIdentity: WaffoPancakeBuyerIdentityFromUserID(topUp.UserId),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "ORD_5dXBtmF2HLlHfbPNm0Wcnz", tradeNo)
|
||||
}
|
||||
|
||||
func TestResolveWaffoPancakeTradeNo_RejectsBuyerIdentityMismatch(t *testing.T) {
|
||||
db := setupWaffoPancakeTestDB(t)
|
||||
|
||||
topUp := &model.TopUp{
|
||||
UserId: 42,
|
||||
Amount: 10,
|
||||
Money: 29,
|
||||
TradeNo: "ORD_identity_mismatch_case",
|
||||
PaymentMethod: model.PaymentMethodWaffoPancake,
|
||||
PaymentProvider: model.PaymentProviderWaffoPancake,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
require.NoError(t, db.Create(topUp).Error)
|
||||
|
||||
// Webhook reports the right order but a different buyer — could be a
|
||||
// crossed-wires bug or a tampered payload. Either way: reject.
|
||||
tradeNo, err := ResolveWaffoPancakeTradeNo(&WaffoPancakeWebhookEvent{
|
||||
Data: WaffoPancakeWebhookData{
|
||||
OrderID: "ORD_identity_mismatch_case",
|
||||
MerchantProvidedBuyerIdentity: WaffoPancakeBuyerIdentityFromUserID(99), // wrong user
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Empty(t, tradeNo)
|
||||
require.Contains(t, err.Error(), "buyer identity mismatch")
|
||||
}
|
||||
|
||||
func TestResolveWaffoPancakeTradeNo_RejectsMissingBuyerIdentity(t *testing.T) {
|
||||
db := setupWaffoPancakeTestDB(t)
|
||||
|
||||
topUp := &model.TopUp{
|
||||
UserId: 7,
|
||||
Amount: 10,
|
||||
Money: 29,
|
||||
TradeNo: "ORD_missing_identity",
|
||||
PaymentMethod: model.PaymentMethodWaffoPancake,
|
||||
PaymentProvider: model.PaymentProviderWaffoPancake,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
require.NoError(t, db.Create(topUp).Error)
|
||||
|
||||
// An empty MerchantProvidedBuyerIdentity means the order was either created
|
||||
// via the (now-deprecated) anonymous flow or the field was stripped — also
|
||||
// reject so that we never credit anonymous orders to a specific user.
|
||||
tradeNo, err := ResolveWaffoPancakeTradeNo(&WaffoPancakeWebhookEvent{
|
||||
Data: WaffoPancakeWebhookData{
|
||||
OrderID: "ORD_missing_identity",
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Empty(t, tradeNo)
|
||||
require.Contains(t, err.Error(), "buyer identity mismatch")
|
||||
}
|
||||
|
||||
func TestResolveWaffoPancakeTradeNo_FailsWhenWebhookOrderIDIsUnknown(t *testing.T) {
|
||||
db := setupWaffoPancakeTestDB(t)
|
||||
|
||||
@@ -95,14 +137,15 @@ func TestResolveWaffoPancakeTradeNo_FailsWhenWebhookOrderIDIsUnknown(t *testing.
|
||||
Amount: 10,
|
||||
Money: 29,
|
||||
TradeNo: "WAFFO_PANCAKE-42-123456-abc123",
|
||||
PaymentMethod: model.PaymentMethodWaffoPancake,
|
||||
PaymentMethod: model.PaymentMethodWaffoPancake,
|
||||
PaymentProvider: model.PaymentProviderWaffoPancake,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
require.NoError(t, db.Create(topUp).Error)
|
||||
|
||||
tradeNo, err := ResolveWaffoPancakeTradeNo(&waffoPancakeWebhookEvent{
|
||||
Data: waffoPancakeWebhookData{
|
||||
tradeNo, err := ResolveWaffoPancakeTradeNo(&WaffoPancakeWebhookEvent{
|
||||
Data: WaffoPancakeWebhookData{
|
||||
OrderID: "ORD_unknown",
|
||||
BuyerEmail: user.Email,
|
||||
Amount: "29.00",
|
||||
@@ -112,46 +155,107 @@ func TestResolveWaffoPancakeTradeNo_FailsWhenWebhookOrderIDIsUnknown(t *testing.
|
||||
require.Empty(t, tradeNo)
|
||||
}
|
||||
|
||||
func TestResolveWaffoPancakeWebhookEnvironment(t *testing.T) {
|
||||
originalSandbox := setting.WaffoPancakeSandbox
|
||||
t.Cleanup(func() {
|
||||
setting.WaffoPancakeSandbox = originalSandbox
|
||||
// Parity tests for ResolveWaffoPancakeSubscriptionTradeNo — same four cases
|
||||
// as the TopUp resolver above, exercised against SubscriptionOrder records.
|
||||
// Drift between the two webhook flows is a real risk because they share
|
||||
// the same buyer-identity defence-in-depth pattern.
|
||||
|
||||
func TestResolveWaffoPancakeSubscriptionTradeNo_UsesWebhookOrderIDWhenLocalOrderExists(t *testing.T) {
|
||||
db := setupWaffoPancakeTestDB(t)
|
||||
|
||||
order := &model.SubscriptionOrder{
|
||||
UserId: 1,
|
||||
PlanId: 5,
|
||||
Money: 29,
|
||||
TradeNo: "WAFFO_PANCAKE_SUB-1-1700000000-abc123",
|
||||
PaymentMethod: model.PaymentMethodWaffoPancake,
|
||||
PaymentProvider: model.PaymentProviderWaffoPancake,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
require.NoError(t, db.Create(order).Error)
|
||||
|
||||
tradeNo, err := ResolveWaffoPancakeSubscriptionTradeNo(&WaffoPancakeWebhookEvent{
|
||||
Data: WaffoPancakeWebhookData{
|
||||
OrderID: "WAFFO_PANCAKE_SUB-1-1700000000-abc123",
|
||||
MerchantProvidedBuyerIdentity: WaffoPancakeBuyerIdentityFromUserID(order.UserId),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WAFFO_PANCAKE_SUB-1-1700000000-abc123", tradeNo)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
payload string
|
||||
expected string
|
||||
sandbox bool
|
||||
}{
|
||||
{
|
||||
name: "test mode",
|
||||
payload: `{"mode":"test"}`,
|
||||
expected: "test",
|
||||
func TestResolveWaffoPancakeSubscriptionTradeNo_RejectsBuyerIdentityMismatch(t *testing.T) {
|
||||
db := setupWaffoPancakeTestDB(t)
|
||||
|
||||
order := &model.SubscriptionOrder{
|
||||
UserId: 42,
|
||||
PlanId: 5,
|
||||
Money: 29,
|
||||
TradeNo: "WAFFO_PANCAKE_SUB-42-mismatch",
|
||||
PaymentMethod: model.PaymentMethodWaffoPancake,
|
||||
PaymentProvider: model.PaymentProviderWaffoPancake,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
require.NoError(t, db.Create(order).Error)
|
||||
|
||||
tradeNo, err := ResolveWaffoPancakeSubscriptionTradeNo(&WaffoPancakeWebhookEvent{
|
||||
Data: WaffoPancakeWebhookData{
|
||||
OrderID: "WAFFO_PANCAKE_SUB-42-mismatch",
|
||||
MerchantProvidedBuyerIdentity: WaffoPancakeBuyerIdentityFromUserID(99), // wrong user
|
||||
},
|
||||
{
|
||||
name: "prod mode",
|
||||
payload: `{"mode":"prod"}`,
|
||||
expected: "prod",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Empty(t, tradeNo)
|
||||
require.Contains(t, err.Error(), "buyer identity mismatch")
|
||||
}
|
||||
|
||||
func TestResolveWaffoPancakeSubscriptionTradeNo_RejectsMissingBuyerIdentity(t *testing.T) {
|
||||
db := setupWaffoPancakeTestDB(t)
|
||||
|
||||
order := &model.SubscriptionOrder{
|
||||
UserId: 7,
|
||||
PlanId: 5,
|
||||
Money: 29,
|
||||
TradeNo: "WAFFO_PANCAKE_SUB-7-missing-identity",
|
||||
PaymentMethod: model.PaymentMethodWaffoPancake,
|
||||
PaymentProvider: model.PaymentProviderWaffoPancake,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
require.NoError(t, db.Create(order).Error)
|
||||
|
||||
tradeNo, err := ResolveWaffoPancakeSubscriptionTradeNo(&WaffoPancakeWebhookEvent{
|
||||
Data: WaffoPancakeWebhookData{
|
||||
OrderID: "WAFFO_PANCAKE_SUB-7-missing-identity",
|
||||
},
|
||||
{
|
||||
name: "missing mode falls back to sandbox",
|
||||
payload: `{}`,
|
||||
expected: "test",
|
||||
sandbox: true,
|
||||
},
|
||||
{
|
||||
name: "invalid mode falls back to prod",
|
||||
payload: `{"mode":"staging"}`,
|
||||
expected: "prod",
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Empty(t, tradeNo)
|
||||
require.Contains(t, err.Error(), "buyer identity mismatch")
|
||||
}
|
||||
|
||||
func TestResolveWaffoPancakeSubscriptionTradeNo_FailsWhenWebhookOrderIDIsUnknown(t *testing.T) {
|
||||
db := setupWaffoPancakeTestDB(t)
|
||||
|
||||
order := &model.SubscriptionOrder{
|
||||
UserId: 42,
|
||||
PlanId: 5,
|
||||
Money: 29,
|
||||
TradeNo: "WAFFO_PANCAKE_SUB-42-real-order",
|
||||
PaymentMethod: model.PaymentMethodWaffoPancake,
|
||||
PaymentProvider: model.PaymentProviderWaffoPancake,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
require.NoError(t, db.Create(order).Error)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
setting.WaffoPancakeSandbox = tc.sandbox
|
||||
environment := resolveWaffoPancakeWebhookEnvironment(tc.payload)
|
||||
require.Equal(t, tc.expected, environment)
|
||||
})
|
||||
}
|
||||
tradeNo, err := ResolveWaffoPancakeSubscriptionTradeNo(&WaffoPancakeWebhookEvent{
|
||||
Data: WaffoPancakeWebhookData{
|
||||
OrderID: "WAFFO_PANCAKE_SUB-unknown",
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Empty(t, tradeNo)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user