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/Models/OrderTransaction.php +69 -4 1.3.20 → 1.6.5 View file →
@@ -3,10 +3,13 @@
3 3 namespace FluentCart\App\Models;
4 4
5 5 use FluentCart\Api\StoreSettings;
6 6 use FluentCart\App\Helpers\Status;
7 +use FluentCart\App\Services\DateTime\DateTime;
7 8 use FluentCart\App\Models\Concerns\CanSearch;
9 +use FluentCart\App\Modules\PaymentMethods\Core\AbstractPaymentGateway;
8 10 use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager;
11 +use FluentCart\App\Services\Payments\PaymentHelper;
9 12 use FluentCart\Framework\Database\Orm\Relations\HasOne;
10 13 use FluentCart\Framework\Support\Arr;
11 14
12 15 /**
@@ -23,9 +26,9 @@
23 26 use CanSearch;
24 27
25 28 protected $table = 'fct_order_transactions';
26 29
27 - protected $appends = ['url'];
30 + protected $appends = ['url', 'refundable'];
28 31 /**
29 32 * The attributes that are mass assignable.
30 33 *
31 34 * @var array
@@ -102,8 +105,23 @@
102 105 if (empty($model->uuid)) {
103 106 $model->uuid = md5(time() . wp_generate_uuid4());
104 107 }
105 108 });
109 +
110 + // created_at is the checkout time, which can be weeks before the money
111 + // moves (payment links, delayed webhooks) — the settlement moment is
112 + // only observable at this transition, so record it here. Gateways that
113 + // know the exact remote charge time set meta.settled_at themselves;
114 + // this fallback never overwrites it.
115 + static::saving(function ($model) {
116 + if ($model->status === Status::TRANSACTION_SUCCEEDED && $model->isDirty('status')) {
117 + $meta = $model->meta;
118 + if (empty($meta['settled_at'])) {
119 + $meta['settled_at'] = DateTime::gmtNow()->format('Y-m-d H:i:s');
120 + $model->meta = $meta;
121 + }
122 + }
123 + });
106 124 }
107 125
108 126 public function getUrlAttribute($value)
109 127 {
@@ -154,8 +172,13 @@
154 172 {
155 173 return $this->hasOne(Order::class, 'id', 'order_id');
156 174 }
157 175
176 + public function getRefundableAttribute(): int
177 + {
178 + return $this->getMaxRefundableAmount();
179 + }
180 +
158 181 public function getMaxRefundableAmount()
159 182 {
160 183 if ($this->status !== Status::TRANSACTION_SUCCEEDED) {
161 184 return 0;
@@ -160,9 +183,11 @@
160 183 if ($this->status !== Status::TRANSACTION_SUCCEEDED) {
161 184 return 0;
162 185 }
163 186 $refundAmount = (int)(Arr::get($this->meta, 'refunded_total', 0));
164 - return $this->total - $refundAmount;
187 + $baseAmount = $this->total - $refundAmount;
188 + $filtered = apply_filters('fluent_cart/transaction/max_refundable_amount', $baseAmount, $this);
189 + return max(0, min((int) $filtered, $baseAmount));
165 190 }
166 191
167 192 public function getPaymentMethodText()
168 193 {
@@ -172,9 +197,16 @@
172 197
173 198 return $this->payment_method;
174 199 }
175 200
176 - public function getReceiptPageUrl($filtered = false)
201 + /**
202 + * $filtered defaults to true because every caller redirects the customer and
203 + * therefore wants the filter. It defaulted to false, so a caller that omitted
204 + * the argument silently got an unfilterable URL — which is how the Stripe
205 + * confirmation redirect lost its filter in a refactor. The parameter is kept
206 + * so an explicit `false` can still ask for the raw URL.
207 + */
208 + public function getReceiptPageUrl($filtered = true)
177 209 {
178 210 $url = add_query_arg([
179 211 'trx_hash' => $this->uuid
180 212 ], (new StoreSettings())->getReceiptPage());
@@ -183,13 +215,46 @@
183 215 $context = [
184 216 'transaction' => $this,
185 217 'order' => $this->order,
186 218 ];
187 - $url = apply_filters_deprecated('fluentcart/transaction/receipt_page_url', [$url, $context], '1.3.16', 'fluent_cart/transaction/receipt_page_url', 'Use fluent_cart/transaction/receipt_page_url instead of fluentcart/transaction/receipt_page_url.');
188 219 $url = apply_filters('fluent_cart/transaction/receipt_page_url', $url, $context);
189 220 }
190 221
191 222 return $url;
223 + }
224 +
225 + /**
226 + * Canonical post-payment redirect URL for this transaction.
227 + * Always fires the fluent_cart/payment/success_url filter.
228 + *
229 + * @param array $args Extra query args merged into the URL
230 + * @return string
231 + */
232 + public function getSuccessUrl($args = [])
233 + {
234 + return (new PaymentHelper((string)$this->payment_method))->successUrl($this, $args);
235 + }
236 +
237 + public function syncPendingTransaction()
238 + {
239 + if ($this->transaction_type !== Status::TRANSACTION_TYPE_CHARGE) {
240 + return new \WP_Error('invalid_transaction_type', __('Only charge transactions can be synced from the payment gateway.', 'fluent-cart'));
241 + }
242 +
243 + if ($this->status !== Status::TRANSACTION_PENDING) {
244 + return new \WP_Error('invalid_transaction_status', __('Only pending transactions can be synced from the payment gateway.', 'fluent-cart'));
245 + }
246 +
247 + if (!$this->vendor_charge_id) {
248 + return new \WP_Error('missing_vendor_charge_id', __('This transaction has no gateway charge ID to sync against.', 'fluent-cart'));
249 + }
250 +
251 + $gateway = GatewayManager::getInstance($this->payment_method);
252 + if ($gateway instanceof AbstractPaymentGateway) {
253 + return $gateway->syncRemoteTransaction($this);
254 + }
255 +
256 + return new \WP_Error('invalid_payment_method', __('This payment method does not support remote transaction sync', 'fluent-cart'));
192 257 }
193 258
194 259 public function acceptDispute($args = [])
195 260 {