| @@ -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 | { |