From ef598a5f7ba6b3c1180a1aeee38fa806731acad3 Mon Sep 17 00:00:00 2001 From: Rich Lott / Artful Robot Date: Mon, 25 Nov 2019 13:04:18 +0000 Subject: [PATCH 1/6] begin documenting payment processors --- docs/extensions/payment-processors/create.md | 88 ++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/docs/extensions/payment-processors/create.md b/docs/extensions/payment-processors/create.md index 44757394..4c8725b6 100644 --- a/docs/extensions/payment-processors/create.md +++ b/docs/extensions/payment-processors/create.md @@ -1,5 +1,93 @@ # Create a Payment Processor Extension +This page explains how to develop a payment processor extension. + +## Intro + +A payment processor extension typically includes: + +1. a Payment Processor class (required) - this provides CiviCRM with a standard interface/API bridge to the third party processor, and a way for CiviCRM to know which features are supported. This must extend `CRM_Core_Payment`. +2. an IPN class - optional, IPN = Instant Payment Notification, although they are often asynchronous and not "instant". Many third parties talk instead about *Webhooks*. It refers to the processing data sent from the third party on various events (e.g. completed/confirmed payment, cancellation of recurring payment, and often many more situations - depends heavily on the third party, often configurable in their account administration facilities). CiviCRM provides a base class for IPN classes `CRM_Core_Payment_BaseIPN`, and a menu route at `civicrm/payment/ipn/` where `` is the payment processor ID. +3. Customisations and additions to the settings. e.g. might use an private *API key instead* of a *password*. + + +## The Payment Processor Object + +A payment processor object extends `CRM_Core_Payment`. This class provides CiviCRM with a standard interface/API bridge to the third party processor. It should be in the directory: + +``` +/CRM/Core/Payment/MyExtension.php +``` + +The class handles data to do with the third party processor's needs and it should not have much need to store or alter core CiviCRM objects like Contributions or Financial Transaction records - that logic should be handled by CiviCRM's APIs, not the payment processor code. + +CiviCRM's Contribution and Event pages will use the processor but the class should not assume that those are the only consumers; it should be able to be used by other processes too, e.g. Drupal webform or an entirely bespoke process. + +### Responsibilities + +When (attempting) to take a payment: + +- When creating a recurring contribution, provide `trxn_id`, `invoice_id`, `processor_id` and `payment_token_id` if/where needed. +- When attempting to take payment, or when recording a payment result (IPN), provide `trxn_id`, `invoice_id`, `total_amount`, `fee_amount` +- On recurring payments, it is responsible for managing the following: + - `start_date` + - `modified_date` ??? Seems confusing between "last time payment received" (in schema xml) but should it be when, e.g. the amount was last changed? + - `failure_count`. Number of failed charge attempts since last success. Business rule could be set to deactivate on more than x failures. + - `failure_retry_date` + - `contribution_status_id` + - `next_sched_contribution_date` + + +## Outline of payment processes + +1. (If recurring:) Create a Recurring Contribution record +2. Create a Contribution record and related entities (the recurring record, memberships, events etc.) or "Order" object with status *Pending* +3. Take, or attempt to take, the payment using the payment processor +4. Create a Payment for the Contribution. CiviCRM will then complete the Order/Contribution, setting the contribution's status to *Completed* and triggering updates on any related entities. + +(UNCLEAR:) Later on, in the case of a recurring contribution, the payment processor may be involved to trigger taking a payment, or your extension's IPN code will be used to process a repeat transaction. + +Pre CiviCRM 5.(WHAT?) this process used `Contribution.transact`, `Contribution.completeTransaction`, `Contribution.repeatTransaction` but from CiviCRM 5.(WHAT?) these should be replaced. + +## IDs galore + +There are lots of different mostly string IDs used throughout the process. + +### ContributionRecur IDs + +Where an ID **is bold** it is expected to, or might be, provided by your payment processor. Everything else should be handled by CiviCRM. + +- `id` integer ID, the primary key for the `civicrm_contribution_recur` table. +- `payment_processor_id` integer ID, foreign key to `civicrm_payment_processor.id` which stores the configuration data for the payment processor. Not to be confused with the next item... +- **`processor_id`** string provided by the third party to uniquely identify this recurring payment. Many third parties might provide a *subscription ID* which might be suitable. CiviCRM does not use this internally but it may be useful to payment processors to match a recurring contribution record to the relevant object of the third party's API. +- **`payment_token_id`** string. Optionally used to store a third party token used for administering the recurring payment arrangement. +- **`trxn_id`** string. This is used differently by each payment processor and could be a subscription ID, bank account details, something else or not used. +- **`invoice_id`** string. Must be unique across all ContributionRecur records. May come from the third party or be generated by your payment processor (IS THIS RIGHT?). +- **`next_sched_contribution_date`** Next Scheduled Recurring Contribution +- `financial_type_id` integer points to one of the site's configured Contribution Types like "Donation", or "Event Fees". Payment processors should not normally have anything to do with this; it's not relevant to the third party's purposes. +- `payment_instrument_id` integer points to one of the site's configured *Payment Methods*. **Your payment processor should normally install its own payment method**. The payment instrument for a (recurring or single) contribution is supposed to be set to the payment processor's configured payment instrument value. +- `campaign_id` integer foreign key to a CiviCRM Campaign (if in use). The payment processor should not have much to do with this. +- `contact_id` integer foreign key to a CiviCRM Contact. +- `contribution_status_id` +- `contribution_type_id`, next_sched_contribution - deprecated a long time ago; do not use. + +### Contribution records + +- `id` integer ID, the primary key for the `civicrm_contribution` table. +- `financial_type_id` integer as for recurring records. +- `contribution_page_id` integer foreign key for payments that were generated by a CiviCRM contribution page. +- `payment_instrument_id` integer as for recurring records. +- **`trxn_id`** string. This is used differently by each payment processor and could be a subscription ID, account+check number, etc. Must be unique. +- **`invoice_id`** string. Must be unique across all Contribution records. May come from the third party or be generated by your payment processor (IS THIS RIGHT?). +- `contribution_recur_id` integer foreign key used when this is part of a recurring contribution. +- `address_id` Conditional foreign key to `civicrm_address.id`. We insert an address record for each contribution when we have associated billing name and address data. +- `campaign_id` integer foreign key to a CiviCRM Campaign (if in use). The payment processor should not have much to do with this. +- `contact_id`/`contribution_contact_id` integer foreign key to a CiviCRM Contact. +- `creditnote_id` string. ??? unique credit note id, system generated or passed in +- `contribution_type_id` `solicitor_id` - deprecated a long time ago; do not use. + +# Original page - kept during WIP + This page provides a step-by-step guide to creating a payment processor extension. It uses an example of one created at the University of California, Merced. It is a basic payment processor which works like this: From 91e49e1fa254d6c17ef64440bd236d138914eb54 Mon Sep 17 00:00:00 2001 From: Rich Lott / Artful Robot Date: Mon, 25 Nov 2019 13:08:06 +0000 Subject: [PATCH 2/6] add note on exceptions --- docs/extensions/payment-processors/create.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensions/payment-processors/create.md b/docs/extensions/payment-processors/create.md index 4c8725b6..7813d04e 100644 --- a/docs/extensions/payment-processors/create.md +++ b/docs/extensions/payment-processors/create.md @@ -21,7 +21,7 @@ A payment processor object extends `CRM_Core_Payment`. This class provides CiviC The class handles data to do with the third party processor's needs and it should not have much need to store or alter core CiviCRM objects like Contributions or Financial Transaction records - that logic should be handled by CiviCRM's APIs, not the payment processor code. -CiviCRM's Contribution and Event pages will use the processor but the class should not assume that those are the only consumers; it should be able to be used by other processes too, e.g. Drupal webform or an entirely bespoke process. +CiviCRM's Contribution and Event pages will use the processor but the class should not assume that those are the only consumers; it should be able to be used by other processes too, e.g. Drupal webform or an entirely bespoke process. **Therefore they should not call functions which assume a user context** such as redirects, exits, or setting status messages like `CRM_Core_Error::statusBounce`. Most methods should throw a `PaymentProcessorException` when they are unable to proceed. ### Responsibilities From 1c6e096285fd449f54ea712a179c0f7e4a4fc423 Mon Sep 17 00:00:00 2001 From: Rich Lott / Artful Robot Date: Tue, 26 Nov 2019 12:42:44 +0000 Subject: [PATCH 3/6] begin documenting propertybag --- docs/extensions/payment-processors/create.md | 146 ++++++++++++++++++- 1 file changed, 140 insertions(+), 6 deletions(-) diff --git a/docs/extensions/payment-processors/create.md b/docs/extensions/payment-processors/create.md index 7813d04e..f7f07a22 100644 --- a/docs/extensions/payment-processors/create.md +++ b/docs/extensions/payment-processors/create.md @@ -4,24 +4,158 @@ This page explains how to develop a payment processor extension. ## Intro -A payment processor extension typically includes: +A payment processor integration extension typically includes: 1. a Payment Processor class (required) - this provides CiviCRM with a standard interface/API bridge to the third party processor, and a way for CiviCRM to know which features are supported. This must extend `CRM_Core_Payment`. -2. an IPN class - optional, IPN = Instant Payment Notification, although they are often asynchronous and not "instant". Many third parties talk instead about *Webhooks*. It refers to the processing data sent from the third party on various events (e.g. completed/confirmed payment, cancellation of recurring payment, and often many more situations - depends heavily on the third party, often configurable in their account administration facilities). CiviCRM provides a base class for IPN classes `CRM_Core_Payment_BaseIPN`, and a menu route at `civicrm/payment/ipn/` where `` is the payment processor ID. + +2. an IPN class - optional, IPN means *Instant Payment Notification*, although they are usually asynchronous and not "instant". Many third parties talk instead about *Webhooks*. + + It refers to the data sent from the third party on various events e.g.: + + - completed/confirmed payment, + - cancellation of recurring payment, + - often many more situations - depends heavily on the third party, often configurable in their account administration facilities + + CiviCRM provides a base class for IPN classes `CRM_Core_Payment_BaseIPN`, and a menu route at `civicrm/payment/ipn/` where `` is the payment processor ID. + 3. Customisations and additions to the settings. e.g. might use an private *API key instead* of a *password*. +4. Other libraries and helpers for the particular Payment Processor service. + -## The Payment Processor Object +## The Payment Class -A payment processor object extends `CRM_Core_Payment`. This class provides CiviCRM with a standard interface/API bridge to the third party processor. It should be in the directory: +A payment processor object extends `CRM_Core_Payment`. This class provides CiviCRM with a standard interface/API bridge to the third party processor. It should be found at: ``` /CRM/Core/Payment/MyExtension.php ``` -The class handles data to do with the third party processor's needs and it should not have much need to store or alter core CiviCRM objects like Contributions or Financial Transaction records - that logic should be handled by CiviCRM's APIs, not the payment processor code. +The class handles data to do with the third party processor's needs. Different methods will require different data from CiviCRM, e.g. from Contribution or ContributionRecur records, and configuration data for the Payment Processor Service. + +!!!important + Try to avoid infringing on CiviCRM's logic. The methods in your extension should take inputs, communicate with the third party, and return output data that CiviCRM can use to perform its logic. If you find your extension is sending emails, duplicating logic, updating or creating records in CiviCRM, outputting user content (e.g. status messages) then stop, check and consider separating out your code into different methods. + +Because things get complex and because `CRM_Core_Payment` is a bridge between CiviCRM's logic and the payment processor service's needs, it's all too easy to end up combining business logic (like updating membership end dates, or deciding whether to send a receipt email) with your calls to the external service. If you do need to do this try to separate out your class methods. + +CiviCRM's Contribution and Event pages are obvious users of your `CRM_Core_Paymnet` class but you should not assume that those are the only consumers; it should be able to be used by other processes too, e.g. Drupal webform or an entirely bespoke process. **Therefore they should not call functions that assume a user context** such as redirects, exits, or setting status messages like `CRM_Core_Error::statusBounce`. + +!!! note + Most methods should throw a `Civi\Payment\Exception\PaymentProcessorException` when they are unable to fulfill the expectations of a method. + +## Introducing `PropertyBag` objects + +Currently as of 5.19 `$params` is a sprawling array with who-knows-what keys. From 5.21, a better way is to pass in a `Civi\Payment\PropertyBag` object instead. + +!!! danger "todo" + update 5.21 to actual value + +This object has getters and setters that enforce standardised property names and a certain level of validation and type casting. For example, the property `contactID` (note capitalisation) has `getContactID()` and `setContactID()`. + +For backwards compatibility, this class implements `ArrayAccess` which means if old code does `$propertyBag['contact_id'] = '123'` or `$propertyBag['contactID'] = 123` it will translate this to the new `contactID` property and use that setter which will ensure that accesing the property returns the integer value *123*. When this happens deprecation messages are emitted to the log file. New code should not use array access. + +### Checking for existence of a property + +Calling a getter for a property that has not been set will throw a `BadMethodCall` exception. + +Code can require certain properties by calling +`$propertyBag->require(['contactID', 'contributionID'])` which will throw an `InvalidArgumentException` if any property is missing. These calls should go at the top of your methods so that it's clear to a developer. + +You can check whether a property has been set using `$propertyBag->has('contactID')` which will return `TRUE` or `FALSE`. + +### Multiple values, e.g. changing amounts + +All the getters and setters take an optional extra parameter called `$label`. This can be used to store two (or more) different versions of a property, e.g. 'old' and 'new' + +```php +setAmount(1.23, 'old'); +$propertyBag->setAmount(2.46, 'new'); +//... +$propertyBag->getAmount('old'); // 1.23 +$propertyBag->getAmount('new'); // 2.46 +$propertyBag->getAmount(); // throws BadMethodCall +``` + +This means the value is still validated and type-cast as an amount (in this example). + +### Custom payment processor-specific data + +!!! danger "todo" + This is not yet implemented in core. + +Sometimes a payment processor will requrie custom data. e.g. Stripe may use a `paymentMethodID` and a `paymentIntentID` on its `doPayment()` method. + +Property names should be prefixed, e.g. `stripe_paymentMethodID` and set using `PropertyBag->setCustomProperty($prop, $value, $label = 'default')`. + +The payment class is responsible for validating such data; anything is allowed by `setCustomProperty`, including `NULL`. + +Where support for custom properties is needed for a method, e.g. `doPayment()`, you should implement a method as follows: + +```php +setCustomProperty( 'stripe_paymentIntentID', $input['paymentIntentID']); + + // We might need a paymentMethodID but sometimes not. + if (!empty($input['paymentMethodID'])) { + $propertyBag->setCustomProperty( 'stripe_paymentMethodID', $input['paymentMethodID']); + } + } + ... +} +``` + + +## The `doPayment()` method + +`doPayment()` will be called with the following data on the `PropertyBag` in `$params`: + +- `contactID` +- `contributionID` (See [dev/financial#53](https://lab.civicrm.org/dev/financial/issues/53)) +- `amount` e.g. *1.23* +- `isRecur` +- `email` A billing address *foo@example.com* that should be used by the external processor (if it uses it at all) + +!!! danger + The following is a proposal + + +I propose that the following changes are made throughout core: old: `$paymentProcessor->doPayment($params);` new: + +```php +getPropertyBagFromFormData($params); +$paymentProcessor->extractCustomPropertiesForDoPayment($propertyBag, $params, 'contribute'); +$result = $paymentProcessor->doPayment($propertyBag, 'contribute'); +``` + +Where `getPropertyBagFromFormData()` deals with all the `setAmount()` etc. as needed, and also handles all the quick-form-specific logic - e.g. getting the correct billing email (see MJWShared trait for example). + +---------------------------- +**stop reading here, not done the rest yet.** -CiviCRM's Contribution and Event pages will use the processor but the class should not assume that those are the only consumers; it should be able to be used by other processes too, e.g. Drupal webform or an entirely bespoke process. **Therefore they should not call functions which assume a user context** such as redirects, exits, or setting status messages like `CRM_Core_Error::statusBounce`. Most methods should throw a `PaymentProcessorException` when they are unable to proceed. ### Responsibilities From 95f0c6c80b883809ec0f24ae2eb1c3c3112f5bc0 Mon Sep 17 00:00:00 2001 From: Rich Lott / Artful Robot Date: Tue, 14 Jan 2020 09:59:26 +0000 Subject: [PATCH 4/6] Improve payment processor creation docs --- docs/extensions/payment-processors/create.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/extensions/payment-processors/create.md b/docs/extensions/payment-processors/create.md index f7f07a22..1d07c3be 100644 --- a/docs/extensions/payment-processors/create.md +++ b/docs/extensions/payment-processors/create.md @@ -87,13 +87,15 @@ This means the value is still validated and type-cast as an amount (in this exam !!! danger "todo" This is not yet implemented in core. -Sometimes a payment processor will requrie custom data. e.g. Stripe may use a `paymentMethodID` and a `paymentIntentID` on its `doPayment()` method. +Sometimes a payment processor will requrie custom data. e.g. A company called Stripe offers payment processing gateway services with its own API which requires some extra parameters called `paymentMethodID` and `paymentIntentID` parameters - these are what that particular 3rd party requires and separate to anything in CiviCRM (CiviCRM also uses the concept of "payment methods" and these have IDs, but here we're talking about something Stripe needs). -Property names should be prefixed, e.g. `stripe_paymentMethodID` and set using `PropertyBag->setCustomProperty($prop, $value, $label = 'default')`. +In order for us to be able to implement the `doPayment()` method for Stripe, we'll need data for these custom, bespoke-to-the-third-party parameters passing in, via the `PropertyBag`. + +So that any custom, non-CiviCRM data is handled unambiguously, these property names should be prefixed, e.g. `stripe_paymentMethodID` and set using `PropertyBag->setCustomProperty($prop, $value, $label = 'default')`. The payment class is responsible for validating such data; anything is allowed by `setCustomProperty`, including `NULL`. -Where support for custom properties is needed for a method, e.g. `doPayment()`, you should implement a method as follows: +Where support for custom properties is needed for a method, e.g. `doPayment()`, you should implement a method as follows. This example assumes that the third party requires a special parameter called `paymentIntentID` - your payment processor might require one called `customerToken`, `destinationAccountID`, `looksFriendlyButErodesDemocracyCode` or anything else - here we use `paymentIntentID` as a real world example from Stripe. ```php setCustomProperty( 'stripe_paymentIntentID', $input['paymentIntentID']); - // We might need a paymentMethodID but sometimes not. + // Following our example of Stripe, the third party might require a parameter called + // paymentMethodID but only if we don't have a paymentMethodID - this logic is dictated + // by the third party API, and is included here just as an example. We can store this on + // another prefixed custom property, if it's set/needed: if (!empty($input['paymentMethodID'])) { $propertyBag->setCustomProperty( 'stripe_paymentMethodID', $input['paymentMethodID']); } From 7f30f08bb86e97e4960323563d23f981a0f576ca Mon Sep 17 00:00:00 2001 From: Rich Lott / Artful Robot Date: Tue, 14 Jan 2020 10:11:15 +0000 Subject: [PATCH 5/6] Improve payment processor creation docs more --- docs/extensions/payment-processors/create.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/extensions/payment-processors/create.md b/docs/extensions/payment-processors/create.md index 1d07c3be..fbab7f80 100644 --- a/docs/extensions/payment-processors/create.md +++ b/docs/extensions/payment-processors/create.md @@ -25,7 +25,7 @@ A payment processor integration extension typically includes: ## The Payment Class -A payment processor object extends `CRM_Core_Payment`. This class provides CiviCRM with a standard interface/API bridge to the third party processor. It should be found at: +A payment processor object *extends* `CRM_Core_Payment`. This class provides CiviCRM with a standard interface/API bridge to the third party processor. It should be found at: ``` /CRM/Core/Payment/MyExtension.php @@ -36,9 +36,9 @@ The class handles data to do with the third party processor's needs. Different m !!!important Try to avoid infringing on CiviCRM's logic. The methods in your extension should take inputs, communicate with the third party, and return output data that CiviCRM can use to perform its logic. If you find your extension is sending emails, duplicating logic, updating or creating records in CiviCRM, outputting user content (e.g. status messages) then stop, check and consider separating out your code into different methods. -Because things get complex and because `CRM_Core_Payment` is a bridge between CiviCRM's logic and the payment processor service's needs, it's all too easy to end up combining business logic (like updating membership end dates, or deciding whether to send a receipt email) with your calls to the external service. If you do need to do this try to separate out your class methods. +Because things get complex and because `CRM_Core_Payment` is a bridge between CiviCRM's logic and the payment processor service's needs, it's all too easy to end up combining business logic (like updating membership end dates, or deciding whether to send a receipt email) with your calls to the external service. Your extended `CRM_Core_Payment` class should **not** alter business logic. If you find yourself needing to then first discuss this e.g. on the dev channel at to check if there's a better way. -CiviCRM's Contribution and Event pages are obvious users of your `CRM_Core_Paymnet` class but you should not assume that those are the only consumers; it should be able to be used by other processes too, e.g. Drupal webform or an entirely bespoke process. **Therefore they should not call functions that assume a user context** such as redirects, exits, or setting status messages like `CRM_Core_Error::statusBounce`. +CiviCRM's Contribution and Event pages are obvious users of your extended `CRM_Core_Payment` class but you should not assume that those are the only consumers; it should be able to be used by other processes too, e.g. Drupal webform or an entirely bespoke process. **Therefore they should not call functions that assume a user context** such as redirects, exits, or setting status messages like `CRM_Core_Error::statusBounce`. !!! note Most methods should throw a `Civi\Payment\Exception\PaymentProcessorException` when they are unable to fulfill the expectations of a method. From 1b9c2b7b9b6022215d6d52e56cada661295c039b Mon Sep 17 00:00:00 2001 From: Rich Lott / Artful Robot Date: Mon, 13 Apr 2020 09:58:10 +0100 Subject: [PATCH 6/6] Get Payment Processor creation how-to to "good enough", "better than before" standard, if not perfect! --- docs/extensions/payment-processors/create.md | 140 ++++--------------- 1 file changed, 25 insertions(+), 115 deletions(-) diff --git a/docs/extensions/payment-processors/create.md b/docs/extensions/payment-processors/create.md index fbab7f80..3c77b2e8 100644 --- a/docs/extensions/payment-processors/create.md +++ b/docs/extensions/payment-processors/create.md @@ -45,10 +45,7 @@ CiviCRM's Contribution and Event pages are obvious users of your extended `CRM_C ## Introducing `PropertyBag` objects -Currently as of 5.19 `$params` is a sprawling array with who-knows-what keys. From 5.21, a better way is to pass in a `Civi\Payment\PropertyBag` object instead. - -!!! danger "todo" - update 5.21 to actual value +Currently as of CiviCRM v5.24 `$params` is a sprawling array with who-knows-what keys. However, we are moving to a more prescriptive, typed way to pass in parameters by using a `Civi\Payment\PropertyBag` object instead of an array. This object has getters and setters that enforce standardised property names and a certain level of validation and type casting. For example, the property `contactID` (note capitalisation) has `getContactID()` and `setContactID()`. @@ -84,10 +81,10 @@ This means the value is still validated and type-cast as an amount (in this exam ### Custom payment processor-specific data -!!! danger "todo" - This is not yet implemented in core. +!!! warning "Warning" + This is currently holding back a fuller adoption of PropertyBag. -Sometimes a payment processor will requrie custom data. e.g. A company called Stripe offers payment processing gateway services with its own API which requires some extra parameters called `paymentMethodID` and `paymentIntentID` parameters - these are what that particular 3rd party requires and separate to anything in CiviCRM (CiviCRM also uses the concept of "payment methods" and these have IDs, but here we're talking about something Stripe needs). +Sometimes a payment processor will requrie custom data. e.g. A company called Stripe offers payment processing gateway services with its own API which requires some extra parameters called `paymentMethodID` and `paymentIntentID` - these are what that particular 3rd party requires and separate to anything in CiviCRM (CiviCRM also uses the concept of "payment methods" and these have IDs, but here we're talking about something Stripe needs). In order for us to be able to implement the `doPayment()` method for Stripe, we'll need data for these custom, bespoke-to-the-third-party parameters passing in, via the `PropertyBag`. @@ -95,118 +92,32 @@ So that any custom, non-CiviCRM data is handled unambiguously, these property na The payment class is responsible for validating such data; anything is allowed by `setCustomProperty`, including `NULL`. -Where support for custom properties is needed for a method, e.g. `doPayment()`, you should implement a method as follows. This example assumes that the third party requires a special parameter called `paymentIntentID` - your payment processor might require one called `customerToken`, `destinationAccountID`, `looksFriendlyButErodesDemocracyCode` or anything else - here we use `paymentIntentID` as a real world example from Stripe. - -```php -setCustomProperty( 'stripe_paymentIntentID', $input['paymentIntentID']); - - // Following our example of Stripe, the third party might require a parameter called - // paymentMethodID but only if we don't have a paymentMethodID - this logic is dictated - // by the third party API, and is included here just as an example. We can store this on - // another prefixed custom property, if it's set/needed: - if (!empty($input['paymentMethodID'])) { - $propertyBag->setCustomProperty( 'stripe_paymentMethodID', $input['paymentMethodID']); - } - } - ... -} -``` - - -## The `doPayment()` method - -`doPayment()` will be called with the following data on the `PropertyBag` in `$params`: +**However**, payment classes are rarely responsible for passing data in, this responsibility is for core and custom implementations. Core's contribution forms and other UIs need a way to take the data POSTed by the forms, and arrange it into a standard format for payment classes. They will also have to pass the rest of the data to the payment class so that the payment class can extract and validate anything that is bespoke to that payment processor; i.e. only Stripe is going to know to expect a `paymentMethodID` in this data because this does not exist for other processors. **As of 5.24, this does not exist** and data is still passed into a PropertyBag as an array, which means that unrecognised keys will be added as custom properties, but emit a deprecation warning in your logs. -- `contactID` -- `contributionID` (See [dev/financial#53](https://lab.civicrm.org/dev/financial/issues/53)) -- `amount` e.g. *1.23* -- `isRecur` -- `email` A billing address *foo@example.com* that should be used by the external processor (if it uses it at all) +Best practice right now would be to: -!!! danger - The following is a proposal +- use PropertyBag getters for the data you want, whether that's a core field or use `getCustomProperty` for anything else. - -I propose that the following changes are made throughout core: old: `$paymentProcessor->doPayment($params);` new: - -```php -getPropertyBagFromFormData($params); -$paymentProcessor->extractCustomPropertiesForDoPayment($propertyBag, $params, 'contribute'); -$result = $paymentProcessor->doPayment($propertyBag, 'contribute'); -``` - -Where `getPropertyBagFromFormData()` deals with all the `setAmount()` etc. as needed, and also handles all the quick-form-specific logic - e.g. getting the correct billing email (see MJWShared trait for example). +- where your processor requires adding in custom data to the form, prefix it with your extension's name to avoid ambiguity with core fields. e.g. your forms might use a field called `myprocessor_weirdCustomToken` and you would access this via `$propertyBag->getCustomProperty('myprocessor_weirdCustomToken)`. ---------------------------- **stop reading here, not done the rest yet.** - -### Responsibilities - -When (attempting) to take a payment: - -- When creating a recurring contribution, provide `trxn_id`, `invoice_id`, `processor_id` and `payment_token_id` if/where needed. -- When attempting to take payment, or when recording a payment result (IPN), provide `trxn_id`, `invoice_id`, `total_amount`, `fee_amount` -- On recurring payments, it is responsible for managing the following: - - `start_date` - - `modified_date` ??? Seems confusing between "last time payment received" (in schema xml) but should it be when, e.g. the amount was last changed? - - `failure_count`. Number of failed charge attempts since last success. Business rule could be set to deactivate on more than x failures. - - `failure_retry_date` - - `contribution_status_id` - - `next_sched_contribution_date` - - -## Outline of payment processes - -1. (If recurring:) Create a Recurring Contribution record -2. Create a Contribution record and related entities (the recurring record, memberships, events etc.) or "Order" object with status *Pending* -3. Take, or attempt to take, the payment using the payment processor -4. Create a Payment for the Contribution. CiviCRM will then complete the Order/Contribution, setting the contribution's status to *Completed* and triggering updates on any related entities. - -(UNCLEAR:) Later on, in the case of a recurring contribution, the payment processor may be involved to trigger taking a payment, or your extension's IPN code will be used to process a repeat transaction. - -Pre CiviCRM 5.(WHAT?) this process used `Contribution.transact`, `Contribution.completeTransaction`, `Contribution.repeatTransaction` but from CiviCRM 5.(WHAT?) these should be replaced. - ## IDs galore There are lots of different mostly string IDs used throughout the process. ### ContributionRecur IDs -Where an ID **is bold** it is expected to, or might be, provided by your payment processor. Everything else should be handled by CiviCRM. +Where an ID has a \* by it, the ID, it's up to your payment processor to provide this (if relevant). Everything else should be handled by CiviCRM. - `id` integer ID, the primary key for the `civicrm_contribution_recur` table. - `payment_processor_id` integer ID, foreign key to `civicrm_payment_processor.id` which stores the configuration data for the payment processor. Not to be confused with the next item... -- **`processor_id`** string provided by the third party to uniquely identify this recurring payment. Many third parties might provide a *subscription ID* which might be suitable. CiviCRM does not use this internally but it may be useful to payment processors to match a recurring contribution record to the relevant object of the third party's API. -- **`payment_token_id`** string. Optionally used to store a third party token used for administering the recurring payment arrangement. -- **`trxn_id`** string. This is used differently by each payment processor and could be a subscription ID, bank account details, something else or not used. -- **`invoice_id`** string. Must be unique across all ContributionRecur records. May come from the third party or be generated by your payment processor (IS THIS RIGHT?). -- **`next_sched_contribution_date`** Next Scheduled Recurring Contribution +- `processor_id` \* string provided by the third party to uniquely identify this recurring payment. Many third parties might provide a *subscription ID* which might be suitable. CiviCRM does not use this internally but it may be useful to payment processors to match a recurring contribution record to the relevant object of the third party's API. +- `payment_token_id` \* string. Optionally used to store a third party token used for administering the recurring payment arrangement. +- `trxn_id` \* string. This is used differently by each payment processor and could be a subscription ID, bank account details, something else or not used. +- `invoice_id` \* string. Must be unique across all ContributionRecur records. May come from the third party or be generated by your payment processor (IS THIS RIGHT?). +- `next_sched_contribution_date` \* Next Scheduled Recurring Contribution - `financial_type_id` integer points to one of the site's configured Contribution Types like "Donation", or "Event Fees". Payment processors should not normally have anything to do with this; it's not relevant to the third party's purposes. - `payment_instrument_id` integer points to one of the site's configured *Payment Methods*. **Your payment processor should normally install its own payment method**. The payment instrument for a (recurring or single) contribution is supposed to be set to the payment processor's configured payment instrument value. - `campaign_id` integer foreign key to a CiviCRM Campaign (if in use). The payment processor should not have much to do with this. @@ -220,8 +131,8 @@ Where an ID **is bold** it is expected to, or might be, provided by your payment - `financial_type_id` integer as for recurring records. - `contribution_page_id` integer foreign key for payments that were generated by a CiviCRM contribution page. - `payment_instrument_id` integer as for recurring records. -- **`trxn_id`** string. This is used differently by each payment processor and could be a subscription ID, account+check number, etc. Must be unique. -- **`invoice_id`** string. Must be unique across all Contribution records. May come from the third party or be generated by your payment processor (IS THIS RIGHT?). +- `trxn_id` \* string. This is used differently by each payment processor and could be a subscription ID, account+check number, etc. Must be unique. +- `invoice_id` \* string. Must be unique across all Contribution records. May come from the third party or be generated by your payment processor (IS THIS RIGHT?). - `contribution_recur_id` integer foreign key used when this is part of a recurring contribution. - `address_id` Conditional foreign key to `civicrm_address.id`. We insert an address record for each contribution when we have associated billing name and address data. - `campaign_id` integer foreign key to a CiviCRM Campaign (if in use). The payment processor should not have much to do with this. @@ -278,8 +189,8 @@ Checkout - the info is entered at the processors' site. 1. Use civix to [generate an skeleton extension](../civix.md#generate-module) 1. Identify the processor type {:#type} - - Read about [processor types](types.md) to find out which type you have. + + Read about [processor types](/extensions/payment-processors/types.md) to find out which type you have. 1. Add the processor to the database @@ -295,19 +206,15 @@ Checkout - the info is entered at the processors' site. Edit your [info.xml file](../info-xml.md) and add the [typeInfo section](../info-xml.md#typeInfo) with all relevant child elements. +## Example processor - -### Create the Default Payment Processor File - -Depending on your billing mode there are different considerations. The file -will live in `CRM/Core/Payment` and have the same name as entered into -your `processor_type` table. - +!!! warning + The rest of this page is not up to date and needs review. It is left here as some of it may still be helpful, but it should not be considered accurate or best practice. ### Create Initial Processing File In our example our file name is UCMPaymentCollection so the name of the -file we are going to create is UCMPaymentCollection.php +file we are going to create within our extension's directory is `CRM/Core/Payment/UCMPaymentCollection.php` It should have this basic template. @@ -881,6 +788,9 @@ your extension with the new library. ## Testing Processor Plugins {:#testing} +!!! warning + This section is still mostly valid advice but your extension should also contain a suite of PHPUnit tests that show how your code is supposed to function in all the given situations it handles. + Here's some suggestions of what you might test once you have written your payment processor plug in.