package order import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strings" "time" "git.k6n.net/mats/go-cart-actor/pkg/flow" ) type fulfillmentWebhookParams struct { URL string `json:"url"` Method string `json:"method,omitempty"` Headers map[string]string `json:"headers,omitempty"` ContentType string `json:"contentType,omitempty"` } // RegisterFulfillmentWebhookHook registers the "fulfillment_webhook" flow hook. // It posts the current order + latest fulfillment to an external integration, // with templated URL/headers so flows can target dropship or ERP adapters // without custom Go code. Params: // // { // "url": "https://example.test/hooks/{{ .OrderID }}", // "method": "POST", // "headers": { // "X-Integration": "dropship", // "Authorization": "Bearer {{ index .Vars \"token\" }}" // } // } func RegisterFulfillmentWebhookHook(reg *flow.Registry, app Applier, client *http.Client) { if client == nil { client = &http.Client{Timeout: 10 * time.Second} } reg.HookWithMeta("fulfillment_webhook", func(ctx context.Context, st *flow.State, info flow.HookInfo, params json.RawMessage) error { var p fulfillmentWebhookParams if err := json.Unmarshal(params, &p); err != nil { return fmt.Errorf("fulfillment_webhook: bad params: %w", err) } if strings.TrimSpace(p.URL) == "" { return fmt.Errorf("fulfillment_webhook: missing url") } method := strings.ToUpper(strings.TrimSpace(p.Method)) if method == "" { method = http.MethodPost } if p.ContentType == "" { p.ContentType = "application/json" } o, err := app.Get(ctx, st.ID) if err != nil { return err } data := buildFlowTemplateData(o, st, info) if data.Fulfillment == nil { return fmt.Errorf("fulfillment_webhook: order %s has no fulfillment yet", data.OrderID) } url, err := renderTextTemplate("fulfillment-webhook-url", p.URL, data) if err != nil { return fmt.Errorf("fulfillment_webhook: render url: %w", err) } body, err := json.Marshal(map[string]any{ "orderId": data.OrderID, "step": data.Step, "phase": data.Phase, "error": data.Error, "order": data.Order, "fulfillment": data.Fulfillment, "billingAddress": data.BillingAddress, "shippingAddress": data.ShippingAddress, "vars": data.Vars, }) if err != nil { return fmt.Errorf("fulfillment_webhook: encode body: %w", err) } req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body)) if err != nil { return fmt.Errorf("fulfillment_webhook: build request: %w", err) } req.Header.Set("Content-Type", p.ContentType) for key, raw := range p.Headers { value, err := renderTextTemplate("fulfillment-webhook-header-"+key, raw, data) if err != nil { return fmt.Errorf("fulfillment_webhook: render header %s: %w", key, err) } req.Header.Set(key, value) } resp, err := client.Do(req) if err != nil { return fmt.Errorf("fulfillment_webhook: post %s: %w", url, err) } defer resp.Body.Close() if resp.StatusCode >= 300 { return fmt.Errorf("fulfillment_webhook: %s returned %d", url, resp.StatusCode) } return nil }, flow.CapabilityMeta{ Description: "POST the order and latest fulfillment to an external dropship or ERP endpoint.", ExampleParams: json.RawMessage(`{"url":"https://dropship.example/hooks/{{ .OrderID }}","method":"POST","headers":{"X-Integration":"dropship {{ .Order.OrderReference }}","Authorization":"Bearer {{ index .Vars \"token\" }}"}}`), }) }