Payouts reference
The Ignition Payouts add-on records disbursements a platform operator has already made to a creator, and what each one settles. It moves no money: every payout is a record of a payment made outside WordPress.
It exposes the Payout as a first-class entity — a filterable transitions map, creation and status-change actions, and a read API — so another add-on can react to a payout or extend how one is recorded.
Dependency floors
Section titled “Dependency floors”The add-on refuses to bootstrap unless both hold, and shows a notice-error naming the one that failed:
| Constant | Floor | Enforced |
|---|---|---|
IGCF_PAYOUTS_REQUIRES_CORE |
0.23.0 |
Always. |
IGCF_PAYOUTS_REQUIRES_PLATFORM |
0.4.0 |
Always — igcf-platform must also be active. An absent Platform Mode is below the floor, not exempt from it. |
It bootstraps on plugins_loaded at priority 30, later than igcf-platform, so the Platform Mode constant is defined by the time the gate reads it.
Actions
Section titled “Actions”igcf_payouts_payout_created
Section titled “igcf_payouts_payout_created”do_action( 'igcf_payouts_payout_created', int $payout_id, int $actor_id );Fires after a payout row and all of its link rows have been committed. The actor resolves as $args['actor_id'], then get_current_user_id(), then 0 for a system context.
This is the only creation signal. Recording a payout writes its '' → paid audit row directly and never runs a transition, so igcf_payouts_payout_paid does not fire on creation. The contra row written by a reversal does not fire this action either — subscribe to the transition actions for that.
The three-tier transition surface
Section titled “The three-tier transition surface”Every status change fires three actions, in this order:
do_action( 'igcf_payouts_payout_status_changed', int $payout_id, string $old_status, string $new_status, int $actor_id );do_action( "igcf_payouts_payout_{$new_status}", int $payout_id, int $actor_id );do_action( "igcf_payouts_payout_{$old_status}_to_{$new_status}", int $payout_id, int $actor_id );The same shape core gives its own entities — a catch-all, a “reached this status” hook, and a directional one. Concrete names under the shipped transition map: igcf_payouts_payout_paid, igcf_payouts_payout_failed, igcf_payouts_payout_reversed, igcf_payouts_payout_pending_to_paid, igcf_payouts_payout_pending_to_failed, igcf_payouts_payout_paid_to_reversed.
All three fire only after both the row update and the audit-log insert have succeeded, so a subscriber never sees a change that was rolled back. A same-status call is idempotent and fires nothing.
What actually fires in 0.1.0. The only status a payout is born with is paid, and the only transition any shipped code path performs is paid → reversed. So igcf_payouts_payout_created, igcf_payouts_payout_status_changed, igcf_payouts_payout_reversed and igcf_payouts_payout_paid_to_reversed fire; the pending and failed legs exist in the map for the later send-money path and never fire on their own.
Filters
Section titled “Filters”igcf_payouts_payout_transitions
Section titled “igcf_payouts_payout_transitions”apply_filters( 'igcf_payouts_payout_transitions', array $transitions );The legal transition map, status => array of allowed next statuses. Consulted before every write; adding an entry here is the only supported way to widen what a payout may do. Ships as:
array( 'pending' => array( 'paid', 'failed' ), 'paid' => array( 'reversed' ), 'failed' => array(), 'reversed' => array(),)igcf_payouts_payout_statuses
Section titled “igcf_payouts_payout_statuses”apply_filters( 'igcf_payouts_payout_statuses', array $statuses );status slug => translated label. Ships with pending, paid, failed, reversed. Add a status here and to the transitions map together — one without the other is not reachable.
igcf_payouts_payout_methods
Section titled “igcf_payouts_payout_methods”apply_filters( 'igcf_payouts_payout_methods', array $methods );method slug => translated label. Ships with offline_check, offline_bank_transfer, offline_cash, offline_other. Creation validates the submitted method against this registry, so a gateway add-on adding a send-money rail must register its key here before a payout can carry it.
igcf_payouts_payout_before_create
Section titled “igcf_payouts_payout_before_create”apply_filters( 'igcf_payouts_payout_before_create', array $data, array $context );The column data about to be inserted, filtered after all validation and before the transaction opens. $context carries recipient_user_id, amount and attempt_ids as the caller passed them.
Four keys are re-asserted immediately after the filter and cannot be overridden: recipient_user_id, currency, mode and status. The first three are derived from the covered payment attempts rather than taken on trust; status is hard-coded, so a payout is always created paid. amount, computed_amount, method, reference, private_note, paid_at and the timestamps are yours to change. Return an array — there is no type guard.
igcf_payouts_payout_before_{$new_status}
Section titled “igcf_payouts_payout_before_{$new_status}”apply_filters( "igcf_payouts_payout_before_{$new_status}", null, int $payout_id, int $actor_id );A veto. Return a WP_Error to refuse the transition; it is returned to the caller verbatim and nothing is written. Fires only for a transition the map already permits, and before the update, so a veto leaves no row change and no audit entry.
igcf_payouts_payout_query_args
Section titled “igcf_payouts_payout_query_args”apply_filters( 'igcf_payouts_payout_query_args', array $args );Applied to every payout query before parsing. Filtered values still pass through the orderby allowlist, the order allowlist and the per-page clamp.
Note that the History screen’s summary totals are computed from the screen’s own filters, not through this hook. An add-on that narrows the ledger here moves the rows without moving the total above them.
Shared with core
Section titled “Shared with core”igcf_db_in_transaction and igcf_db_server_reports_transaction_state are applied by this add-on’s write paths exactly as core’s refund service applies them. They are the same hook names — a subscriber affects both.
Read API
Section titled “Read API”Igcf_Payouts_Payout_Service is append-only: there is no update() and no delete(). All methods are public static; amounts are in minor units.
| Method | Returns |
|---|---|
get( $id ) |
One Igcf_Payouts_Payout, or null. |
query( array $args ) |
array{ payouts, total, pages }. Args: recipient_user_id, status (string or array), mode, currency, payment_attempt_id, include_reversals (default true), date_query with after/before on paid_at, orderby (paid_at|created_at|amount|status), order, per_page (clamped 1–100), page. |
get_covered_attempt_ids( $payout_id ) |
Every payment-attempt ID this payout links to, whatever its status. |
covered_attempt_ids( array $attempt_ids ) |
Bulk reverse lookup, attempt_id => payout_id, restricted to coverage that is still in force. Use this rather than one lookup per attempt. |
get_reversal( $payout_id ) |
The contra row that reversed this payout, or null. |
get_status_log( $payout_id ) |
The append-only audit trail, oldest first. |
get_statuses() / get_status_label( $status ) |
The status registry and one label. |
get_transitions() |
The filtered transition map. |
get_methods() / get_method_label( $method ) |
The method registry and one label. |
“In force” means status = 'paid' and not itself a reversal. Every coverage query in the add-on uses that definition, so a reversed payout stops covering anything and its payments become payable again.
Write API
Section titled “Write API”| Method | Behavior |
|---|---|
create( $recipient_user_id, $amount, array $attempt_ids, array $args ) |
Records a payout that has already been made. Always creates it paid and takes no status argument. Returns the new payout ID or a WP_Error. Optional $args: computed_amount (defaults to $amount), currency, method (defaults to offline_other), reference, private_note, paid_at, note, actor_id. |
transition_status( $payout_id, $new_status, $actor_id, array $args ) |
Moves a payout along the map. $args: failure_code, note, defer_hooks. Returns array{ status, statusLabel, message }, plus idempotent => true on a no-op. It uses the $actor_id you pass verbatim — it does not resolve one. |
reverse( $payout_id, array $args ) |
Reverses a payout. $args: reason, actor_id. Returns the contra row’s ID or a WP_Error. |
create() derives the recipient, currency and mode from the covered attempts — the campaign’s author, the pledge’s currency, the pledge’s mode — and refuses when the caller’s arguments disagree, when the batch mixes recipients, currencies or modes, when an attempt has not succeeded, or when an in-force payout already covers one of them. Error codes are namespaced igcf_payout_*; the full set is documented on the method.
Calling reverse() twice is safe: the second call finds the existing contra row and returns its ID rather than creating a second one. A unique key on the reversal pointer enforces one reversal per payout at the database level.
What a reversal is
Section titled “What a reversal is”Both a status change and a new row, in one transaction:
- The original transitions
paid → reversedand keeps every one of its values. - A contra row is inserted into the same table, carrying the same recipient, amount, currency, mode and method, with
status = 'reversed'and a non-null pointer at the original. The amount is stored positive — the sign is implied by the pointer, and it is the reader’s job to negate it. - The contra row is linked to no payment attempts and writes no audit entry of its own. The single audit entry is the original’s
paid → reversed. - Refunds the reversed payout had absorbed are not un-linked, but they stop counting: absorption is also tested against in-force payouts, so the refund stands again and the next payout absorbs it.
Contra rows would double-count in any total, which is what query()’s include_reversals argument is for.
Core seams consumed
Section titled “Core seams consumed”| Seam | How it is used |
|---|---|
igcf_payment_attempt_operator_still_holds |
Answered. Returns false for a payment an in-force payout already covers; every other case passes the incoming value through. The add-on never overrides core’s stamp — it only reports money that has since gone out. |
igcf_payment_attempt_net_to_creator |
Read as one input, never answered. Core documents that value as a display figure computed before refunds; this add-on subtracts recorded refunds and already-covered attempts to turn it into an amount to pay. |
igcf_pledge_detail_attempt_columns / igcf_pledge_detail_attempt_cells |
Answered with the Payout column on the wp-admin pledge detail page. The column is contributed only when the pledge has a succeeded attempt and money the operator holds; on any other pledge the table is byte-identical to a core-only install. Deliberately not a settlement term — a payout happens later and spans many pledges, so it would break the settlement arithmetic. |
Igcf_Payment_Attempt_Service::operator_custody_per_attempt() |
The bulk custody read behind the queue and the pledge column. |
Igcf_Refund_Service::total_refunded_per_attempt() |
Per-attempt refund totals, in bulk, so the queue does not do one lookup per row. |
Igcf_Pledge_Service::get_processing_fees_for_pledges() |
The bulk fee map. Read in bulk rather than through the per-attempt fee filter, because only the map distinguishes an absent fee from a reported zero — which is exactly what the unreliable-figure mark keys on. |
Igcf_Campaign_Service::compute_result() |
The funding verdict behind Ready to Pay versus Held Back. Never re-derived locally. |
Igcf_Currencies |
Validation, exponent, symbol and position. A payout is one recipient in one currency. |
Igcf_View_Mode |
Resolved once per screen and threaded through every link, field and query. Test money never reaches a live queue or a live total. |
Igcf_Dashboard_Nav_Registry::register_item() |
The creator’s Payouts dashboard section, at priority 15 and gated on edit_igcf_campaigns. |
igcf_locate_template |
Resolves the igcf-payouts/ template namespace. A theme override still wins — core’s own search runs first. |
The email framework (Igcf_Email, Igcf_Email_Registry, Igcf_Merge_Tags, igcf_notification_categories, igcf_email_preview_data) |
The payout_recorded email, its payout merge-tag context, its creator notification category, and its admin preview sample. |
igcf_uninstall / igcf_activated |
Table teardown when core is uninstalled, and table creation when core provisions a site. |
Templates
Section titled “Templates”Both are overridable from a theme, under yourtheme/ignition-crowdfunding/:
| Template | Renders |
|---|---|
igcf-payouts/dashboard-payouts.php |
The creator’s Payouts dashboard section. Read-only: no form, no nonce, no JavaScript. It fetches its own data and never reads the payout’s private note. |
igcf-payouts/emails/payout-recorded.php |
The payout-recorded email body, inside core’s email layout. |
Five merge tags are registered in the payout context for operators customising the subject or body: {payout_amount}, {payout_date}, {payout_method}, {payout_reference}, {payouts_url}. The default template renders from its own data and uses none of them.
Storage
Section titled “Storage”Four custom tables, created together and dropped together — on the add-on’s own uninstall, and on core’s:
| Table | Row per | Holds |
|---|---|---|
{prefix}igcf_payouts_disbursements |
Payout | Recipient, the amount sent and the amount computed, currency, mode, method, status, reference, private note, the reversal pointer, and when it was paid. |
{prefix}igcf_payouts_attempts_link |
Payment settled | Which succeeded payment attempts a payout covered. This is what stops a creator being paid twice and what takes a payment out of the queue. |
{prefix}igcf_payouts_refunds_link |
Refund absorbed | Which refunds a payout has already netted, so the next basis calculation does not subtract them again. |
{prefix}igcf_payouts_status_log |
Status change | Append-only: old status, new status, actor and note. Creation writes an entry whose old status is the empty string. |
Both amount and computed_amount are stored on every payout, so a difference between what the operator sent and what the plugin suggested stays visible on the record forever rather than being reconciled away.
Tables self-migrate on an in-place update.
Related
Section titled “Related”- Payouts user guide
- Platform Mode reference — required dependency.
- Service Layer
- Filters Reference