diff --git a/docs/.artifacts/4.x/events.json b/docs/.artifacts/4.x/events.json index 960f8f91e..cec725196 100644 --- a/docs/.artifacts/4.x/events.json +++ b/docs/.artifacts/4.x/events.json @@ -37530,6 +37530,12 @@ "type": "craft\\events\\RegisterComponentTypesEvent", "desc": "The event that is triggered when registering field types." }, + { + "class": "craft\\services\\Fields", + "name": "EVENT_DEFINE_COMPATIBLE_FIELD_TYPES", + "type": "craft\\events\\DefineCompatibleFieldTypesEvent", + "desc": "The event that is triggered when defining the compatible field types for a field." + }, { "class": "craft\\services\\Fields", "name": "EVENT_BEFORE_SAVE_FIELD_GROUP", diff --git a/docs/.artifacts/commerce/4.x/events.json b/docs/.artifacts/commerce/4.x/events.json index 08bdbe3ec..fcf430cd9 100644 --- a/docs/.artifacts/commerce/4.x/events.json +++ b/docs/.artifacts/commerce/4.x/events.json @@ -2458,6 +2458,24 @@ "type": "yii\\base\\ActionEvent", "desc": "an event raised right after executing a controller action." }, + { + "class": "craft\\commerce\\console\\controllers\\GatewaysController", + "name": "EVENT_DEFINE_ACTIONS", + "type": "craft\\events\\DefineConsoleActionsEvent", + "desc": "The event that is triggered when defining custom actions for this controller." + }, + { + "class": "craft\\commerce\\console\\controllers\\GatewaysController", + "name": "EVENT_BEFORE_ACTION", + "type": "yii\\base\\ActionEvent", + "desc": "an event raised right before executing a controller action. You may set `ActionEvent::isValid` to be false to cancel the action execution." + }, + { + "class": "craft\\commerce\\console\\controllers\\GatewaysController", + "name": "EVENT_AFTER_ACTION", + "type": "yii\\base\\ActionEvent", + "desc": "an event raised right after executing a controller action." + }, { "class": "craft\\commerce\\console\\controllers\\ResetDataController", "name": "EVENT_DEFINE_ACTIONS", diff --git a/docs/.vuepress/theme/components/SidebarLink.vue b/docs/.vuepress/theme/components/SidebarLink.vue index ce4bb8b06..9b8e197c7 100644 --- a/docs/.vuepress/theme/components/SidebarLink.vue +++ b/docs/.vuepress/theme/components/SidebarLink.vue @@ -60,6 +60,12 @@ export default { }; function renderLink(h, to, text, active, level) { + const hashPosition = to.indexOf('#'); + const path = to.substring(0, hashPosition > 0 ? hashPosition : to.length); + const segments = path.split('/'); + const lastSegment = segments[segments.length - 1]; + const handle = lastSegment.replace('.html', ''); + const component = { props: { to, @@ -69,6 +75,8 @@ function renderLink(h, to, text, active, level) { class: { active, "sidebar-link": true, + // Include a “slug” identifier when this isn't a jump link: + [`slug-${handle || 'root'}`]: hashPosition < 0, }, }; diff --git a/docs/4.x/deployment.md b/docs/4.x/deployment.md index b7193f96c..38dd09018 100644 --- a/docs/4.x/deployment.md +++ b/docs/4.x/deployment.md @@ -101,7 +101,7 @@ Proprietary and open source cloud computing solutions are both options for hosti ## Deployment -Broadly, we’re defining _deployment_ as the process of publishing code changes to a live website. +Broadly, we’re defining _deployment_ as the process of publishing code changes to a live website. For the following examples, we’ll assume your project uses the standard [directory structure](directory-structure.md). ::: tip Be sure and read our [Deployment Best Practices](kb:deployment-best-practices) article for some high-level recommendations. What follows is intended for technical users who are tasked with extending their workflow to a web server. @@ -161,7 +161,7 @@ With a generic deployment framework in place, we’re ready to get into a few co Let’s assume you’ve cloned your project onto a host, and configured it to serve requests directly out of the `web/` directory. -Within the project directory, a simple Git-based deployment might look like this: +Within the project’s root directory, a simple Git-based deployment might look like this: ```bash # Fetch new code: diff --git a/docs/4.x/extend/controllers.md b/docs/4.x/extend/controllers.md index 868712e31..3b8cd6b01 100644 --- a/docs/4.x/extend/controllers.md +++ b/docs/4.x/extend/controllers.md @@ -405,7 +405,7 @@ public function actionEdit(?int $id = null, ?Widget $widget = null) $widgets = MyPlugin::getInstance()->getWidgets(); // Do we have an incoming Widget ID from the route? - if ($widgetId !== null) { + if ($id !== null) { // Was a Widget model passed back via route params? It should take priority: if ($widget === null) { // Nope, let’s look it up: @@ -473,16 +473,16 @@ public function actionSave() if (!$widgets->saveWidget($widget)) { // Hand the model back to the original route: return $this->asModelFailure( - $widget, + $widget, // Model, passed back under the key, below... Craft::t('my-plugin', 'Something went wrong!'), // Flash message 'widget', // Route param key ); } return $this->asModelSuccess( - $widget, + $widget, // Model (included in JSON responses) Craft::t('my-plugin', 'Widget saved.'), // Flash message - 'widget', // Route param key + 'widget', // Key the model will be under (in JSON responses) 'my-plugin/widgets/{id}', // Redirect “object template” ); } diff --git a/docs/4.x/extend/element-types.md b/docs/4.x/extend/element-types.md index d55ac8bd5..1f6b9f376 100644 --- a/docs/4.x/extend/element-types.md +++ b/docs/4.x/extend/element-types.md @@ -144,7 +144,7 @@ use craft\helpers\Db; public function afterSave(bool $isNew) { if (!$this->propagating) { - Db::upsert('{{%products}}', [ + Db::upsert('{{%plugin_products}}', [ 'id' => $this->id, ], [ 'price' => $this->price, diff --git a/docs/4.x/extend/queue-jobs.md b/docs/4.x/extend/queue-jobs.md index d9f85fc7c..ea17c0074 100644 --- a/docs/4.x/extend/queue-jobs.md +++ b/docs/4.x/extend/queue-jobs.md @@ -103,11 +103,11 @@ public function execute($queue): void ### Dealing with Failed Jobs -In our first example, exceptions from the mailer can bubble out of our job—but in the second example, we’re to ensuring the job is not halted prematurely. +In our first example, exceptions from the mailer can bubble out of our job—but in the second example, we catch those errors so the job is not halted prematurely. This decision is up to you: if the work in a job is nonessential (or will be done again later, like <craft4:craft\queue\jobs\GeneratePendingTransforms>), you can catch and log errors and let the job end nominally; if the work is critical (like synchronizing something to an external API), it may be better to let the exception bubble out of `execute()`. -The queue wraps every job in its own `try` block, and will flag any jobs that generate exceptions as failed. The exception message that caused the failure will be recorded along with the job. Failed jobs can be retried from the control panel or with the `php craft queue/retry [id]` command. +The queue wraps every job in its own `try` block, and will mark any jobs that throw exceptions as _failed_. The exception message that caused the failure will be recorded along with the job. Failed jobs can be retried from the control panel or with the `php craft queue/retry [id]` command. #### Retryable Jobs diff --git a/docs/4.x/graphql.md b/docs/4.x/graphql.md index 1dc1c8352..6656a9347 100644 --- a/docs/4.x/graphql.md +++ b/docs/4.x/graphql.md @@ -177,7 +177,7 @@ You can manage your schemas in the control panel at **GraphQL** → **Schemas**. ### Using the GraphiQL IDE -The easiest way to start exploring your GraphQL API is with the built-in [GraphiQL](https://github.com/graphql/graphiql) IDE, which is available in the control panel from **GraphQL** → **Explore**. +The easiest way to start exploring your GraphQL API is with the built-in [GraphiQL](https://github.com/graphql/graphiql) IDE, which is available in the control panel from **GraphQL** → **GraphiQL**.  @@ -1460,6 +1460,7 @@ This is the interface implemented by all users. | `status`| `String` | The element’s status. | `dateCreated`| `DateTime` | The date the element was created. | `dateUpdated`| `DateTime` | The date the element was last updated. +| `photo`| `AssetInterface` | The user’s photo. | `friendlyName`| `String` | The user’s first name or username. | `fullName`| `String` | The user’s full name. | `name`| `String!` | The user’s full name or username. diff --git a/docs/4.x/installation.md b/docs/4.x/installation.md index 191723820..526393832 100644 --- a/docs/4.x/installation.md +++ b/docs/4.x/installation.md @@ -78,7 +78,7 @@ Done for the day? [`ddev stop`](https://ddev.readthedocs.io/en/stable/users/basi ### Workflow + Collaboration -We encourage starting with a local development environment (rather than a remote host) as a means of of a defined workflow—whatever it may be—to the reliability and longevity of a website. +We encourage starting with a local development environment (rather than a remote host) fosters a workflow that will support the reliability and longevity of a website. <See path="./deployment.md#workflow" label="Defining a Workflow" /> diff --git a/docs/commerce/4.x/dev/controller-actions.md b/docs/commerce/4.x/dev/controller-actions.md index d7ab4e22c..856b7f667 100644 --- a/docs/commerce/4.x/dev/controller-actions.md +++ b/docs/commerce/4.x/dev/controller-actions.md @@ -12,22 +12,22 @@ We recommend reviewing the main Craft documentation on [working with controller ## Available Actions -Action | Description ------- | ----------- -<badge vertical="baseline" type="verb">POST</badge> [cart/complete](#post-cart-complete) | Completes an order without payment. -<badge vertical="baseline" type="verb">GET</badge> [cart/get-cart](#get-cart-get-cart) | Returns the current cart as JSON. -<badge vertical="baseline" type="verb">GET/POST</badge> [cart/load-cart](#get-post-cart-load-cart) | Loads a cookie for the given cart. -<badge vertical="baseline" type="verb">POST</badge> [cart/forget-cart](#get-post-cart-forget-cart) | Loads a cookie for the given cart. -<badge vertical="baseline" type="verb">POST</badge> [cart/update-cart](#post-cart-update-cart) | Manage a customer’s current [cart](../orders-carts.md). -<badge vertical="baseline" type="verb">POST</badge> [payment-sources/add](#post-payment-sources-add) | Creates a new payment source. -<badge vertical="baseline" type="verb">POST</badge> [payment-sources/delete](#post-payment-sources-delete) | Deletes a payment source. -<badge vertical="baseline" type="verb">GET</badge> [payments/complete-payment](#get-payments-complete-payment) | Processes customer’s return from an off-site payment. -<badge vertical="baseline" type="verb">POST</badge> [payments/pay](#post-payments-pay) | Makes a payment on an order. -<badge vertical="baseline" type="verb">GET</badge> [downloads/pdf](#get-downloads-pdf) | Returns an order PDF as a file. -<badge vertical="baseline" type="verb">POST</badge> [subscriptions/subscribe](#post-subscriptions-subscribe) | Starts a new subscription. -<badge vertical="baseline" type="verb">POST</badge> [subscriptions/cancel](#post-subscriptions-cancel) | Cancels an active subscription. -<badge vertical="baseline" type="verb">POST</badge> [subscriptions/switch](#post-subscriptions-switch) | Switch an active subscription’s plan. -<badge vertical="baseline" type="verb">POST</badge> [subscriptions/reactivate](#post-subscriptions-reactivate) | Reactivates a canceled subscription. +Methods | Action | Description +--- | --- | --- +<badge vertical="baseline" type="verb">POST</badge> | [cart/complete](#post-cart-complete) | Completes an order without payment. +<badge vertical="baseline" type="verb">GET</badge> | [cart/get-cart](#get-cart-get-cart) | Returns the current cart as JSON. +<badge vertical="baseline" type="verb">GET/POST</badge> | [cart/load-cart](#get-post-cart-load-cart) | Loads a cookie for the given cart. +<badge vertical="baseline" type="verb">POST</badge> | [cart/forget-cart](#get-post-cart-forget-cart) | Removes a cookie for the current cart. <Since ver="4.3.0" product="Commerce" repo="craftcms/commerce" feature="Forgetting carts" /> +<badge vertical="baseline" type="verb">POST</badge> | [cart/update-cart](#post-cart-update-cart) | Manage a customer’s current [cart](../orders-carts.md). +<badge vertical="baseline" type="verb">POST</badge> | [payment-sources/add](#post-payment-sources-add) | Creates a new payment source. +<badge vertical="baseline" type="verb">POST</badge> | [payment-sources/delete](#post-payment-sources-delete) | Deletes a payment source. +<badge vertical="baseline" type="verb">GET</badge> | [payments/complete-payment](#get-payments-complete-payment) | Processes customer’s return from an off-site payment. +<badge vertical="baseline" type="verb">POST</badge> | [payments/pay](#post-payments-pay) | Makes a payment on an order. +<badge vertical="baseline" type="verb">GET</badge> | [downloads/pdf](#get-downloads-pdf) | Returns an order PDF as a file. +<badge vertical="baseline" type="verb">POST</badge> | [subscriptions/subscribe](#post-subscriptions-subscribe) | Starts a new subscription. +<badge vertical="baseline" type="verb">POST</badge> | [subscriptions/cancel](#post-subscriptions-cancel) | Cancels an active subscription. +<badge vertical="baseline" type="verb">POST</badge> | [subscriptions/switch](#post-subscriptions-switch) | Switch an active subscription’s plan. +<badge vertical="baseline" type="verb">POST</badge> | [subscriptions/reactivate](#post-subscriptions-reactivate) | Reactivates a canceled subscription. [Address management](/4.x/addresses.md/#managing-addresses) actions are part of the main Craft documentation. Commerce also allows address information to be set directly on a cart via <badge vertical="baseline" type="verb">POST</badge> [cart/update-cart](#post-cart-update-cart). @@ -208,15 +208,16 @@ State | `application/json` ### <badge vertical="baseline" type="verb">POST</badge> `payment-sources/add` -Creates a new payment source. +Creates a new payment source for the current customer. #### Supported Params Param | Description ----- | ----------- -`*` | All body parameters will be provided directly to the gateway’s [payment form](../payment-form-models.md) model. +`*` | All body parameters will be provided directly to the gateway’s payment form model. `description` | Description for the payment source. `gatewayId` | ID of the new payment source’s gateway, which must support payment sources. +`isPrimaryPaymentSource` | Send a non-empty value to make this the customer’s primary payment source. #### Response @@ -232,7 +233,7 @@ State | `text/html` | `application/json` </span> ::: warning -Note that successful requests will return the [payment _source_](../saving-payment-sources.md) that was created; failures will bounce back the [payment _form_](../payment-form-models.md) with errors. +Note that the models available in success and failure states are different! ::: ### <badge vertical="baseline" type="verb">POST</badge> `payment-sources/delete` @@ -271,7 +272,7 @@ Param | Description `email` | Email address of the person responsible for payment, which must match the email address on the order. Required if the order being paid is not the active cart. `gatewayId` | The payment gateway ID to be used for payment. `number` | The order number payment should be applied to. When ommitted, payment is applied to the current cart. -`paymentAmount` | Hashed payment amount, expressed in the cart’s `paymentCurrency`, available only if [partial payments](../making-payments.md#checkout-with-partial-payment) are allowed. +`paymentAmount` | Hashed payment amount, expressed in the cart’s `paymentCurrency`. Available only if [partial payments](../making-payments.md#checkout-with-partial-payment) are allowed. `paymentCurrency` | ISO code of a configured [payment currency](../payment-currencies.md) to be used for the payment. `paymentSourceId` | The ID for a payment source that should be used for payment. `registerUserOnOrderComplete` | Whether the customer should have an account created on order completion. diff --git a/docs/commerce/4.x/making-payments.md b/docs/commerce/4.x/making-payments.md index 6d4d56075..7b514abaf 100644 --- a/docs/commerce/4.x/making-payments.md +++ b/docs/commerce/4.x/making-payments.md @@ -1,24 +1,26 @@ # Making Payments -Once you’ve set up [the store](configuration.md) and [payment gateways](payment-gateways.md), you can start accepting payments. - -Commerce supports taking payments from the customer at checkout and from the store manager via the Craft control panel. Payments can be required for order completion, deferred until later, or made in parts depending on your store’s configuration and gateway support. +Commerce supports taking payments from the customer at checkout and by a store manager via the Craft control panel. Payments can be [required](#full-payment-at-checkout) for order completion, [deferred](#checkout-without-payment) until later, or made [in parts](#checkout-with-partial-payment), depending on your store’s configuration and [gateway](payment-gateways.md) support. ## Full Payment at Checkout -It’s most common to have a customer provide information for the payment gateway and pay in full to complete an order. +The most common checkout process involves a customer paying in full by furnishing payment method details to a gateway. + +In this example, we’ll assume a customer has finished shopping, and that we know what payment gateway they intend to use—it could be that the store only uses one gateway, or that they were given an opportunity to select a gateway in a previous step. -In this example, we’ll assume a customer has finished shopping and building a cart and that we know what payment gateway they intend to use. (It could be that the store only uses one gateway, or they explicitly chose a gateway in a previous step.) +::: tip +Send a `gatewayId` param to the [`commerce/cart/update-cart` action](dev/controller-actions.md#post-cart-update-cart) to select a gateway ahead of time. +::: -The payment gateway is set on the cart, and any forms meant for it must be namespaced. This template uses `cart.gateway.getPaymentFormHtml()` to render the form fields required by the payment gateway, posting them to the [`commerce/payments/pay`](./dev/controller-actions.html#post-payments-pay) controller action: +This template uses `cart.gateway.getPaymentFormHtml()` to render the form fields required by the payment gateway, posting them to the [`commerce/payments/pay`](./dev/controller-actions.html#post-payments-pay) controller action: ```twig {# @var cart craft\commerce\elements\Order #} <form method="post"> {{ csrfInput() }} {{ actionInput('commerce/payments/pay') }} - {{ redirectInput('/commerce/customer/order?number={number}') }} - {{ hiddenInput('cancelUrl', '/commerce/checkout/payment'|hash) }} + {{ redirectInput('commerce/customer/order?number={number}') }} + {{ hiddenInput('cancelUrl', 'commerce/checkout/payment'|hash) }} {% namespace cart.gateway.handle|commercePaymentFormNamespace %} {{ cart.gateway.getPaymentFormHtml({})|raw }} @@ -29,116 +31,34 @@ The payment gateway is set on the cart, and any forms meant for it must be names ``` ::: tip -Using `gateway.getPaymentFormHtml()` is the quick way to get form elements from the gateway plugin; often you’ll want to render and style these form fields based on the needs of your site and gateway. -::: - -This manual form example assumes the availability of a `paymentForm` variable, as discussed in [Payment Form Models](payment-form-models.md), and might be what a simple credit card payment form would look like: - -<ToggleTip :height="465"> - -```twig -{% import '_includes/forms.twig' as forms %} -<form method="post"> - {{ csrfInput() }} - {{ actionInput('commerce/payments/pay') }} - {{ redirectInput('/commerce/customer/order?number={number}') }} - {{ hiddenInput('cancelUrl', '/commerce/checkout/payment'|hash) }} +Using `gateway.getPaymentFormHtml()` is the quickest way to get form elements from the gateway; often, you’ll want to render and style these form fields based on the needs of your site and gateway. - {% namespace cart.gateway.handle|commercePaymentFormNamespace %} - {# First and last name #} - <fieldset> - <legend>Card Holder</legend> - - {{ forms.text({ - name: 'firstName', - maxlength: 70, - placeholder: 'First Name', - autocomplete: false, - class: 'card-holder-first-name'~(paymentForm.getErrors('firstName') ? ' error'), - value: paymentForm.firstName, - required: true, - }) }} - - {{ forms.text({ - name: 'lastName', - maxlength: 70, - placeholder: 'Last Name', - autocomplete: false, - class: 'card-holder-last-name'~(paymentForm.getErrors('lastName') ? ' error'), - value: paymentForm.lastName, - required: true, - }) }} - - {% set errors = [] %} - {% for attributeKey in ['firstName', 'lastName'] %} - {% set errors = errors|merge(paymentForm.getErrors(attributeKey)) %} - {% endfor %} - - {{ forms.errorList(errors) }} - </fieldset> - - {# Card number #} - <fieldset> - <legend>Card</legend> - - {{ forms.text({ - name: 'number', - maxlength: 19, - placeholder: 'Card Number', - autocomplete: false, - class: 'card-number'~(paymentForm.getErrors('number') ? ' error'), - value: paymentForm.number - }) }} - - {{ forms.text({ - name: 'expiry', - class: 'card-expiry'~(paymentForm.getErrors('month') or paymentForm.getErrors('year') ? ' error'), - type: 'tel', - placeholder: 'MM / YYYY', - value: paymentForm.expiry - }) }} - - {{ forms.text({ - name: 'cvv', - type: 'tel', - placeholder: 'CVV', - class: 'card-cvc'~(paymentForm.getErrors('cvv') ? ' error'), - value: paymentForm.cvv - }) }} - - {% set errors = [] %} - {% for attributeKey in ['number', 'month', 'year', 'cvv'] %} - {% set errors = errors|merge(paymentForm.getErrors(attributeKey)) %} - {% endfor %} - - {{ forms.errorList(errors) }} - </fieldset> - - <button>Pay Now</button> - {% endnamespace %} -</form> -``` - -</ToggleTip> +Each gateway will have unique requirements for the data you submit when making a payment—consult its documentation for more information. +::: ## Deferred Payment at Checkout -Commerce provides two options if you don’t want to require full payment from a customer to complete an order: +Commerce provides a couple options if you don’t want to require full payment from a customer to complete an order: -1. Require full payment with an authorize transaction to be captured later by the store manager. (Requires a gateway that supports authorize transactions.) -2. Enable the [allowCheckoutWithoutPayment](config-settings.md#allowcheckoutwithoutpayment) setting and have the customer complete checkout—without payment—via the [`commerce/cart/complete`](./dev/controller-actions.md#post-cart-complete) controller action. +1. Authorize a payment to be captured later by the store manager (not all gateways support authorize-only transactions); +1. Configure your store to allow [checkout without payment](#checkout-without-payment); +1. Use the built-in [Manual gateway](payment-gateways.md#manual-gateway) to track an offsite payment process; + +::: tip +The first-party Stripe gateway supports additional payment plans via third-party services, but their availability varies by region, customer creditworthiness, and other factors. +::: ### Checkout Without Payment -Once the [allowCheckoutWithoutPayment](config-settings.md#allowcheckoutwithoutpayment) setting is enabled, the customer can submit a post request to the [`commerce/cart/complete`](./dev/controller-actions.md#post-cart-complete) controller action to complete the order without any payment. +Once the [allowCheckoutWithoutPayment](config-settings.md#allowcheckoutwithoutpayment) setting is enabled, the customer can submit a POST request to the [`commerce/cart/complete`](./dev/controller-actions.md#post-cart-complete) controller action to complete the order without any payment. ```twig <form method="post"> {{ csrfInput() }} {{ actionInput('commerce/cart/complete') }} - {{ redirectInput('/shop/customer/order?number='~cart.number~'&success=true') }} + {{ redirectInput('/shop/customer/order?number={number}&success=true') }} - <button>Commit to buy</button> + <button>Buy Now + Pay Later</button> </form> ``` @@ -151,13 +71,15 @@ Like the [`commerce/payments/pay`](./dev/controller-actions.html#post-payments-p ::: warning If you enable order completion without payment, completed orders will have the same status as any others. Don’t forget to make sure store managers are aware of the change and prepared to confirm payment before fulfilling orders! + +If you use these workflows, consider adding columns to the main Order [element indexes](/4.x/elements.md#indexes) for _Date Paid_ or _Amount Paid_ so that it is clear which orders need attention. ::: ### Checkout with Partial Payment A _partial_ payment is one that’s less than an order’s outstanding balance at any point in time. -If you’d like to permit customers to check out with partial payments and the gateway supports them, you can enable the [allowPartialPaymentOnCheckout](config-settings.md#allowpartialpaymentoncheckout) setting to allow an additional `paymentAmount` field when posting to the [`commerce/payments/pay`](./dev/controller-actions.html#post-payments-pay) controller action. (If no `paymentAmount` field is submitted, the order’s oustanding balance amount will be applied.) +If you’d like to permit customers to check out with partial payments and the gateway supports them, you can enable the [allowPartialPaymentOnCheckout](config-settings.md#allowpartialpaymentoncheckout) setting to allow an additional hashed `paymentAmount` field when posting to the [`commerce/payments/pay`](./dev/controller-actions.html#post-payments-pay) controller action. If no `paymentAmount` field is submitted, the order’s outstanding balance will be used. ::: tip Multiple payments can still be made on an order when `allowPartialPaymentOnCheckout` is `false`, as long as each payment is equal to the outstanding balance at the time it was made. @@ -189,11 +111,11 @@ This example provides a dropdown menu that allows the customer to choose half or %} <select name="paymentAmount"> - <option value="{{ halfAmount|hash }}">50% - ({{ halfAmount|commerceCurrency(cart.paymentCurrency) }}) + <option value="{{ halfAmount|hash }}"> + 50% ({{ halfAmount|commerceCurrency(cart.paymentCurrency) }}) </option> - <option value="{{ fullAmount|hash }}">100% - ({{ fullAmount|commerceCurrency(cart.paymentCurrency) }}) + <option value="{{ fullAmount|hash }}"> + 100% ({{ fullAmount|commerceCurrency(cart.paymentCurrency) }}) </option> </select> @@ -206,6 +128,8 @@ A customer may return to make additional payments similarly to the [outstanding You can use the [`paidInFull`](extend/events.md#paidinfull) event if you need to add any custom functionality when an order is paid in full. ::: +An order is considered “paid in full” as long as the total amount paid did at one point reach the order’s total—even if a payment is refunded. + ### Paying an Outstanding Balance You can allow a customer to pay the outstanding balance on a cart or order using the [`commerce/payments/pay`](./dev/controller-actions.html#post-payments-pay) controller action similarly to taking full payment at checkout, taking care to explicitly provide the order number whose outstanding balance should be paid. @@ -214,7 +138,7 @@ You can allow a customer to pay the outstanding balance on a cart or order using There’s a full example of this in the [example templates](example-templates.md) at [shop/checkout/pay-static.twig](https://github.com/craftcms/commerce/tree/main/example-templates/dist/shop/checkout/pay-static.twig). ::: -Here we’re pretending the relevant order number is 12345, the customer’s email address is email@address.foo, and the gateway is already set on the order: +Here, we’re pretending the relevant order number is `12345`, the customer’s email address is `email@address.foo`, and the gateway is already set on the order: ```twig {% set number = '12345' %} @@ -225,7 +149,7 @@ Here we’re pretending the relevant order number is 12345, the customer’s ema <form method="post"> {{ csrfInput() }} {{ actionInput('commerce/payments/pay') }} - {{ redirectInput('/shop/customer/order?number='~cart.number~'&success=true') }} + {{ redirectInput('/shop/customer/order?number={number}&success=true') }} {{ hiddenInput('cancelUrl', cancelUrl) }} {{ hiddenInput('email', email) }} {{ hiddenInput('orderNumber', cart.number) }} @@ -238,7 +162,7 @@ Here we’re pretending the relevant order number is 12345, the customer’s ema </form> ``` -If you’d like to have the customer pay with a different gateway than whatever’s specified on the cart/order, pass the `gatewayId` in the form: +If you’d like to have the customer pay with a different gateway than whatever was specified on the cart/order, pass the `gatewayId` in the form: ```twig {# ... #} diff --git a/docs/commerce/4.x/payment-form-models.md b/docs/commerce/4.x/payment-form-models.md index 46efb8961..006d4837a 100644 --- a/docs/commerce/4.x/payment-form-models.md +++ b/docs/commerce/4.x/payment-form-models.md @@ -12,13 +12,21 @@ Generally, you shouldn’t be concerned with the specific type of payment form m The following payment form model attributes exist for gateways handling credit card information: -| Attribute | Validation | Description | -| -------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `token` | not validated | If a token is found on the payment form, no validation of other fields is performed and the data is ignored.<br><br>The token represents a pre-validated credit card and is provided by a gateway’s client-side JavaScript library. One example of this is [Stripe.js](https://stripe.com/docs/stripe-js). | -| `firstName` | required | The first name on the customer’s credit card. | -| `lastName` | required | The last name of the customer’s credit card. | -| `month` | required, min: `1`, max: `12` | Integer representing the month of credit card expiry. | -| `year` | required, min: current year, max: current year + 12 | Integer representing the year of credit card expiry. | -| `CVV` | minLength: 3, maxLength: 4 | Integer found on the back of the card for security. | -| `number` | [Luhn algorithm](https://en.wikipedia.org/wiki/Luhn_algorithm) | The credit card number itself. | -| `threeDSecure` | not validated | A flag indicating whether 3D Secure authentication is being performed for the transaction. | +::: danger +**We highly discourage creating gateways that directly capture credit card information. Instead, the `token` attribute can be used to transport a one-time use identifier for a payment method that is [tokenized](#tokenization) by a client-side script.** +::: + +| Attribute | Validation | Description | +| --- | --- | --- | +| `token` | not validated | If a token is present on the payment form, no validation of other fields is performed and the data is ignored. | +| `firstName` | required | The first name on the customer’s credit card. | +| `lastName` | required | The last name of the customer’s credit card. | +| `month` | required, min: `1`, max: `12` | Integer representing the month of credit card expiry. | +| `year` | required, min: current year, max: current year + 12 | Integer representing the year of credit card expiry. | +| `CVV` | minLength: 3, maxLength: 4 | Integer found on the back of the card for security. | +| `number` | [Luhn algorithm](https://en.wikipedia.org/wiki/Luhn_algorithm) | The credit card number itself. | +| `threeDSecure` | not validated | A flag indicating whether 3D Secure authentication is being performed for the transaction. | + +## Tokenization + +Whenever possible, gateways should pre-validate credit card information using the processor’s client-side JavaScript library. The [Stripe](https://plugins.craftcms.com/commerce-stripe) plugin does exactly this—many other payment processors have equivalent tokenization systems that avoid sending sensitive information to your server. diff --git a/docs/commerce/4.x/payment-gateways.md b/docs/commerce/4.x/payment-gateways.md index 902ef77c0..20470a38b 100644 --- a/docs/commerce/4.x/payment-gateways.md +++ b/docs/commerce/4.x/payment-gateways.md @@ -1,84 +1,103 @@ # Payment Gateways -Craft Commerce payments gateways are provided by Craft CMS plugins. +As a self-hosted ecommerce solution, Commerce handles payments differently from popular software-as-a-service products—instead of providing a fixed list of payment processors, we designed flexible gateway and transaction APIs that let -To create a payment gateway you must install the appropriate plugin, navigate to **Commerce** → **Settings** → **Gateways**, and add configuration for that gateway. For more detailed instructions, see each plugin’s `README.md` file. +To capture live payments, you must install a [payment gateway plugin](#first-party-gateway-plugins). Commerce comes with two [built-in gateways](#built-in-gateways), but they are intended primarily for testing. -Payment gateways generally fit in one of two categories: +In the control panel, navigate to **Commerce** → **Settings** → **Gateways**, and click **+ New gateway**. Each gateway requires different settings—for more detailed instructions, see the plugin’s documentation. Many payment processors require third-party accounts, and provide credentials for communicating with their infrastructure that must be added to the gateway’s configuration. -- External or _offsite_ gateways. -- Merchant-hosted or _onsite_ gateways. +::: tip +When providing secrets in the control panel, we recommend using the special [environment variable syntax](/4.x/config/README.md#control-panel-settings) to prevent them leaking into project config. +::: -Merchant-hosted gateways collect the customer’s credit card details directly on your site, but have much stricter requirements such as an SSL certificate for your server. You will also be subject to much more rigorous security requirements under the PCI DSS (Payment Card Industry Data Security Standard). These security requirements are your responsibility, but some gateways allow payment card tokenization. +Payment gateways (and the specific methods they support) generally use one of two payment flows: + +- **External or _offsite_ gateways:** The customer is redirected to a payment portal hosted by the processor, and is returned to your site once a payment is completed. Your site never sees information about the customer’s payment method—instead, the gateway receives and validates a temporary token, and signals to Commerce that the transaction was successful. +- **Merchant-hosted or _onsite_ gateways:** Payment details are sent directly to your store, and the gateway forwards them to the payment processor. These implementations have _much_ higher risk profiles and are subject to rigorous security requirements under the PCI DSS (Payment Card Industry Data Security Standard). + +Most gateways available for Commerce use a [tokenization](https://squareup.com/us/en/the-bottom-line/managing-your-finances/what-does-tokenization-actually-mean) process in the customer’s browser that (at a technical level) has a great deal in common with an _offsite_ gateway, while preserving the smooth checkout experience of an _onsite_ gateway. ## First-Party Gateway Plugins -| Plugin | Gateways | Remarks | 3D Secure Support | -| ------------------------------------------------------------------------ | --------------------------------------- | ------------------------------------------------------------------ | ------------------- | -| [Stripe](https://plugins.craftcms.com/commerce-stripe) | Stripe | Uses Stripe SDK; only first-party gateway to support subscriptions | Yes | -| [PayPal Checkout](https://plugins.craftcms.com/commerce-paypal-checkout) | PayPal Checkout | | Yes | -| [Sage Pay](https://plugins.craftcms.com/commerce-sagepay) | SagePay Direct; SagePay Server | | Yes | -| [MultiSafepay](https://plugins.craftcms.com/commerce-multisafepay) | MultiSafePay REST | Does not support authorize charges | Yes | -| [Worldpay](https://plugins.craftcms.com/commerce-worldpay) | Worldpay JSON | | No | -| [eWay](https://plugins.craftcms.com/commerce-eway) | eWAY Rapid | Supports storing payment information | Yes | -| [Mollie](https://plugins.craftcms.com/commerce-mollie) | Mollie | Does not support authorize charges | Yes | -| [PayPal](https://plugins.craftcms.com/commerce-paypal) _deprecated_ | PayPal Pro; PayPal REST; PayPal Express | PayPal REST supports storing payment information | Only PayPal Express | +| Plugin | Gateways | Remarks | 3D Secure Support | +| --- | --- | --- | --- | +| [Stripe](https://plugins.craftcms.com/commerce-stripe) | Stripe | Uses Stripe’s Payment Intents API; only first-party gateway to support subscriptions | Yes | +| [PayPal Checkout](https://plugins.craftcms.com/commerce-paypal-checkout) | PayPal Checkout | | Yes | +| [Sage Pay](https://plugins.craftcms.com/commerce-sagepay) | SagePay Direct; SagePay Server | | Yes | +| [MultiSafepay](https://plugins.craftcms.com/commerce-multisafepay) | MultiSafePay REST | Does not support authorize charges | Yes | +| [Worldpay](https://plugins.craftcms.com/commerce-worldpay) | Worldpay JSON | | No | +| [eWay](https://plugins.craftcms.com/commerce-eway) | eWAY Rapid | Supports storing payment information | Yes | +| [Mollie](https://plugins.craftcms.com/commerce-mollie) | Mollie | Does not support authorize charges | Yes | +| [PayPal](https://plugins.craftcms.com/commerce-paypal) _deprecated_ | PayPal Pro; PayPal REST; PayPal Express | PayPal REST supports storing payment information | Only PayPal Express | + +Additional third-party gateways can be found in the [Plugin Store](https://plugins.craftcms.com/categories/ecommerce?craft4). + +Before using a plugin-provided gateway, consult the its readme for specifics. Gateways themselves do not implement the logic to process payments against financial institutions, and therefore have external dependencies and fees. -## Dummy Gateway +## Built-in Gateways -After installation, Craft Commerce will install some demo products and a basic config along with a Dummy payment gateway for testing. +### Dummy Gateway -This dummy gateway driver is only for testing with placeholder credit card numbers. A valid card number ending in an even digit will get a successful response. If the last digit is an odd number, the driver will return a generic failure response: +The _Dummy_ gateway is only for testing with placeholder credit card numbers. A “valid” card number (passing a simple [Luhn](https://en.wikipedia.org/wiki/Luhn_algorithm) check) ending in an _even_ digit will simulate a successful payment. If the last digit is _odd_, the gateway will treat it as a failed payment: Example Card Number | Dummy Gateway Response ------------------- | ---------------------- -4242424242424242 | <span class="text-green"> <check-mark class="inline" /> Success</span> -4444333322221111 | <span class="text-red"> <x-mark class="inline" /> Failure</span> +4242424242424242 | <span class="text-green"> <check-mark class="inline" /> Success</span> +4444333322221111 | <span class="text-red"> <x-mark class="inline" /> Failure</span> -## Manual Gateway +::: danger +**Do not** use real credit card information when testing, as it may be captured as plain text in logs or caches. +::: -The manual payment gateway is a special gateway that does not communicate with any third party. +### Manual Gateway -You should use the manual payment gateway to accept checks or bank deposits: it simply authorizes all payments allowing the order to proceed. Once the payment is received, the payment can be manually marked as captured in the control panel. +The _Manual_ payment gateway does not communicate with any third party, nor accept any additional data during checkout. -## Other Gateway Specifics +You should use the Manual payment gateway to accept checks, bank deposits, or other offline payment: it “authorizes” all payments, allowing the order to be submitted into the default order status. Once the payment is received, the payment can be manually marked as “captured” in the control panel by an administrator. -Before using a plugin-provided gateway, consult the plugin’s readme for specifics pertaining to the gateway. +Multiple manual gateways can be created to track different kinds of offline payments, like _Cash_ or _Check_. Each gateway can also be made available to customers only when their order total is zero—perfect for things like free sample packs or event tickets. ## Adding Gateways -Additional payment gateways can be added to Commerce with relatively little work. The [first-party gateway plugins](#first-party-gateway-plugins), with the exception of Stripe, use the [Omnipay payment library](https://github.com/craftcms/commerce-omnipay) and can be used as point of reference when creating your own. +Additional payment gateways can be added to Commerce with relatively little work. All our [first-party gateway plugins](#first-party-gateway-plugins) (with the exception of Stripe) use the [Omnipay library](https://github.com/craftcms/commerce-omnipay) and can be used as a point of reference when creating your own. -See the _Extending Commerce_ section’s [Payment Gateway Types](extend/payment-gateway-types.md) page to learn about building your own gateway plugin or module. +See the _Extending Commerce_ section’s [Payment Gateway Types](extend/payment-gateway-types.md) page to learn about building your own gateway in a plugin or module. ## Storing Config Outside of the Database When you’re configuring gateways in the Craft control panel, we recommend using [environment variables](/4.x/config/#control-panel-settings) so environment-specific settings and sensitive API keys don’t end up in the database or project config. -If you must override gateway settings, you can still do that using a standard config file for your gateway plugin (i.e. `config/commerce-stripe.php`)—but be aware that you’ll only be able to provide one set of settings for that gateway. +Gateways may expose options via their plugin settings file (i.e. `config/commerce-stripe.php`), but they will apply to _all_ instances of that gateway. ## Payment Sources -Craft Commerce supports storing payment sources for select gateways. Storing a payment source allows for a more streamlined shopping experience for your customers. +Some gateways support storing and reusing payment sources for a more streamlined customer experience. This is typically a limitation of the payment processor’s API—Commerce itself makes the functionality available to all gateway plugins. The following [first-party provided gateways](#first-party-gateway-plugins) support payment sources: - Stripe -- PayPal REST +- PayPal REST (Deprecated) - eWAY Rapid ## 3D Secure Payments -3D Secure payments add another authentication step for payments. If a payment has been completed using 3D Secure authentication, the liability for fraudulent charges is shifted from the merchant to the card issuer. -Support for this feature depends on the gateway used and its settings. +3D Secure is an important authentication step for customers in many markets. If a payment has been completed using 3D Secure authentication, the liability for fraudulent charges is shifted from the merchant to the card issuer. Support for this feature depends on the gateway used and its settings. + +Transactions that require additional offsite authorization (indicated by the processor) are typically marked as a “Redirect,” and get completed or captured after the customer is returned to the store. Each gateway handles this in a way that is specific to the payment processor’s + +::: tip +If you see payments stuck in “Redirect” status, it may be because the customer never completed an authorization challenge. Gateways can report completed-but-failed challenges back to Commerce, so that the customer may retry. +::: + +Gateways that support 3D Secure (or other asynchronous verification processes, like installment plans) may require webhooks to be configured for payments to work as expected. ## Partial Refunds -All [first-party provided gateways](#first-party-gateway-plugins) support partial refunds as of Commerce 2.0. +All [first-party provided gateways](#first-party-gateway-plugins) support partial refunds. You may only issue refunds to the original payment method used in a transaction, and up to the amount paid in that transaction. If [multiple payments](making-payments.md#checkout-with-partial-payment) were made, you must refund them separately. ## Templating -### craft.commerce.gateways.getAllCustomerEnabledGateways +### craft.commerce.gateways.getAllCustomerEnabledGateways() Returns all payment gateways available to the customer.