Skip to content

Commit

Permalink
Merge branch 'main' into shopx-1683-update-upgrade-guides
Browse files Browse the repository at this point in the history
  • Loading branch information
IKarbowiak authored Jan 15, 2025
2 parents 474cf16 + c307cf5 commit 79da8bf
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 5 deletions.
28 changes: 25 additions & 3 deletions docs/developer/checkout/cookbook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ To allow for this, you can create a checkout with a total price of `0`. This can

## Example of checkout flows

### Creating order before processing payment
### Creating order before processing payment (using custom app)

The advantage of this flow that prices, discount and stock are frozen before payment is created.

Expand All @@ -35,7 +35,9 @@ sequenceDiagram
Custom App -->>- Customer: Order paid
```

### Processing payment before creating an order
### Processing payment before creating an order (using custom app)

In this flow payment is made in Checkout, before Order is created - this way stocks are not reserved until a payment is made.

```mermaid
sequenceDiagram
Expand All @@ -45,13 +47,33 @@ sequenceDiagram
Custom App ->>+ Saleor: Create transaction for checkout
Saleor -->>- Custom App: Transaction created and <br>attached to checkout
Custom App -->>- Customer: Payment paid
Customer->>+Custom App: Conver checkout into an order
Customer->>+Custom App: Convert checkout into an order
Custom App ->>+ Saleor: Create order from checkout
Saleor ->> Saleor: Attach checkout's transactions to order
Saleor -->>- Custom App: Paid order
Custom App-->>-Customer: Order
```

### Processing payment before creating an order (using Saleor Transaction API)

Similar to previous flow, but instead of communicating with a custom app directly, Saleor Transaction API is used, payment app can be easily swapped for another.

Learn more about this integration in [Transactions Overview](/developer/payments/overview.mdx).

```mermaid
sequenceDiagram
Customer ->>+ Saleor: Request to make<br> transaction for Checkout
Saleor ->>+ Payment app: Sync webhook
Payment app ->>+ Payment provider: Process payment for checkout
Payment provider -->>- Payment app: Payment with proper status
Payment app ->>- Saleor: Return result
Saleor -->> Saleor: Transaction created and <br>attached to checkout
Saleor -->>- Customer: Transaction paid
Customer->>+Saleor: Convert checkout into an order<br>(checkoutComplete mutation)
Saleor ->> Saleor: Attach checkout's transactions to order
Saleor-->>-Customer: Order
```

### Creating order from checkout without Payments

Creating unpaid orders is possible for channels that have [`allowUnpaidOrders`](/api-reference/miscellaneous/objects/order-settings#ordersettingsallowunpaidordersboolean---) setting enabled.
Expand Down
10 changes: 10 additions & 0 deletions docs/developer/checkout/lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ To avoid overloading the database, unfinished and unpaid checkouts are automatic
- anonymous checkouts (neither user nor email is set) with lines after 30 days,
- user checkouts (either user or email is set) with lines after 90 days.

:::note
The checkout deletion task is triggered by [celery beat scheduler](developer/running-saleor/task-queue.mdx#periodic-tasks).
This feature will not work without task queue configuration.
:::

## Releasing Funds for Abandoned Checkouts

Payments for items left in the cart by customers who did not complete the purchase will be refunded to the customer's account.
Expand All @@ -58,6 +63,11 @@ The release action is triggered only once. If a subscription for a release event

To fetch paid checkouts, use the below query:

:::note
The releasing funds for abandoned checkouts task is triggered by [celery beat scheduler](developer/running-saleor/task-queue.mdx#periodic-tasks).
This feature will not work without task queue configuration.
:::

```graphql {4}
{
checkouts(first: 10, filter: { authorizeStatus: [PARTIAL, FULL] }) {
Expand Down
5 changes: 5 additions & 0 deletions docs/developer/checkout/order-expiration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ In the above example, orders will expire after 360 minutes (6 hours), and after
Once an unpaid order is created (via [`checkoutComplete`](api-reference/checkout/mutations/checkout-complete.mdx) or [`orderCreateFromCheckout`](api-reference/orders/mutations/order-create-from-checkout.mdx)), it remains in `UNCONFIRMED` status. If the customer does not complete the payment within `expireOrdersAfter` minutes after that, the status will change to `EXPIRED` and stock allocation will be released.
After `deleteExpiredOrdersAfter` days orders in `EXPIRED` status will be deleted and disappear from the order list.

:::note
The order expiration and order deletion tasks are triggered by [celery beat scheduler](developer/running-saleor/task-queue.mdx#periodic-tasks).
This feature will not work without task queue configuration.
:::

## How to disable order expiration

To disable order expiration, set `expireOrdersAfter` to 0.
Expand Down
5 changes: 5 additions & 0 deletions docs/developer/community/contributing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,11 @@ Also, do not forget about the docstring, especially in a complicated function.
So far, we have mainly used the `GinIndex` and `ilike` operators for searching, but currently, we are testing a new solution with the use of `SearchVector` and `SearchRank`.
You can find it in this PR [#9344](https://github.com/saleor/saleor/pull/9344).

:::note
The search vector update task is triggered by [celery beat scheduler](developer/running-saleor/task-queue.mdx#periodic-tasks).
This feature will not work without task queue configuration.
:::

### API

- Use `id` for mutation inputs instead of `model_name_id`.
Expand Down
4 changes: 2 additions & 2 deletions docs/developer/discounts/promotions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ When multiple promotions apply to one product, only the discount from the promot
The discounts from rules within the single promotion are not summed up.

:::note
The catalogue discounts from promotions are calculated in the background. So the discounted
price will not be instantly visible after promotion creation.
The catalogue discounts from promotions are calculated in the background, triggered by [celery beat scheduler](developer/running-saleor/task-queue.mdx#periodic-tasks).
So the discounted price will not be instantly visible after promotion creation (default interval is set to 30 seconds).
:::

### Examples of the use cases
Expand Down
2 changes: 2 additions & 0 deletions docs/developer/payments/lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,8 @@ Each event can be excluded from calculation if it has matching `*_FAILURE` event

To learn more about each event type, see [Transaction event types](#transaction-event-types) section.

To learn more about amount calculations see [Price calculations](/developer/payments/price-calculations.mdx) page.

#### Events deduplication

Each event in `TransactionItem` is identified by `pspReference`. This field is used to de-duplicate events in case there were reported multiple times by the payment app. Deduplication is done within single `TransactionItem`.
Expand Down
203 changes: 203 additions & 0 deletions docs/developer/payments/price-calculations.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
---
title: Price calculations
---

This document provides an overview of how various transaction amount fields in a `TransactionItem` are calculated and influenced. Additionally, it provides details on how `totalBalance` is calculated for Checkout and Order entities.

## Transaction Amount Fields Overview

The following fields are calculated based on transaction events and their types:

- **`authorizedAmount`**: The total amount successfully authorized.
- **`chargedAmount`**: The total amount successfully charged.
- **`refundedAmount`**: The total amount successfully refunded.
- **`canceledAmount`**: The total amount successfully canceled.
- **`authorizePendingAmount`**: The total amount of pending authorizations.
- **`chargePendingAmount`**: The total amount of pending charges.
- **`refundPendingAmount`**: The total amount of pending refunds.
- **`cancelPendingAmount`**: The total amount of pending cancellations.

### Calculation process

When the transaction amounts are recalculated, firstly all values are set to zero,
then the calculations are applied in the following order:
1. recalculate amounts based on events without PSP reference
2. recalculate amounts based on `AUTHORIZATION` events
3. recalculate amounts based on `CHARGE` events
4. recalculate amounts based on `REFUND` events
5. recalculate amounts based on `CANCEL` events
At each step, the amounts are incrementally adjusted by adding or subtracting values
calculated in the current step to the totals from the previous step.

### Assumptions

#### 1. Event Grouping:
- Events are grouped based on their PSP reference and type of action (e.g., authorization, charge).
- Grouping ensures that events of the same type and PSP reference are properly matched to calculate amounts accurately.
#### 2. Pending Amounts:
- Pending amounts (`authorizePendingAmount`, `chargePendingAmount`, `refundPendingAmount`, `cancelPendingAmount`) are increased only if a `REQUEST` event exists for the corresponding PSP reference.
- If a success or failure event is also associated with the same PSP reference, the system assumes the requested amount has already been processed, and the pending amount will not be increased.
#### 3. `SUCCESS` and `FAILURE` Events:
- The transaction amount (e.g., `authorizedAmount`, `chargedAmount`) is increased only when a `SUCCESS` event exists.
- If both a `SUCCESS` and `FAILURE` event are present for the same PSP reference, the system compares their creation timestamps:
- If the `FAILURE` event is newer, the `SUCCESS` event is ignored.
- If the `SUCCESS` event is newer, it is used for the calculations.
#### 4. Events Without PSP References Resulting from `TransactionCreate` and `TransactionUpdate`:
- Events without PSP references can result from using the [`TransactionCreate`](/api-reference/payments/mutations/transaction-create.mdx)
or [`TransactionUpdate`](/api-reference/payments/mutations/transaction-update.mdx) mutations. These events are automatically created to ensure accurate transaction amount calculations.
The following events might lack a PSP reference:
- `AUTHORIZATION_SUCCESS`
- `AUTHORIZATION_ADJUSTMENT`
- `CHARGE_SUCCESS`
- `CHARGE_BACK`
- `REFUND_SUCCESS`
- `REFUND_REVERSE`
- `CANCEL_SUCCESS`
#### 5. Adjustment Events:
- The `AUTHORIZATION_ADJUSTMENT`, overrides previous amount from previous `AUTHORIZATION` events, this means that if an adjustment is present,
older authorization events are ignored, and the adjusted amount becomes the new authorized value.

This logic ensures that transaction amounts reflect the current state of the transaction based on its event history while accounting for both processed and pending actions.

### Calculation details

#### For events without `pspReference`

The events that lacks the `pspReference` resulting from `TransactionCreate` and `TransactionUpdate` mutations are influence the transaction amounts as follow:
- `AUTHORIZATION_SUCCESS` increases `authorizedAmount` by the event amount
- `AUTHORIZATION_ADJUSTMENT` overrides the existing `authorizedAmount` by the even amount
- `CHARGE_SUCCESS` increases `chargedAmount` by the event amount
- `CHARGE_BACK` reduces `chargedAmount` by the event amount
- `REFUND_SUCCESS` increases `refundedAmount` by the event amount
- `REFUND_REVERSE` increases `chargedAmount` by the event amount
- `CANCEL_SUCCESS` increases `canceledAmount` by the event amount

#### For events with `pspReference`

:::note
Events like `X_REQUEST`, generated by Saleor when calling the payment app, are excluded from calculations until a corresponding event with a valid `psp_reference` is received from the payment app.
For example, an `AUTHORIZATION_REQUEST` event will not affect the calculations until an `AUTHORIZATION_SUCCESS` or a similar event containing a `psp_reference` is returned by the payment app.
:::

The following calculation rules apply to events that share the same `pspReference`.

#### Authorized Value ([`transactionItem.authorizedAmount`](/api-reference/payments/objects/transaction-item#transactionitemauthorizedamountmoney---))
- **GIVEN** there is an `AUTHORIZATION_SUCCESS` event
- **WHEN** there is no `AUTHORIZATION_FAILURE`
- **OR WHEN** the `AUTHORIZATION_FAILURE` event is older than `AUTHORIZATION_SUCCESS`
- **THEN** value increases by the `AUTHORIZATION_SUCCESS` event's amount.
- **GIVEN** an `AUTHORIZATION_ADJUSTMENT` event
- **WHEN** it's the latest event of this type
- **THEN** value is overwritten by the `AUTHORIZATION_ADJUSTMENT` event amount.
- **GIVEN** there is a `CHARGE_REQUEST` or `CHARGE_SUCCESS` event:
- **THEN** value is reduced by the event's amount.
- **GIVEN** there is a `CANCEL_REQUEST` or `CANCEL_SUCCESS` event:
- **THEN** value is reduced by the event's amount.
- **GIVEN** all calculations were performed and we have result `authorizationAmount`
- **WHEN** calculated `authorizationAmount` is below `0`
- **THEN** `authorizationAmount` is set to `0` (it cannot be lower than 0)

#### Authorization Pending Value ([`transactionItem.authorizePendingAmount`](/api-reference/payments/objects/transaction-item#transactionitemauthorizependingamountmoney---))
- **GIVEN** there is an `AUTHORIZATION_REQUEST` event:
- **WHEN** there is no `AUTHORIZATION_FAILURE` or `AUTHORIZATION_SUCCESS`
- **THEN** value increases by the `AUTHORIZATION_REQUEST` event's amount

#### Charge Value ([`transactionItem.chargedAmount`](/api-reference/payments/objects/transaction-item#transactionitemchargedamountmoney---))
- **GIVEN** there is a `CHARGE_SUCCESS` event
- **WHEN** there is no `CHARGE_FAILURE`
- **OR WHEN** the `CHARGE_FAILURE` event is older than `CHARGE_SUCCESS`
- **THEN** value increases by the `CHARGE_SUCCESS` event's amount.
- **GIVEN** there is a `CHARGE_BACK` event
- **THEN**value is reduced by the event's amount
- **GIVEN** there is a `REFUND_REQUEST` or `REFUND_SUCCESS` event:
- **THEN** value is reduced by the event's amount.
- **GIVEN** there is a `REFUND_REVERSE`event:
- **THEN** value is increased by the event's amount.
- This value can be negative.

:::note
Please note that chargedAmount is not affected by `CANCEL` events, as these are solely used for matching with `AUTHORIZATION` events.
:::

#### Charge Pending Value ([`transactionItem.chargePendingAmount`](/api-reference/payments/objects/transaction-item#transactionitemchargependingamountmoney---))
- **GIVEN** there is a `CHARGE_REQUEST` event:
- **WHEN** there is no `CHARGE_FAILURE` or `CHARGE_SUCCESS`
- **THEN** value increases by the `CHARGE_REQUEST` event's amount


#### Refunded Value ([`transactionItem.refundedAmount`](/api-reference/payments/objects/transaction-item#transactionitemrefundedamountmoney---))
- **GIVEN** there is a `REFUND_SUCCESS` event
- **WHEN** there is no `REFUND_FAILURE`
- **OR WHEN** the `REFUND_FAILURE` event is older than `REFUND_SUCCESS`
- **THEN** value increases by the `REFUND_SUCCESS` event's amount.
- **GIVEN** there is a `REFUND_REVERSE` event:
- **THEN** value is reduced by the event's amount.

:::note
`CHARGE_BACK` events, which behave similarly to refunds by reducing the `chargedAmount`, are not included in `refundedAmount`. This is because they are initiated by the issuing bank, not the merchant.
:::

#### Refund Pending Value ([`transactionItem.refundPendingAmount`](/api-reference/payments/objects/transaction-item#transactionitemrefundpendingamountmoney---))
- **GIVEN** there is a `REFUND_REQUEST` event:
- **WHEN** there is no `REFUND_FAILURE` or `REFUND_SUCCESS`
- **THEN** value increases by the `REFUND_REQUEST` event's amount


#### Canceled Value ([`transactionItem.canceledAmount`](/api-reference/payments/objects/transaction-item#transactionitemcanceledamountmoney---) )
- **GIVEN** there is a `CANCEL_SUCCESS` event
- **WHEN** there is no `CANCEL_FAILURE`
- **OR WHEN** the `CANCEL_FAILURE` event is older than `CANCEL_SUCCESS`
- **THEN** value increases by the `CANCEL_SUCCESS` event's amount.

#### Cancel Pending Value ([`transactionItem.cancelPendingAmount`](/api-reference/payments/objects/transaction-item#transactionitemcancelpendingamountmoney---))
- **GIVEN** there is a `CANCEL_REQUEST` event:
- **WHEN** there is no `CANCEL_FAILURE` or `CANCEL_REQUEST`
- **THEN** value increases by the `CANCEL_REQUEST` event's amount


## Total Balance Calculation

The `totalBalance` represents the difference between the expected total cost and the total amount charged (including pending charges).

This balance indicates whether the customer has overpaid (positive balance) or underpaid (negative balance) for the order.

Below is an explanation of how `totalBalance` is computed.

### Total balance for Checkout ([`checkout.totalBalance`](/api-reference/checkout/objects/checkout#checkouttotalbalancemoney---))

For a Checkout, `totalBalance` reflects the remaining balance after considering the total checkout cost, charged amounts, and pending charges.
It is calculated as the difference between the sum of all successful and pending charges across associated transactions and the checkout’s total price:

```
totalBalance = totalCharged - checkout.totalPrice
```

where:
```
totalCharged = (
sum(transaction.chargedAmount for transaction in transactions)
+ sum(transaction.chargePendingAmount for transaction in transactions)
)
```

### Total balance for Order ([`order.totalBalance`](/api-reference/orders/objects/order#ordertotalbalancemoney---))

For an Order, `totalBalance` represents the remaining balance after accounting for `order.totalPrice`, charged amounts, and granted refunds.
It is calculated as the difference between the total charged (including pending charges) and the order cost adjusted for granted refunds:

```
totalBalance = totalCharged - (order.totalPrice - totalGrantedRefund)
```

where `totalCharged` is sum of all successful and pending charges across associated transactions:
```
totalCharged = (
sum(transaction.amountCharged for transaction in transactions)
+ sum(transaction.amountChargePending for transaction in transactions)
)
```

and `totalGrantedRefund` is a total of all refunds issued to the customer:
```
totalGrantedRefund = sum(grantedRefund.amount for grantedRefund in grantedRefunds)
```
Loading

0 comments on commit 79da8bf

Please sign in to comment.