PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
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 1.3.20 1.3.19 trunk All 48 releases
← All changes | app/Helpers/OrderItemHelper.php +79 -18 1.3.27 → 1.6.5 View file →
@@ -1,8 +1,10 @@
1 1 <?php
2 2
3 3 namespace FluentCart\App\Helpers;
4 4
5 +use FluentCart\Api\Resource\OrderResource;
6 +use FluentCart\App\Models\Order;
5 7 use FluentCart\App\Models\OrderItem;
6 8 use FluentCart\Framework\Support\Arr;
7 9
8 10 class OrderItemHelper
@@ -103,34 +105,93 @@
103 105
104 106
105 107 public function processCustom($product, $orderId)
106 108 {
107 - $price = Arr::get($product, 'item_price', false);
108 - $quantity = Arr::get($product, 'quantity', false);
109 + if (!$orderId) {
110 + throw new \Exception(esc_html__('Order Not valid!', 'fluent-cart'));
111 + }
109 112
110 - if (!Arr::get($product, 'item_name', false)) {
113 + // The published spec (openapi/orders/create-custom-order-item.json) names
114 + // these `title` and `price`; the original implementation read `item_name`
115 + // and `item_price`. Accept both so neither contract is broken.
116 + $title = sanitize_text_field((string)Arr::get($product, 'title', Arr::get($product, 'item_name', '')));
117 +
118 + if (!$title) {
111 119 throw new \Exception(esc_html__('Item must have a name!', 'fluent-cart'));
112 120 }
113 121
114 - if (!$price || !$quantity) {
122 + // Price arrives already in cents.
123 + $unitPrice = Helper::roundCent(Arr::get($product, 'price', Arr::get($product, 'item_price', 0)));
124 + $quantity = intval(Arr::get($product, 'quantity', 0));
125 +
126 + if ($unitPrice <= 0 || $quantity <= 0) {
115 127 throw new \Exception(esc_html__('Price, Quantity field should not be empty or zero!', 'fluent-cart'));
116 128 }
117 129
118 - //$total = floatVal($price * 100 * $quantity);
119 - $total = floatVal($price * $quantity);
130 + $lineTotal = $unitPrice * $quantity;
120 131
121 - $type = Arr::get($product, 'fulfillment_type', 'physical');
132 + $fulfillmentType = Arr::get($product, 'fulfillment_type', Status::FULFILLMENT_TYPE_PHYSICAL);
122 133
123 - $otherData = [
124 - 'order_id' => $orderId,
125 - 'variation_type' => $type,
126 - //'item_price' => $price * 100,
127 - 'item_price' => $price,
128 - 'item_total' => $total,
129 - 'line_total' => $total,
130 - 'tax_amount' => 0,
131 - 'discount_total' => 0,
132 - ];
134 + if (!in_array($fulfillmentType, [Status::FULFILLMENT_TYPE_PHYSICAL, Status::FULFILLMENT_TYPE_DIGITAL], true)) {
135 + $fulfillmentType = Status::FULFILLMENT_TYPE_PHYSICAL;
136 + }
133 137
134 - return $this->sanitize(array_merge($product, $otherData));
138 + $order = Order::query()->find(intval($orderId));
139 +
140 + if (!$order) {
141 + throw new \Exception(esc_html__('Order Not valid!', 'fluent-cart'));
142 + }
143 +
144 + // Same rule as OrderController::updateOrder — a subscription order's
145 + // total is bound to its pending charge, which nothing on this path
146 + // resynchronizes.
147 + if ($order->isSubscription()) {
148 + throw new \Exception(esc_html__('Subscription Order cannot be edited.', 'fluent-cart'));
149 + }
150 +
151 + $db = Order::query()->getConnection();
152 + $db->beginTransaction();
153 +
154 + try {
155 + // Serialize on the order row so concurrent additions rebuild the
156 + // aggregates one at a time, and a failed rebuild rolls the line
157 + // back out. The tax pass opens its own transaction inside this
158 + // one, which the connection runs as a savepoint.
159 + $locked = Order::query()
160 + ->where('id', $order->id)
161 + ->lockForUpdate()
162 + ->first();
163 +
164 + if (!$locked) {
165 + throw new \Exception(esc_html__('Order Not valid!', 'fluent-cart'));
166 + }
167 +
168 + $orderItem = OrderItem::create([
169 + 'order_id' => $locked->id,
170 + 'fulfillment_type' => $fulfillmentType,
171 + 'title' => $title,
172 + 'post_title' => $title,
173 + 'quantity' => $quantity,
174 + 'unit_price' => $unitPrice,
175 + 'subtotal' => $lineTotal,
176 + 'line_total' => $lineTotal,
177 + 'tax_amount' => 0,
178 + 'discount_total' => 0,
179 + // Same rule as the order-edit insert path: digital lines need
180 + // no shipment, so they are born fulfilled.
181 + 'fulfilled_quantity' => $fulfillmentType === Status::FULFILLMENT_TYPE_PHYSICAL ? 0 : $quantity,
182 + ]);
183 +
184 + // A line added on its own leaves the order carrying the subtotal it
185 + // had before that line existed; the whole-order save posts
186 + // client-computed totals instead and never reaches here.
187 + OrderResource::syncItemDerivedTotals($locked);
188 +
189 + $db->commit();
190 + } catch (\Exception $e) {
191 + $db->rollBack();
192 + throw $e;
193 + }
194 +
195 + return $orderItem;
135 196 }
136 197 }