Skip to content

MAE-1319: Add ArrayFieldFilter for GoCardless webhook routing#14

Open
erawat wants to merge 1 commit into
masterfrom
CIVIMM-511-gocardless-array-filter
Open

MAE-1319: Add ArrayFieldFilter for GoCardless webhook routing#14
erawat wants to merge 1 commit into
masterfrom
CIVIMM-511-gocardless-array-filter

Conversation

@erawat
Copy link
Copy Markdown
Member

@erawat erawat commented May 12, 2026

Overview

Add a new ArrayFieldFilter strategy to support GoCardless webhook routing via Svix. GoCardless webhooks contain an events array where each event has a nested links.organisation field — unlike Stripe where account is a top-level field. This filter iterates the array and matches if ANY item contains the target organisation, forwarding the full payload to the destination.

Before

Only SimpleFieldFilter existed, which checks a single top-level field. This works for Stripe (account) but not for GoCardless's array-based multi-organisation payloads. The middleware hardcoded buildRoutingFilter() for all processor types.

After

  • New ArrayFieldFilter generates JavaScript that iterates the events array with null-safe nested field access
  • SvixProcessorConfig::getFilterStrategy() returns the correct filter strategy per processor type (Stripe → SimpleFieldFilter, GoCardless → ArrayFieldFilter)
  • SvixWebhookMiddleware uses the config-driven filter strategy instead of the hardcoded buildRoutingFilter()
  • escapeJsString() extracted into shared EscapesJsStrings trait used by both filters

Technical Details

New files:

  • Civi/Svixclient/Filter/ArrayFieldFilter.php — Implements FilterStrategyInterface. Generates JS that iterates an array field, checks nested paths with null-safe guards, and returns the webhook on first match
  • Civi/Svixclient/Filter/EscapesJsStrings.php — Shared trait for JS string escaping (extracted from SimpleFieldFilter)
  • tests/phpunit/Civi/Svixclient/Filter/ArrayFieldFilterTest.php — 13 test cases covering JS generation, escaping, nested paths, and SvixProcessorConfig integration

Modified files:

  • Civi/Svixclient/Filter/SimpleFieldFilter.php — Uses EscapesJsStrings trait instead of private method
  • Civi/Svixclient/Enum/SvixProcessorConfig.php — New getFilterStrategy() method
  • Civi/Svixclient/Service/SvixWebhookMiddleware.php — Line 253: buildRoutingFilter()buildFilter($config->getFilterStrategy())

Generated JavaScript example (GoCardless):

function handler(webhook) {
    var items = webhook.payload.events || [];
    for (var i = 0; i < items.length; i++) {
        if (items[i].links && items[i].links.organisation === 'OR000123') {
            return webhook;
        }
    }
    webhook.cancel = true;
    return webhook;
}

Core overrides

N/A

Comments

  • Part of the GoCardless → Svix migration epic (CIVIMM-506)
  • No changes to existing Stripe behaviour — SimpleFieldFilter is functionally unchanged
  • The buildRoutingFilter() static method on CRM_Svixclient_Client still exists for backward compatibility but is no longer used internally

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request implements a flexible filtering mechanism for Svix webhook routing to accommodate GoCardless payloads. By moving from a hardcoded filtering approach to a strategy-based pattern, the system can now correctly handle both simple top-level field matching (Stripe) and complex array-based nested field matching (GoCardless).

Highlights

  • New ArrayFieldFilter Strategy: Introduced a new filtering strategy to support GoCardless webhooks, which require iterating through an 'events' array to match nested organisation IDs.
  • Refactored JavaScript Escaping: Extracted JavaScript string escaping logic into a shared 'EscapesJsStrings' trait to ensure consistency across different filter strategies.
  • Config-Driven Routing: Updated 'SvixWebhookMiddleware' to dynamically select the appropriate filter strategy based on the processor type, replacing the previous hardcoded approach.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a strategy pattern for webhook filtering by adding an ArrayFieldFilter class, specifically designed to handle payloads with arrays of events such as those from GoCardless. It refactors JavaScript string escaping into a shared trait and updates the SvixProcessorConfig enum to provide processor-specific filter strategies. The review feedback identifies opportunities to improve the robustness of the generated JavaScript by validating input paths and ensuring null-safety for nested array accessors.

Comment thread Civi/Svixclient/Filter/ArrayFieldFilter.php
Comment thread Civi/Svixclient/Filter/ArrayFieldFilter.php
Comment thread Civi/Svixclient/Filter/ArrayFieldFilter.php
@erawat erawat changed the title CIVIMM-511: Add ArrayFieldFilter for GoCardless webhook routing MAE-1319: Add ArrayFieldFilter for GoCardless webhook routing May 13, 2026
GoCardless webhooks contain an events array where each event has a
nested links.organisation field. Unlike Stripe where account is a
top-level field, a single GoCardless webhook can contain events for
multiple organisations. The new ArrayFieldFilter iterates the array
and matches if ANY item contains the target organisation.

- Extract escapeJsString() into shared EscapesJsStrings trait
- Add ArrayFieldFilter implementing FilterStrategyInterface
- Add getFilterStrategy() to SvixProcessorConfig enum
- Update SvixWebhookMiddleware to use config-driven filter strategy
@erawat erawat force-pushed the CIVIMM-511-gocardless-array-filter branch from 187b90b to 3eba010 Compare May 13, 2026 08:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants