Skip to content

Service Layer

Ignition Crowdfunding extracts business logic into service classes that can be called directly from PHP. These are the same methods used internally by the REST API and Abilities API.

Any service method that aggregates across campaigns requires a scope argument saying which side of the Live/Test divide it should count. There is no default. A query that omits it triggers _doing_it_wrong() and returns the empty shape rather than a number.

That is deliberate. A silent fallback to 'live' would hand you a plausible wrong figure, and a wrong revenue number is the kind of thing that ends up pasted into a board deck. A visibly empty screen gets reported.

Value Means
'live' Live pledges only.
'test' Test pledges only.
'unscoped' Both, deliberately. Say so explicitly — there is no 'all', because 'all' reads like a default rather than a decision.

Scoping runs off a durable mode column stamped onto each pledge row when it is created, not off the parent campaign’s current status. A pledge therefore keeps the side it was written on even after its campaign is permanently deleted.

Where the value comes from. Admin screens call Igcf_View_Mode::from_request() once and pass the result down as scope. Services never resolve the mode themselves — they are given it. Anything outside wp-admin (cron, webhooks, a public template) has no view mode and should pass an explicit 'live' or 'unscoped'.

When you can leave it out. A query already narrowed to one campaign or one pledge is single-sided by construction, so campaign_id, campaign_ids, pledge_id or payment_attempt_id excuses you from supplying scope. Passing an invalid value is still refused, narrowed or not.

Money paths take no scope at all. Igcf_Refund_Service::query() and Igcf_Payment_Attempt_Service::query() have no scope parameter, by design. Their callers are single-record gateway lookups where “which set?” is a meaningless question — and a viewer’s chosen view must never be able to reach a decision about real money.

// Cross-campaign: scope required.
$stats = Igcf_Pledge_Service::get_payment_method_stats( array( 'scope' => Igcf_View_Mode::from_request() ) );
// Narrowed to one campaign: scope optional.
$rows = Igcf_Pledge_Service::query( array( 'campaign_id' => $campaign_id ) );

Handles campaign creation, updates, deletion, and state transitions.

Method Parameters Returns
create( array $data ) title, description, excerpt, status, author_id, goal, funding_model, start_date, end_date, featured_image_id, categories, tags, is_test (bool — admin only; promotes the new campaign to igcf_test with the [TEST] title prefix). int|WP_Error — Campaign ID on success. Returns WP_Error('igcf_test_capability', …) if is_test is set without edit_others_igcf_campaigns.
update( int $campaign_id, array $data ) Same keys as create(), all optional. int|WP_Error — Campaign ID on success.
delete( int $campaign_id, bool $force = false ) $force: true to permanently delete, false to trash. true|WP_Error
transition_status( int $campaign_id, string $new_status, int $actor_id = 0, array $args = [] ) $new_status: the target campaign status slug (e.g. igcf_active, igcf_paused, igcf_ended, igcf_closed, or draft to withdraw). Validated against the state machine from get_transitions() — an edge that isn’t registered returns WP_Error('igcf_invalid_transition'). $args: optional, e.g. ['note' => '...'] (falls back to ['reason' => '...']) for the audit log. array|WP_Error — ['status' => '...', 'status_label' => '...'] on success. Add-ons that register extra statuses/edges (such as the moderation statuses added by Ignition Platform Mode) can drive to them through this same method.
close( int $campaign_id, int $actor_id = 0 ) Closes an Ended campaign through transition_status() and writes the status-history note itself: Closed after the result was declared. or Closed without a recorded outcome. The note is decided under the same row lock as the status change, so a result declared at the same moment can’t leave a wrong note. $actor_id defaults to the current user. Use this rather than transition_status( $id, 'igcf_closed' ) so every route to Closed records the same thing. array|WP_Error — the transition_status() result plus message.
declare_result( int $campaign_id, int $actor_id = 0 ) Declares the outcome of an Ended campaign. The verdict is computed server-side (not supplied). Stamps _igcf_result, _igcf_result_declared_at, _igcf_result_declared_by, and fires igcf_campaign_result_declared. Idempotent — a second call returns WP_Error('igcf_already_declared'). Does not change post_status. array|WP_Error — ['result' => 'successful'|'failed'|'funded', 'message' => '...']. WP_Error('igcf_not_ended') when the campaign is not Ended.
announce_goal_reached( int $campaign_id, int $actor_id = 0 ) Announces a reached goal on an Active campaign. Stamps _igcf_goal_announced and fires igcf_campaign_goal_announced. array|WP_Error — ['message' => '...', 'date' => '...'].
compute_result( int $campaign_id ) Computes the verdict without writing anything: 'funded' for the flexible model, otherwise 'successful' / 'failed' from the collected amount against the goal. string

These helpers formalize the campaign content-visibility contract — who may see a campaign’s content (description, tiers, pledge options, backer list) before it is public. Display modules and the backers REST endpoint gate on them; templates and add-ons can too.

Function Returns Description
igcf_is_campaign_previewer( $campaign, $user = null ) bool Whether $user (default: current user) may preview the campaign. Holders of edit_others_igcf_campaigns and the campaign’s author qualify; anonymous visitors never do.
igcf_campaign_content_visible( $campaign ) bool Whether the campaign’s content is visible to the current user: true for the public statuses (igcf_preview, igcf_active, igcf_ended) and for previewers on any status.
Igcf_Campaign_Post_Type::is_pre_publication_status( string $status ) bool Whether $status is pre-publication (draft, igcf_pending_review, igcf_rejected, igcf_preview). Pre-publication campaigns whose content is visible render their pledge surfaces display-only; halted and terminal statuses show a not-accepting-pledges notice instead. The igcf_pending_review and igcf_rejected statuses are registered only when Ignition Platform Mode is active, so a direct-mode install never sees them here.

Isolation layer for test campaigns. Test campaigns use the igcf_test post status, carry the [TEST] title prefix, are publish-locked, and are excluded from public archive queries. This class also owns the SQL fragment behind query scope. See the Test Campaigns user guide for the feature overview.

Method Parameters Returns
is_test( int $campaign_id ) — bool — Whether the campaign sits at the igcf_test post status. The status is the single source of truth for test-ness.
get_test_campaign_ids() — int[] — IDs of every test campaign. Cached in a transient that is invalidated on status transitions and deletes.
campaign_scope_clause( string $scope, string $alias = '' ) $scope: 'live', 'test' or 'unscoped'. $alias: column prefix, e.g. 'p.'. string — " AND p.mode = 'live'", the 'test' equivalent, or '' for 'unscoped'. Reads the durable pledge stamp. Throws InvalidArgumentException on any other value.
is_valid_scope( $scope ) — bool — Whether the value is one of the three accepted scopes.
scope_unusable( array $args ) Query args. bool — True when scope is invalid, or absent where it is required. False when a narrowing key (campaign_id, campaign_ids, pledge_id, payment_attempt_id) already pins the query to one side. Use it to guard your own cross-campaign queries the same way core does.
pledge_exclusion_clause( string $column = 'campaign_id' ) $column: SQL column name to compare against. array — ['sql' => '…', 'params' => [int, …]] for splicing into a $wpdb->prepare() call. Excludes test-campaign rows by parent id. Prefer campaign_scope_clause() for anything counting pledges — the stamp survives the campaign being deleted, the id list does not.
strip_marker( string $text ) $text: any string. string — $text with a leading [TEST] removed if present.
prefix_with_marker( string $text ) $text: any string. string — $text prefixed with [TEST] if it isn’t already. Used for email subjects and other places the marker should appear.
promote( int $post_id ) $post_id: campaign to promote to test status. true|WP_Error — Ensures the [TEST] title prefix, then transitions the post to the igcf_test status. The status is the sole marker of test-ness; there is no companion meta flag.

All methods are public static (except the singleton-bound invalidate_cache* and enforce_publish_lock handlers, which are not part of the API surface).

Stateless utility class for reward level CRUD operations.

Method Parameters Returns
create( int $campaign_id, array $args ) title (required), description, amount (minor units, required), quantity_limit (default 0 = unlimited), one_per_backer (default false), requires_shipping (default false — when true, backers pledging for this tier must provide a shipping address), image_id, menu_order int|WP_Error — Reward level post ID on success.
update( int $reward_level_id, array $args ) Same keys as create(), all optional. int|WP_Error — Reward level post ID on success.
delete( int $reward_level_id ) — true|WP_Error
clone_for_campaign( int $source_campaign_id, int $target_campaign_id ) Clones every published reward level from $source_campaign_id onto $target_campaign_id. Copies post fields, the lifecycle meta keys (_igcf_amount, _igcf_quantity_limit, _igcf_one_per_backer, _igcf_requires_shipping, _igcf_active), and featured image. Resets _igcf_quantity_claimed to 0 on each clone. Inactive source tiers stay inactive. Per-tier insertion failures are skipped; the remaining tiers still clone. int — Number of reward levels successfully cloned.
deactivate( int $reward_level_id ) — Soft-deactivates by flipping _igcf_active to 0. bool
get_active_by_campaign( int $campaign_id ) — WP_Post[] — Active reward levels for the campaign, ordered by menu_order.
recalculate_quantity( int $reward_level_id ) — Recomputes _igcf_quantity_claimed from current pledges. int — The new claimed count.
is_sold_out( int $reward_level_id ) — bool

All methods are public static.

Handles campaign update CRUD and the update status machine.

Method Parameters Returns
create( int $campaign_id, array $data ) title (required), content, image_id, author_id int|WP_Error — Update ID on success. The initial status is resolved through the igcf_determine_update_status filter (core default 'publish').
update( int $update_id, int $campaign_id, array $data ) title (required), content, image_id int|WP_Error — Update ID on success.
delete( int $update_id, int $campaign_id ) — true|WP_Error
moderate( int $update_id, string $action, array $args = [] ) $action: approve (publish), reject (transition to igcf_rejected), or delete. $args: optional, e.g. ['reason' => '...'] for reject. array|WP_Error — ['status' => '...', 'status_label' => '...'] on success. The igcf_rejected update status is registered only when Ignition Platform Mode is active, so reject is meaningful in platform mode; the add-on drives this seam and owns the rejection-reason lifecycle.

Handles pledge creation, querying, status transitions, and campaign total recalculation.

Method Parameters Returns
create( int $campaign_id, int $backer_id, int $amount, array $args = [] ) $campaign_id: campaign post ID. $backer_id: backer user ID. $amount: amount in minor units. $args: optional currency (default site currency), payment_method (default 'offline'), and shipping_address (associative array keyed by shipping schema field key; JSON-encoded and stored in the shipping_address column when supplied). int|WP_Error — Pledge ID on success.
get( int $pledge_id ) — Igcf_Pledge|null — Pledge object or null if not found.
query( array $args = [] ) scope (required) unless narrowed by campaign_id / pledge_id. Plus campaign_id, backer_id, status (string or array), date_query (after/before), amount_min, amount_max, search, orderby (default 'created_at'), order (default 'DESC'), per_page (default 20, max 100), page (default 1). array — ['pledges' => Igcf_Pledge[], 'total' => int, 'pages' => int].
query_backer_aggregates( array $args = [] ) scope (required) unless narrowed by campaign_id. Plus campaign_id, date_query, search. The status arg is intentionally ignored — per-status counts and totals appear as separate columns. stdClass[] — Rows grouped by (backer_id, currency) with pending_count, pending_total, succeeded_count, succeeded_total, canceled_count, canceled_total, refunded_count, refunded_total, a net figure, first_pledge_at, last_pledge_at, campaign_count.
build_args_from_request( array $accept = ['status','campaign_id','m','s'] ) $accept: filter keys to honor; defaults to all four. Pass a subset to drop a key (e.g., ['campaign_id','m','s'] to ignore the status filter). array — Query args derived from $_GET. Caller is responsible for verifying the request (nonce + capability) before invoking.
get_statuses() — array — Associative array of status_key => label. Default: pending, succeeded, canceled.
get_status_label( string $status ) — string — Translated label, or the raw status if unrecognized.
get_transitions() — array — ['pending' => ['succeeded', 'canceled'], 'succeeded' => [], 'canceled' => []]. Both targets are terminal.
transition_status( int $pledge_id, string $new_status, int $actor_id, string $note = '' ) $pledge_id: pledge ID. $new_status: target status. $actor_id: user performing the transition. $note: optional audit log note. array|WP_Error — ['status' => '...', 'statusLabel' => '...', 'message' => '...'] on success.
get_status_log( int $pledge_id ) — array — Array of log entry objects ordered by created_at ASC.
get_combined_status_log( int $pledge_id ) — stdClass[] — Interleaved pledge / payment-attempt / refund log entries, newest first. Powers the admin pledge-detail history.
get_backer_visible_status_log( int $pledge_id ) — array — The backer-facing log: attempt rows are dropped and succeeded refunds surface as “Refund issued: $X” entries.
cancel_reasons() — array<string,string> — The cancel-reason registry (igcf_pledge_cancel_reasons).
displayed_status( Igcf_Pledge $pledge ) — string — The render-time status folding in refunds: pending / succeeded / canceled / partial_refund / refunded.
get_displayed_status_label( string $displayed_status ) — string — Translated label for a displayed status.
refund_indicator_label( string $displayed_status ) — string — "Full Refund", "Partial Refund", or "". Single source for the frontend chip, the wp-admin Refund column, and the CSV Refund column.
status_to_semantic( string $status ) — string — success / warning / neutral / error for badge colouring.
status_badge_view( string $displayed_status, string $raw_status ) — array{label, semantic, chip_label} — The lifecycle badge plus optional refund chip. Maps both refunded and partial_refund to a succeeded badge + chip.
compute_refund_display( string $status, int $gross, int $refunded ) Query-free given the gross succeeded and refunded sums. array{displayed_status, has_refund, is_full_refund, gross, refunded, net}.
get_pledge_refund_display( Igcf_Pledge $pledge ) Refines net through igcf_pledge_net_amounts, so the figure matches the list table and exports. Use for one pledge on an admin or creator surface. Same shape as compute_refund_display(), resolving the sums for one pledge.
get_pledge_refund_amounts( Igcf_Pledge $pledge ) The same sums with no net seam applied — net stays the backer’s own gross minus refunded. Use for backer-facing surfaces, where a platform’s deduction from the creator must never change the figure the backer sees. compute_refund_display()’s shape plus refunded_pledge_share: the pledge-space share of those refunds, clamped to the pledge amount. On a charge carrying voluntary additions a refund returns some money that was never pledge money, so the share is what a campaign total should subtract — refunded is what left the card. 0 for a non-succeeded pledge and for a succeeded one with no refund, and only the refunded case pays for the extra query.
count_by_status( array $args = [] ) scope (required) for a sitewide count; pass campaign_id instead to count one campaign. array — Associative array of status => count.
count_fully_refunded( ?int $campaign_id = null ) — int — Number of fully-refunded pledges (sitewide when null).
get_net_for_pledges( array $pledge_ids ) — array<int,int> — Net (succeeded attempts minus succeeded refunds) keyed by pledge id, refined through igcf_pledge_net_amounts. Pledges with no succeeded attempt are absent. May be negative once a gateway reports a processing fee. Powers the Net column.
get_processing_fees_for_pledges( array $pledge_ids ) The fee the payment processor kept, per pledge, in one batched call. Core records none of its own, so the unfiltered map is empty; the active gateway answers igcf_pledge_processing_fees with the same figure it subtracts from net, reported here as its own term. array<int,int> — Keyed by pledge id, covering only pledges a gateway had a settled figure for. Absence means “never recorded” and is not a reported zero — render an empty cell for absence, 0.00 for a present zero.
get_payment_method_stats( array $args = [] ) scope (required). array<string, array{count:int, amount:int}> — Pledge count and total amount sitewide grouped by payment_method. Canceled pledges are excluded. Drives the Pledges column on the Payments admin tab.
recalculate_totals( int $campaign_id ) — void — Recomputes the campaign snapshot meta: _igcf_amount_collected (gross succeeded total — the goal/verdict figure), _igcf_amount_pledged (the public display figure, net of refunds), _igcf_pledge_count, and _igcf_backer_count. Fires igcf_campaign_totals_recalculated.
find_expirable_pledges( int $limit = 50 ) Oldest-first pending pledges on a non-durable payment method, with no succeeded attempt, idle past the igcf_pledge_expiry_minutes window. Batch-capped by $limit. Igcf_Pledge[]
expire_abandoned_pledges( int $limit = 50 ) Cancels each expirable pledge — fails its latest attempt, then cancels the pledge with reason payment_abandoned (system actor 0) — and fires igcf_pledge_expired. This is the igcf_expire_abandoned_pledges hourly cron callback. int — number of pledges expired.

Read-only data object representing a pledge database row. Constructed from $wpdb->get_row().

Property Type Description
$id int Pledge ID.
$campaign_id int Campaign post ID.
$backer_id int Backer user ID.
$amount int Amount in minor currency units (e.g., 2500 for $25.00).
$currency string Three-letter currency code (e.g., "USD").
$payment_method string The backer’s intended payment method (e.g., "offline", "stripe"). Actual transactions live on Igcf_Payment_Attempt.
$status string Lifecycle status slug: pending, succeeded, canceled.
$cancel_reason string|null Reason recorded when the pledge was canceled (from the igcf_pledge_cancel_reasons registry), or null.
$created_at string Creation timestamp in MySQL datetime format.
$updated_at string Last update timestamp in MySQL datetime format.
$shipping_address string|null Raw JSON-encoded shipping address for the pledge, or null when the pledge does not require shipping. Decode via Igcf_Shipping::get_decoded( $pledge ) so the igcf_pledge_shipping_address filter applies.
pending → succeeded
pending → canceled

Both succeeded and canceled are terminal. A pledge moves to succeeded when one of its payment attempts succeeds (via the igcf_payment_attempt_succeeded listener) — never by a direct status write. A succeeded pledge stays succeeded after a refund; the refund is a separate record (see Igcf_Refund_Service).

All methods are public static and can be called without instantiation. All methods fire the corresponding action hooks documented in the actions reference.

Handles payment attempts — one row per try at collecting money for a pledge. A succeeding attempt is what drives its parent pledge to succeeded; gateway add-ons translate their events into transition_status() calls here rather than touching pledge status. Follows the same static-method, three-state-machine, audit-log pattern as Igcf_Pledge_Service.

Method Parameters Returns
create( int $pledge_id, string $payment_method, int $amount, array $args = [] ) $amount in minor units (the transaction total, which may exceed the pledge amount under fee recovery). int|WP_Error — Attempt ID.
get( int $id ) — Igcf_Payment_Attempt|null
transition_status( int $attempt_id, string $new_status, int $actor_id, array $args = [] ) $new_status: succeeded or failed. array|WP_Error
get_status_log( int $attempt_id ) — array
query( array $args = [] ) — array{attempts, total, pages}
get_for_pledge( int $pledge_id ) / get_for_pledges( array $pledge_ids ) / latest_for_pledge( int $pledge_id ) — Attempts for one or many pledges.
get_statuses() / get_status_label( string $status ) / get_transitions() — Status registry (pending, succeeded, failed) and transitions map.
failure_codes() — array<string,string> — The igcf_payment_attempt_failure_codes registry.
get_application_fee_components( Igcf_Payment_Attempt $attempt ) The platform-fee cut broken into named parts (minor units) for a recorded attempt — e.g. commission from Platform Fees, tip from Contributions. Wraps the igcf_payment_attempt_application_fee_components filter; each add-on contributes one distinct key, so callback priority is cosmetic. Keys are normalised through sanitize_key(), values clamped to max( 0, (int) ); the sum is what get_application_fee() returns. Empty on a core-only install. array<string,int>
get_application_fee( Igcf_Payment_Attempt $attempt ) Applies igcf_payment_attempt_application_fee to the sum of the named components above (default 0; core does not consume the result — it’s a seam for add-ons). int
projected_charge_context( array $context ) Attempt-free projection for checkout: what a hypothetical charge on a campaign would look like before any pledge or Igcf_Payment_Attempt row exists. $context carries campaign_id, pledge_amount (minor units), payment_method, and currency. Wraps igcf_projected_charge_context — a gateway add-on sets settles_application_fee at a low priority, then Platform Fees adds components['commission'] at a higher priority (priority is load-bearing here). Components are normalised as above. array{settles_application_fee:bool, components:array<string,int>}
get_processing_fee( Igcf_Payment_Attempt $attempt ) What the payment processor charged for this attempt, as reported by the gateway that took it. Applies igcf_payment_attempt_processing_fee (default 0, clamped to max( 0, (int) )). Exactly one gateway answers per attempt. Core consumes this: it is subtracted from the get_net_to_creator() seed. int
get_net_to_creator( Igcf_Payment_Attempt $attempt ) Applies igcf_payment_attempt_net_to_creator to a seed of the attempt amount minus get_processing_fee(). Since 0.22.0 the result is no longer floored at zero and may be negative — a fully refunded card payment leaves it under zero because the processor keeps its fee. It is computed before refunds: a display figure, not a payout basis. int
settles_application_fee( Igcf_Payment_Attempt $attempt ) Applies igcf_payment_attempt_settles_application_fee (default false). Answers whether a gateway will actually settle this attempt’s application fee at capture — a fact about the gateway handling the attempt, not about whether a fee is configured. Use this rather than calling the filter directly. bool
has_operator_custody( Igcf_Payment_Attempt $attempt ) Whether the operator held this payment’s money, as stamped on the row at create time. Reads the recorded column only — no filter — so changing the site’s routing setting later never moves the answer for an existing payment. bool
operator_still_holds( Igcf_Payment_Attempt $attempt ) Whether the operator is holding it right now. Applies igcf_payment_attempt_operator_still_holds, defaulting to the stamp above, so a core-only install reads exactly what was recorded. An add-on that records payouts answers false once a payment has been paid out. bool
operator_custody_per_attempt( array $attempt_ids ) Batched form of has_operator_custody(), in one query. Only requested, positive ids appear; an id with no row is absent (treat absence as false). Empty input runs no query. array<int,bool>

All methods are public static.

igcf_payment_attempts carries an operator_custody column (added at database version 10). It is written once, at the single create choke point, from igcf_payment_attempt_operator_custody, whose seed is ! igcf_pledges_go_to_creator() — a site that collects centrally is custodial. It is set after the igcf_payment_attempt_before_create filter, so an add-on cannot alter it, and it is never re-derived on read.

The pair matters because the two questions have different answers over time: the stamp says the operator was holding this money, which never stops being true, while operator_still_holds() says whether they hold it now, which stops being true the moment a payout covers the payment. The wp-admin money statement reads the second to decide whether the Net row reads owed or received.

Handles refunds — money flowing back against a succeeded payment attempt. Multiple refunds per attempt are allowed (partial refunds). A fully-refunded pledge stays succeeded; the refund rows carry the financial reality. on_refund_succeeded() releases the reward slot when the pledge becomes fully refunded.

Method Parameters Returns
create( int $payment_attempt_id, int $amount, array $args = [] ) $amount in minor units (≤ the attempt amount). $args: optional reason and gateway_reference (the gateway’s own refund id). If the supplied non-null gateway_reference already exists on another refund, create() adopts that row and returns its id (no duplicate igcf_refund_created action or audit entry), so a duplicated gateway webhook is idempotent. Two refusals were added in 0.22.0: igcf_payment_attempt_not_refundable and igcf_refund_partial_not_attributable. int|WP_Error — Refund ID.
in_transaction() Whether a transaction is already open on this connection — consulted by create() before it issues START TRANSACTION, so a caller’s own transaction is not implicitly committed. An explicit igcf_db_in_transaction declaration always wins; otherwise the server answers where it can (MariaDB), and otherwise the answer is 0. int — 1 when a transaction is open, 0 otherwise.
set_gateway_reference( int $refund_id, string $reference ) Records the gateway’s own refund id on an existing refund and refreshes its updated_at. Use when the reference is only known after the outbound refund runs (e.g. on igcf_refund_created). true|WP_Error — igcf_refund_not_found if the row is missing, igcf_refund_update_failed on write failure.
get( int $id ) — Igcf_Refund|null
transition_status( int $refund_id, string $new_status, int $actor_id, array $args = [] ) $new_status: succeeded or failed. array|WP_Error
get_status_log( int $refund_id ) — array
query( array $args = [] ) — array
get_for_payment_attempt( int $attempt_id ) — Refunds for one attempt.
total_refunded_for_attempt( int $attempt_id ) / total_refunded_for_attempts( array $ids ) / total_refunded_per_attempt( array $ids ) Succeeded refund totals. int / array
refundable_remaining( Igcf_Payment_Attempt $attempt ) The amount still refundable against the attempt: the attempt amount minus all pending and succeeded refunds. Pending refunds reserve their amount, so a refund awaiting processing blocks oversubscription. Drives the admin Issue refund gate. int — Minor units; 0 when nothing remains or $attempt is invalid.
count_for_attempts( array $ids ) / count_per_attempt( array $ids ) Refund counts. int / array
campaign_refund_summary( int $campaign_id ) — Per-campaign refund totals.
get_statuses() / get_status_label( string $status ) / get_transitions() — Status registry (pending, succeeded, failed) and transitions map.
reasons() — array<string,string> — The igcf_refund_reasons registry.
failure_codes() — array<string,string> — The igcf_refund_failure_codes registry (code => label). Ships empty from core — core writes no refund failure code of its own; gateway add-ons populate it (e.g. igcf-paypal adds paypal_split_payee_unavailable). Contrast Igcf_Payment_Attempt_Service::failure_codes(), which seeds its own codes.
get_proportional_fee_reversal( Igcf_Refund $refund ) Applies igcf_refund_proportional_fee_reversal (default 0; a seam for Platform Fees). int

All methods are public static.

The single definition of what a charge is made of. One class serves both paths: the projected charge shown at checkout and the recorded charge shown after payment. Rows carry integer minor units, never formatted strings. final; all methods public static.

Method Parameters Returns
for_attempt( Igcf_Payment_Attempt $attempt ) The recorded breakdown for a payment attempt. total is the amount actually charged ($attempt->amount). array — breakdown shape below.
projected( array $context ) The projected breakdown for a checkout context (pledge_amount, currency, …). total is the pledge amount plus the sum of all components. array — breakdown shape below.
is_renderable( array $breakdown ) — bool — true iff the breakdown carries at least one non-pledge component and the component amounts sum exactly to total. Pure — no side effects. This is the guard igcf_render_pledge_breakdown() applies before emitting anything.

Both for_attempt() and projected() seed a canonical pledge row and then apply the igcf_pledge_breakdown_components filter so add-ons can contribute rows.

Breakdown shape (returned by for_attempt() / projected()):

array{
source: string, // 'attempt' (recorded) or 'projection' (projected)
currency: string, // ISO 4217 code
pledge: int, // pledge amount in minor units
components: array, // component rows, shape below
total: int, // authoritative charge total in minor units
}

Component-row shape (each entry of components):

array{
key: string, // sanitize_key()'d identifier, e.g. 'pledge', 'tip', 'commission'
label: string, // display label
amount: int, // minor units
order: int, // sort weight
}

Rows are sorted by order then key. A canonical pledge row (order 100) is always present — a contributing filter may relabel or reorder it, but core forces its amount back to the authoritative pledge amount and restores the row if a filter dropped it, so total is never a figure the gateway did not charge. Duplicate keys drop to the first writer (a _doing_it_wrong() notice fires on the collision).

igcf_render_pledge_breakdown( array $breakdown, array $args = array() ) — theme-overridable renderer for a breakdown, a thin wrapper over the partials/pledge-breakdown template. Outputs nothing when the breakdown is not renderable (see Igcf_Pledge_Breakdown::is_renderable()), so it is always safe to call — on a core-only install with no contributed components it emits no markup.

$args:

Key Default Description
total_label "Total charged" Label for the total row.
live false true on the checkout review step (the live, pre-charge projection).
variant — Presentation variant, e.g. 'email'.
// Checkout review step: render the projected breakdown for the pledge.
$context = array(
'source' => 'checkout_review',
'campaign_id' => $campaign_id,
'pledge_amount' => $amount_minor,
'reward_level_id' => $reward_level_id,
'payment_method' => $selected_gateway_id,
'currency' => igcf_get_currency(),
);
igcf_render_pledge_breakdown(
Igcf_Pledge_Breakdown::projected( $context ),
array(
'total_label' => __( 'Total charged', 'ignition-crowdfunding' ),
'live' => true,
)
);

The companion to Igcf_Pledge_Breakdown. The breakdown answers what was this charge made of?; this answers where did it go? — one term per destination that ended up holding a share of it. Both are scoped to a single payment attempt and both balance to the attempt’s own amount. This class renders nothing. final; all methods public static.

Method Parameters Returns
for_attempt( Igcf_Payment_Attempt $attempt, Igcf_Pledge $pledge ) The settlement of one attempt’s charge. Seeds core’s own returned term, applies igcf_pledge_settlement_terms, then appends net as the residual. Describes whatever attempt it is handed — filtering to succeeded attempts is the caller’s business. array{currency:string, charged:int, terms:array, unaccounted:int, pending_refunded:int}
normalize_terms( array $terms ) Drops malformed terms, sanitises keys, keeps the first writer of a duplicate key, then sorts by order then key. Pure — no database, no filters. Reserved keys are not enforced here. array — Normalised, sorted terms.
derive_net( int $charged, array $terms ) Net as the residual: the charge less every other term. Signed and never floored. int — May be negative.
unaccounted_for( int $charged, array $components ) Check A: the charge less the breakdown components it is supposed to be made of. 0 on a healthy install; anything else is money the charge half cannot explain, which a renderer shows rather than hides. int — May be negative.
seam_net_matches( Igcf_Pledge $pledge, int $derived_net ) Check B: how far the derived Net has drifted from the igcf_pledge_net_amounts seam the pledge list, the exports and the creator dashboard read. int — Signed difference, derived minus seam. 0 when they agree.

Term shape (each entry of terms):

array{
key: string, // sanitize_key()'d, unique. 'returned' and 'net' are core's.
label: string, // plain text, in the contributing add-on's own text domain
destination: string, // 'gateway' | 'platform' | 'creator' | 'backer'
amount: int, // minor units, >= 0 — core derives the sign
state: string, // 'received' | 'owed' | 'reversed'
order: int, // sort weight
}

Core owns ordering, signs, totalling, the destination wording, escaping, its own returned and net terms, and both arithmetic checks. returned counts succeeded refunds only, so money still in flight is reported separately as pending_refunded and never inside a term. net’s state follows Igcf_Payment_Attempt_Service::operator_still_holds() — owed while the operator holds the money, received once it has reached the creator.

There is deliberately no cross-half balance figure. Net is the residual, so the terms sum to the charge for every possible input — including a spurious term an add-on invents — and a check that cannot fail is not a check. unaccounted and seam_net_matches() are the two that can fail, and both are surfaced on the wp-admin money statement.

Global functions wrapping the currency registry. All take an optional ISO 4217 $code and fall back to the site currency when it is omitted — pass the pledge’s or attempt’s own currency wherever you have one, or a figure will be formatted with the wrong symbol and the wrong number of decimal places.

Function Description
igcf_to_minor( $amount, $code = '' ) Major units to minor (25.50 → 2550).
igcf_to_major( $minor, $code = '' ) Minor units to major, as a float.
igcf_to_major_string( $minor, $code = '' ) Minor units to a plain, separator-free major-unit string (3050 → "30.50"), for machine-readable contexts: form values, CSV cells, hidden fields.
igcf_get_currency_symbol() The site currency’s symbol.
igcf_format_amount( $minor, $code = '' ) Human display with the symbol ('$25.50', '25.50 kr'). Returns an unescaped string — escape it at the echo.
igcf_format_amount_signed( $minor, $code = '' ) The same, but with the sign outside the symbol: -$4.67 rather than $-4.67, because money is read sign-first. A non-negative amount comes back byte-for-byte identical to igcf_format_amount(). Use it wherever a figure can legitimately go below zero — a net after a refunded card payment, for instance. Also unescaped.
igcf_money_step( $code = '' ) The smallest amount the currency can express, as a string for a number field’s step and min: "0.01" for dollars, "1" for yen, "0.001" for Bahraini dinar. Use it instead of a hard-coded 0.01 so a money field never invites more precision than the currency has.

Read-only reporting aggregations for admin dashboards — pure query/compute helpers with no state. Powers the admin Dashboard revenue widget and is a stable seam for reporting add-ons that need the same weekly cash-flow figures.

Method Parameters Returns
get_weekly_cashflow( array $args = [] ) scope (required), plus weeks — the number of weeks back from (and including) the current partial week, default 12, clamped to at least 1. array[] — One row per week, oldest first: { week_start (Y-m-d), received, pending, refunded }, all in minor units.
get_week_starts( int $weeks = 12 ) — DateTimeImmutable[] — Week-start datetimes, oldest first. Honors the WordPress start_of_week option and is computed in the site time zone.
nice_ceil( int $value ) A non-negative minor-unit value. int — Rounds up to a clean 1 / 2 / 2.5 / 5 × 10ⁿ gridline step (0 stays 0).

Cash-flow anchoring: each amount is bucketed by the date the money actually moved. pending sums igcf_pledges.amount for pledges still awaiting payment, by created_at. received and refunded sum succeeded payment attempts and refunds by COALESCE(processed_at, updated_at) — processed_at is optional gateway-settlement metadata that core/offline flows never set, so the cash-moved date falls back to the succeeded-transition timestamp (updated_at). Gateway add-ons that record a settlement date bucket by it automatically.

All methods are public static.

Handles guest user creation, detection, reuse, and cleanup. Guest users are real WordPress users with the igcf_backer role, a random password, and _igcf_is_guest user meta.

Method Parameters Returns
create( string $email ) $email: guest’s email address. Sets user_login and user_email to the email, display_name to “Guest”, role to igcf_backer, and _igcf_is_guest meta to true. Does not send a welcome email. int|WP_Error — User ID on success.
find_by_email( string $email ) $email: email address to search. Only matches users with _igcf_is_guest meta. int|null — User ID or null if no guest user found.
find_or_create( string $email ) $email: email address. Finds an existing guest user by email; creates one if none exists. Primary entry point for the guest checkout flow. int|WP_Error — User ID on success.
is_guest( int $user_id ) $user_id: WordPress user ID. bool — Whether the user has _igcf_is_guest meta.
cleanup_orphaned() — Deletes guest users created more than 24 hours ago that have no pledges, or whose pledges are all canceled with the payment_abandoned reason; guests with any live pledge (or one canceled for another reason) are retained. Called by the igcf_guest_cleanup daily cron event. int — Number of users deleted.

Guest users cannot reset their WordPress password. The service hooks into the allow_password_reset filter and returns false for any user with _igcf_is_guest meta. This prevents unverified account claiming via the WordPress password reset flow.

All methods are public static. See Actions for hooks fired during guest user creation and deletion.

Stateless helper class for pledge-scoped shipping address capture. Reward tiers opt in via the _igcf_requires_shipping meta (exposed as the Igcf_Shipping::REWARD_LEVEL_META constant). When a backer pledges for an opt-in tier, the checkout review template renders the schema-driven fieldset and Igcf_Pledge_Service::create() JSON-encodes the sanitized address into the shipping_address column.

Method Parameters Returns
default_field_schema() — array — Default shipping field schema (Recipient name, Address line 1/2, City, State / Region, Postal code, Country). Each entry has key, label, required (bool), autocomplete.
get_field_schema() — array — default_field_schema() passed through the igcf_pledge_shipping_field_schema filter. Falls back to the defaults if the filter returns a non-array.
default_requires_shipping( array $context ) $context: [ 'reward_level_id' => int, 'campaign_id' => int ]. bool — true when the reward level’s _igcf_requires_shipping meta is '1'.
requires_shipping( array $context ) Same shape as above. bool — default_requires_shipping() passed through the igcf_pledge_requires_shipping filter. This is the canonical decision point during pledge creation.
sanitize_address( array $raw ) $raw: raw POST input. array — Sanitized address keyed by schema keys; unknown keys are dropped.
find_missing_required( array $address ) — array — Schema keys whose required values are missing from $address.
get_decoded( Igcf_Pledge $pledge ) — array|null — JSON-decodes $pledge->shipping_address, then runs it through the igcf_pledge_shipping_address filter. Returns null when no address is stored.
csv_safe( string $value ) — string — Escapes leading =, +, -, @ characters so CSV cells can’t be interpreted as formulas by spreadsheet apps.
render_read_only( array $address ) — string — Escaped HTML address block (with class igcf-shipping-summary) suitable for read-only display on the backer pledge detail, admin pledge detail, etc.
Constant Value Description
Igcf_Shipping::REWARD_LEVEL_META '_igcf_requires_shipping' Post meta key on igcf_reward_level posts that signals “this tier requires shipping.”

All methods are public static.

Static registry for the unified dashboard sidebar nav. Replaces the removed igcf_dashboard_tabs and igcf_account_tab_sections filters. Top-level items render in the sidebar; children render indented underneath their parent. Nesting is capped at one level — registering a child whose parent is itself a child is rejected via _doing_it_wrong() and the item is not stored.

Method Parameters Returns
register_item( string $id, array $args ) $id: unique identifier. $args: label (required, translatable string), priority (int, default 50 — sort weight within siblings), capability (string, optional — visibility gate; items without a capability are visible to all logged-in users), callback (callable, optional — render callback), template (string, optional — template name for igcf_get_template()), parent (string, optional — parent item id). void — _doing_it_wrong() on validation failure (missing label, unknown parent, or parent already a child).
get_items() — array — All registered items keyed by id, unfiltered by visibility.
get_visible_top_level_items() — array — Items without a parent that the current user can see, sorted by priority. A parent stays visible if any child is visible, even when the parent’s own capability check would fail.
get_visible_children( string $parent_id ) — array — Visible children of $parent_id, sorted by priority.

All methods are public static. To resolve which item should be active on the current request, hand the registry the tab (and optional section) query args from $_GET.

Three collaborating classes implement the payment gateway framework introduced in 0.12.0. See Registering a Payment Gateway for the end-to-end recipe.

Abstract base class. Concrete add-on gateways subclass this and register an instance with igcf_register_payment_gateway().

Method Required Returns Description
get_id() Yes string Unique gateway id. Used in URLs (?gateway={id}), settings keys, and as the value stored on each pledge’s payment_method column. Use your add-on’s prefix (e.g., stripe, paypal).
get_label() Yes string Human-readable label shown in the Payments tab listing and on the configuration sub-page.
get_description() Yes string Short one-line description below the label in the listing.
is_configured() Yes bool true when the gateway has every credential / setting it needs to accept a pledge. Drives the Not configured status badge and locks the Enabled toggle when false.
register_settings_fields( string $page_slug ) Yes void Called when the gateway’s configuration sub-page renders. Use the standard WordPress Settings API (add_settings_section(), add_settings_field(), register_setting()) targeted at $page_slug. Core renders the form wrapper, nonce, and Save button.
render_checkout_field( int $campaign_id, int $amount_minor ) No (no-op by default) void Override to render gateway-specific UI in the checkout payment-method section (e.g., a Stripe Elements card form).
get_unavailable_reasons( int $campaign_id ) No (defaults to one not_configured reason when is_configured() is false) array Every reason the gateway can’t be offered on this campaign. Each is an array of code, message (a plain translated sentence, escaped on output) and fix_url (admin URL or ''). Empty means offered. Override this to add campaign-specific rules, and report every failure so the administrator sees them all at once.
is_available_for_campaign( int $campaign_id ) No (defaults to get_unavailable_reasons() being empty) bool Whether the gateway can accept a pledge for a specific campaign. Override get_unavailable_reasons() instead, so the administrator can see why. A gateway that overrides only this method to return false is still hidden, and is shown to administrators with a generic isn’t available for this campaign reason.
has_operator_settings() No (defaults to false) bool Whether the gateway’s configuration screen has a field an operator may change (a key declared on igcf_operator_settings). Operators see the Payments tab only when some gateway returns true, and reach a gateway’s screen only when it does. No shipped gateway returns true.
supports( string $capability ) No (defaults to false) bool Whether the gateway advertises a named capability — a stable, code-level property such as platform_mode (the gateway can split a pledge between the platform and a beneficiary). Unlike the availability predicates this is not admin-configurable, so no filter wraps it; gateways override it to advertise what they support. Dependent add-ons query a capability rather than a gateway slug or version.

Singleton registry that holds every registered gateway instance.

Method Parameters Returns
instance() — Igcf_Payment_Gateway_Registry — singleton accessor.
register( Igcf_Payment_Gateway $gateway ) $gateway: gateway instance. Registering the same id twice overwrites the earlier instance. void
get( string $id ) $id: gateway id. Igcf_Payment_Gateway|null
get_all() — Igcf_Payment_Gateway[] — every registered gateway keyed by id, in registration order.
get_ids() — string[] — registered ids in registration order.
register_core_gateways() — Called once on init priority 5. Seeds the Offline gateway, then fires the igcf_register_payment_gateways action so add-ons can register their own. void

Most add-ons should never call the registry directly — use igcf_register_payment_gateway(), which delegates to instance()->register().

Single source of truth for reading and writing the payment_gateway_enabled portion of the plugin settings option.

Method Parameters Returns
is_enabled( string $gateway_id ) — bool — Whether the gateway is currently enabled. Offline ships enabled as the Core seed default; every other gateway defaults to disabled.
get_enabled_ids() — string[] — Currently enabled gateway ids, in registry order.
get_enabled_gateways() — Igcf_Payment_Gateway[] — Resolved gateway instances for the ids above.
explain_availability( int $campaign_id ) — array<string, array{label:string, offered:bool, reasons:array}> — One entry per enabled gateway, keyed by id, in registry order. reasons is the gateway’s own get_unavailable_reasons() list after the igcf_gateway_unavailable_reasons filter (and, when that list is empty, the older igcf_gateway_available_for_campaign veto, reported as hidden_by_filter). offered is true exactly when reasons is empty. Empty array when no gateway is enabled.
get_available_ids( int $campaign_id ) — string[] — The ids explain_availability() reports as offered, in registry order. This is what checkout offers, so checkout and the admin explanation never disagree.
get_available_gateways( int $campaign_id ) — Igcf_Payment_Gateway[] — Resolved gateway instances for the ids above.
get_lock_reason( string $gateway_id ) — string — Why the Enabled checkbox should be locked, or empty if the toggle is interactive. Used by the Payments listing UI. Precedence: not-configured → sole-enabled.
get_status( string $gateway_id ) — array{key:string,label:string,tooltip:string} — Status descriptor used by the listing and sub-page badges. key is one of not-configured, required, enabled, disabled. Empty array when the id is unknown.
set_enabled( string $gateway_id, bool $enabled ) — true|WP_Error — Writes the new state, then fires igcf_payment_gateway_state_changed. Returns one of igcf_gateway_unknown (404), igcf_gateway_not_configured (400), or igcf_gateway_last_enabled (409) on guard failure, or igcf_gateway_forbidden (403) when the settings save guard kept the old state because the current user (an operator) may not switch payment methods. Returns igcf_gateway_save_failed (500) when the save did not take for someone allowed to make it. In both cases nothing is announced. Asking for the state a method already has succeeds.
gateway_supports( string $gateway_id, string $capability ) — bool — Whether the registered gateway with that id advertises the capability (see Igcf_Payment_Gateway::supports()). Null-safe: an unknown or deactivated gateway id returns false rather than fataling, so a persisted slug is always safe to pass.
get_ids_supporting( string $capability ) — string[] — Ids of every registered gateway advertising the capability, in registry order (independent of enabled state). The capability counterpart to get_available_ids(). Empty array if none.

All methods are public static. The set_enabled() writer is the same code path the igcf/set-payment-gateway-enabled ability invokes — both share the guard set.

Staff are the people who run a site’s fundraising: administrators (anyone with manage_options) and operators. An operator is a user carrying Ignition’s operator marker on the current site. They keep their WordPress role, and Ignition adds its powers each time WordPress checks a permission, so nothing is written to a role.

Check permissions, never the marker. Three Ignition permissions exist for this, all added at check time:

Permission Who has it What core uses it for
igcf_staff Anyone with manage_options, and operators The Ignition menu and home screen, the Dashboard widget, email previews, staff emails and the staff notification switches, and sending staff to wp-admin after sign-in.
igcf_manage_settings Anyone with manage_options, and operators Opening Ignition > Settings, saving the settings an operator may change, and marking starter images. What they may actually change is decided per key; see igcf_operator_settings.
igcf_manage_campaign_terms Anyone with manage_categories, and operators Managing campaign categories and tags. Assigning terms to a campaign still uses WordPress’s default edit_posts.

Operators also receive Ignition’s campaign, pledge and user-summary permissions, plus upload_files and read. They never receive manage_options, manage_categories, or any power over users, plugins, pages or posts.

Method Returns
get_users( array $args = [] ) Every staff member on the current site. Takes get_users() arguments; the staff condition is applied last, and any role__in or blog_id you pass is dropped, so a caller can’t widen the result. Use this instead of querying for administrators.
is_staff( int $user_id ) bool — whether the user is an administrator or an operator on the current site.
get_home_url() string — where staff land after signing in (wp-admin today).
Method Returns
is_operator( int $user_id ) bool — whether the user carries the operator marker on the current site. Administrators are not operators unless also marked.
grant( int $user_id ) bool — makes the user an operator on the current site and fires igcf_operator_granted. false for an unknown user; true otherwise, including when they already were one (no action fires then).
revoke( int $user_id ) bool — the reverse, firing igcf_operator_revoked. The user’s WordPress role is untouched.
get_capabilities() string[] — every permission an operator receives.

Like WP_User::add_cap(), grant() and revoke() do no permission check of their own. A caller acting on a request must check current_user_can( 'promote_user', $user_id ) and a nonce first.

Method Returns
can_edit( string $option, string $key ) bool — whether the current user may change one key of an option: always for administrators, for someone with igcf_manage_settings only when the key is declared on igcf_operator_settings, otherwise never. Use it to decide which fields to show. The save itself is guarded separately, so hiding a field is presentation only.
get_map() array — every well-formed igcf_operator_settings declaration, keyed by option name.

Stateless aggregation service that powers the wp-admin Backers user-summary page. Every method here counts across campaigns, so every one of them takes a scope.

Method Parameters Returns
query_paginated( array $args = [] ) scope (required). Plus search (display name / email / login), filter (all|registered|guest|unverified), orderby (allowlist: name, email, total_pledged, total_received, campaigns_backed, last_pledge_at), order (ASC|DESC), per_page, page. array — [ 'rows' => stdClass[], 'total' => int, 'pages' => int ]. Each row carries user_id, display_name, user_email, status, total_pledged (pending + succeeded), total_received (collected, net of refunds), campaigns_backed, last_pledge_at, currency.
get_aggregates_for_user( array $args = [] ) scope (required), plus backer_id. Note backer_id does not narrow the scope: one backer can pledge on both sides. stdClass|null — Pivot for the backer’s pledges powering the detail-page stat tiles: total_pledged, total_received, refunded, canceled (each with a count), currency, first/last pledge dates, and a derived average. Returns null when the backer has no pledges in scope.
query_for_export( array $args = [] ) scope (required), plus search and filter as above. stdClass[] — The same aggregate rows the Backers CSV export streams.

All methods are public static and join wp_users with the custom wp_igcf_pledges table, filtering on that table’s durable mode column.

Moved to the Ignition Platform Mode add-on. The wp-admin Creators user-summary page and its aggregation service ship with Platform Mode, since the campaign-creator persona lives there; a direct-mode core install has no Creators screen. See the Platform Mode Creators list guide.