FluentCart orders and transactions
Drive order state from verified transactions and FluentCart lifecycle services.
Do not equate order creation, successful payment, fulfillment, and subscription
renewal.
Read order-event-map.md before attaching an
irreversible side effect or updating any status.
Keep state dimensions separate
An order independently carries:
- status: commercial/fulfillment progression;
- payment_status: money progression;
- shipping_status: physical delivery progression;
- type: payment, subscription, renewal, and related variants;
- mode: test or live.
Use Status, StatusHelper, OrderResource, Orders, PaymentInstance, and Refund as
appropriate. A direct update to payment_status does not create/reconcile a
transaction, recount totals, complete the cart, reserve/release stock, activate
a subscription, or dispatch the normal events.
Choose the event by guarantee
| Requirement |
Use |
| Durable order aggregate created/updated for checkout |
fluent_cart/order_created |
| Initial order transitioned to paid |
fluent_cart/order_paid |
| Deferred normal post-payment processing point |
fluent_cart/order_paid_done |
| Payment attempt became failed |
fluent_cart/order_payment_failed |
| Refund recorded and totals/status synchronized |
fluent_cart/order_refunded |
| Any subscription renewal settled |
fluent_cart/subscription_renewed |
| Store-managed renewal invoice transitioned to paid |
fluent_cart/renewal_paid |
| One status dimension changed |
fluent_cart/{dimension}_status_changed |
order_created does not mean paid. order_paid is synchronous and its internal
listener recounts order/customer state and may create a WP user.
order_paid_done is emitted later by Action Scheduler after a valid paid order is
reloaded. Use it for third-party fulfillment/integration work that belongs after
the standard deferred path.
Never hook business logic to
fluent_cart/order_paid_async_private_handle. Core labels it private. The legacy
misspelled ansyc variant exists only to drain old queued jobs.
Make event callbacks replay-safe
add_action('fluent_cart/order_paid_done', static function (array $data): void {
$order = $data['order'] ?? null;
if (!$order || $order->mode !== 'live') {
return;
}
// Atomically claim an addon-owned idempotency record keyed by order ID
// and operation before sending or granting anything.
});
StatusHelper atomically claims the pending-to-paid transition to reduce webhook
and browser-confirmation races. That does not make downstream callbacks
exactly-once. Store an addon-owned operation key and make retries converge.
Handle transactions and refunds
- Use transaction IDs/UUIDs in the correct namespace; do not use an invoice
number as a provider idempotency key.
- Verify provider object, signature, currency, amount, mode, order/customer
reference, and prior processed state.
- Reconcile through StatusHelper instead of announcing paid from a browser
redirect.
- Use Refund::processRefund() for a local/admin initiated refund and the
gateway's processRefund implementation for the provider operation.
- Use Refund::createOrRecordRefund() for webhook reconciliation; it deduplicates
by provider refund ID and can match a pending local refund.
- Keep all amounts integer minor units and enforce both order and transaction
refundable ceilings.
- Test partial, full, repeated, and out-of-order refund notifications.
Query and authorize
- Scope customer-facing queries by the resolved customer, not UUID alone.
- Treat Order.uuid as an opaque lookup value but not a guaranteed database
unique key; legacy rows and the current schema use a non-unique index.
- Eager-load only the relationships needed by the job.
- Bound report/admin queries and preserve mode/currency distinctions.
- Redact provider payloads, customer PII, and payment tokens from logs.
Cross-references
- Use fluentcart-payment-gateways for provider confirmation and webhooks.
- Use fluentcart-subscriptions-renewals for renewal semantics.
- Use fluentcart-integrations-jobs for outbound feeds and queues.
References
- Official order hooks: https://dev.fluentcart.com/hooks/actions/orders/
- Verified Free source paths:
- fluent-cart/app/Models/Order.php
- fluent-cart/app/Models/OrderItem.php
- fluent-cart/app/Models/OrderTransaction.php
- fluent-cart/api/Resource/OrderResource.php
- fluent-cart/app/Helpers/Status.php
- fluent-cart/app/Helpers/StatusHelper.php
- fluent-cart/app/Events/Order/
- fluent-cart/app/Services/Payments/Refund.php
- fluent-cart/app/Hooks/actions.php
1---2name: fluentcart-orders-transactions3description: Implements and audits FluentCart order, order-item, transaction, status, payment-settlement, refund, renewal-order, and lifecycle-hook behavior. Use when reading or mutating fct_orders or fct_order_transactions, selecting order_created, order_paid, order_paid_done, order_payment_failed, order_refunded, or dynamic status hooks, marking an order paid, reconciling a webhook, refunding money, or preventing duplicate fulfillment and incorrect status transitions.4---56# FluentCart orders and transactions78Drive order state from verified transactions and FluentCart lifecycle services.9Do not equate order creation, successful payment, fulfillment, and subscription10renewal.1112Read [order-event-map.md](references/order-event-map.md) before attaching an13irreversible side effect or updating any status.1415## Keep state dimensions separate1617An order independently carries:1819- status: commercial/fulfillment progression;20- payment_status: money progression;21- shipping_status: physical delivery progression;22- type: payment, subscription, renewal, and related variants;23- mode: test or live.2425Use Status, StatusHelper, OrderResource, Orders, PaymentInstance, and Refund as26appropriate. A direct update to payment_status does not create/reconcile a27transaction, recount totals, complete the cart, reserve/release stock, activate28a subscription, or dispatch the normal events.2930## Choose the event by guarantee3132| Requirement | Use |33|---|---|34| Durable order aggregate created/updated for checkout | fluent_cart/order_created |35| Initial order transitioned to paid | fluent_cart/order_paid |36| Deferred normal post-payment processing point | fluent_cart/order_paid_done |37| Payment attempt became failed | fluent_cart/order_payment_failed |38| Refund recorded and totals/status synchronized | fluent_cart/order_refunded |39| Any subscription renewal settled | fluent_cart/subscription_renewed |40| Store-managed renewal invoice transitioned to paid | fluent_cart/renewal_paid |41| One status dimension changed | fluent_cart/{dimension}_status_changed |4243order_created does not mean paid. order_paid is synchronous and its internal44listener recounts order/customer state and may create a WP user.45order_paid_done is emitted later by Action Scheduler after a valid paid order is46reloaded. Use it for third-party fulfillment/integration work that belongs after47the standard deferred path.4849Never hook business logic to50fluent_cart/order_paid_async_private_handle. Core labels it private. The legacy51misspelled ansyc variant exists only to drain old queued jobs.5253## Make event callbacks replay-safe5455~~~php56add_action('fluent_cart/order_paid_done', static function (array $data): void {57 $order = $data['order'] ?? null;58 if (!$order || $order->mode !== 'live') {59 return;60 }6162 // Atomically claim an addon-owned idempotency record keyed by order ID63 // and operation before sending or granting anything.64});65~~~6667StatusHelper atomically claims the pending-to-paid transition to reduce webhook68and browser-confirmation races. That does not make downstream callbacks69exactly-once. Store an addon-owned operation key and make retries converge.7071## Handle transactions and refunds7273- Use transaction IDs/UUIDs in the correct namespace; do not use an invoice74 number as a provider idempotency key.75- Verify provider object, signature, currency, amount, mode, order/customer76 reference, and prior processed state.77- Reconcile through StatusHelper instead of announcing paid from a browser78 redirect.79- Use Refund::processRefund() for a local/admin initiated refund and the80 gateway's processRefund implementation for the provider operation.81- Use Refund::createOrRecordRefund() for webhook reconciliation; it deduplicates82 by provider refund ID and can match a pending local refund.83- Keep all amounts integer minor units and enforce both order and transaction84 refundable ceilings.85- Test partial, full, repeated, and out-of-order refund notifications.8687## Query and authorize8889- Scope customer-facing queries by the resolved customer, not UUID alone.90- Treat Order.uuid as an opaque lookup value but not a guaranteed database91 unique key; legacy rows and the current schema use a non-unique index.92- Eager-load only the relationships needed by the job.93- Bound report/admin queries and preserve mode/currency distinctions.94- Redact provider payloads, customer PII, and payment tokens from logs.9596## Cross-references9798- Use fluentcart-payment-gateways for provider confirmation and webhooks.99- Use fluentcart-subscriptions-renewals for renewal semantics.100- Use fluentcart-integrations-jobs for outbound feeds and queues.101102## References103104- Official order hooks: <https://dev.fluentcart.com/hooks/actions/orders/>105- Verified Free source paths:106 - fluent-cart/app/Models/Order.php107 - fluent-cart/app/Models/OrderItem.php108 - fluent-cart/app/Models/OrderTransaction.php109 - fluent-cart/api/Resource/OrderResource.php110 - fluent-cart/app/Helpers/Status.php111 - fluent-cart/app/Helpers/StatusHelper.php112 - fluent-cart/app/Events/Order/113 - fluent-cart/app/Services/Payments/Refund.php114 - fluent-cart/app/Hooks/actions.php