优化结构
This commit is contained in:
@@ -493,6 +493,30 @@ func (uc *PaymentUsecase) Refund(ctx context.Context, provider, tradeNo string,
|
||||
return uc.refundWithOrder(ctx, provider, tradeNo, amount)
|
||||
}
|
||||
|
||||
// Fulfill retries delivery for an already-paid order. It is intentionally
|
||||
// idempotent: the repository lease prevents concurrent attempts and a
|
||||
// succeeded order is returned as a duplicate without invoking the handler.
|
||||
func (uc *PaymentUsecase) Fulfill(ctx context.Context, provider, tradeNo string) (*PaymentResult, error) {
|
||||
if !validPaymentText(provider, 64) || !validPaymentText(tradeNo, 128) {
|
||||
return nil, errors.New("发货参数不完整")
|
||||
}
|
||||
if uc.orders == nil {
|
||||
return nil, errors.New("支付订单仓储未接入")
|
||||
}
|
||||
order, err := uc.orders.FindPaymentOrder(ctx, provider, tradeNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := paymentResultFromOrder(order)
|
||||
if result == nil {
|
||||
return nil, ErrPaymentOrderNotFound
|
||||
}
|
||||
if order.PaymentStatus != PaymentStatusPaid {
|
||||
return nil, ErrPaymentOrderState
|
||||
}
|
||||
return uc.fulfillOrder(ctx, order, result)
|
||||
}
|
||||
|
||||
// Callback verifies the notification in the adapter, then always performs a
|
||||
// provider-side query before it can dispatch fulfillment. Callback payloads
|
||||
// are never trusted for amount, currency, or final payment state.
|
||||
@@ -671,10 +695,21 @@ func (uc *PaymentUsecase) reconcilePaymentResult(ctx context.Context, result, ca
|
||||
return result, nil
|
||||
}
|
||||
|
||||
fulfilled, err := uc.fulfillOrder(ctx, order, result)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
return fulfilled, nil
|
||||
}
|
||||
|
||||
func (uc *PaymentUsecase) fulfillOrder(ctx context.Context, order *PaymentOrder, result *PaymentResult) (*PaymentResult, error) {
|
||||
if order == nil || result == nil {
|
||||
return nil, errors.New("支付发货订单为空")
|
||||
}
|
||||
order, token, duplicate, err := uc.orders.BeginPaymentFulfillment(ctx, result.Provider, result.TradeNo, 10*time.Minute)
|
||||
if err != nil {
|
||||
uc.logger.Error(ctx, "payment.order.fulfillment_begin_failed", "provider", result.Provider, "trade_no", result.TradeNo, "error", err)
|
||||
return fail(err)
|
||||
return nil, err
|
||||
}
|
||||
if duplicate {
|
||||
result.Duplicate = true
|
||||
@@ -685,29 +720,23 @@ func (uc *PaymentUsecase) reconcilePaymentResult(ctx context.Context, result, ca
|
||||
if handler == nil {
|
||||
err = fmt.Errorf("支付发货 handler 未注册: %s", order.BusinessType)
|
||||
_, _ = uc.orders.CompletePaymentFulfillment(ctx, result.Provider, result.TradeNo, token, false, err.Error())
|
||||
uc.logger.Error(ctx, "payment.order.fulfillment_handler_missing", "provider", result.Provider, "trade_no", result.TradeNo, "business_type", order.BusinessType, "error", err)
|
||||
return fail(err)
|
||||
return nil, err
|
||||
}
|
||||
confirmation := &PaymentConfirmation{
|
||||
ID: order.ConfirmationID, Provider: order.Provider, TradeNo: order.TradeNo,
|
||||
ProviderTradeNo: result.ProviderTradeNo, BusinessType: order.BusinessType,
|
||||
BusinessID: order.BusinessID, Subject: order.Subject, Amount: order.Amount,
|
||||
OriginalAmount: order.OriginalAmount, PayerPaidAmount: order.PayerPaidAmount,
|
||||
CashPaidAmount: order.CashPaidAmount, PointPaidAmount: order.PointPaidAmount,
|
||||
confirmation := &PaymentConfirmation{ID: order.ConfirmationID, Provider: order.Provider, TradeNo: order.TradeNo,
|
||||
ProviderTradeNo: result.ProviderTradeNo, BusinessType: order.BusinessType, BusinessID: order.BusinessID,
|
||||
Subject: order.Subject, Amount: order.Amount, OriginalAmount: order.OriginalAmount,
|
||||
PayerPaidAmount: order.PayerPaidAmount, CashPaidAmount: order.CashPaidAmount, PointPaidAmount: order.PointPaidAmount,
|
||||
DiscountAmount: order.DiscountAmount, ProviderDiscountAmount: order.ProviderDiscountAmount,
|
||||
MerchantDiscountAmount: order.MerchantDiscountAmount, SettlementAmount: order.SettlementAmount,
|
||||
Currency: order.Currency, PayerCurrency: order.PayerCurrency, AmountBreakdownKnown: order.AmountBreakdownKnown,
|
||||
Payload: result.Payload, CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
Payload: result.Payload, CreatedAt: time.Now().UTC()}
|
||||
if err = handler.Fulfill(ctx, confirmation); err != nil {
|
||||
_, _ = uc.orders.CompletePaymentFulfillment(ctx, result.Provider, result.TradeNo, token, false, err.Error())
|
||||
uc.logger.Error(ctx, "payment.order.fulfillment_failed", "provider", result.Provider, "trade_no", result.TradeNo, "confirmation_id", confirmation.ID, "error", err)
|
||||
return fail(err)
|
||||
return nil, err
|
||||
}
|
||||
order, err = uc.orders.CompletePaymentFulfillment(ctx, result.Provider, result.TradeNo, token, true, "")
|
||||
if err != nil {
|
||||
uc.logger.Error(ctx, "payment.order.fulfillment_record_failed", "provider", result.Provider, "trade_no", result.TradeNo, "confirmation_id", confirmation.ID, "error", err)
|
||||
return fail(err)
|
||||
return nil, err
|
||||
}
|
||||
result.Status = "fulfilled"
|
||||
return attachOrderResult(result, order), nil
|
||||
|
||||
@@ -330,6 +330,24 @@ func TestPersistentPaymentCallbackFulfillsAndPersistsDuplicate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentFulfillRetriesFailedOrder(t *testing.T) {
|
||||
orders := &paymentOrderRepoStub{order: &PaymentOrder{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: PaymentStatusPaid, FulfillmentStatus: FulfillmentStatusFailed, RefundStatus: RefundStatusNone, ConfirmationID: "11111111-1111-1111-1111-111111111111"}}
|
||||
uc := NewPaymentUsecase(&paymentRepoStub{}, orders, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
handler := &paymentFulfillmentStub{}
|
||||
registry := NewPaymentFulfillmentRegistry()
|
||||
if err := registry.Register(handler); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uc.SetFulfillmentRegistry(registry)
|
||||
result, err := uc.Fulfill(context.Background(), PaymentAlipay, "order-1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Status != "fulfilled" || handler.calls != 1 || orders.order.FulfillmentStatus != FulfillmentStatusSucceeded {
|
||||
t.Fatalf("fulfillment retry failed: result=%+v calls=%d status=%s", result, handler.calls, orders.order.FulfillmentStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentConfirmationAcceptsProviderDiscount(t *testing.T) {
|
||||
intent := &PaymentIntent{Provider: PaymentAlipay, TradeNo: "T1", Amount: 1000, Currency: "CNY", BusinessType: "item", BusinessID: "I1"}
|
||||
result := &PaymentResult{Provider: PaymentAlipay, TradeNo: "T1", ProviderTradeNo: "P1", Status: "success", Amount: 1000, PayerPaidAmount: 800, CashPaidAmount: 800, DiscountAmount: 200, ProviderDiscountAmount: 150, MerchantDiscountAmount: 50, SettlementAmount: 950, Currency: "CNY", AmountBreakdownKnown: true}
|
||||
|
||||
Reference in New Issue
Block a user