package payment import ( "context" "errors" "log/slog" "net" "time" ) // PaymentLogger isolates payment audit logging from the payment workflow and // lets an application replace slog with its own structured logger. type PaymentLogger interface { Info(context.Context, string, ...any) Error(context.Context, string, ...any) } type slogPaymentLogger struct { logger *slog.Logger } func NewPaymentLogger(logger *slog.Logger) PaymentLogger { if logger == nil { logger = slog.Default() } return &slogPaymentLogger{logger: logger.With("mod", "payment")} } func (l *slogPaymentLogger) Info(ctx context.Context, message string, args ...any) { l.logger.InfoContext(ctx, message, args...) } func (l *slogPaymentLogger) Error(ctx context.Context, message string, args ...any) { for i := 0; i+1 < len(args); i += 2 { if key, ok := args[i].(string); ok && (key == "error" || key == "err") { args[i] = "error_code" if err, ok := args[i+1].(error); ok { args[i+1] = paymentErrorCode(err) } else { args[i+1] = "redacted" } } } l.logger.ErrorContext(ctx, message, args...) } func paymentErrorCode(err error) string { switch { case errors.Is(err, ErrPaymentOrderNotFound): return "order_not_found" case errors.Is(err, ErrPaymentProviderNotFound): return "provider_not_configured" case errors.Is(err, ErrPaymentOrderConflict): return "order_conflict" case errors.Is(err, ErrPaymentProviderConflict): return "provider_conflict" case errors.Is(err, ErrPaymentOrderBusy): return "order_busy" case errors.Is(err, ErrPaymentOrderState): return "invalid_order_state" case errors.Is(err, ErrPaymentOperationRejected): return "provider_rejected" case errors.Is(err, context.Canceled): return "cancelled" case errors.Is(err, context.DeadlineExceeded): return "timeout" default: var networkError net.Error if errors.As(err, &networkError) && networkError.Timeout() { return "timeout" } return "operation_failed" } } func (uc *PaymentUsecase) logPaymentOperation(ctx context.Context, operation string, started time.Time, provider, tradeNo string, result *PaymentResult, err error) { attrs := []any{"provider", provider, "trade_no", tradeNo, "duration_ms", time.Since(started).Milliseconds()} if result != nil { attrs = append(attrs, "provider_trade_no", result.ProviderTradeNo, "provider_status", result.Status, "order_status", result.OrderStatus, "fulfillment_status", result.FulfillmentStatus, "refund_status", result.RefundStatus, "duplicate", result.Duplicate) } if err != nil { uc.logger.Error(ctx, "payment."+operation+".failed", append(attrs, "error_code", paymentErrorCode(err))...) return } uc.logger.Info(ctx, "payment."+operation+".completed", attrs...) }