// Package fake is an in-process Provider used to exercise the pay v2 pipeline // end-to-end without any real channel. Real adapters (crypto/alipay/stripe) land // in P3. provider_ref = "FAKE-"+OutTradeNo+"-"+; render_type = crypto_address. // The nanosecond suffix is fake-only: Attempt has uniqueIndex(channel,provider_ref), // and a retry on the same order/method would otherwise reuse the same OutTradeNo and // collide. Real channels naturally mint a distinct ref per create call. package fake import ( "context" "encoding/json" "errors" "strconv" "sync" "time" "github.com/wangjia/pay/internal/provider" ) type Provider struct { mu sync.Mutex queryResults map[string]provider.PaidEvent supportsRefund bool refundRef string refundStatus provider.PaidStatus refundErr error // settleCurrencies overrides the default ["USDT"] capability (测试缝/dev 缝: // providerbuild.BuildRegistry 在 MOCK_CHANNEL_ENABLED 下把它设成 ["CNY","USDT"] // 以对齐 pangolin 下单实际用的结算币种;默认 nil 时保持既有测试预期不变)。 settleCurrencies []string lastMetadata map[string]string // 测试缝:记录最近一次 Create 收到的 Metadata(jiu 反馈波,验证 gateway 透传) } func New() *Provider { return &Provider{queryResults: map[string]provider.PaidEvent{}} } func (p *Provider) Method() string { return "fake" } // SetSettleCurrencies overrides Capabilities().SettleCurrencies (测试缝/dev 缝,见字段注释)。 // SettleCurrencies[0] 驱动 gateway.CreateOrder 实际选用的结算币种(设计 §4.1)。 func (p *Provider) SetSettleCurrencies(cs []string) { p.mu.Lock() defer p.mu.Unlock() p.settleCurrencies = cs } func (p *Provider) Capabilities() provider.Capabilities { p.mu.Lock() cs := p.settleCurrencies p.mu.Unlock() if len(cs) == 0 { cs = []string{"USDT"} } return provider.Capabilities{ RenderTypes: []provider.RenderType{provider.RenderCryptoAddress}, SupportsRefund: p.supportsRefund, SettleCurrencies: cs, Regions: []string{"global"}, } } func (p *Provider) Create(_ context.Context, req provider.CreateRequest) (*provider.Session, error) { p.mu.Lock() p.lastMetadata = req.Metadata p.mu.Unlock() exp := time.Now().Add(15 * time.Minute) ref := "FAKE-" + req.OutTradeNo + "-" + strconv.FormatInt(time.Now().UnixNano(), 36) // render 由 metadata["render"] 驱动(pangolin /buy 联调门控,见 CLAUDE.md 交代); // 只影响这里的展示形态,不影响 provider_ref 生成/VerifyCallback/结算。 switch req.Metadata["render"] { case string(provider.RenderRedirect): return &provider.Session{ ProviderRef: ref, RenderType: provider.RenderRedirect, Payload: map[string]any{ "url": "http://localhost:8090/mock-cashier?out_trade_no=" + req.OutTradeNo, }, ExpiresAt: &exp, }, nil case string(provider.RenderQR): return &provider.Session{ ProviderRef: ref, RenderType: provider.RenderQR, Payload: map[string]any{ "qr_content": "weixin://wxpay/mock/" + req.OutTradeNo, "display_amount": strconv.FormatInt(req.AmountMinor, 10), "currency": req.Currency, }, ExpiresAt: &exp, }, nil default: return &provider.Session{ ProviderRef: ref, RenderType: provider.RenderCryptoAddress, Payload: map[string]any{ "address": "TFake" + req.Account.AccountID + req.OutTradeNo, "amount_minor": req.AmountMinor, "currency": req.Currency, }, ExpiresAt: &exp, }, nil } } // VerifyCallback 解析测试注入的 JSON 回调体成归一化 PaidEvent。 func (p *Provider) VerifyCallback(_ context.Context, in provider.CallbackInput) (*provider.PaidEvent, error) { var b struct { ProviderRef string `json:"provider_ref"` Status string `json:"status"` AmountMinor int64 `json:"amount_minor"` Currency string `json:"currency"` } if err := json.Unmarshal(in.Raw, &b); err != nil { return nil, err } if b.ProviderRef == "" { return nil, errors.New("fake: missing provider_ref") } return &provider.PaidEvent{ ProviderRef: b.ProviderRef, Status: provider.PaidStatus(b.Status), PaidAmountMinor: b.AmountMinor, PaidCurrency: b.Currency, Raw: string(in.Raw), }, nil } func (p *Provider) Query(_ context.Context, req provider.QueryRequest) (*provider.PaidEvent, error) { p.mu.Lock() defer p.mu.Unlock() if ev, ok := p.queryResults[req.ProviderRef]; ok { e := ev return &e, nil } return &provider.PaidEvent{ProviderRef: req.ProviderRef, Status: provider.PaidPending}, nil } // LastMetadata returns the Metadata passed to the most recent Create call (test seam). func (p *Provider) LastMetadata() map[string]string { p.mu.Lock() defer p.mu.Unlock() return p.lastMetadata } // SetQueryResult primes Query to report a specific event (test seam). func (p *Provider) SetQueryResult(providerRef string, ev provider.PaidEvent) { p.mu.Lock() defer p.mu.Unlock() p.queryResults[providerRef] = ev } // EnableRefund 令 fake 表现为可退渠道并预置一次退款结果(测试缝)。 func (p *Provider) EnableRefund(refundRef string, status provider.PaidStatus, err error) { p.mu.Lock() defer p.mu.Unlock() p.supportsRefund = true p.refundRef = refundRef p.refundStatus = status p.refundErr = err } // Refund implements provider.RefundingProvider (测试缝:忽略入参,回放预置结果)。 func (p *Provider) Refund(_ context.Context, _, _ string, _ int64, _ string) (string, provider.PaidStatus, error) { p.mu.Lock() defer p.mu.Unlock() if p.refundErr != nil { return "", provider.PaidFailed, p.refundErr } return p.refundRef, p.refundStatus, nil }