· বাংলাদেশ সময়
Laravel ইন্টিগ্রেশন
Laravel শপ EcomSolveBD-তে কানেক্ট করুন
মালিক ড্যাশবোর্ডে Custom / Headless কানেক্ট করেন। ডেভেলপার Laravel প্যাকেজ বসান। পাঁচ ধাপেই শেষ।
কানেক্ট করলে যা পাবেন
অর্ডার এক জায়গায়
Laravel চেকআউটের অর্ডার ড্যাশবোর্ডে আসবে — কনফার্ম, কুরিয়ার, প্রফিট একসাথে।
ভিজিটর ও অ্যাট্রিবিউশন
কে কোন অ্যাড থেকে এসেছে — ট্র্যাকিং অটো চলে।
অ্যাডস ও GA4
Meta / TikTok / Google Ads পাঠানো SaaS থেকে। GA4-এ শুধু Measurement ID।
স্ট্যাটাস ও ফিড
স্ট্যাটাস দুই দিকে সিঙ্ক। Meta / TikTok / Google ক্যাটালগ XML রেডি।
ধাপে ধাপে সেটআপ
১–৩ আপনি করতে পারবেন। ৪ ডেভেলপার। ৫ একসাথে টেস্ট।
- ১
অ্যাকাউন্ট খুলুন
আপনি- ecomsolvebd.com এ রেজিস্টার / লগইন করুন
- ড্যাশবোর্ডে ঢুকুন
- ২
Website → Laravel package
আপনি- Website → Connect / Create (কানেক্ট / তৈরি)
- “আপনার কি ইতিমধ্যে … ওয়েবসাইট আছে?” → হ্যাঁ
- Custom / Headless কার্ড → Laravel package
- শপ URL দিন — যেমন https://myshop.com
- চাইলে GA4 ID (G-…) দিন
- Connect Laravel চাপুন
সফল হলে Tracking Merchant ID ও Webhook Secret দেখাবে। Secret এখনই কপি করে রাখুন।
- ৩
ডেভেলপারকে পাঠান
আপনি- Tracking Merchant ID
- Webhook Secret
- লাইভ শপ URL
- ৪
সাইটে প্যাকেজ বসান
ডেভেলপার- composer require ecomsolvebd/laravel-tracker
- php artisan vendor:publish --tag=ecomsolvebd-config
- Blade <head>-এ @ecomsolvebdGtag ও @ecomsolvebdTracker
- .env-এ MERCHANT_KEY + WEBHOOK_SECRET (+ API_BASE)
- চেকআউট সফল হলে OrderWebhookPoster::postOrder()
- ECOMSOLVEBD_FUNNEL_AUTO=false + resources/js/esb-track.js → সব ১৯টা ব্রাউজার ইভেন্ট (নিচের টেবিল)
- .env-এ ECOMSOLVEBD_FEED_* কলাম আপনার Product মডেলে ম্যাপ → ৪টা XML ফিড
প্যাকেজ ≥ v1.0.8। ইভেন্ট টেবিলের প্রতিটি ইভেন্ট ও প্রোডাক্ট ফিড বাধ্যতামূলক — শুধু page_view বা অর্ডার দিয়ে কাজ শেষ নয়। নিচের চেকলিস্ট ডেভেলপার/AI-কে দিন।
ECOMSOLVEBD_API_BASE=https://api.ecomsolvebd.com ECOMSOLVEBD_MERCHANT_KEY=… ECOMSOLVEBD_WEBHOOK_SECRET=… ECOMSOLVEBD_GA4_MEASUREMENT_ID=G-… # optional # Blade <head> @ecomsolvebdGtag @ecomsolvebdTracker
- ৫
টেস্ট করুন
দুজনে- প্রোডাক্ট দেখুন → কার্টে যোগ/বাদ → কার্ট → চেকআউট → প্রতিটি ফিল্ড পূরণ → অর্ডার
- Dashboard → Events: টেবিলের প্রতিটি ইভেন্ট এসেছে কিনা মিলিয়ে নিন
- টেস্ট অর্ডার → Dashboard → Orders
- SaaS-এ স্ট্যাটাস বদলান → Laravel অ্যাডমিনে চেক
- https://yoursite.com/feed/products.xml খুলুন → Dashboard → Products → Feed URL → Fetch now
আরও ফিচার
অর্ডার স্ট্যাটাস
EcomSolveBD ও Laravel অ্যাডমিন একে অপরের সাথে স্ট্যাটাস সিঙ্ক করে।
- · SaaS → Laravel: অটো (প্যাকেজ)
- · Laravel অ্যাডমিন → SaaS: অটো (v1.0.8+)
- · Dashboard Create Order → Laravel অ্যাডমিন: v1.0.9+ (`POST /ecomsolvebd/order-create`)
- · ম্যাপ: pending · confirmed · delivered · cancelled · returned
প্রোডাক্ট ফিড
চারটা পাবলিক XML (বাধ্যতামূলক) — Woo-এর মতো। প্রোডাক্ট এডিট Laravel-এই থাকে। কলাম ম্যাপিং নিচে।
- /feed/products.xml — EcomSolveBD ক্যাটালগ
- /feed/facebook.xml — Meta ক্যাটালগ
- /feed/tiktok.xml — TikTok ক্যাটালগ (RSS/XML)
- /feed/google.xml — Google Merchant
First-party ট্র্যাকিং
ব্রাউজার ইভেন্ট আপনার ডোমেইন দিয়ে যায় — অ্যাড ব্লকারে কম হারায়।
- · ডিফল্ট Auto: /ecomsolvebd/collect প্রক্সি
- · Website → Custom / Headless → Laravel → CNAME সেট → Verify DNS
- · Verify হলে tracking.yourdomain.com ব্যবহার হয়
ডেভেলপার · বাধ্যতামূলক
সব ইভেন্ট — কখন, কী পেলোড, কোথায় কোড
নিয়ম: নিচের তালিকার প্রতিটি ইভেন্ট বাধ্যতামূলক — যদি সেই UI মুহূর্ত/ফিল্ড আপনার সাইটে থাকে।
- আগে সাইটের প্রোডাক্ট পেজ, কার্ট, চেকআউট ও চেকআউট ফর্মের প্রতিটি ইনপুট লিস্ট করুন; তারপর প্রতিটির সাথে এই টেবিলের ইভেন্ট ম্যাপ করুন।
- “শর্তসাপেক্ষ” ইভেন্ট শুধু তখনই বাদ দিন যখন সাইটে সেই জিনিসটাই নেই (যেমন পোস্ট কোড ফিল্ড নেই) — কারণটা কোডে কমেন্ট করে রাখুন।
- item_id = প্রোডাক্ট ফিডের <id> = অর্ডার আইটেমের productId — তিন জায়গায় একই ID, নইলে রিপোর্টে প্রোডাক্ট মিলবে না।
- currency = ISO কোড (BDT), value/price = number (string নয়)।
Laravel: @ecomsolvebdTracker <head>-এ window.esbTrack দেয়। Auto-bind শুধু HTML data-attribute/URL দেখে আন্দাজ করে — সঠিক দাম/qty/ID-এর জন্য নিচের esb-track.js হেল্পার দিয়ে প্রতিটি ইভেন্ট নিজে কল করুন এবং ECOMSOLVEBD_FUNNEL_AUTO=false দিন যাতে ডাবল না হয়।
| ইভেন্ট | কখন ফায়ার করবেন | পেলোড | বাধ্যতামূলক? |
|---|---|---|---|
| ব্রাউজ ও কার্ট | |||
| landing_page_view | ভিজিটরের একদম প্রথম ভিজিটের প্রথম হিট (esb_first_touch তখনো নেই) — প্রতি ভিজিটরে জীবনে একবারই; সেই হিটে page_view যাবে না। Next.js EsbTracker / Laravel @ecomsolvebdTracker / Custom বুটস্ট্র্যাপ / Woo-Shopify ট্র্যাকার নিজেই পাঠায় — নিজে আর কখনো পাঠাবেন না | — | হ্যাঁ |
| page_view | প্রথম ভিজিটের landing হিট বাদে প্রতিটি পেজ লোড ও SPA রুট পরিবর্তনে (পরের ভিজিট, পরের পেজ — সব page_view) | — | হ্যাঁ |
| view_item | প্রোডাক্ট ডিটেইল পেজ/কুইক-ভিউ দেখানোর সময় (প্রতি প্রোডাক্টে একবার) | ecommerce { currency, value, items[1] } | হ্যাঁ |
| add_to_cart | কার্টে যোগ সফল হওয়ার পর (প্রতিটি add, Buy Now সহ) | ecommerce { currency, value = price × qty, items[1] } | হ্যাঁ |
| remove_from_cart | কার্ট থেকে লাইন সরালে বা qty কমালে | ecommerce { currency, value, items[1] (removed qty) } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| view_cart | কার্ট পেজ / কার্ট ড্রয়ার খোলার সময় | ecommerce { currency, value = subtotal, items[] } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| চেকআউট | |||
| begin_checkout | চেকআউট পেজ/স্টেপ খোলার সময় (প্রতি চেকআউট সেশনে একবার) | ecommerce { currency, value, items[] } | হ্যাঁ |
| add_shipping_info | ডেলিভারি এরিয়া/শিপিং মেথড বাছাই করলে | ecommerce { currency, value, shipping_tier, items[] } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| add_payment_info | পেমেন্ট মেথড (COD, bKash…) বাছাই করলে | ecommerce { currency, value, payment_type, items[] } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| চেকআউট ফর্ম ও ফিল্ড | |||
| checkout_form_started | চেকআউট ফর্মে প্রথম ফোকাস/টাইপ (একবার) | {} | হ্যাঁ |
| first_name_added | First name / পূর্ণ নাম ফিল্ড পূরণ হলে (change/blur) — আসল নাম পাঠান (Checkout drop-off-এ Customer) | { name, field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| last_name_added | Last name ফিল্ড পূরণ হলে — আসল মান পাঠান (সার্ভার first + last জোড়া দেয়) | { last_name, field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| phone_number_added | মোবাইল নম্বর পূরণ হলে — আসল নম্বর পাঠান (abandon recovery) | { phone } | হ্যাঁ |
| email_address_added | ইমেইল পূরণ হলে — আসল ইমেইল পাঠান | { email } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| address_added | ঠিকানা ফিল্ড পূরণ হলে | { field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| city_added | City / জেলা (district) বাছাই বা পূরণ হলে | { field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| state_added | State / বিভাগ / থানা-উপজেলা বাছাই হলে | { field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| postal_code_added | পোস্ট কোড পূরণ হলে | { field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| country_added | দেশ বাছাই হলে | { field_value_len } | শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ |
| checkout_form_completed | ফর্ম সাবমিট (Place order চাপলে) | {} | হ্যাঁ |
Device & technology — প্রতিটা ইভেন্টে
Device, OS with Version, Device model, Browser — Details reports, সব ফিল্টার, অর্ডার জার্নি আর অ্যাডস Purchase এই চারটা ফিল্ড থেকে আসে। @ecomsolvebdTracker (v1.0.13+) নিজেই প্রতিটা ইভেন্টে পাঠায় — আলাদা কোড লাগবে না।
// Sent on EVERY event (same labels as the WooCommerce tracker)
{
"device": "Mobile", // Mobile | Tablet | Desktop | TV
"device_model": "iPhone", // "Samsung A52", "Windows NT 10.0" — omit if unknown
"os": "iOS 18.7", // "Android 14", "Windows 10/11", "macOS 14.5"
"browser": "Safari 18.7.5" // "Chrome 155.0.0.0", "Samsung Internet 25.0"
}সাইট না পাঠালে EcomSolveBD ক্রেতার user-agent থেকে নিজে ভরে দেয় (তাই proxy অবশ্যই X-Client-User-Agent পাঠাবে); সাইটের পাঠানো মান কখনো বদলানো হয় না।
ব্রাউজার থেকে কখনো পাঠাবেন না
purchase— অর্ডার যায় সার্ভার webhook দিয়ে; অ্যাডস Purchase EcomSolveBD নিজে পাঠায়।checkout_form_abandoned— সার্ভার নিজে লেখে (ফোন/ইমেইলের পর idle টাইমার)।click,scroll— সার্ভার ফেলে দেয়।
Abandoned checkout কীভাবে তৈরি হয়
- ক্রেতা চেকআউটে ফোন বা ইমেইল লিখলে phone_number_added { phone } / email_address_added { email } পাঠান — আসল মানসহ। মান ছাড়া পাঠালে টাইমার চালু হয় না।
- সার্ভার ঐ ভিজিটরের (esb_vid) জন্য idle টাইমার চালু করে (ডিফল্ট ১০ মিনিট)। এরপর যেকোনো ইভেন্ট (page_view, অন্য ফিল্ড…) এলে টাইমার আবার শুরু থেকে গোনে।
- অর্ডার webhook এলে টাইমার বাতিল হয় — কিন্তু শুধু তখনই, যখন webhook-এর trackingUserId = ব্রাউজারের esb_vid কুকি। ভুল বা খালি হলে ক্রেতা অর্ডার দিলেও ১০ মিনিট পর তাকে abandoned দেখাবে।
- অর্ডার না এলে টাইমার শেষে সার্ভার নিজে checkout_form_abandoned লেখে → Dashboard-এর Checkout drop-off / Abandon recovery-তে নাম, ফোন ও ইমেইলসহ দেখা যায়। নাম আসে first_name_added { name } (+ last_name_added { last_name }) থেকে।
- অর্ডার পাঠান চেকআউটের সেই request থেকেই (যেখানে esb_vid কুকি আছে), অথবা esb_vid অর্ডারে সেভ করে পরে পাঠান। কুকি ছাড়া queue/cron থেকে পাঠালে visitor মিলবে না।
1) সেটআপ — auto বন্ধ, হেল্পার লোড
Auto-bind শুধু CSS ক্লাস/URL দেখে আন্দাজ করে (দাম 0, qty 1 হতে পারে)। তাই বন্ধ রেখে প্রতিটি ইভেন্ট নিজে কল করুন।
.env + layout <head># .env — you fire every event yourself (exact price / qty / id); turn off CSS/URL guessing ECOMSOLVEBD_FUNNEL_AUTO=false # <head> of every storefront layout (resources/views/layouts/*.blade.php) @ecomsolvebdGtag @ecomsolvebdTracker {{-- every full page load: landing_page_view on first-ever visit, page_view after --}} @vite('resources/js/esb-track.js')2) ইভেন্ট হেল্পার (একবার তৈরি করুন)
সব ইভেন্টের জন্য একটাই ফাইল। CURRENCY আপনার স্টোরের কারেন্সি দিন।
resources/js/esb-track.js// resources/js/esb-track.js — bundle with Vite/Mix (or inline in a <script type="module">) // window.esbTrack is provided by @ecomsolvebdTracker in <head>. // One helper for EVERY event — import { esb } and call it at each UI moment. const CURRENCY = "BDT"; // your store currency (ISO 4217) const queue = []; let flushing = false; function send(event, props) { // window.esbTrack is the transport (queued until it exists) if (typeof window.esbTrack === "function") { while (queue.length) window.esbTrack(...queue.shift()); window.esbTrack(event, props); return; } queue.push([event, props]); if (!flushing) { flushing = true; const t = setInterval(() => { if (typeof window.esbTrack !== "function") return; clearInterval(t); while (queue.length) window.esbTrack(...queue.shift()); }, 200); } } const lineValue = (items) => items.reduce((sum, it) => sum + Number(it.price) * Number(it.quantity || 1), 0); /** item: { item_id, item_name, price, quantity, item_variant?, item_category? } */ function ecommerce(items, extra) { const clean = items.map((it) => ({ item_id: String(it.item_id), // SAME id as product feed <id> and order productId item_name: String(it.item_name), price: Number(it.price), quantity: Number(it.quantity || 1), ...(it.item_variant ? { item_variant: String(it.item_variant) } : {}), ...(it.item_category ? { item_category: String(it.item_category) } : {}), })); return { ecommerce: { currency: CURRENCY, value: lineValue(clean), items: clean, ...(extra || {}) } }; } let formStarted = false; const sentFieldValues = new Set(); export const esb = { pageView: () => send("page_view"), viewItem: (item) => send("view_item", ecommerce([{ ...item, quantity: 1 }])), addToCart: (item) => send("add_to_cart", ecommerce([item])), removeFromCart: (item) => send("remove_from_cart", ecommerce([item])), viewCart: (items) => send("view_cart", ecommerce(items)), beginCheckout: (items) => send("begin_checkout", ecommerce(items)), addShippingInfo: (items, shippingTier) => send("add_shipping_info", ecommerce(items, { shipping_tier: String(shippingTier) })), addPaymentInfo: (items, paymentType) => send("add_payment_info", ecommerce(items, { payment_type: String(paymentType) })), checkoutFormStarted: () => { if (formStarted) return; formStarted = true; send("checkout_form_started", {}); }, /** * field: "first_name" | "last_name" | "phone" | "email" | "address" * | "city" | "state" | "postal_code" | "country" * Call on change/blur of that input with its current value. */ field: (field, value) => { const v = String(value ?? "").trim(); if (!v) return; const event = { first_name: "first_name_added", last_name: "last_name_added", phone: "phone_number_added", email: "email_address_added", address: "address_added", city: "city_added", state: "state_added", postal_code: "postal_code_added", country: "country_added", }[field]; if (!event) return; const key = event + ":" + v.toLowerCase(); if (sentFieldValues.has(key)) return; // same value already sent sentFieldValues.add(key); esb.checkoutFormStarted(); if (event === "phone_number_added") return send(event, { phone: v }); if (event === "email_address_added") return send(event, { email: v }); if (event === "first_name_added") return send(event, { name: v, field_value_len: v.length }); if (event === "last_name_added") return send(event, { last_name: v, field_value_len: v.length }); send(event, { field_value_len: v.length }); }, checkoutFormCompleted: () => send("checkout_form_completed", {}), }; // Blade pages call window.esb.* from inline scripts (module runs before DOMContentLoaded) window.esb = esb;3) Blade কল-সাইট — প্রতিটি ইভেন্ট
প্রোডাক্ট, কার্ট, চেকআউট, শিপিং/পেমেন্ট, ফর্মের প্রতিটি ফিল্ড। আপনার ভিউ/রিলেশন নামে বদলে নিন।
resources/views/**{{-- Product page (resources/views/products/show.blade.php) --}} <script> document.addEventListener('DOMContentLoaded', () => { window.esb.viewItem(@json([ 'item_id' => (string) $product->id, // = feed <id> = order product_id 'item_name' => $product->name, 'price' => (float) ($product->sale_price ?? $product->price), ])); }); </script> {{-- Add to cart: call after your AJAX/form add succeeds --}} <script> async function addToCart(product, qty) { const res = await fetch('/cart/add', { method: 'POST', /* … your existing request … */ }); if (res.ok) window.esb.addToCart({ item_id: String(product.id), item_name: product.name, price: product.price, quantity: qty }); } // Remove line / qty decreased: // window.esb.removeFromCart({ item_id, item_name, price, quantity: removedQty }); </script> {{-- Cart page (cart.blade.php) and checkout page (checkout.blade.php) --}} @php $esbItems = $cart->lines->map(fn ($l) => [ 'item_id' => (string) $l->product_id, 'item_name' => $l->name, 'price' => (float) $l->unit_price, 'quantity' => (int) $l->qty, ])->values(); @endphp <script> document.addEventListener('DOMContentLoaded', () => { window.esb.viewCart(@json($esbItems)); // cart page // window.esb.beginCheckout(@json($esbItems)); // checkout page instead }); </script> {{-- Checkout: shipping zone + payment method selectors --}} <select name="shipping_zone" onchange="window.esb.addShippingInfo(window.__esbItems, this.value)">…</select> <input type="radio" name="payment_method" value="cod" onchange="window.esb.addPaymentInfo(window.__esbItems, this.value)"> <script>window.__esbItems = @json($esbItems);</script> {{-- Checkout form: one esb.field() per input that exists --}} <form id="checkout-form" onfocusin="window.esb.checkoutFormStarted()" onsubmit="window.esb.checkoutFormCompleted()"> <input name="name" onblur="window.esb.field('first_name', this.value)"> <input name="last_name" onblur="window.esb.field('last_name', this.value)"> <input name="phone" onblur="window.esb.field('phone', this.value)"> <input name="email" onblur="window.esb.field('email', this.value)"> <textarea name="address" onblur="window.esb.field('address', this.value)"></textarea> <select name="district" onchange="window.esb.field('city', this.value)">…</select> <select name="thana" onchange="window.esb.field('state', this.value)">…</select> <input name="postcode" onblur="window.esb.field('postal_code', this.value)"> <select name="country" onchange="window.esb.field('country', this.value)">…</select> </form> {{-- Livewire / Alpine: call the same window.esb.* from your component events. --}}4) অর্ডার — সার্ভার webhook
অর্ডার ও আইটেম সেভ হওয়ার পর। item product_id = ইভেন্ট item_id = ফিড id।
checkout controller / listener// After the order (and its items) are saved — controller, job or OrderPlaced listener use EcomSolveBD\LaravelTracker\OrderPayloadFactory; use EcomSolveBD\LaravelTracker\OrderWebhookPoster; // Run this inside the checkout request: fromOrder() reads the browser's esb_vid cookie there. // From a queue job / cron there is no cookie → save it during checkout: $order->tracking_user_id = $_COOKIE['esb_vid'] ?? null // (JS sets esb_vid unencrypted, so EncryptCookies hides it from request()->cookie()). // Without it the order cancels no abandon timer → false "abandoned" rows. $order->loadMissing('items'); // relation must be named items | lines | lineItems | products app(OrderWebhookPoster::class)->postOrder(OrderPayloadFactory::fromOrder($order)); // fromOrder() reads these attributes — add accessors if your columns differ: // order: order_number | number | id, currency, customer_name, phone / customer.phone, email, // district, upazila | thana, address, shipping_total, discount_total, notes // item: product_id (= feed <id> = event item_id), sku, name | title, quantity | qty, // unit_price | price, variant_title // Or build the array yourself with the same keys and pass it to postOrder().
সব কল-সাইট একনজরে
import { esb } from "./esb-track"; // or window.esb = esb in a <script type="module">
// Map YOUR models → { item_id, item_name, price, quantity }.
const toItem = (p, qty = 1) => ({
item_id: String(p.id), // same id as feed <id>
item_name: p.name,
price: Number(p.price), // the price the buyer sees (sale price if any)
quantity: qty,
item_variant: p.variantName, // optional
});
// 1) Product page — once when the product is shown
esb.viewItem(toItem(product));
// 2) Add to cart — after the add succeeds (also on "Buy now")
esb.addToCart(toItem(product, qty));
// 3) Remove from cart / qty decreased — removed quantity only
esb.removeFromCart(toItem(line.product, removedQty));
// 4) Cart page or cart drawer opened
esb.viewCart(cart.lines.map((l) => toItem(l.product, l.qty)));
// 5) Checkout page/step opened (once)
esb.beginCheckout(cart.lines.map((l) => toItem(l.product, l.qty)));
// 6) Delivery area / shipping method chosen (e.g. "inside_dhaka")
esb.addShippingInfo(cartItems, shippingZone);
// 7) Payment method chosen (e.g. "cod", "bkash")
esb.addPaymentInfo(cartItems, paymentMethod);
// 8) Checkout form — every input that exists, on change/blur:
// <input name="name" onBlur={(e) => esb.field("first_name", e.target.value)} />
// <input name="lastName" onBlur={(e) => esb.field("last_name", e.target.value)} />
// <input name="phone" onBlur={(e) => esb.field("phone", e.target.value)} />
// <input name="email" onBlur={(e) => esb.field("email", e.target.value)} />
// <textarea name="address" onBlur={(e) => esb.field("address", e.target.value)} />
// <select name="district" onChange={(e) => esb.field("city", e.target.value)} />
// <select name="thana" onChange={(e) => esb.field("state", e.target.value)} />
// <input name="zip" onBlur={(e) => esb.field("postal_code", e.target.value)} />
// <select name="country" onChange={(e) => esb.field("country", e.target.value)} />
// (field() also fires checkout_form_started on the first filled input;
// call esb.checkoutFormStarted() on the form's first focus as well.)
// 9) Place order clicked (form submit) — before/while you call your order API
esb.checkoutFormCompleted();
// The order itself goes to EcomSolveBD from your SERVER (order webhook) — not from here.ডেভেলপার · বাধ্যতামূলক
প্রোডাক্ট ফিড — সেটআপ
রুট প্যাকেজ নিজেই বানায়, কিন্তু কলাম ম্যাপিং আপনার মডেলের সাথে না মেলালে ফিড খালি বা ভুল আসে।
- চারটা পাবলিক GET URL (লগইন/মিডলওয়্যার/লোকেল রিডাইরেক্ট ছাড়া): /feed/products.xml (EcomSolveBD), /feed/facebook.xml, /feed/tiktok.xml, /feed/google.xml।
- সব active/published প্রোডাক্ট আসবে — খালি লিস্ট বা placeholder ডাটা নয়। ভ্যারিয়েশন থাকলে প্রতিটি ভ্যারিয়েন্ট আলাদা row (id = parentId-variantId)।
- id = ইভেন্টের item_id = অর্ডারের productId। price = ক্রেতা যে দাম দেয় (সেল প্রাইস থাকলে সেটা)।
- ইমেজ ও লিংক সম্পূর্ণ https URL (relative path নয়)। Meta/TikTok/Google ফিডে ইমেজ নেই বা price ≤ 0 হলে সেই প্রোডাক্ট বাদ দিন।
- XML escape করুন (& < > " ')। description-এ HTML নয়, plain text।
- কস্ট (buying_cost ইত্যাদি) ফিডে দেবেন না — EcomSolveBD-তে সেট করা কস্ট সিঙ্কে মুছে যায় না।
1) ফিড কলাম ম্যাপিং (.env)
আগে আপনার Product মডেলের কলাম/accessor দেখুন, তারপর প্রতিটি লাইন মিলিয়ে দিন। ডিফল্ট শুধু ডেমো শপের জন্য।
.env# .env — the package serves /feed/{products,facebook,tiktok,google}.xml automatically. # The defaults match a DEMO shop only — map them to YOUR Product model or the feed is empty/wrong. ECOMSOLVEBD_FEEDS_ENABLED=true ECOMSOLVEBD_FEED_CURRENCY=BDT ECOMSOLVEBD_FEED_STORE_NAME="My Shop" ECOMSOLVEBD_PRODUCT_MODEL="App\Models\Product" ECOMSOLVEBD_FEED_COL_ID=id ECOMSOLVEBD_FEED_COL_TITLE=name # column or accessor ECOMSOLVEBD_FEED_COL_PRICE=price # selling price (use an accessor if sale price exists) ECOMSOLVEBD_FEED_COL_SKU=sku ECOMSOLVEBD_FEED_COL_IMAGE=image_url # accessor returning a full https URL is safest ECOMSOLVEBD_FEED_COL_STOCK=stock # integer qty column ("" = always in stock) ECOMSOLVEBD_FEED_COL_IN_STOCK= # optional boolean column instead of qty ECOMSOLVEBD_FEED_COL_SLUG=slug ECOMSOLVEBD_FEED_COL_BRAND= ECOMSOLVEBD_FEED_PRODUCT_URL=/product/{slug} # MUST match your real PDP route ({slug} or {id}) ECOMSOLVEBD_FEED_ACTIVE_COLUMN=is_active # "" if you have no such column ECOMSOLVEBD_FEED_ACTIVE_VALUE=true ECOMSOLVEBD_FEED_STATUS_COLUMN= # e.g. status ECOMSOLVEBD_FEED_PUBLISHED_STATUSES=published,active ECOMSOLVEBD_FEED_CATEGORY_RELATION=category # belongsTo / belongsToMany, "" if none ECOMSOLVEBD_FEED_CATEGORY_NAME=name # Variations (one feed row per variant, id = "{productId}-{variantId}") — "" = no variants ECOMSOLVEBD_FEED_VARIATIONS_RELATION= ECOMSOLVEBD_FEED_VARIANT_PRICE=price ECOMSOLVEBD_FEED_VARIANT_SKU=sku ECOMSOLVEBD_FEED_VARIANT_STOCK=stock # With variants, use the same "{productId}-{variantId}" as event item_id and order product_id. php artisan config:clear # after every .env change2) কাস্টম ProductFeedProvider (দরকার হলে)
.env দিয়ে না মিললে নিজের provider ক্লাস।
app/Feeds/MyProductFeed.php// Only if .env mapping cannot express your schema (CMS, external API, complex pricing) // app/Feeds/MyProductFeed.php → ECOMSOLVEBD_FEED_PROVIDER="App\Feeds\MyProductFeed" namespace App\Feeds; use App\Models\Product; use EcomSolveBD\LaravelTracker\Feed\FeedCatalogProduct; use EcomSolveBD\LaravelTracker\Feed\FeedChannelItem; use EcomSolveBD\LaravelTracker\Feed\ProductFeedProvider; final class MyProductFeed implements ProductFeedProvider { public function catalogProducts(): iterable { foreach (Product::where('is_active', true)->with('category')->cursor() as $p) { yield new FeedCatalogProduct( id: (string) $p->id, title: $p->name, price: number_format((float) ($p->sale_price ?? $p->price), 2, '.', ''), currency: 'BDT', category: (string) $p->category?->name, image: url($p->image_path), // absolute stockStatus: $p->stock > 0 ? 'in_stock' : 'out_of_stock', stockQty: (string) max(0, (int) $p->stock), productUrl: route('products.show', $p->slug), sku: (string) $p->sku, ); } } public function channelItems(): iterable { foreach (Product::where('is_active', true)->with('category')->cursor() as $p) { $price = (float) ($p->sale_price ?? $p->price); $image = url($p->image_path); if ($price <= 0 || ! str_starts_with($image, 'https://')) { continue; // Meta/TikTok/Google reject these } $in = $p->stock > 0; yield new FeedChannelItem( id: (string) $p->id, skuId: (string) ($p->sku ?: $p->id), title: mb_substr($p->name, 0, 150), description: mb_substr(strip_tags((string) $p->description) ?: $p->name, 0, 5000), availabilityMeta: $in ? 'in stock' : 'out of stock', availabilityGoogle: $in ? 'in_stock' : 'out_of_stock', price: number_format($price, 2, '.', '').' BDT', link: route('products.show', $p->slug), imageLink: $image, brand: config('app.name'), productType: (string) ($p->category?->name ?: 'General'), ); } } }3) যাচাই + ড্যাশবোর্ডে বসানো
ফিড খুলে আসল প্রোডাক্ট দেখুন, তারপর Dashboard → Products → Feed URL → Save → Fetch now।
verify# Replace with your public store origin curl -sI https://your-store.example/feed/products.xml | head -n 5 # HTTP 200 + application/xml curl -s https://your-store.example/feed/products.xml | head -n 40 # real <product> rows curl -s https://your-store.example/feed/facebook.xml | head -n 30 curl -s https://your-store.example/feed/google.xml | head -n 30 # Then in EcomSolveBD: # Dashboard → Products → Feed URL = https://your-store.example/feed/products.xml # → Save → Fetch now → products appear in the list (auto re-fetch interval on the same card) # Ads catalogs: # Meta Commerce Manager → Data sources → Scheduled feed → /feed/facebook.xml # TikTok Ads → Catalog → Data feed → /feed/tiktok.xml # Google Merchant Center → Products → Feeds → Scheduled fetch → /feed/google.xml
<?xml version="1.0" encoding="UTF-8"?>
<products generated_at="2026-10-23T10:00:00Z" currency="BDT">
<product>
<id>1042</id> <!-- required · same as event item_id + order productId -->
<title>Cotton Panjabi — Navy</title> <!-- required -->
<price>1450.00</price> <!-- required · selling price, number only -->
<currency>BDT</currency>
<category>Panjabi</category>
<image>https://shop.example/img/1042.jpg</image> <!-- absolute https -->
<sku>PJ-NAVY-42</sku>
<product_url>https://shop.example/product/cotton-panjabi-navy</product_url>
<stock>in_stock</stock> <!-- in_stock | out_of_stock -->
<stock_qty>25</stock_qty> <!-- integer, or "unlimited"; 0 = out of stock -->
</product>
</products><?xml version="1.0" encoding="utf-8"?> <rss version="2.0" xmlns:g="http://base.google.com/ns/1.0"> <channel> <title>My Shop Product Feed</title> <link>https://shop.example/</link> <description>Product Feed</description> <item> <g:id>1042</g:id> <g:title>Cotton Panjabi — Navy</g:title> <g:description>Plain-text description (no HTML)</g:description> <g:link>https://shop.example/product/cotton-panjabi-navy</g:link> <g:image_link>https://shop.example/img/1042.jpg</g:image_link> <g:brand>My Shop</g:brand> <g:condition>new</g:condition> <g:availability>in stock</g:availability> <!-- google.xml: in_stock / out_of_stock --> <g:price>1450.00 BDT</g:price> <g:product_type>Men > Panjabi</g:product_type> <!-- google.xml only: <g:identifier_exists>no</g:identifier_exists> when no GTIN/MPN --> <!-- tiktok.xml only: <sku_id>PJ-NAVY-42</sku_id> --> </item> </channel> </rss>
শেষ চেকলিস্ট — ডেভেলপার / AI এজেন্টের জন্য
ডেভেলপার বা AI এজেন্টকে এই চেকলিস্টটা হুবহু দিন — শেষে প্রতিটি ইভেন্ট কোন ফাইলে ফায়ার হচ্ছে তার রিপোর্ট চাইবে।
EcomSolveBD × Laravel — implementation checklist (do ALL items; do not skip silently)
1. Inventory the site first: product page, quick view, cart page/drawer, checkout page,
shipping selector, payment selector, and EVERY checkout form input (name attribute + meaning).
2. Install the base (env, tracker, collect/attribution proxies, order webhook, order-status) per this page.
3. Create the event helper from this page and fire EVERY event in the events table at its UI moment:
(landing_page_view = tracker/bootstrap sends it once on the visitor's first-ever load — do not fire it yourself)
page_view, view_item, add_to_cart, remove_from_cart, view_cart, begin_checkout,
add_shipping_info, add_payment_info, checkout_form_started, first_name_added, last_name_added,
phone_number_added, email_address_added, address_added, city_added, state_added,
postal_code_added, country_added, checkout_form_completed.
If the site has no such UI/field, skip that event and write a code comment explaining why.
4. Use ONE product id everywhere: event item_id = feed <id> = order item productId.
Every event also carries device, device_model, os, browser (Device & technology section) —
the tracker/bootstrap adds them; never strip them from the collect body.
5. Product feed: Map the ECOMSOLVEBD_FEED_* .env columns to the real Product model (or bind a custom ProductFeedProvider) so /feed/products.xml, /feed/facebook.xml, /feed/tiktok.xml and /feed/google.xml list every active product with absolute https image, price > 0, stock and product URL.
6. Order: post the order from the SERVER after it is saved (never a browser "purchase").
trackingUserId MUST be the browser's esb_vid cookie (read it in the checkout request, or store it
on the order) — otherwise buyers who ordered still appear as abandoned checkouts.
phone_number_added / email_address_added must carry the real { phone } / { email } value,
first_name_added the real { name } and last_name_added the real { last_name } (Customer column).
Names, phones and emails stay in EcomSolveBD — they are not forwarded to GA4 / Meta / TikTok.
7. Verify (report results for each):
- Open the site, view a product, add/remove from cart, open cart, open checkout, fill every field,
place a test order.
- Dashboard → Events / Realtime: every event above appears with correct item_id, price, quantity.
- Dashboard → Orders: the test order is there with the same productId.
- curl -s https://<store>/feed/products.xml | head -40 → HTTP 200, real products, absolute https images.
- Dashboard → Products → Feed URL = https://<store>/feed/products.xml → Save → Fetch now → products listed.
8. Final report: a table "event → file:line where it fires" + list of skipped events with reasons.সাধারণ প্রশ্ন
- কোডিং জানি না — পারব?
- হ্যাঁ। ধাপ ১–৩ আপনি করবেন। ধাপ ৪ ডেভেলপারকে দিবেন।
- প্রোডাক্ট EcomSolveBD থেকে এডিট হবে?
- না। প্রোডাক্ট Laravel অ্যাডমিনেই ম্যানেজ করবেন।
- অর্ডার কীভাবে আসে?
- চেকআউট সফল হলে সার্ভার webhook — ব্রাউজার purchase ইভেন্ট নয়। Dashboard Create Order (v1.0.9+) Laravel অ্যাডমিনেও যায়।
- Secret হারিয়ে গেলে?
- Website → Custom / Headless → Laravel (open to manage) → আবার Connect করুন, নতুন Secret কপি করুন।
সমস্যা হলে
page_view আসছে না
@ecomsolvebdTracker আছে কিনা, Merchant Key মিলেছে কিনা, অ্যাড ব্লকার বন্ধ করে দেখুন।
অর্ডার আসছে না
আগে Connect Laravel করুন। Secret মিলিয়ে webhook লগ চেক করুন।
স্ট্যাটাস আপডেট হচ্ছে না
base URL HTTPS সঠিক কিনা, প্যাকেজ আপডেট, order_number মিলছে কিনা দেখুন।
· বাংলাদেশ সময়