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'))
{{ session('message') }}
@endif
@if(session('error'))
{{ 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
@if(isset($clientOffers) && $clientOffers->count() > 0)
Date & time
Product
Cost
Sale
Offer
Profit
Status
Actions
@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
{{ $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)
View details
@elseif($offer->status === \App\Models\ClientOffer::STATUS_ACCEPTED)
View details
@if($offer->wasAcceptanceNotificationSentToday())
Notified
@else
@endif
@elseif($offer->status === \App\Models\ClientOffer::STATUS_COMPLETED)
Order placed
@elseif($offer->status === \App\Models\ClientOffer::STATUS_AWAITING_CUSTOMER)
View details
@if($offer->wasAcceptanceNotificationSentToday())
Notified
@else
@endif
@elseif($offer->status === \App\Models\ClientOffer::STATUS_COUNTER_DECLINED)
View details
@elseif($declinedByRulesViewable)
View details
@else
—
@endif
@endforeach
@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.
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)
@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 }}
Edit
@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
Add rule
Offer must be greater than product cost
Offer must be less than sale price
Tier (custom range: From $ to $, then % or Fixed $)
Define a range by cost. When the product cost falls in this range, the offer must meet the margin % or profit.
Decline message
Shown when this rule declines the offer. Leave empty for default.
Add rule
Customer
Name —
Email —
Phone —
Product & offer
Cost —
Offer amount
—
Sale price —
Discount amount —
Profit (gross) —
Fees (Offer settings)
Applied from Settings → Offer. Used to estimate net profit.
Gateway / payment fee (%) —
Gateway fee ($) —
Shipping fee ($) —
Real profit (net) —
Customer notifications
Email
—
SMS
—
Coupon / discount
Created at —
Discount code —
Expires at —
No coupon yet. This offer is pending acceptance.
Rules applied to this coupon
Automatic discount: one customer, one product, fixed amount off; combinable with shipping. Valid until the expiration date above.
Waiting for the customer to accept or decline the proposed price (email and SMS were sent with personal links).
Rules decline & counter-offer
Send proposal to customer
The customer receives email and SMS (if a valid US phone is on file) with links to accept or decline your price. When they accept, this same row moves to Pending so you can accept it here and create the coupon. No duplicate offer is created.
@csrf
Proposed price ($)
Optional note to customer
—
Send proposal to customer
@endsection
@push('scripts')
@endpush