Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • extensions/mjwshared
  • jitendra/mjwshared
  • scardinius/mjwshared
  • artfulrobot/mjwshared
  • capo/mjwshared
  • agilewarefj/mjwshared
  • JonGold/mjwshared
  • aaron/mjwshared
  • sluc23/mjwshared
  • marsh/mjwshared
  • konadave/mjwshared
  • wmortada/mjwshared
  • jtwyman/mjwshared
  • pradeep/mjwshared
  • AllenShaw/mjwshared
  • jamie/mjwshared
  • JKingsnorth/mjwshared
  • ufundo/mjwshared
  • DaveD/mjwshared
19 results
Show changes
Showing
with 1947 additions and 461 deletions
docs/images/refundpaymenticon.png

30.9 KiB

docs/images/refundui-events.png

42.6 KiB

docs/images/refundui-membership.png

39.2 KiB

# Payment Shared library
This library is used by all payment processors by MJW and other extensions.
It provides multiple functions such as APIs, refund UI, shared code and a compatibility layer to support multiple versions of CiviCRM without requiring explicit support in the payment processor.
The main "goals" of this extension are:
- Provide an abstraction layer between CiviCRM core and payment extensions so
that we don't have to write and maintain the same code in every extension.
- Provide a compatibility layer between CiviCRM core and payment extensions so
that we don't force sites to upgrade CiviCRM core versions just to keep their Payment Processor working.
(Generally we target a minimum CiviCRM core version based on the last security release).
- Provide a "staging" environment for proving new APIs / interfaces that should eventually become
a standard part of CiviCRM core.
- Provide a rapid way of fixing bugs in Payment Processing without forcing a CiviCRM core update
(eg. we can issue a new release of "Payment Shared" containing a workaround for bugs in specific versions
of CiviCRM core).
## Setup
#### Job.process_paymentprocessor_webhooks
This job processes new webhook events in the `civicrm_paymentprocessor_webhook` table.
* Run: Always
* Domain-specific: YES. This job MUST be run on every domain you have setup if using multisite/multidomain.
## Support and Maintenance
This extension is supported and maintained by:
[![MJW Consulting](images/mjwconsulting.jpg)](https://www.mjwconsult.co.uk)
We offer paid [support and development](https://mjw.pt/support) as well as a [troubleshooting/investigation service](https://mjw.pt/investigation).
# Payment Processor Implementation
This library provides two helper "traits":
- `CRM_Core_Payment_MJWTrait` - helpers to implement your `CRM_Core_Payment_Xx` class.
- `CRM_Core_Payment_MJWIPNTrait` - helpers to implement IPN/webhook processing for your payment processor.
## The Payment Processor
### doPayment()
```php
public function doPayment(&$params, $component = 'contribute') {
/* @var \Civi\Payment\PropertyBag $propertyBag */
$propertyBag = $this->beginDoPayment($params);
// Set payment pending
$this->setStatusPaymentPending($propertyBag)
// ... Handle the actual payment / communicate with external servers etc.
// Payment succeeded?
$this->setStatusPaymentCompleted($propertyBag)
return $this->endDoPayment($propertyBag);
}
```
## changeSubscriptionAmount() (Edit recurring contribution)
```php
public function changeSubscriptionAmount(&$message = '', $params = []) {
$propertyBag = $this->beginChangeSubscriptionAmount($params);
// ... Handle update subscription / communicate with external servers.
// On error throw exception
return TRUE;
}
```
## updateSubscriptionBillingInfo() (Update payment info + address)
```php
public function updateSubscriptionBillingInfo(&$message = '', $params = []) {
$propertyBag = $this->beginUpdateSubscriptionBillingInfo($params);
// ... Handle update billing info / communicate with external servers.
// On error throw exception
return TRUE;
}
```
## doCancelRecurring() (Cancel recurring contribution)
```php
public function doCancelRecurring(PropertyBag $propertyBag) {
// By default we always notify the processor and we don't give the user the option
// because supportsCancelRecurringNotifyOptional() = FALSE
if (!$propertyBag->has('isNotifyProcessorOnCancelRecur')) {
// If isNotifyProcessorOnCancelRecur is NOT set then we set our default
$propertyBag->setIsNotifyProcessorOnCancelRecur(TRUE);
}
$notifyProcessor = $propertyBag->getIsNotifyProcessorOnCancelRecur();
if (!$notifyProcessor) {
return ['message' => E::ts('Successfully cancelled the subscription in CiviCRM ONLY.')];
}
if (!$propertyBag->has('recurProcessorID')) {
$errorMessage = E::ts('The recurring contribution cannot be cancelled (No reference (contribution_recur.processor_id) found).');
\Civi::log()->error($errorMessage);
throw new \Civi\Payment\Exception\PaymentProcessorException($errorMessage);
}
// ... Handle cancel recurring / communicate with external servers.
// If we failed to cancel
if ($failed) {
throw new \Civi\Payment\Exception\PaymentProcessorException($this->handleError(NULL, 'Failed to cancel'));
}
return ['message' => E::ts('Successfully cancelled the subscription at XYZ.net.')];
}
```
## doRefund() (Refund full or partial contribution)
```php
/**
* Submit a refund payment
*
* @param array $params
* Assoc array of input parameters for this transaction.
*
* @return array
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
public function doRefund(&$params): array {
$propertyBag = $this->beginDoRefund($params);
if (!$propertyBag->has('transactionID') || !$propertyBag->has('amount')) {
$errorMessage = $this->getLogPrefix() . 'doRefund: Missing mandatory parameters: transactionID, amount';
$this->logger->logError($errorMessage);
throw new PaymentProcessorException($errorMessage);
}
// Implement code to prepare request and communicate with Payment Processor
// ...
// Set $refundTransactionID based on response from Payment Processor
if ($refundFailed) {
throw new PaymentProcessorException('Refund failed: ' . $failureMessage);
}
// If we didn't throw an exception then refund succeeded.
$refundParams = [
'refund_trxn_id' => $refundTransactionID,
'refund_status' => 'Completed',
'fee_amount' => 0,
];
return $refundParams;
}
```
# Refunds UI
If supported by the payment processor (eg. Stripe) you can issue a full or partial refund from within CiviCRM.
It is enabled by default via the setting `mjwshared_refundpaymentui` which can be found at
*Administer->CiviContribute->Payment Shared Settings: Enable refund payment via UI?*
It allows you to issue refunds for `Completed` payments.
It also allows you to choose whether to cancel the **event registration** if there are any linked to the contribution (via line-items).
To access the refunds UI you must have **edit contributions** permission.
1. Click the "arrow" to expand the contribution and show payments.
2. To access the refund form click the "undo" icon by the payment:
![Refund icon](images/refundpaymenticon.png)
You will see a refund form.
If the contribution was used to pay for a membership you can optionally cancel the membership:
![Refund UI - memberships](images/refundui-membership.png)
If the contribution was used to pay for an event you can optionally cancel the event registration:
![Refund UI - events](images/refundui-events.png)
......@@ -9,7 +9,293 @@ Releases use the following numbering system:
* **[BC]**: Items marked with [BC] indicate a breaking change that will require updates to your code if you are using that code in your extension.
## Releaes 0.9.3
## Release 1.3.4 (2025-02-26)
* Make sure we always activate customdata from managed entities
* Add civicrm-ext type to composer.json
## Release 1.3.3 (2025-01-06)
* Change ManagedEntity to update policy always (prevents conflicts with other extensions and fixes issues with them disappearing in certain circumstances).
* Fix regression on required checkbox fields.
* Fix JS crash if name is undefined.
## Release 1.3.2 (2024-10-13)
* Add check for webhooks stuck in processing status - if this is triggered something probably needs fixing manually.
* Replace deprecated exception.
* Switch to entity framework v2.
* Switch to mgd files to define cg_extend_objects - used to allow custom fields on "FinancialTrxn" entity. Added in a way that doesn't conflict with other extensions.
## Release 1.3.1 (2024-08-26)
* [!46](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/46) Ensure autorenew works with memberships.
## Release 1.3 (2024-07-17)
* Add new API4 functions:
* ContributionRecur.updateAmountOnRecurMJW
* Membership.LinkToRecurMJW
* Membership.UnlinkFromRecurMJW
* PriceFieldValue.GetDefaultPriceFieldValueForContributionMJW
* PriceFieldValue.GetDefaultPriceFieldValueForMembershipMJW
* PaymentMJW.create
See [API docs](api.md) for more information.
* Add search identifier and raw data options to Payment Processor Webhooks UI.
## Release 1.2.22 (2024-03-09)
* Fix message display on PaymentProcessorWebhook UI when message is NULL.
* Add generic Logger class `\Civi\MJW\Logger` - use this to standardise/simplify log messages.
* Update getDefaultCurrencyForForm() to use standard form functions where available.
* Update some functions to use API4 internally.
* Replace deprecated `CiviCRM_API3_Exception`.
* More robust `error_url` handling.
* Fix currency shown on refund form.
## Release 1.2.21 (2024-02-09)
* Switch Payment processor webhooks menu entry to managed entity.
* Add a settings page (Administer->CiviContribute->Payment Shared Settings).
* [!44](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/44) Ensure email confirm is sent for events when using stripe checkout (Fixes extensions/stripe#456).
* Add feature to disable (hide) core 'Record Refund' link on edit contribution.
* Add handling for billingCountry on update subscription.
## Release 1.2.20 (2023-12-16)
* Fix CustomField params for Mjwpayment.create_payment.
* Fix setting of recur ID for changeSubscriptionAmount.
## Release 1.2.19 (2023-12-15)
* Add CustomFields to API3 MJWPayment.create spec - makes the custom fields "discoverable" via the API3 explorer.
* Update beginChangeSubscriptionAmount() function - for Payment Processors that allow you to update the subscription amount it should now be slightly more reliable.
## Release 1.2.18 (2023-11-21)
* [!42](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/42) avoid extra redirect; correctly show auth.net errors on failed transaction.
## Release 1.2.17 (2023-10-16)
**You MUST update to this version if running CiviCRM 5.66 otherwise webhooks will stop working**
* Fix message field should not be required (CiviCRM core enforces required fields for API4 from 5.66).
* Cleanup custom fields.
## Release 1.2.16 (2023-10-16)
**Do not use this release**
It was released with a broken upgrader.
## Release 1.2.15 (2023-09-10)
* Fix [civicrm-core/#4553](https://lab.civicrm.org/dev/core/-/issues/4553) getBillingEmail() function causes fatal error when email is not passed in correctly.
## Release 1.2.14 (2023-08-14)
* [!41](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/41) The extension fails to install or upgrade if the option value for cg extends already present in the database (fix compatibility with extensions that use custom fields on financial transactions).
## Release 1.2.13 (2023-06-26)
* smartyv2-1.0.1 does not work on 5.59 because of https://github.com/civicrm/civicrm-core/commit/7168793c03ef57c06bbfe45f5ff873ebb3657806
so we set minimum version to 5.58 which triggers civix to ship as an extension mixin.
## Release 1.2.12 (2023-06-25)
* Add Payment_details custom field group and allow custom fields to be saved via API3 `Mjwpayment.create`.
* Pass through custom params in `updateContributionCompleted()`.
* Convert some internals to API4.
* Refactor API call to fix payment processor name issue.
* Use getter for `_paymentProcessor`.
## Release 1.2.11 (2023-01-30)
* Remove our version of CRM_Core_Payment::getAmount() as it was merged into core in 5.37.
* Fix undefined variable basePage on frontend formbuilder pages.
## Release 1.2.10 (2022-11-22)
* Make sure calculateTaxAmount always returns a valid float.
* Stop recommending installation of minifier extension (it can cause problems with some angularjs scripts).
* Check both frontend/backend URL for AJAX requests.
* Add indexes to civicrm_paymentprocessor_webhook - see [Stripe#395](https://lab.civicrm.org/extensions/stripe/-/issues/395).
* Fix translation [!31](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/31).
## Release 1.2.9 (2022-10-14)
* Add psr0 classloader to info.xml.
* Don't update (set update to never) Paymentprocessorwebhooks managed job (stops it re-enabling automatically).
* Upgrade civix.
* Fix [#18](https://lab.civicrm.org/extensions/mjwshared/-/issues/18) Don't add refund link if no payment processor.
## Release 1.2.8 (2022-08-19)
* Multiple participants: Handle 100% discount. Fix [Stripe#372](https://lab.civicrm.org/extensions/stripe/-/issues/372) etc. when additional participant amount is more than first participant.
* Convert getTokenParameter() to use propertyBag.
* Fix [!35](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/35) style issues with Greenwich; also nicer formatting of raw data.
## Release 1.2.7 (2022-07-20)
* Minimum supported version of Stripe extension is now 6.7.
* Add `\Civi\Paymentshared\WebhookEventIgnoredException` for use by payment processors.
* Fix deprecated API4 join.
## Release 1.2.6 (2022-06-14)
* Add support for percentagepricesetfield/extrafee extensions (was previously supported but broke in 1.2.3).
* Support partial refunds.
* Fix [#8](https://lab.civicrm.org/extensions/mjwshared/-/issues/8) Support cancelling memberships when issuing refunds.
## Release 1.2.5 (2022-05-19)
* Separate `trxn_id` and `order_reference` params and prefer `trxn_id` in return values from `doPayment()`. This means that both are now available for use.
* Refunds: Add a lock around recording refund payment in `MJWIPNTrait::updateContributionRefund()`. This means we should not record a duplicate refund if both UI and IPN are processed at the same time.
* Update return params from `doRefund()`.
* [!33](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/33) Add processor filter to webhooks list page.
## Release 1.2.4
* Fix [!30](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/30) Pledges are also recurring.
* Set `civicrm_contribution_recur.processor_id` if still using `trxn_id`.
## Release 1.2.3
* [!29](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/29) Throw exception on error (when using handleError() function).
* Fix [#10](https://lab.civicrm.org/extensions/mjwshared/-/issues/10) Replace .prop() with .attr() when selecting BillingFormID.
* Improve `CRM.payment.getIsRecur()` so it returns early if recur is not supported.
* `Mjwpayment.get_contribution` API supports contribution_id - update spec.
* Replace core `calculateTotalFee()` javascript function with our own. This makes us independent of core changes - eg. see https://github.com/civicrm/civicrm-core/pull/22759.
* Calculate the total amount for multiple event participants when calling `CRM.payment.getTotalAmount()`.
## Release 1.2.2
* Add result parameter to webhookEventNotMatched and update example.
* Add getter/setter for contributionRecurID in IPN trait.
* Add link to example hook implementation for webhookEventNotMatched (https://github.com/mjwconsult/civicrm-stripewebhookrules).
* Enable js debugging for drupal webform.
## Release 1.2.1
* Fix display of 'Error' status on webhook UI.
* More helpful error messages when IPN processing fails.
* Job.process_paymentprocessor_webhooks needs to be domain-specific (setup to run on each domain).
* Add event_id and queue_limit to Job.process_paymentprocessor_webhooks.
* Add deleted count to Job.process_paymentprocessor_webhooks.
* Only delete old webhook entries for our domain (when multiple domains configured).
* Add system check to make sure that scheduled jobs are setup on all domains.
## Release 1.2
**Thanks to [ArtfulRobot](https://artfulrobot.uk) this release improves the webhook queueing system
and adds a user interface to view/manage webhooks.**
* Implement `processWebhookEvent()` - This receives and processes the row from `civicrm_paymentprocessor_webhook` (from `PaymentprocessorWebhook`).
* Update schema and add indexes to `civicrm_paymentprocessor_webhooks` table.
* Improve api3 `Job.ProcessPaymentprocessorWebhooks` return data and add time.
* Add angular app to view/manage webhooks.
* Fully remove support for CiviCRM older that 5.35.
## Release 1.1
**This release *should* be compatible with payment processors that require 1.0 or higher.
But make sure you test before upgrading.**
* Add multiple functions to CRM.payment (that were previously in civicrmStripe.js):
* resetBillingFieldsRequiredForJQueryValidate
* setBillingFieldsRequiredForJQueryValidate
* addDrupalWebformActionElement
* doStandardFormSubmit
* validateReCaptcha
* addSupportForCiviDiscount
* displayError
* swalFire
* swalClose
* triggerEvent
* Minor (backwards-compatible) fixes/changes to existing functions (eg. setting class variables directly instead of relying on return values).
* Refactor checks class, move checks from Stripe to mjwshared:
* Check for Sweetalert extension.
* Check for "Separate Membership Payment" is enabled.
* Support X.X-dev versioning for system checks - display a warning if dev version, version check no longer fails if eg. using 1.1-dev and minimum requirement is 1.1.
* Return a fixed set of params from `doPayment()` - see [dev/financial/issues#141](https://lab.civicrm.org/dev/financial/-/issues/141).
* Move cast to PropertyBag to beginDoPayment (reduce lines of code required in doPayment).
* Automatically handle deprecated `trxn_id` on `civicrm_contribution_recur` (copy from `processor_id` and add deprecated warnings).
* Add `beginUpdateSubscriptionBillingInfo()` and `beginChangeSubscriptionAmount()` methods - see [Payment Processor](paymentprocessor.md).
* Convert internal method `getContactID()` to require propertyBag.
* Define contributionRecur property on IPN class.
* Add new [hook `webhookEventNotMatched`](hooks.md).
* Add handling for multiple js payment processors and delayed crmBillingFormReloadComplete event trigger.
* Fix invalid currency on some event registration forms.
* Fix for non-default Wordpress basepage and AJAX reload of payment elements.
## Release 1.0.1
* Fix [!22](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/22) Handle deprecated API4 joins in PaymentProcessorwebhook API.
## Release 1.0
* Add PaymentprocessorWebhook entity, API and scheduled job that allows for queueing and scheduling of webhooks - see [Webhook Queue](webhookqueue.md)
* Fully remove support for CiviCRM older than 5.28.
* Add IPN getters/setters to provide object oriented initialisation.
* IPN data can be array, object or string.
* Clear cancel_date when setting a contribution back to pending.
* Support CiviCRM multi-domain (add default domain to API calls).
* Add `handleErrorThrowsException` option to MJWTrait (to help with testing).
* Total Amount is always required when completing contribution (`MJWIPNTrait::updateContributionCompleted()`).
* Set the 'payment_status' and add helper functions on doPayment() - see https://lab.civicrm.org/dev/financial/-/issues/141.
* Don't require total_amount for repeatContribution - it is set automatically via the template contribution of the recurring contribution.
* Fix [!19](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/19) Contributions held for fraud then approved don't send receipts - Send receipts on one-time payment notifications (if configured to do so via Contribution page).
* Enable [Refund UI](https://docs.civicrm.org/mjwshared/en/latest/refunds/) by default.
* "Javascript debugging" is now moved from Stripe to this library. If you have it enabled you will need to enable it again.
## Release 0.9.12
* Fix [#7](https://lab.civicrm.org/extensions/mjwshared/-/issues/7) Parse through thousands separators in calculateTaxAmount.
* Fix [!18](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/18) Incorrect financial transaction on repeatTransaction (Always pass payment_processor_id to Mjwshared.create_payment).
## Release 0.9.11
* Add `supportsRecur()` function to CRM.payment.
* Add `getPaymentProcessorSelectorValue()` function to CRM.payment.
* Fix [!15](https://lab.civicrm.org/extensions/mjwshared/-/merge_requests/15) Stripe loading on drupal 8 webforms.
## Release 0.9.10
* Add `getBillingEmail()` and `getBillingName()` functions to CRM.payment library.
## Release 0.9.9
* Trap and log exceptions triggered when calling repeatcontribution.
* Fix [Stripe!121](https://lab.civicrm.org/extensions/stripe/-/merge_requests/121) Drupal Webform: Recognize 0 installments as recurring.
* Add function processZeroAmountPayment to check/handle a zero amount payment.
## Release 0.9.8
**This affects new installs only. It should be completely safe to upgrade existing sites from 0.9.7 to 0.9.8**
* Fix [#6](https://lab.civicrm.org/extensions/mjwshared/-/issues/6) Install error on 0.9.7.
- This was because the new settings file referenced a class from the Stripe extension.
## Release 0.9.7
* Add support for issuing refunds via the payment UI for payment processors that support refunds (eg. Stripe).
* Fix [Stripe#260](https://lab.civicrm.org/extensions/stripe/-/issues/260) Refund not communicated back to CiviCRM properly (CiviCRM < 5.32).
## Release 0.9.6
* Fix [Stripe#271](https://lab.civicrm.org/extensions/stripe/-/issues/271) Can't submit credit card memberships: Uncaught (in promise) TypeError: this.form is null
## Release 0.9.5
* Fix [#4](https://lab.civicrm.org/extensions/mjwshared/-/issues/4) Fatal error when is_email_receipt = null.
## Release 0.9.4
* Fix [#2](https://lab.civicrm.org/extensions/mjwshared/-/issues/2) Don't update receive_date when marking a contribution as failed.
## Release 0.9.3
* Add `getBillingSubmit()` to CRM.payment.
......
# Webhook Queue
Payment processors can generate *a lot* of webhooks; when some *event* occurs such as a payment is confirmed, information about this event is packaged up in a special way and sent through an HTTP request to a special CiviCRM *webhook endpoint URL*.
Multiple webhooks can arrive simultaneously, they may be re-sent in the case of network failure for example, may be delayed by various environmental factors. This means that events may be received out of chronological order, and one event may already be obsolete by the time it gets processed.
We must be able to accept this data quickly and efficiently, but processing the events may take time. A sudden flurry of events can degrade the performance of the site or cause time-outs and processing failures. This extension provides a framework for queueing webhooks for scheduled background execution using the `PaymentprocessorWebhook` entity and Scheduled Job.
To use this functionality you must add support to your Payment Processor.
Depending on the 3rd party sending the webhook, the data might contain authentication keys/be encoded, cryptographically signed and may describe a single event or may bundle a lot of events in one go.
On receiving the data, we need to do *as little processing as possible*, to ensure efficiency. Typically this might be: authenticating the request (many webhooks contain a pre-shared secret to check), validating the data, and possibly extracting multiple events into multiple queue items. Perhaps the 3rd party provides some library code function that must be used to unpack this. Most 3rd parties keep a record of what they have sent, and how the receiving server responded, so that they can re-send later in the case of failure. As we're not (necessarily) processing the events at this point, we can reply successfully as long as we were able to unpack the event(s) and put them on the queue.
## PaymentprocessorWebhook entity
The table `civicrm_paymentprocessor_webhook` records each event from an incoming webhook along with information required to process it, a processing status field and a result message.
The fields are:
- `id` CiviCRM-internal integer, as standard.
- `payment_processor_id` this is a foreign key to a configured payment processor.
- `event_id` a string field to store an event's unique identifier, as provided by the 3rd party.
- `trigger` *optional* a string machine-name description of the event, again processor dependent. This might be a field you can extract directly from the webhook data, or it might be something you need to fabricate from various data. Example: Stripe uses a *trigger* field with values like `payment_succeeded`. GoCardless sends an entity and action in separate fields (`payments` and `confirmed`), and implementers can choose how to store these, e.g. GoCardless stores this as the string `payments.confirmed`.
- `created_date` a timestamp recording when the queue item was created.
- `processed_date` a timestamp recording when the queue item was processed.
- `status` a string, described below.
- `identifier` an *optional* string to group possible multiple events together. Stripe uses this since many events may come in about a particular contribution and these then need processing in a particular order.
- `message` an *optional* text string recording the result of the processing. Error messages are useful here, though more detail may be found in other logs, depending on the implementation.
- `data` TEXT. Stores the (rest of the) data received. You may not need to use this, event ID and trigger might be enough (e.g. Stripe), but sometimes the data sent includes more information that is required or useful in processing, e.g. a GoCardless event might include subscription IDs and dates that are useful. The field defined as TEXT, so JSON is a sensible format for encoding the data.
### Processing status
* `new`: The webhook has been received but not yet processed.
* `error`: The webhook has been processed but there was an error.
* `success`: The webhook has been processed successfully.
* `processing`: The webhook is currently being processed by the API3 `Job.process_paymentprocessor_webhooks` (scheduled job).
## Querying the webhook table
Use the API4 `PaymentprocessorWebhook` entity.
## Implementing the queue in your payment processor
Your payment processor will have a subclass of `CRM_Core_Payment` with all its specific code in. This is referred to as the "payment class" throughout this section of documentation.
### First, edit your payment class' `handlePaymentNotification()` method. This should
1. examine, unpack, verify, authenticate etc. the incoming webhook request. (We assume that you already have this code written).
2. Split the webhook data into *events* that you need to process. If the processor sends events you don't use, you might want to skip these at this stage (no point queuing something that doesn't require action!).
3. Assuming there are events you wish to process *now or by schedule*, create queue items for each of them. Example pseudo code below.
4. If you want to process the event right away, you can pass the data to `$this->processWebhookEvent($queueItemArray)`.
5. Create a suitable http response. Typically a blank response with a suitable `http_response_code()`.
```php
<?php
public function handlePaymentNotification() {
try {
/** @var array of whatever the 3rd party events look like (must be JSON serializable) */
$processorEvents = checkAndParseIncomingWebhookDataIntoEvents(file_get_contents('php://input'));
/** @var array of data for PaymentprocessorWebhook entity */
$storedEvents = [];
$eventsToProcessRightNow = [];
foreach ($processorEvents as $processorEvent) {
if (weCompletelyIgnoreThisType($processorEvent['type'])) {
continue;
}
$storedEvent = [
'event_id' => $processorEvent['id'],
'trigger' => $processorEvent['eventType'],
'data' => json_encode($processorEvent),
// 'identifier' => $this->getIdentifierValueForEvent($processorEvent),
];
if (weWantToProcessThisEventNow($processorEvent)) {
$storedEvent['status'] = 'processing';
$eventsToProcessRightNow[$processorEvent['id']] = NULL;
}
}
// Store the events. (They will receive status 'new')
$storedEvents = \Civi\Api4\PaymentprocessorWebhook::save(FALSE)
->setRecords($storedEvents)
->setDefaults(['payment_processor_id' => $this->getID(), 'created_date' => 'now'])
->execute()
->indexBy('event_id')
->getArrayCopy();
if ($eventsToProcessRightNow) {
// Map external event IDs to our new queue IDs.
foreach ($eventsToProcessRightNow as $eventID => $_) {
$eventsToProcessRightNow[$eventID] = $storedEvents[$eventID]['id'];
}
// Reload the queue items (to populate the rest of the fields)
$queueItems = \Civi\Api4\PaymentprocessorWebhook::get(FALSE)
->addWhere('id', 'IN', $eventsToProcessRightNow)
->execute();
foreach ($queueItems as $webhookEvent) {
$this->processWebhookEvent($webhookEvent);
}
}
}
catch (Exception $e) {
// Aah, shucks. Log it and let the 3rd party know it should
// retry later by returning 400, for example.
http_response_code(400);
}
// Assuming you don't need to provide any http body to the 3rd party...
exit;
}
```
### Then, create a `processWebhookEvent(array $webhookEvent)` method in your payment class.
This receives the row from `civicrm_paymentprocessor_webhook` (from `PaymentprocessorWebhook`) as an array. It should:
1. attempt to process the data however it needs to.
2. catch all exceptions
3. update the webhook event entity recording the status success/error and any message
4. return TRUE for success, FALSE for error
```php
<?php
public function processWebhookEvent(array $webhookEvent) :bool {
try {
$webhookEvent['processed_date'] = 'now';
// Pseudo code (doesn't have to be a separate method)
$result = $this->doTheDo(json_decode($webhookEvent['data']));
$webhookEvent['status'] = 'success';
$webhookEvent['message'] = 'have a nice day';
return TRUE;
}
catch (Exception $e) {
$webhookEvent['status'] = 'error';
$webhookEvent['message'] = $e->getMessage();
$result = FALSE;
}
// Update the stored event.
Civi\Api4\PaymentprocessorWebhook::save(FALSE)
->setRecords([$webhookEvent])->execute();
return $result;
}
```
# Legacy notes.
Currently it is only implemented for Stripe and `civicrm_api3_job_process_paymentprocessor_webhooks` function
would need to be modified to call the appropriate API method for that processor instead of `Stripe.Ipn`. The
intention is to support `Ipn` API for any supported processor.
This is the paymentprocessor function that receives the webhook:
```php
public function handlePaymentNotification() {
$rawData = file_get_contents("php://input");
$ipnClass = new CRM_Core_Payment_StripeIPN($rawData);
if ($ipnClass->onReceiveWebhook()) {
http_response_code(200);
}
}
```
This is the paymentprocessor function that is used to manually process a webhook and is called from API3 `Stripe.Ipn`:
```
public static function processPaymentNotification($paymentProcessorID, $rawData, $verifyRequest = TRUE, $emailReceipt = NULL) {
$_GET['processor_id'] = $paymentProcessorID;
$ipnClass = new CRM_Core_Payment_StripeIPN($rawData, $verifyRequest);
$ipnClass->setExceptionMode(FALSE);
if (isset($emailReceipt)) {
$ipnClass->setSendEmailReceipt($emailReceipt);
}
return $ipnClass->processWebhook();
}
```
In your IPN code instead of using a `main()` method create two functions:
* `onReceiveWebhook()`: Triggered whenever a webhook is received. Use this to record the webhook.
* `processWebhook()`: This is the method that actually processes the webhook and may be called immediately or via the scheduled job.
```php
/**
* Get a unique identifier string based on webhook data.
*
* @return string
*/
private function getWebhookUniqueIdentifier() {
return "{$this->charge_id}:{$this->invoice_id}:{$this->subscription_id}";
}
/**
* When CiviCRM receives a Stripe webhook call this method (via handlePaymentNotification()).
* This checks the webhook and either queues or triggers processing (depending on existing webhooks in queue)
*
* @return bool
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
* @throws \Stripe\Exception\UnknownApiErrorException
*/
public function onReceiveWebhook() {
if (!in_array($this->eventType, CRM_Stripe_Webhook::getDefaultEnabledEvents())) {
// We don't handle this event, return 200 OK so Stripe does not retry.
return TRUE;
}
$uniqueIdentifier = $this->getWebhookUniqueIdentifier();
// Get all received webhooks with matching identifier which have not been processed
// This returns all webhooks that match the uniqueIdentifier above and have not been processed.
// For example this would match both invoice.finalized and invoice.payment_succeeded events which must be
// processed sequentially and not simultaneously.
$paymentProcessorWebhooks = \Civi\Api4\PaymentprocessorWebhook::get(FALSE)
->addWhere('payment_processor_id', '=', $this->_paymentProcessor->getID())
->addWhere('identifier', '=', $uniqueIdentifier)
->addWhere('processed_date', 'IS NULL')
->execute();
$processWebhook = FALSE;
if (empty($paymentProcessorWebhooks->rowCount)) {
// We have not received this webhook before. Record and process it.
$processWebhook = TRUE;
}
else {
// We have one or more webhooks with matching identifier
/** @var \CRM_Mjwshared_BAO_PaymentprocessorWebhook $paymentProcessorWebhook */
foreach ($paymentProcessorWebhooks as $paymentProcessorWebhook) {
// Does the eventType match our webhook?
if ($paymentProcessorWebhook->trigger === $this->eventType) {
// Yes, We have already recorded this webhook and it is awaiting processing.
// Exit
return TRUE;
}
}
// We have recorded another webhook with matching identifier but different eventType.
// There is already a recorded webhook with matching identifier that has not yet been processed.
// So we will record this webhook but will not process now (it will be processed later by the scheduled job).
}
\Civi\Api4\PaymentprocessorWebhook::create(FALSE)
->addValue('payment_processor_id', $this->_paymentProcessor->getID())
->addValue('trigger', $this->eventType)
->addValue('identifier', $uniqueIdentifier)
->addValue('event_id', $this->event_id)
->execute();
if (!$processWebhook) {
return TRUE;
}
return $this->processWebhook();
}
/**
* Process the given webhook
*
* @return bool
* @throws \CRM_Core_Exception
* @throws \Civi\API\Exception\UnauthorizedException
*/
public function processWebhook() {
try {
$success = $this->processEventType();
}
catch (Exception $e) {
$success = FALSE;
\Civi::log()->error('StripeIPN: processWebhook failed. ' . $e->getMessage());
}
$uniqueIdentifier = $this->getWebhookUniqueIdentifier();
// Record that we have processed this webhook (success or error)
// If for some reason we ended up with multiple webhooks with the same identifier and same eventType this would
// update all of them as "processed". That is ok because we don't need to process the "same" webhook multiple
// times. Even if they have different event IDs but the same identifier/eventType.
\Civi\Api4\PaymentprocessorWebhook::update(FALSE)
->addWhere('identifier', '=', $uniqueIdentifier)
->addWhere('trigger', '=', $this->eventType)
->addValue('status', $success ? 'success' : 'error')
->addValue('processed_date', 'now')
->execute();
return $success;
}
```
......@@ -10,17 +10,34 @@
</maintainer>
<urls>
<url desc="Main Extension Page">https://lab.civicrm.org/extensions/mjwshared</url>
<url desc="Documentation">https://lab.civicrm.org/extensions/mjwshared/-/blob/master/README.md</url>
<url desc="Support">https://mjw.pt/support/mjwshared</url>
<url desc="Release Notes">https://lab.civicrm.org/extensions/mjwshared/-/blob/master/docs/releasenotes.md</url>
<url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
</urls>
<releaseDate>2020-09-24</releaseDate>
<version>0.9.3</version>
<releaseDate>2025-02-26</releaseDate>
<version>1.3.4</version>
<develStage>stable</develStage>
<compatibility>
<ver>5.28</ver>
<ver>5.74</ver>
</compatibility>
<classloader>
<psr0 prefix="CRM_" path="."/>
<psr4 prefix="Civi\" path="Civi"/>
</classloader>
<civix>
<namespace>CRM/Mjwshared</namespace>
<format>25.01.1</format>
</civix>
<mixins>
<mixin>ang-php@1.0.0</mixin>
<mixin>menu-xml@1.0.0</mixin>
<mixin>mgd-php@1.0.0</mixin>
<mixin>setting-php@1.0.0</mixin>
<mixin>setting-admin@1.0.1</mixin>
<mixin>scan-classes@1.0.0</mixin>
<mixin>entity-types-php@2.0.0</mixin>
<mixin>smarty@1.0.3</mixin>
</mixins>
<upgrader>CiviMix\Schema\Mjwshared\AutomaticUpgrader</upgrader>
</extension>
This diff is collapsed.
<?php
use CRM_Mjwshared_ExtensionUtil as E;
return [
[
'name' => 'Navigation_afsearchPaymentProcessorWebhooks',
'entity' => 'Navigation',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'label' => E::ts('Payment Processor Webhooks'),
'name' => 'afsearchPaymentProcessorWebhooks',
'url' => 'civicrm/paymentprocessorwebhooks',
'icon' => 'crm-i fa-list-alt',
'permission' => [
'edit contributions',
],
'permission_operator' => 'AND',
'parent_id.name' => 'CiviContribute',
'weight' => 19,
],
'match' => ['name', 'domain_id'],
],
],
];
<?php
use CRM_Mjwshared_ExtensionUtil as E;
return [
[
'name' => 'ProcessPaymentProcessorWebhooks',
'entity' => 'Job',
'cleanup' => 'always',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'name' => 'Process PaymentProcessor Webhooks',
'description' => E::ts('Process incomplete payment processor webhooks'),
'run_frequency' => 'Always',
'api_entity' => 'Job',
'api_action' => 'process_paymentprocessor_webhooks',
'parameters' => 'delete_old=-3 month',
],
'match' => [
'name',
],
],
],
];
<?php
use CRM_Mjwshared_ExtensionUtil as E;
return [
[
'name' => 'SavedSearch_Paymentprocessor_Webhook_Detail',
'entity' => 'SavedSearch',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'name' => 'Paymentprocessor_Webhook_Detail',
'label' => E::ts('Paymentprocessor Webhook Detail'),
'api_entity' => 'PaymentprocessorWebhook',
'api_params' => [
'version' => 4,
'select' => [
'id',
'data',
'identifier',
'message',
'trigger',
'status',
],
'orderBy' => [],
'where' => [],
'groupBy' => [],
'join' => [],
'having' => [],
],
],
'match' => ['name'],
],
],
[
'name' => 'SavedSearch_Paymentprocessor_Webhook_Detail_SearchDisplay_Paymentprocessor_Webhook_Detail',
'entity' => 'SearchDisplay',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'name' => 'Paymentprocessor_Webhook_Detail',
'label' => E::ts('Paymentprocessor Webhook Detail'),
'saved_search_id.name' => 'Paymentprocessor_Webhook_Detail',
'type' => 'list',
'settings' => [
'style' => 'ul',
'limit' => 1,
'sort' => [],
'pager' => FALSE,
'columns' => [
[
'type' => 'field',
'key' => 'id',
'dataType' => 'Integer',
'label' => E::ts('ID'),
],
[
'type' => 'field',
'key' => 'identifier',
'dataType' => 'String',
'label' => E::ts('Identifier'),
],
[
'type' => 'field',
'key' => 'message',
'dataType' => 'String',
'break' => FALSE,
'label' => E::ts('Message'),
],
[
'type' => 'field',
'key' => 'trigger',
'dataType' => 'String',
'label' => E::ts('Trigger'),
],
[
'type' => 'field',
'key' => 'status',
'dataType' => 'String',
'label' => E::ts('Status'),
],
[
'type' => 'html',
'key' => 'data',
'dataType' => 'Text',
'label' => E::ts('Data'),
],
],
'placeholder' => 0,
],
],
'match' => [
'saved_search_id',
'name',
],
],
],
];
This diff is collapsed.
<?php
return [
[
'name' => 'cg_extend_objects:FinancialTrxn',
'entity' => 'OptionValue',
'cleanup' => 'unused',
'update' => 'always',
'params' => [
'version' => 4,
'values' => [
'option_group_id.name' => 'cg_extend_objects',
'label' => ts('Financial Transaction (Payment)'),
'value' => 'FinancialTrxn',
'name' => 'civicrm_financial_trxn',
'is_reserved' => TRUE,
'is_active' => TRUE,
],
'match' => ['option_group_id', 'name'],
],
],
];
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
File added
This diff is collapsed.
This diff is collapsed.