@extends('layouts.app') @section('title', 'Offers') @section('content')

Offers

Manage and review offers submitted by customers from your product pages. Configure the "Leave an offer" link that appears below the product price.

Today
Orders with offers {{ $offerStats['orders_with_offers_today'] ?? 0 }}
Pending offers {{ $offerStats['pending_offers_today'] ?? 0 }}
Accepted offers {{ $offerStats['accepted_offers_today'] ?? 0 }}
Declined offers {{ $offerStats['declined_offers_today'] ?? 0 }} ({{ $offerStats['declined_by_rules_today'] ?? 0 }})
Coupons created {{ $offerStats['coupons_created_today'] ?? 0 }}
Coupons deleted {{ $offerStats['coupons_deleted_today'] ?? 0 }}
Total
Orders with offers {{ $offerStats['total_orders_with_offers'] ?? 0 }}
Pending offers {{ $offerStats['total_pending_offers'] ?? 0 }}
Accepted offers {{ $offerStats['total_accepted_offers'] ?? 0 }}
Declined offers {{ $offerStats['total_declined_offers'] ?? 0 }} ({{ $offerStats['total_declined_by_rules'] ?? 0 }})
Coupons created {{ $offerStats['total_coupons_created'] ?? 0 }}
Coupons deleted {{ $offerStats['total_coupons_deleted'] ?? 0 }}

Orders with offers today: orders obtained using discount codes created from accepted offers (completed today). Pending / Accepted / Declined today: offers in that status whose creation or status change happened today. Coupons created today: number of offers accepted today (one coupon per acceptance). Coupons deleted today: offer coupons removed from Shopify today; resets at midnight. Total coupons created: all-time count of coupons created from the Offers menu (accepted or completed offers). Total coupons deleted: cumulative count of offer coupons deleted from Shopify. The number in red (X) next to Declined offers is how many were declined by the wizard rules (before reaching the admin).

Refresh now to see the latest stats and offer list. Configure Offer settings (fees, validity) in Setting →

Client offers

Offers submitted by customers will appear here. Showing 10 per page.

@if(session('message')) @endif @if(session('error')) @endif @php $qp = request()->query(); $offersBaseParams = function($overrides = []) use ($qp) { $merged = array_merge([ 'status' => $qp['status'] ?? '', 'range' => $qp['range'] ?? 'all', 'from' => $qp['from'] ?? null, 'to' => $qp['to'] ?? null, ], $overrides); return array_filter($merged, function ($v) { return $v !== null && $v !== ''; }); }; $offersDateLink = function($range) use ($offersBaseParams) { return route('offers.index', $offersBaseParams(['range' => $range])); }; @endphp
Date
@if(isset($clientOffers) && $clientOffers->count() > 0) @foreach($clientOffers as $offer) @php $createdMiami = $offer->created_at->timezone('America/New_York'); $dateStr = $createdMiami->format('m/d/Y'); $timeStr = $createdMiami->format('g:i A'); $dateTimeStr = $dateStr . ' ' . $timeStr; $statusLabels = \App\Models\ClientOffer::statusLabels(); $statusLabel = $statusLabels[$offer->status] ?? $offer->status; $profit = $offer->profit; $discountAmount = ($offer->sale_price !== null && $offer->offer_amount !== null) ? ((float)$offer->sale_price - (float)$offer->offer_amount) : null; $gatewayFeePct = \App\Models\OfferSetting::getGatewayFeePercent(); $shippingFeeAmount = \App\Models\OfferSetting::getShippingFeeAmount(); $gatewayFeeAmount = (float)$offer->offer_amount * ($gatewayFeePct / 100); $profitAfterFees = \App\Models\BusinessFeeSetting::estimateNetProfit((float) $offer->offer_amount, $offer->cost !== null ? (float) $offer->cost : null); $isAccepted = $offer->status === \App\Models\ClientOffer::STATUS_ACCEPTED; $isCompleted = $offer->status === \App\Models\ClientOffer::STATUS_COMPLETED; $isPending = $offer->status === \App\Models\ClientOffer::STATUS_PENDING; $isDeclinedByRules = $offer->status === \App\Models\ClientOffer::STATUS_DECLINED_BY_RULES; $salePriceFloat = $offer->sale_price !== null ? (float) $offer->sale_price : null; $offerAmountFloat = (float) $offer->offer_amount; $offerNotAboveRetail = true; if ($salePriceFloat !== null && $salePriceFloat > 0) { $offerNotAboveRetail = round($offerAmountFloat, 2) <= round($salePriceFloat, 2); } $declinedByRulesViewable = $isDeclinedByRules && $profit !== null && $profit > 0 && $offerNotAboveRetail; $hasCoupon = $isAccepted || $isCompleted; $isAwaitingCustomer = $offer->status === \App\Models\ClientOffer::STATUS_AWAITING_CUSTOMER; $isCounterDeclined = $offer->status === \App\Models\ClientOffer::STATUS_COUNTER_DECLINED; $originalCustomerOfferBeforeCounter = null; if ($offer->rules_decline_customer_offer_amount !== null && $offer->rules_decline_customer_offer_amount !== '') { $originalCustomerOfferBeforeCounter = (float) $offer->rules_decline_customer_offer_amount; } elseif ($offer->declinedSourceOffer) { $originalCustomerOfferBeforeCounter = (float) $offer->declinedSourceOffer->offer_amount; } $declineRuleFailureHints = []; if ($isDeclinedByRules) { $saleForRules = $offer->sale_price !== null ? (float) $offer->sale_price : 0.0; $costForRules = $offer->cost !== null ? (float) $offer->cost : null; $svc = app(\App\Services\OfferDeclineRuleService::class); $ev = $svc->evaluate((float) $offer->offer_amount, $costForRules, $saleForRules); if (! $ev['pass']) { foreach ($ev['failed_rules'] as $rid => $_l) { $r = \App\Models\OfferDeclineRule::find((int) $rid); if (! $r) { continue; } $h = $svc->acceptableOfferRangeHint($r, $costForRules, $saleForRules); if ($h !== '') { $declineRuleFailureHints[] = $h; } } } } $detailBase = [ 'date_time' => $dateTimeStr, 'created_at' => $offer->created_at->timezone('America/New_York')->format('m/d/Y g:i A'), 'product_title' => $offer->product_title ?? '', 'product_image_url' => $offer->product_image_url ?? '', 'cost' => $offer->cost !== null ? (float) $offer->cost : null, 'offer_amount' => (float) $offer->offer_amount, 'sale_price' => $offer->sale_price !== null ? (float) $offer->sale_price : null, 'profit' => $profit, 'gateway_fee_percent' => $gatewayFeePct, 'shipping_fee_amount' => $shippingFeeAmount, 'gateway_fee_amount' => $gatewayFeeAmount, 'profit_after_fees' => $profitAfterFees, 'discount_amount' => $discountAmount, 'customer_name' => $offer->customer_name ?? '', 'customer_email' => $offer->customer_email ?? '', 'customer_phone' => $offer->customer_phone ?? '', 'question' => $offer->question ?? '', 'decline_rule_failure_labels' => \Illuminate\Support\Facades\Schema::hasColumn('client_offers', 'decline_rule_failure_labels') ? (array) ($offer->decline_rule_failure_labels ?? []) : [], 'decline_rule_failure_hints' => $declineRuleFailureHints, 'original_customer_offer_before_counter' => $originalCustomerOfferBeforeCounter, ]; $offerDetails = null; if ($isAccepted || $isPending || $isCompleted) { $offerDetails = array_merge($detailBase, [ 'is_pending' => $isPending, 'is_completed' => $isCompleted, 'is_declined_by_rules' => false, 'is_awaiting_customer' => false, 'is_counter_declined' => false, 'accept_url' => $isPending ? route('offers.accept-dashboard', $offer) : '', 'decline_url' => $isPending ? route('offers.decline-dashboard', $offer) : '', 'discount_code' => $hasCoupon ? ($offer->discount_code ?? '') : '', 'discount_id' => $hasCoupon ? ($offer->discount_id ?? '') : '', 'expires_at' => $hasCoupon && $offer->expires_at ? $offer->expires_at->timezone('America/New_York')->format('m/d/Y g:i A') : '', 'offer_accepted_email_sent_at' => $hasCoupon && $offer->offer_accepted_email_sent_at ? $offer->offer_accepted_email_sent_at->timezone('America/New_York')->format('m/d/Y g:i A') : '', 'offer_accepted_sms_sent_at' => $hasCoupon && $offer->offer_accepted_sms_sent_at ? $offer->offer_accepted_sms_sent_at->timezone('America/New_York')->format('m/d/Y g:i A') : '', 'offer_accepted_email_opened_at' => $hasCoupon && $offer->offer_accepted_email_opened_at ? $offer->offer_accepted_email_opened_at->timezone('America/New_York')->format('m/d/Y g:i A') : '', 'offer_accepted_sms_status' => $hasCoupon ? ($offer->offer_accepted_sms_status ?? '') : '', 'offer_accepted_sms_fail_reason' => $hasCoupon ? ($offer->offer_accepted_sms_fail_reason ?? '') : '', ]); } elseif ($isAwaitingCustomer || $isCounterDeclined) { $offerDetails = array_merge($detailBase, [ 'is_pending' => false, 'is_completed' => false, 'is_declined_by_rules' => false, 'is_awaiting_customer' => $isAwaitingCustomer, 'is_counter_declined' => $isCounterDeclined, 'accept_url' => '', 'decline_url' => '', 'discount_code' => '', 'discount_id' => '', 'expires_at' => '', 'offer_accepted_email_sent_at' => $offer->offer_accepted_email_sent_at ? $offer->offer_accepted_email_sent_at->timezone('America/New_York')->format('m/d/Y g:i A') : '', 'offer_accepted_sms_sent_at' => $offer->offer_accepted_sms_sent_at ? $offer->offer_accepted_sms_sent_at->timezone('America/New_York')->format('m/d/Y g:i A') : '', 'offer_accepted_email_opened_at' => $offer->offer_accepted_email_opened_at ? $offer->offer_accepted_email_opened_at->timezone('America/New_York')->format('m/d/Y g:i A') : '', 'offer_accepted_sms_status' => (string) ($offer->offer_accepted_sms_status ?? ''), 'offer_accepted_sms_fail_reason' => (string) ($offer->offer_accepted_sms_fail_reason ?? ''), ]); } elseif ($declinedByRulesViewable) { $custEmail = trim((string) ($offer->customer_email ?? '')); $customerEmailOk = $custEmail !== '' && $custEmail !== '—' && filter_var($custEmail, FILTER_VALIDATE_EMAIL); $variantOkForPropose = trim((string) ($offer->variant_id ?? '')) !== ''; $offerDetails = array_merge($detailBase, [ 'is_pending' => false, 'is_completed' => false, 'is_declined_by_rules' => true, 'is_awaiting_customer' => false, 'is_counter_declined' => false, 'accept_url' => '', 'decline_url' => '', 'discount_code' => '', 'discount_id' => '', 'expires_at' => '', 'offer_accepted_email_sent_at' => '', 'offer_accepted_sms_sent_at' => '', 'offer_accepted_email_opened_at' => '', 'offer_accepted_sms_status' => '', 'offer_accepted_sms_fail_reason' => '', 'can_propose_counter_offer' => $customerEmailOk && $variantOkForPropose, 'propose_counter_offer_url' => route('offers.counter-from-declined', $offer), ]); } @endphp @endforeach
Date & time Product Cost Sale Offer Profit Status Actions
{{ $dateStr }} {{ $timeStr }}
@if($offer->product_image_url) @else — @endif {{ $offer->product_title }}
{{ $offer->cost !== null ? '$' . number_format((float)$offer->cost, 2) : '—' }} {{ $offer->sale_price !== null ? '$' . number_format((float)$offer->sale_price, 2) : '—' }} ${{ number_format((float)$offer->offer_amount, 2) }} {{ $profit !== null ? '$' . number_format($profit, 2) : '—' }} {{ $statusLabel }} @if($offer->status === \App\Models\ClientOffer::STATUS_PENDING) @elseif($offer->status === \App\Models\ClientOffer::STATUS_ACCEPTED)
@if($offer->wasAcceptanceNotificationSentToday()) Notified @else
@csrf
@endif
@elseif($offer->status === \App\Models\ClientOffer::STATUS_COMPLETED) Order placed @elseif($offer->status === \App\Models\ClientOffer::STATUS_AWAITING_CUSTOMER)
@if($offer->wasAcceptanceNotificationSentToday()) Notified @else
@csrf
@endif
@elseif($offer->status === \App\Models\ClientOffer::STATUS_COUNTER_DECLINED) @elseif($declinedByRulesViewable) @else — @endif
{{ $clientOffers->appends(request()->query())->links('vendor.pagination.offers') }}
@else

No offers yet. When customers use the "Leave an offer" link on product pages, their offers will appear here.@if(isset($statusFilter) && $statusFilter !== '') Try changing the status filter above.@endif

@endif

Offer link on product page

The link appears below the Add to Cart and Buy Now buttons (full-width, same style) only when "Enable the offer link" is checked and only on products or collections you add here. Choose one mode: search by Products or by Collections (never both at once). Only added products, or products in added collections, will show the link.

@csrf
Label on the product page below Add to Cart / Buy Now (e.g. "Make a private offer").
Caption on collection product cards only (keep it short). Examples: "MAKE AN OFFER", "NAME YOUR PRICE", "OFFER IN ONE TAP". Saved to theme snippet app-offer-collection-label when you click Save settings.
The offer button below the cart buttons is shown only when this is checked and only on products/collections added below.
When checked (default), Make an offer is hidden on the product page and collection cards for products tagged deal of the day (same tag used for Deal of the Day). The app stops syncing the offer-link-allowed tag to those products. Uncheck to allow offers on deal products. After changing, click Save settings and then Update Offer Link if the theme snippet is installed to resync tags.
{{-- Mode: Products OR Collections (only one at a time) --}}

You can only search by one at a time: either products or collections.

{{-- By product: visible only when mode = products --}}

Search by product title. Add products with the button next to the title. Only these products will show the link.

Added products:
    @foreach($addedProducts ?? [] as $product)
  • @if(!empty($product['image'])) @else @endif {{ $product['title'] }}
  • @endforeach
{{-- By collection: visible only when mode = collections --}}

Theme installation

Inject the offer button into the product template so it appears below Add to Cart and Buy Now (full-width, same style). Uses theme {{ config('services.shopify.deals_theme_name', 'non-live theme') }}. If the button appears in the wrong place, set OFFER_LINK_INJECTION_AFTER in .env to an exact string from your theme that appears right after the buy buttons.

@if($hasOfferLinkOnTheme)
@csrf
@else @endif

Offer decline rules

All rules must pass for an offer to be accepted. If any fails, the offer is declined. You can add tiers by range (From $ to $) with a required margin % or fixed profit.

    @foreach($declineRules ?? [] as $rule)
  • {{ $rule->label }}
    @csrf @method('DELETE')
  • @endforeach
@if(($declineRules ?? collect())->isEmpty())

No rules yet. Run php artisan migrate to create the table and default rules (offer > cost, offer < price).

@endif
@csrf
Shown when this rule declines the offer. Leave empty for default.
@endsection @push('scripts') @endpush