package intelligence_finance import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" ) // Default API paths - these can be customized after receiving the actual short codes. const ( defaultPathInvoiceCreate = "/api/invoice/create" defaultPathInvoiceStatusQuery = "/api/invoice/status" defaultPathPaymentCreate = "/api/payment/create" defaultPathPaymentStatusQuery = "/api/payment/status" ) // Client is the intelligence finance API client. type Client struct { baseURL string tenantID string clientID string clientSecret string httpClient *http.Client // Paths for each API endpoint (configurable after onboarding). pathInvoiceCreate string pathInvoiceStatusQuery string pathPaymentCreate string pathPaymentStatusQuery string } // ClientOption is a functional option for configuring the Client. type ClientOption func(*Client) // WithHTTPClient sets a custom HTTP client. func WithHTTPClient(httpClient *http.Client) ClientOption { return func(c *Client) { c.httpClient = httpClient } } // WithInvoiceCreatePath sets a custom path for the invoice create API. func WithInvoiceCreatePath(path string) ClientOption { return func(c *Client) { c.pathInvoiceCreate = path } } // WithInvoiceStatusQueryPath sets a custom path for the invoice status query API. func WithInvoiceStatusQueryPath(path string) ClientOption { return func(c *Client) { c.pathInvoiceStatusQuery = path } } // WithPaymentCreatePath sets a custom path for the payment create API. func WithPaymentCreatePath(path string) ClientOption { return func(c *Client) { c.pathPaymentCreate = path } } // WithPaymentStatusQueryPath sets a custom path for the payment status query API. func WithPaymentStatusQueryPath(path string) ClientOption { return func(c *Client) { c.pathPaymentStatusQuery = path } } // NewClient creates a new intelligence finance API client. // - baseURL: the base URL of the platform API (e.g., "https://api.example.com") // - tenantID: the tenant ID assigned by the platform // - clientID: the application client ID assigned by the platform // - clientSecret: the application client secret assigned by the platform func NewClient(baseURL, tenantID, clientID, clientSecret string, opts ...ClientOption) *Client { c := &Client{ baseURL: strings.TrimRight(baseURL, "/"), tenantID: tenantID, clientID: clientID, clientSecret: clientSecret, httpClient: &http.Client{}, pathInvoiceCreate: defaultPathInvoiceCreate, pathInvoiceStatusQuery: defaultPathInvoiceStatusQuery, pathPaymentCreate: defaultPathPaymentCreate, pathPaymentStatusQuery: defaultPathPaymentStatusQuery, } for _, opt := range opts { opt(c) } return c } // doRequest sends an HTTP request with authentication headers and returns the raw response body. func (c *Client) doRequest(ctx context.Context, path string, request interface{}) ([]byte, error) { bodyBytes, err := json.Marshal(request) if err != nil { return nil, &ErrSigning{Message: "marshal request body", Err: err} } req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(bodyBytes)) if err != nil { return nil, &ErrNetwork{Err: fmt.Errorf("create request: %w", err)} } // Set standard headers req.Header.Set("Content-Type", "application/json") req.Header.Set("tenant-id", c.tenantID) req.Header.Set("client-id", c.clientID) // Generate and set signature headers timestamp := GenerateTimestamp() nonce, err := GenerateNonce(16) if err != nil { return nil, &ErrSigning{Message: "generate nonce", Err: err} } signature := HmacSHA256Base64(c.clientSecret, timestamp+nonce) req.Header.Set("x-bfl-signature-timestamp", timestamp) req.Header.Set("x-bfl-signature-nonce", nonce) req.Header.Set("x-bfl-signature", signature) resp, err := c.httpClient.Do(req) if err != nil { return nil, &ErrNetwork{Err: fmt.Errorf("send request: %w", err)} } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, &ErrNetwork{Err: fmt.Errorf("read response body: %w", err)} } if resp.StatusCode != http.StatusOK { return nil, &ErrInvalidResponse{ Message: fmt.Sprintf("unexpected status code %d: %s", resp.StatusCode, string(respBody)), } } return respBody, nil } // parseResponse parses the API response and extracts the data field. func parseResponse(body []byte, target interface{}) error { var apiResp APIResponse if err := json.Unmarshal(body, &apiResp); err != nil { return &ErrInvalidResponse{Message: fmt.Sprintf("unmarshal response: %v", err)} } if apiResp.Code != 0 { return &APIError{Code: ErrorCode(apiResp.Code), Message: apiResp.Msg} } if target != nil && apiResp.Data != nil { // Convert map to JSON then unmarshal into target dataBytes, err := json.Marshal(apiResp.Data) if err != nil { return &ErrInvalidResponse{Message: fmt.Sprintf("marshal data: %v", err)} } if err := json.Unmarshal(dataBytes, target); err != nil { return &ErrInvalidResponse{Message: fmt.Sprintf("unmarshal data: %v", err)} } } return nil } // parseResponseRaw parses the API response and returns the raw data field. func parseResponseRaw(body []byte) (map[string]interface{}, error) { var apiResp APIResponse if err := json.Unmarshal(body, &apiResp); err != nil { return nil, &ErrInvalidResponse{Message: fmt.Sprintf("unmarshal response: %v", err)} } if apiResp.Code != 0 { return nil, &APIError{Code: ErrorCode(apiResp.Code), Message: apiResp.Msg} } return apiResp.Data, nil } // ============================================================ // Interface 1: CreateInvoice - 订单开票接口 // ============================================================ // CreateInvoice submits an invoice creation request for an order. // It returns the invoice creation response containing status and invoice data. func (c *Client) CreateInvoice(ctx context.Context, req *InvoiceCreateRequest) (*InvoiceCreateResponse, error) { body, err := c.doRequest(ctx, c.pathInvoiceCreate, req) if err != nil { return nil, err } var result InvoiceCreateResponse if err := parseResponse(body, &result); err != nil { return nil, err } return &result, nil } // ============================================================ // Interface 2: QueryInvoiceStatus - 开票状态查询接口 // ============================================================ // QueryInvoiceStatus queries the invoice status for a given order. // It returns the invoice status response containing the current status and invoice data. func (c *Client) QueryInvoiceStatus(ctx context.Context, req *InvoiceStatusQueryRequest) (*InvoiceStatusQueryResponse, error) { body, err := c.doRequest(ctx, c.pathInvoiceStatusQuery, req) if err != nil { return nil, err } var result InvoiceStatusQueryResponse if err := parseResponse(body, &result); err != nil { return nil, err } return &result, nil } // ============================================================ // Interface 3: CreatePaymentOrder - 创建付款单据接口 // ============================================================ // CreatePaymentOrder creates a payment order. // It returns the response containing the unique payment order code. func (c *Client) CreatePaymentOrder(ctx context.Context, req *CreatePaymentOrderRequest) (*CreatePaymentOrderResponse, error) { body, err := c.doRequest(ctx, c.pathPaymentCreate, req) if err != nil { return nil, err } var result CreatePaymentOrderResponse if err := parseResponse(body, &result); err != nil { return nil, err } return &result, nil } // ============================================================ // Interface 4: Payment Notification Handler (平台回调客户接口) // ============================================================ // ParsePaymentNotification parses a payment notification request body // sent by the platform. This is used when the platform calls back to // the customer's system after a payment is completed. // // Usage example in an HTTP handler: // // func handler(w http.ResponseWriter, r *http.Request) { // body, _ := io.ReadAll(r.Body) // notification, err := ParsePaymentNotification(body) // if err != nil { // http.Error(w, "FAILED", http.StatusBadRequest) // return // } // // Process the notification... // w.Write([]byte("SUCCESS")) // } func ParsePaymentNotification(body []byte) (*PaymentNotificationData, error) { var notification PaymentNotificationData if err := json.Unmarshal(body, ¬ification); err != nil { return nil, &ErrInvalidResponse{Message: fmt.Sprintf("parse payment notification: %v", err)} } return ¬ification, nil } // ParseGenericNotification parses a generic notification request body // sent by the platform. The generic notification contains bizType, bizId, and data. func ParseGenericNotification(body []byte) (*NotificationRequest, error) { var notification NotificationRequest if err := json.Unmarshal(body, ¬ification); err != nil { return nil, &ErrInvalidResponse{Message: fmt.Sprintf("parse notification: %v", err)} } return ¬ification, nil } // ============================================================ // Interface 5: QueryPaymentStatus - 支付状态查询接口 // ============================================================ // QueryPaymentStatus queries the payment status for a given payment order. // The response has the same structure as PaymentNotificationData. func (c *Client) QueryPaymentStatus(ctx context.Context, req *PaymentStatusQueryRequest) (*PaymentStatusQueryResponse, error) { body, err := c.doRequest(ctx, c.pathPaymentStatusQuery, req) if err != nil { return nil, err } var result PaymentStatusQueryResponse if err := parseResponse(body, &result); err != nil { return nil, err } return &result, nil }