EcomSolveBD

· বাংলাদেশ সময়

Next.js ইন্টিগ্রেশন

Next.js শপ EcomSolveBD-তে কানেক্ট করুন

  1. ১

    অ্যাকাউন্ট খুলুন

    আপনি
    • ecomsolvebd.com এ রেজিস্টার / লগইন
    • ড্যাশবোর্ডে ঢুকুন
  2. ২

    Website → Next.js

    আপনি
    • Website → Connect / Create (কানেক্ট / তৈরি)
    • “আপনার কি ইতিমধ্যে … ওয়েবসাইট আছে?” → হ্যাঁ
    • Custom / Headless কার্ড → Next.js
    • শপ URL দিন — https://myshop.com
    • চাইলে GA4 ID (G-…)
    • Connect Next.js চাপুন

    Tracking Merchant ID + Webhook Secret কপি করুন — Secret এখনই সেভ করুন।

  3. ৩

    ডেভেলপারকে পাঠান

    আপনি
    • Tracking Merchant ID
    • Webhook Secret
    • লাইভ শপ URL
  1. ৪

    ডেভেলপার: SDK কন্ট্রাক্ট

    ডেভেলপার
    • npm install @ecomsolvebd/next
    • Routes: collect + attribution-config + order-status (+ order-create)
    • Root layout: EsbTracker funnelAuto={false} + EsbPageViews
    • lib/esb-track.ts → সব ১৯টা ব্রাউজার ইভেন্ট (নিচের টেবিল)
    • Checkout সফল → সার্ভার থেকে postOrder()
    • app/feed/[channel]/route.ts + ProductFeedProvider → ৪টা XML ফিড

    ইভেন্ট টেবিলের প্রতিটি ইভেন্ট ও প্রোডাক্ট ফিড বাধ্যতামূলক — শুধু page_view বা অর্ডার দিয়ে কাজ শেষ নয়। নিচের চেকলিস্ট ডেভেলপার/AI-কে দিন।

    ↓ SDK রেফারেন্স
  2. ৫

    টেস্ট করুন

    দুজনে
    • প্রোডাক্ট দেখুন → কার্টে যোগ/বাদ → কার্ট → চেকআউট → প্রতিটি ফিল্ড পূরণ → অর্ডার
    • Dashboard → Events: টেবিলের প্রতিটি ইভেন্ট এসেছে কিনা মিলিয়ে নিন
    • টেস্ট অর্ডার → Dashboard → Orders; SaaS স্ট্যাটাস বদলান → Next.js চেক
    • https://yoursite.com/feed/products.xml খুলুন → Dashboard → Products → Feed URL → Fetch now

SDK রেফারেন্স

@ecomsolvebd/next — App Router

ইন্টিগ্রেশন SDK — path কন্ট্রাক্ট + factories। স্টোর বিল্ড (কার্ট, ORM, CMS, UI) হোস্টের। ডেমো শপ কপি করবেন না; কন্ট্রাক্ট ইমপ্লিমেন্ট করুন।

কন্ট্রাক্ট

Path contract (host must expose these; package does not inject files):

  POST|GET  /api/ecomsolvebd/collect
  GET|POST  /api/ecomsolvebd/attribution-config
  POST      /api/ecomsolvebd/order-status

Exports:
  @ecomsolvebd/next                 → buildOrderPayload, OrderWebhookPoster, …
  @ecomsolvebd/next/client          → EsbTracker (sets window.esbTrack)
  @ecomsolvebd/next/route-handlers  → create*RouteHandlers factories

Also required (sections below): all browser events via lib/esb-track.ts and
GET /feed/{products|facebook|tiktok|google}.xml via app/feed/[channel]/route.ts.

Out of scope for this SDK: your catalog UI, cart store, ORM, CMS, checkout UX.
Prefix paths with src/ when using src/app.

Identity: never fabricate visitor ids. Forward Cookie via buildOrderPayload(..., { cookieHeader }).

১) ইনস্টল npm install @ecomsolvebd/next

  1. 2) Environment

    Merchant Key + Webhook Secret ড্যাশবোর্ড Connect থেকে। Secret শুধু সার্ভার রানটাইমে।

    .env
    ECOMSOLVEBD_API_BASE=https://api.ecomsolvebd.com
    ECOMSOLVEBD_MERCHANT_KEY=<tracking_merchant_id>
    ECOMSOLVEBD_WEBHOOK_SECRET=<webhook_secret>
    ECOMSOLVEBD_GA4_MEASUREMENT_ID=G-XXXXXXXX   # optional
    NEXT_PUBLIC_STORE_URL=https://your-store.example
    # ECOMSOLVEBD_DEPLOY_ENV=staging            # when connected via staging dashboard
  2. 3) Browser tracker

    EsbTracker transport মাউন্ট করে (window.esbTrack)। funnelAuto={false} = হোস্ট ইভেন্ট ওয়্যারিং।

    root layout
    // Root layout — path may be app/layout.tsx or src/app/layout.tsx
    import { EsbTracker } from "@ecomsolvebd/next/client";
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <body>
            <EsbTracker
              merchantKey={process.env.ECOMSOLVEBD_MERCHANT_KEY!}
              // Public store origin (scheme + host). Used to build same-origin collect URL.
              storeBaseUrl={process.env.NEXT_PUBLIC_STORE_URL!}
              apiBase={process.env.ECOMSOLVEBD_API_BASE}
              deployEnv={process.env.ECOMSOLVEBD_DEPLOY_ENV}
              ga4MeasurementId={process.env.ECOMSOLVEBD_GA4_MEASUREMENT_ID}
              // You fire every event via lib/esb-track.ts (see events section)
              funnelAuto={false}
            />
            {/* <Suspense fallback={null}><EsbPageViews /></Suspense> — SPA route-change page_view */}
            {children}
          </body>
        </html>
      );
    }
  3. 4) Collect proxy

    First-party Auto ladder: browser → same-origin → API। Thin wrapper only.

    app/api/ecomsolvebd/collect/route.ts
    // app/api/ecomsolvebd/collect/route.ts
    import { createCollectRouteHandlers } from "@ecomsolvebd/next/route-handlers";
    
    export const { POST, GET } = createCollectRouteHandlers({
      merchantKey: process.env.ECOMSOLVEBD_MERCHANT_KEY!,
      apiBase: process.env.ECOMSOLVEBD_API_BASE,
      deployEnv: process.env.ECOMSOLVEBD_DEPLOY_ENV,
    });
  4. 5) Attribution config proxy

    Same-origin attribution config — path ঠিক রাখুন।

    app/api/ecomsolvebd/attribution-config/route.ts
    // app/api/ecomsolvebd/attribution-config/route.ts
    import { createAttributionConfigRouteHandlers } from "@ecomsolvebd/next/route-handlers";
    
    export const { GET, POST } = createAttributionConfigRouteHandlers({
      merchantKey: process.env.ECOMSOLVEBD_MERCHANT_KEY!,
      apiBase: process.env.ECOMSOLVEBD_API_BASE,
      deployEnv: process.env.ECOMSOLVEBD_DEPLOY_ENV,
    });
  5. 6) Inbound order status

    SaaS → store। findOrder / updateOrder আপনার persistence-এ ম্যাপ — ORM বাধ্যতামূলক নয়।

    app/api/ecomsolvebd/order-status/route.ts
    // app/api/ecomsolvebd/order-status/route.ts  (or src/app/…)
    import { createOrderStatusRouteHandlers } from "@ecomsolvebd/next/route-handlers";
    
    // Wire find/update to YOUR persistence (Prisma, Drizzle, SQL, REST CMS, …).
    // The factory only needs: load by orderNumber → read status → write status.
    
    export const { POST } = createOrderStatusRouteHandlers({
      webhookSecret: process.env.ECOMSOLVEBD_WEBHOOK_SECRET!,
      allowedStatuses: ["pending", "confirmed", "delivered", "cancelled", "returned"],
      findOrder: async (orderNumber) => {
        // return await db.orders.findByNumber(orderNumber)
        return null; // replace — null → handler reports not found
      },
      getCurrentStatus: (order) => {
        // return order.status
        return (order as { status?: string }).status;
      },
      updateOrder: async (order, update) => {
        // Prefer business key orderNumber over PK type (Int vs String/cuid).
        // await db.orders.updateStatus(order.orderNumber, update.status)
        void order;
        void update;
      },
    });
  6. 7) Order ingest

    Checkout success call site (যেখানেই অর্ডার কমিট হয়)। Ads Purchase নয়।

    call site · server
    // Call from YOUR checkout success path (Route Handler, Server Action, etc.)
    // Exact file path depends on your storefront — not prescribed by this SDK.
    import { cookies } from "next/headers";
    import { buildOrderPayload, OrderWebhookPoster } from "@ecomsolvebd/next";
    
    // 1) Persist the order in your own database first, then:
    const poster = new OrderWebhookPoster({
      merchantKey: process.env.ECOMSOLVEBD_MERCHANT_KEY!,
      webhookSecret: process.env.ECOMSOLVEBD_WEBHOOK_SECRET!,
      apiBase: process.env.ECOMSOLVEBD_API_BASE,
    });
    
    const payload = buildOrderPayload(
      {
        orderNumber: order.number,
        // Use your store currency (ISO 4217) — do not hardcode
        currency: order.currency,
        customer: {
          fullName: order.customerName, // name
          phone: order.phone,
          // email: order.email,
        },
        items: order.lines.map((l) => ({
          sku: l.sku,
          title: l.title,
          quantity: l.qty,
          unitPrice: String(l.unitPrice),
          // productId: l.productId,
          // variantTitle: l.variantTitle,
        })),
        // Shipping / address (uncomment when your checkout collects them)
        // district: order.district,
        // upazila: order.upazila,
        // address: order.address,
        // notes: order.notes,
        // Money (flat fields — not a nested totals object)
        // shippingTotal: String(order.shippingTotal),
        // discountTotal: String(order.discountTotal),
        // taxTotal: String(order.taxTotal),
      },
      { cookieHeader: (await cookies()).toString() },
    );
    
    try {
      await poster.postOrder(payload);
    } catch (err) {
      console.error("[ecomsolvebd] postOrder failed", err);
    }
    // Ingest only — ads Purchase is sent later from the EcomSolveBD dashboard / courier flow.

নোট

  • Client export: EsbTracker (@ecomsolvebd/next/client) — অন্য নাম নেই।
  • Route handlers হোস্ট অ্যাপে thin wrappers; লজিক প্যাকেজে।
  • বাধ্যতামূলক: সব ব্রাউজার ইভেন্ট (lib/esb-track.ts) ও প্রোডাক্ট ফিড (app/feed/[channel]/route.ts) — নিচের দুই সেকশন।
  • NEXT_PUBLIC_STORE_URL = পাবলিক origin (https://shop.example) — proxy URL বানাতে লাগে।

আরও ফিচার

অর্ডার স্ট্যাটাস

EcomSolveBD ও Next.js অ্যাডমিন স্ট্যাটাস সিঙ্ক করে।

  • · SaaS → Next.js: order-status route handler
  • · Next.js → SaaS: status webhook (প্যাকেজ)
  • · Dashboard Create Order → Next.js: v1.0.1+ (`POST /api/ecomsolvebd/order-create`)
  • · ম্যাপ: pending · confirmed · delivered · cancelled · returned

প্রোডাক্ট ফিড

চারটা XML (বাধ্যতামূলক) — প্রোডাক্ট Next.js/API-তেই ম্যানেজ। সেটআপ নিচে।

  • /feed/products.xml — EcomSolveBD ক্যাটালগ
  • /feed/facebook.xml — Meta
  • /feed/tiktok.xml — TikTok (RSS/XML)
  • /feed/google.xml — Google Merchant

First-party ট্র্যাকিং

ইভেন্ট আপনার ডোমেইন দিয়ে — অ্যাড ব্লকারে কম হারায়।

  • · Auto: same-origin /api/ecomsolvebd/collect → API
  • · Dashboard: CNAME → Verify DNS → tracking.{domain}
  • · ডেভ: proxy routes; owner: DNS (কোড নয়)

SDK রেফারেন্স

সব ইভেন্ট — কখন, কী পেলোড, কোথায় কোড

কন্ট্রাক্ট
Transport (provided by EsbTracker in the root layout):
  window.esbTrack(eventName, props?)          → same-origin /api/ecomsolvebd/collect
  window.esbBindCheckoutForm(selector, map?)  → optional form auto-bind
  "esb:ready" window event                    → fired once the transport exists

EsbTracker itself sends ONE hit when it mounts: landing_page_view on the visitor's first-ever visit
(no esb_first_touch yet — once per visitor, no page_view on that hit), page_view on every later load.
Never fire landing_page_view yourself. Everything else is yours:
  <EsbTracker funnelAuto={false} … />  → no guessing from CSS/URLs; you call every event
  via lib/esb-track.ts (below), which queues calls made before "esb:ready".

Required events (fire each one whose UI moment exists on the site):
  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
Orders: server webhook (OrderWebhookPoster.postOrder) — never a browser "purchase".

নিয়ম: নিচের তালিকার প্রতিটি ইভেন্ট বাধ্যতামূলক — যদি সেই UI মুহূর্ত/ফিল্ড আপনার সাইটে থাকে।

  • আগে সাইটের প্রোডাক্ট পেজ, কার্ট, চেকআউট ও চেকআউট ফর্মের প্রতিটি ইনপুট লিস্ট করুন; তারপর প্রতিটির সাথে এই টেবিলের ইভেন্ট ম্যাপ করুন।
  • “শর্তসাপেক্ষ” ইভেন্ট শুধু তখনই বাদ দিন যখন সাইটে সেই জিনিসটাই নেই (যেমন পোস্ট কোড ফিল্ড নেই) — কারণটা কোডে কমেন্ট করে রাখুন।
  • item_id = প্রোডাক্ট ফিডের <id> = অর্ডার আইটেমের productId — তিন জায়গায় একই ID, নইলে রিপোর্টে প্রোডাক্ট মিলবে না।
  • currency = ISO কোড (BDT), value/price = number (string নয়)।

Next.js: EsbTracker মাউন্ট হলে window.esbTrack তৈরি হয়। নিচের esb-track.ts হেল্পার দিয়ে কল করুন — tracker রেডি হওয়ার আগের কল কিউ হয়, হারায় না। EsbTracker-এ funnelAuto={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_addedFirst name / পূর্ণ নাম ফিল্ড পূরণ হলে (change/blur) — আসল নাম পাঠান (Checkout drop-off-এ Customer){ name, field_value_len }শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ
last_name_addedLast name ফিল্ড পূরণ হলে — আসল মান পাঠান (সার্ভার first + last জোড়া দেয়){ last_name, field_value_len }শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ
phone_number_addedমোবাইল নম্বর পূরণ হলে — আসল নম্বর পাঠান (abandon recovery){ phone }হ্যাঁ
email_address_addedইমেইল পূরণ হলে — আসল ইমেইল পাঠান{ email }শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ
address_addedঠিকানা ফিল্ড পূরণ হলে{ field_value_len }শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ
city_addedCity / জেলা (district) বাছাই বা পূরণ হলে{ field_value_len }শর্তসাপেক্ষ — সাইটে থাকলে হ্যাঁ
state_addedState / বিভাগ / থানা-উপজেলা বাছাই হলে{ 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 এই চারটা ফিল্ড থেকে আসে। EsbTracker (@ecomsolvebd/next v1.0.2+) নিজেই প্রতিটা ইভেন্টে পাঠায় — আলাদা কোড লাগবে না। নিজের ট্র্যাকার বানালে deviceContextFromUserAgent() ব্যবহার করুন।

collect body
// 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 কীভাবে তৈরি হয়

  1. ক্রেতা চেকআউটে ফোন বা ইমেইল লিখলে phone_number_added { phone } / email_address_added { email } পাঠান — আসল মানসহ। মান ছাড়া পাঠালে টাইমার চালু হয় না।
  2. সার্ভার ঐ ভিজিটরের (esb_vid) জন্য idle টাইমার চালু করে (ডিফল্ট ১০ মিনিট)। এরপর যেকোনো ইভেন্ট (page_view, অন্য ফিল্ড…) এলে টাইমার আবার শুরু থেকে গোনে।
  3. অর্ডার webhook এলে টাইমার বাতিল হয় — কিন্তু শুধু তখনই, যখন webhook-এর trackingUserId = ব্রাউজারের esb_vid কুকি। ভুল বা খালি হলে ক্রেতা অর্ডার দিলেও ১০ মিনিট পর তাকে abandoned দেখাবে।
  4. অর্ডার না এলে টাইমার শেষে সার্ভার নিজে checkout_form_abandoned লেখে → Dashboard-এর Checkout drop-off / Abandon recovery-তে নাম, ফোন ও ইমেইলসহ দেখা যায়। নাম আসে first_name_added { name } (+ last_name_added { last_name }) থেকে।
  5. অর্ডার পাঠান চেকআউটের সেই request থেকেই (যেখানে esb_vid কুকি আছে), অথবা esb_vid অর্ডারে সেভ করে পরে পাঠান। কুকি ছাড়া queue/cron থেকে পাঠালে visitor মিলবে না।

কোড — ধাপে ধাপে (সবগুলো লাগবে)

  1. 1) ইভেন্ট হেল্পার (একবার তৈরি করুন)

    সব ইভেন্টের জন্য একটাই ফাইল। tracker রেডি হওয়ার আগের কল কিউতে থাকে। CURRENCY আপনার স্টোরের কারেন্সি দিন।

    lib/esb-track.ts
    // lib/esb-track.ts — import only from client components ("use client")
    // One helper for EVERY event. Calls made before EsbTracker is ready are queued.
    type Props = Record<string, unknown>;
    type EsbWindow = Window & { esbTrack?: (event: string, props?: Props) => boolean };
    
    export type EsbItem = {
      item_id: string; // SAME id as product feed <id> and order item productId
      item_name: string;
      price: number;
      quantity?: number;
      item_variant?: string;
      item_category?: string;
    };
    
    export type EsbField =
      | "first_name" | "last_name" | "phone" | "email" | "address"
      | "city" | "state" | "postal_code" | "country";
    
    const CURRENCY = "BDT"; // your store currency (ISO 4217)
    const queue: Array<[string, Props | undefined]> = [];
    let waiting = false;
    
    function send(event: string, props?: Props) {
      if (typeof window === "undefined") return;
      const w = window as EsbWindow;
      if (w.esbTrack) {
        w.esbTrack(event, props);
        return;
      }
      queue.push([event, props]);
      if (waiting) return;
      waiting = true;
      window.addEventListener(
        "esb:ready",
        () => {
          while (queue.length) {
            const [e, p] = queue.shift()!;
            (window as EsbWindow).esbTrack?.(e, p);
          }
        },
        { once: true },
      );
    }
    
    function ecommerce(items: EsbItem[], extra?: Props): Props {
      const clean = items.map((it) => ({
        item_id: String(it.item_id),
        item_name: String(it.item_name),
        price: Number(it.price),
        quantity: Number(it.quantity ?? 1),
        ...(it.item_variant ? { item_variant: it.item_variant } : {}),
        ...(it.item_category ? { item_category: it.item_category } : {}),
      }));
      const value = clean.reduce((s, it) => s + it.price * it.quantity, 0);
      return { ecommerce: { currency: CURRENCY, value, items: clean, ...(extra ?? {}) } };
    }
    
    const FIELD_EVENT: Record<EsbField, string> = {
      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",
    };
    
    let formStarted = false;
    const sentFieldValues = new Set<string>();
    
    export const esb = {
      pageView: () => send("page_view"),
      viewItem: (item: EsbItem) => send("view_item", ecommerce([{ ...item, quantity: 1 }])),
      addToCart: (item: EsbItem) => send("add_to_cart", ecommerce([item])),
      removeFromCart: (item: EsbItem) => send("remove_from_cart", ecommerce([item])),
      viewCart: (items: EsbItem[]) => send("view_cart", ecommerce(items)),
      beginCheckout: (items: EsbItem[]) => send("begin_checkout", ecommerce(items)),
      addShippingInfo: (items: EsbItem[], shippingTier: string) =>
        send("add_shipping_info", ecommerce(items, { shipping_tier: shippingTier })),
      addPaymentInfo: (items: EsbItem[], paymentType: string) =>
        send("add_payment_info", ecommerce(items, { payment_type: paymentType })),
    
      checkoutFormStarted: () => {
        if (formStarted) return;
        formStarted = true;
        send("checkout_form_started", {});
      },
      /** Call on change/blur of a checkout input with its current value. */
      field: (field: EsbField, value: string) => {
        const v = String(value ?? "").trim();
        if (!v) return;
        const event = FIELD_EVENT[field];
        const key = `${event}:${v.toLowerCase()}`;
        if (sentFieldValues.has(key)) return; // same value already sent
        sentFieldValues.add(key);
        esb.checkoutFormStarted();
        if (field === "phone") return send(event, { phone: v });
        if (field === "email") return send(event, { email: v });
        if (field === "first_name") return send(event, { name: v, field_value_len: v.length });
        if (field === "last_name") return send(event, { last_name: v, field_value_len: v.length });
        send(event, { field_value_len: v.length });
      },
      checkoutFormCompleted: () => send("checkout_form_completed", {}),
    };
  2. 2) page_view — রুট পরিবর্তন

    প্রথম লোডের হিট EsbTracker নিজে পাঠায় (ভিজিটরের একদম প্রথম ভিজিটে landing_page_view, পরে page_view); SPA নেভিগেশনের জন্য এই কম্পোনেন্ট।

    components/esb-page-views.tsx
    // components/esb-page-views.tsx — SPA route changes → page_view (EsbTracker already sent the first hit)
    "use client";
    import { useEffect, useRef } from "react";
    import { usePathname, useSearchParams } from "next/navigation";
    import { esb } from "@/lib/esb-track";
    
    export function EsbPageViews() {
      const pathname = usePathname();
      const search = useSearchParams();
      const first = useRef(true);
      useEffect(() => {
        if (first.current) {
          first.current = false; // initial load → EsbTracker sent landing_page_view / page_view on mount
          return;
        }
        esb.pageView();
      }, [pathname, search]);
      return null;
    }
    
    // app/layout.tsx — next to <EsbTracker … />
    // import { Suspense } from "react";
    // <Suspense fallback={null}><EsbPageViews /></Suspense>   // useSearchParams needs Suspense
  3. 3) view_item — প্রোডাক্ট পেজ

    সার্ভার PDP থেকে প্রোডাক্ট ডাটা দিয়ে ছোট client কম্পোনেন্ট রেন্ডার করুন।

    components/track-view-item.tsx
    // Product page — a tiny client component rendered by your (server) PDP
    "use client";
    import { useEffect } from "react";
    import { esb } from "@/lib/esb-track";
    
    export function TrackViewItem({ id, name, price, variant }: {
      id: string; name: string; price: number; variant?: string;
    }) {
      useEffect(() => {
        esb.viewItem({ item_id: id, item_name: name, price, item_variant: variant });
      }, [id, name, price, variant]);
      return null;
    }
    
    // app/product/[slug]/page.tsx (server):
    // <TrackViewItem id={String(product.id)} name={product.name} price={Number(product.salePrice ?? product.price)} />
  4. 4) add_to_cart · remove_from_cart · view_cart

    কার্ট লজিক যেখানে আছে সেখানেই কল — বাটন যত জায়গায় থাকুক (কার্ড, PDP, Buy Now) সব কভার হবে।

    your cart store / handlers
    // Inside YOUR cart store / handlers (Zustand, Redux, Context, Server Action result, …)
    import { esb } from "@/lib/esb-track";
    
    async function onAddToCart(product, qty) {
      await addLine(product.id, qty);                 // your logic
      esb.addToCart({ item_id: String(product.id), item_name: product.name, price: product.price, quantity: qty });
    }
    
    function onRemoveLine(line, removedQty = line.qty) {
      removeLine(line.id);                            // your logic (also when qty is decreased)
      esb.removeFromCart({ item_id: String(line.productId), item_name: line.name, price: line.price, quantity: removedQty });
    }
    
    // Cart page/drawer opened:
    // useEffect(() => { if (open) esb.viewCart(lines.map(toItem)); }, [open]);
  5. 5) begin_checkout · add_shipping_info · add_payment_info

    চেকআউট খোলা, ডেলিভারি এরিয়া/শিপিং বাছাই, পেমেন্ট মেথড বাছাই।

    checkout page
    // Checkout page (client component)
    "use client";
    import { useEffect } from "react";
    import { esb, type EsbItem } from "@/lib/esb-track";
    
    export function CheckoutTracking({ items }: { items: EsbItem[] }) {
      useEffect(() => {
        esb.beginCheckout(items);           // once per checkout visit
      }, []); // eslint-disable-line react-hooks/exhaustive-deps
      return null;
    }
    
    // Shipping zone / method selected (e.g. "inside_dhaka" | "outside_dhaka"):
    //   onChange={(z) => { setZone(z); esb.addShippingInfo(items, z); }}
    // Payment method selected (e.g. "cod" | "bkash"):
    //   onChange={(m) => { setPayment(m); esb.addPaymentInfo(items, m); }}
  6. 6) চেকআউট ফর্ম — ১১টা ফর্ম/ফিল্ড ইভেন্ট

    checkout_form_started, প্রতিটি ফিল্ডের *_added, checkout_form_completed। সাইটে যে ইনপুট আছে তার প্রতিটিতে esb.field()।

    checkout form
    // Checkout form — call esb.field() for EVERY input that exists (any input names work)
    <form
      onFocus={() => esb.checkoutFormStarted()}
      onSubmit={(e) => { esb.checkoutFormCompleted(); handleSubmit(e); }}
    >
      <input name="fullName" onBlur={(e) => esb.field("first_name", e.target.value)} />
      <input name="lastName" onBlur={(e) => esb.field("last_name", e.target.value)} />
      <input name="phone" type="tel" onBlur={(e) => esb.field("phone", e.target.value)} />
      <input name="email" type="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="postcode" onBlur={(e) => esb.field("postal_code", e.target.value)} />
      <select name="country" onChange={(e) => esb.field("country", e.target.value)} />
    </form>
    
    // Field → event: first_name→first_name_added · last_name→last_name_added · phone→phone_number_added
    // email→email_address_added · address→address_added · city (district)→city_added
    // state (division / thana / upazila)→state_added · postal_code→postal_code_added · country→country_added
    // Phone, email and name send the real value ({ phone } / { email } / { name } / { last_name }) —
    // required for abandoned-checkout recovery (Customer, phone, email in Checkout drop-off).
    
    // ALTERNATIVE (no per-input code): window.esbBindCheckoutForm(selector, fieldMap)
    //  - matches inputs by their name attribute (lowercased; "billing[city]" → "city")
    //  - passing fieldMap REPLACES the defaults — list every input:
    //    window.esbBindCheckoutForm("form#checkout", {
    //      fullname: "first_name_added", lastname: "last_name_added", address: "address_added",
    //      district: "city_added", thana: "state_added", postcode: "postal_code_added", country: "country_added",
    //    }); // phone/email are detected automatically from type/name/placeholder
    //  - call it after the form is in the DOM AND after "esb:ready" (it returns false otherwise)
  7. 7) অর্ডার — সার্ভার webhook

    অর্ডার DB-তে সেভ হওয়ার পর সার্ভার থেকে। productId = item_id = ফিড id।

    call site · server
    // Server — after YOUR order is committed (Route Handler, Server Action, worker, …)
    import { cookies } from "next/headers";
    import { buildOrderPayload, OrderWebhookPoster } from "@ecomsolvebd/next";
    
    const poster = new OrderWebhookPoster({
      merchantKey: process.env.ECOMSOLVEBD_MERCHANT_KEY!,
      webhookSecret: process.env.ECOMSOLVEBD_WEBHOOK_SECRET!,
      apiBase: process.env.ECOMSOLVEBD_API_BASE,
    });
    
    const payload = buildOrderPayload(
      {
        orderNumber: String(order.number), // stable storefront order number
        currency: String(order.currency),  // ISO 4217, e.g. "BDT"
        customer: { fullName: order.customerName, phone: order.phone, email: order.email },
        items: order.lines.map((line) => ({
          productId: String(line.productId), // SAME id as event item_id + feed <id>
          sku: line.sku,
          title: line.title,
          variantTitle: line.variantTitle,
          quantity: line.qty,
          unitPrice: String(line.unitPrice),
        })),
        district: order.district,
        upazila: order.upazila,
        address: order.address,
        notes: order.notes,
        shippingTotal: String(order.shippingTotal),
        discountTotal: String(order.discountTotal ?? 0),
      },
      { cookieHeader: (await cookies()).toString() }, // forwards esb_vid → links order to the visitor
    );
    
    try {
      await poster.postOrder(payload);
    } catch (err) {
      console.error("[ecomsolvebd] postOrder failed", err); // never fail the shopper's checkout
    }
    // Ingest only — ads Purchase is sent later from EcomSolveBD (confirm / courier).

সব কল-সাইট একনজরে

call sites
import { esb } from "@/lib/esb-track"; // client components only

// 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.

SDK রেফারেন্স · বাধ্যতামূলক

প্রোডাক্ট ফিড — সেটআপ

EcomSolveBD-এর প্রোডাক্ট লিস্ট, প্রফিট/কস্ট, ইনবক্স প্রোডাক্ট কার্ড ও অ্যাড ক্যাটালগ এই ফিড থেকেই আসে — এটা ঐচ্ছিক নয়।

  • চারটা পাবলিক 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. 1) Feed route (একটা ফাইল, চারটা URL)

    ফোল্ডারের নাম [channel] — [channel].xml নয়। /feed/* পাবলিক রাখুন (middleware/auth/i18n বাদ)।

    app/feed/[channel]/route.ts
    // app/feed/[channel]/route.ts   (src/app/feed/[channel]/route.ts with a src/ dir)
    // One file serves all four URLs: /feed/products.xml · /feed/facebook.xml · /feed/tiktok.xml · /feed/google.xml
    // The [channel] segment receives "products.xml" etc.; the package strips ".xml".
    // Do NOT name the folder "[channel].xml" and no next.config rewrite is needed.
    import { createFeedRouteHandlers } from "@ecomsolvebd/next/route-handlers";
    import { productFeedProvider } from "@/lib/esb-product-feed";
    
    export const dynamic = "force-dynamic"; // always read the live catalog
    
    export const { GET } = createFeedRouteHandlers({
      storeOrigin: process.env.NEXT_PUBLIC_STORE_URL!, // https://your-store.example
      currency: process.env.ECOMSOLVEBD_FEED_CURRENCY || "BDT",
      storeName: process.env.ECOMSOLVEBD_FEED_STORE_NAME || "My Shop",
      provider: productFeedProvider,
    });
    
    // middleware.ts / proxy.ts: make sure /feed/* is NOT behind auth, i18n or locale redirects
    // (e.g. add "feed" to the matcher's excluded paths).
  2. 2) ProductFeedProvider — আসল ক্যাটালগ থেকে

    db.product… লাইনটা আপনার ডাটা সোর্সে বদলান; বাকি ম্যাপিং রাখুন। খালি array রেখে দেবেন না।

    lib/esb-product-feed.ts
    // lib/esb-product-feed.ts — read YOUR real catalog (Prisma, Drizzle, CMS, REST API, …)
    import type { FeedCatalogProduct, FeedChannelItem, ProductFeedProvider } from "@ecomsolvebd/next";
    
    const ORIGIN = process.env.NEXT_PUBLIC_STORE_URL!.replace(/\/$/, "");
    const CURRENCY = process.env.ECOMSOLVEBD_FEED_CURRENCY || "BDT";
    const BRAND = process.env.ECOMSOLVEBD_FEED_STORE_NAME || "My Shop";
    
    const abs = (u?: string | null) => (!u ? "" : /^https?:\/\//i.test(u) ? u : ORIGIN + "/" + u.replace(/^\//, ""));
    const plain = (html?: string | null) => String(html ?? "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
    
    /** One row per sellable unit. Expand variants here (id = `${productId}-${variantId}`). */
    async function loadRows() {
      // ↓ replace with your query — ONLY active/published products, ALL of them (no paging limit)
      const products = await db.product.findMany({
        where: { isActive: true },
        include: { category: true, images: true, variants: true },
      });
    
      return products.flatMap((p) => {
        const base = {
          title: p.name,
          description: plain(p.description) || p.name,
          category: p.category?.name ?? "",
          image: abs(p.images[0]?.url ?? p.thumbnail),
          url: `${ORIGIN}/product/${p.slug}`,          // your real PDP URL pattern
        };
        if (p.variants.length === 0) {
          return [{ ...base, id: String(p.id), sku: p.sku ?? String(p.id),
            price: Number(p.salePrice ?? p.price), stock: p.stock }];
        }
        return p.variants.map((v) => ({
          ...base,
          id: `${p.id}-${v.id}`,
          title: `${p.name} — ${v.name}`,
          sku: v.sku ?? `${p.id}-${v.id}`,
          image: abs(v.image) || base.image,
          price: Number(v.salePrice ?? v.price),
          stock: v.stock,
        }));
      });
    }
    
    export const productFeedProvider: ProductFeedProvider = {
      // → /feed/products.xml (EcomSolveBD catalog). Include every row, even out of stock.
      async catalogProducts(): Promise<FeedCatalogProduct[]> {
        const rows = await loadRows();
        return rows.map((r) => ({
          id: r.id,                                   // = event item_id = order productId
          title: r.title,
          price: r.price.toFixed(2),
          currency: CURRENCY,
          category: r.category,
          image: r.image,
          stockStatus: r.stock === null || r.stock > 0 ? "in_stock" : "out_of_stock",
          stockQty: r.stock === null ? "unlimited" : String(Math.max(0, r.stock)),
          productUrl: r.url,
          sku: r.sku,
        }));
      },
    
      // → facebook.xml / tiktok.xml / google.xml. The package does NOT filter — skip bad rows here.
      async channelItems(): Promise<FeedChannelItem[]> {
        const rows = await loadRows();
        return rows
          .filter((r) => r.price > 0 && /^https:\/\//i.test(r.image))
          .map((r) => {
            const inStock = r.stock === null || r.stock > 0;
            return {
              id: r.id,
              skuId: r.sku,
              title: r.title.slice(0, 150),
              description: r.description.slice(0, 5000),
              availabilityMeta: inStock ? "in stock" : "out of stock",
              availabilityGoogle: inStock ? "in_stock" : "out_of_stock",
              price: `${r.price.toFixed(2)} ${CURRENCY}`,
              link: r.url,
              imageLink: r.image,
              brand: BRAND,
              productType: r.category || "General",
            };
          });
      },
    };
  3. 3) যাচাই + ড্যাশবোর্ডে বসানো

    ডিপ্লয়ের পর URL খুলে দেখুন, তারপর 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

সঠিক আউটপুট — /feed/products.xml

products.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>

সঠিক আউটপুট — facebook/tiktok/google

facebook.xml
<?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 &gt; 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 এজেন্টকে এই চেকলিস্টটা হুবহু দিন — শেষে প্রতিটি ইভেন্ট কোন ফাইলে ফায়ার হচ্ছে তার রিপোর্ট চাইবে।

checklist.txt
EcomSolveBD × Next.js — 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: Add app/feed/[channel]/route.ts + the next.config rewrite + a real ProductFeedProvider (no empty arrays, no placeholders) so /feed/products.xml, /feed/facebook.xml, /feed/tiktok.xml and /feed/google.xml list every active product.
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 থেকে এডিট?
না। প্রোডাক্ট Next.js/API-তেই।
অর্ডার কীভাবে আসে?
চেকআউট সফলে সার্ভার webhook — ব্রাউজার purchase নয়। Dashboard Create Order (v1.0.1+) Next.js অ্যাডমিনেও যায়।
Secret হারিয়ে গেলে?
Website → Custom / Headless → Next.js (open to manage) → আবার Connect, নতুন Secret।

সমস্যা হলে

page_view / ecommerce আসছে না

Collect proxy + EsbTracker। বাকি সব ইভেন্ট: lib/esb-track.ts-এর esb.*() কল প্রতিটি UI মুহূর্তে।

tracker-config CORS লাল

Collect same-origin proxy দিয়ে চলে; tracker-config API Origin reflect করে (SaaS)। Hard-refresh করে আবার দেখুন।

অর্ডার আসছে না

Connect Next.js আগে। Secret মিলিয়ে postOrder() লগ চেক করুন।

স্ট্যাটাস সিঙ্ক নয়

order-status route + HTTPS base URL + order_number মিলছে কিনা।

সাহায্য লাগলে

শপ URL + স্ক্রিনশট + এরর মেসেজ পাঠান।

· বাংলাদেশ সময়