// 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 } func New() *Provider { return &Provider{queryResults: map[string]provider.PaidEvent{}} } func (p *Provider) Method() string { return "fake" } func (p *Provider) Capabilities() provider.Capabilities { return provider.Capabilities{ RenderTypes: []provider.RenderType{provider.RenderCryptoAddress}, SupportsRefund: false, SettleCurrencies: []string{"USDT"}, Regions: []string{"global"}, } } func (p *Provider) Create(_ context.Context, req provider.CreateRequest) (*provider.Session, error) { exp := time.Now().Add(15 * time.Minute) ref := "FAKE-" + req.OutTradeNo + "-" + strconv.FormatInt(time.Now().UnixNano(), 36) 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 } // 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 }