PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 All 51 releases
← All changes | app/Http/Controllers/OrderController.php +113 -48 1.6.3 → 1.7.1 View file →
@@ -211,9 +211,12 @@
211 211 ], 400);
212 212 }
213 213
214 214
215 - $requestData = $request->getSafe($request->sanitize());
215 + $requestData = array_intersect_key(
216 + $request->getSafe($request->sanitize()),
217 + $request->all()
218 + );
216 219
217 220 $totalPaid = Arr::get($request->all(), 'total_paid');
218 221 $updatedTotal = Arr::get($requestData, 'total_amount');
219 222
@@ -234,9 +237,9 @@
234 237 // if new shipping total is already adjusted in total amount, then no need to adjust again, right now not adjusted before
235 238 // ToDo: adjust changed shipping total in total amount prior to this
236 239 $shippingTotal = Arr::get($requestData, 'shipping_total', 0);
237 240 $oldShippingTotal = Arr::get($order, 'shipping_total', 0);
238 - if ($shippingTotal != $oldShippingTotal) {
241 + if (array_key_exists('shipping_total', $requestData) && $shippingTotal != $oldShippingTotal) {
239 242 $diff = $shippingTotal - $oldShippingTotal;
240 243 if ($diff < 0) {
241 244 $requestData['total_amount'] = $updatedTotal - abs($diff);
242 245 } else {
@@ -826,62 +829,109 @@
826 829
827 830
828 831 public function markAsPaid(Request $request, Order $order)
829 832 {
830 - $dueAmount = intval($order->total_amount - $order->total_paid);
833 + $db = Order::query()->getConnection();
834 + $db->beginTransaction();
831 835
832 - if ($dueAmount <= 0) {
833 - return $this->sendError([
834 - 'message' => __('Order has already been paid', 'fluent-cart')
835 - ], 423);
836 - }
836 + try {
837 + $locked = Order::query()
838 + ->where('id', $order->id)
839 + ->lockForUpdate()
840 + ->first();
837 841
838 - if (Arr::get($order, 'status') === 'canceled') {
839 - return $this->sendError([
840 - 'message' => __('Unable to mark paid for canceled order', 'fluent-cart')
841 - ], 423);
842 - }
842 + if (!$locked) {
843 + $db->rollBack();
844 + return $this->sendError([
845 + 'message' => __('Order not found', 'fluent-cart')
846 + ], 404);
847 + }
843 848
844 - // Reuse existing pending transaction without vendor_charge_id instead of creating a new one
845 - $transaction = $order->transactions
846 - ->where('status', Status::TRANSACTION_PENDING)
847 - ->filter(function ($t) {
848 - return empty($t->vendor_charge_id);
849 - })
850 - ->first();
849 + $dueAmount = intval($locked->total_amount - $locked->total_paid);
851 850
852 - $newTransactionData = [
853 - 'total' => $dueAmount,
854 - 'status' => Status::TRANSACTION_SUCCEEDED,
855 - 'payment_method' => sanitize_text_field($request->payment_method),
856 - 'vendor_charge_id' => sanitize_text_field($request->vendor_charge_id),
857 - 'payment_mode' => sanitize_text_field($order->mode),
858 - 'payment_method_type' => sanitize_text_field($request->payment_method),
859 - 'order_type' => sanitize_text_field($order->type),
860 - 'currency' => sanitize_text_field($order->currency),
861 - ];
851 + if ($dueAmount <= 0) {
852 + $db->rollBack();
853 + return $this->sendError([
854 + 'message' => __('Order has already been paid', 'fluent-cart')
855 + ], 423);
856 + }
862 857
863 - if ($transaction) {
864 - // Don't include transaction_type in the update — the existing value is always 'charge'
865 - // and overwriting it with the request value would break syncSubscriptionStates bill_count.
866 - $transaction->update($newTransactionData);
867 - } else {
868 - $transaction = OrderTransaction::query()->create(
869 - array_merge($newTransactionData, [
870 - 'order_id' => $order->id,
871 - 'transaction_type' => Status::TRANSACTION_TYPE_CHARGE,
872 - ])
873 - );
858 + if ($locked->status === Status::ORDER_CANCELED) {
859 + $db->rollBack();
860 + return $this->sendError([
861 + 'message' => __('Unable to mark paid for canceled order', 'fluent-cart')
862 + ], 423);
863 + }
864 +
865 + // Reuse an existing pending transaction without vendor_charge_id instead of
866 + // creating a new one. Queried fresh (not via the route-bound relation) so it
867 + // reflects the state under the lock.
868 + $transaction = OrderTransaction::query()
869 + ->where('order_id', $locked->id)
870 + ->where('status', Status::TRANSACTION_PENDING)
871 + ->where(function ($query) {
872 + $query->whereNull('vendor_charge_id')
873 + ->orWhere('vendor_charge_id', '');
874 + })
875 + ->orderBy('id', 'asc')
876 + ->lockForUpdate()
877 + ->first();
878 +
879 + $newTransactionData = [
880 + 'total' => $dueAmount,
881 + 'status' => Status::TRANSACTION_SUCCEEDED,
882 + 'payment_method' => sanitize_text_field($request->payment_method),
883 + 'vendor_charge_id' => sanitize_text_field($request->vendor_charge_id),
884 + 'payment_mode' => sanitize_text_field($locked->mode),
885 + 'payment_method_type' => sanitize_text_field($request->payment_method),
886 + 'order_type' => sanitize_text_field($locked->type),
887 + 'currency' => sanitize_text_field($locked->currency),
888 + ];
889 +
890 + if ($transaction) {
891 + // Don't include transaction_type in the update — the existing value is always 'charge'
892 + // and overwriting it with the request value would break syncSubscriptionStates bill_count.
893 + $transaction->update($newTransactionData);
894 + } else {
895 + $transaction = OrderTransaction::query()->create(
896 + array_merge($newTransactionData, [
897 + 'order_id' => $locked->id,
898 + 'transaction_type' => Status::TRANSACTION_TYPE_CHARGE,
899 + ])
900 + );
901 + }
902 +
903 + // Persist the settled balance while the row lock is held so the next request
904 + // to acquire it computes due = 0. payment_status is deliberately left alone:
905 + // syncOrderStatuses() owns the atomic pending → paid claim that dispatches
906 + // OrderPaid exactly once, and it runs after commit so third-party hook
907 + // callbacks (emails, integrations, subscription activation) never execute
908 + // while the order row is locked.
909 + $locked->total_paid = (int) OrderTransaction::query()
910 + ->where('order_id', $locked->id)
911 + ->whereIn('status', Status::getTransactionSuccessStatuses())
912 + ->sum('total');
913 +
914 + $locked->save();
915 +
916 + $db->commit();
917 + } catch (\Throwable $e) {
918 + $db->rollBack();
919 + throw $e;
874 920 }
875 921
876 - $note = sanitize_text_field($request->get('mark_paid_note', ''));
877 - if ($note) {
878 - $order->note = $note;
879 - $order->save();
922 + (new StatusHelper($locked))->syncOrderStatuses($transaction);
923 +
924 + $paymentNote = sanitize_textarea_field($request->get('mark_paid_note', ''));
925 + if ($paymentNote) {
926 + $locked->addLog(
927 + __('Payment note', 'fluent-cart'),
928 + nl2br(esc_html($paymentNote)),
929 + 'info',
930 + wp_get_current_user()->display_name
931 + );
880 932 }
881 933
882 - (new StatusHelper($order))->syncOrderStatuses($transaction);
883 -
884 934 return $this->response->sendSuccess([
885 935 'message' => __('Order has been marked as paid', 'fluent-cart')
886 936 ]);
887 937 }
@@ -1050,10 +1100,25 @@
1050 1100 'message' => __('The selected transaction does not match with the provided order', 'fluent-cart')
1051 1101 ]);
1052 1102 }
1053 1103
1104 + // Money already counted into the order cannot be changed from a dropdown; returning
1105 + // it is a refund, which records a refund transaction through the refund action (FC-SEC-09).
1106 + if ($transaction->status === Status::TRANSACTION_SUCCEEDED) {
1107 + return $this->sendError([
1108 + 'message' => __('A succeeded transaction cannot be changed here. Use the refund action to return the payment.', 'fluent-cart')
1109 + ], 422);
1110 + }
1111 +
1054 1112 $transaction->updateStatus($newStatus);
1055 - $order->updatePaymentStatus($newStatus);
1113 +
1114 + if ($newStatus === Status::TRANSACTION_SUCCEEDED) {
1115 + // 'succeeded' is a transaction word, not an order payment status: derive the
1116 + // order's paid state and total_paid from its transactions, as mark-as-paid does.
1117 + (new StatusHelper($order))->syncOrderStatuses($transaction);
1118 + } else {
1119 + $order->updatePaymentStatus($newStatus);
1120 + }
1056 1121
1057 1122 return [
1058 1123 'transaction' => $transaction,
1059 1124 'message' => __('Payment status has been successfully updated', 'fluent-cart')