সরবরাহকারীর API: অর্ডার দখল করা, নিজের সঙ্গে দৌড় না লাগিয়ে
GET /orders কিছুই দখল করে না, ওটা শুধু পড়ে। দুটো worker চালালে দুজনেই একই অর্ডার হাতে পাবে আর দুজনেই ডেলিভারি করার চেষ্টা করবে। যে এন্ডপয়েন্টটা সত্যিই দখল করে সেটা আলাদা, আর তার সঙ্গে একটা ইজারা আর দুটো সীমা জড়িয়ে আছে।
Sarah Johnsonএই লেখাটার সব কারিগরি নামগুলো ইংরেজিতেই রাখা হয়েছে, আর সেটা ইচ্ছে করে। কারণ যিনি এই পাতাটা পড়ছেন তিনি ওই শব্দগুলো কোডে টাইপ করবেন, অনুবাদ করবেন না। ব্যাখ্যাটা বাংলায়, নামগুলো যেমন আছে তেমন।
প্রথম সিদ্ধান্তটাই সবচেয়ে গুরুত্বপূর্ণ, আর বেশিরভাগ ইন্টিগ্রেশন ওখানেই ভুল করে: কোন এন্ডপয়েন্ট দিয়ে কাজ তুলে আনবেন।
GET /orders একটা পড়া। যে চাইবে তাকেই মিলে যাওয়া অর্ডারগুলো ফেরত দেবে, আর কিছুই বদলাবে না। দুটো worker চালান, বা একটা worker যেটা ব্যর্থ হলে আবার চেষ্টা করে, দুজনের হাতেই একই অর্ডার যাবে আর দুজনেই ডেলিভারি করার চেষ্টা করবে।
যে এন্ডপয়েন্টটা কাজ দখল করে সেটা POST /orders-pull। ওটা আপনার অপেক্ষমাণ ম্যানুয়াল অর্ডারগুলো বেছে নেয়, প্রতিটাকে pending থেকে processing অবস্থায় ঘোরায় শর্তসাপেক্ষে বদলের মাধ্যমে, আর তারপর কেবল সেগুলোই ফেরত দেয় যেগুলোর বদলটা সত্যিই ঘটেছে। এক মিলিসেকেন্ড পরে দ্বিতীয় worker চাইলে তার শর্তটা মেলে না, আর ওই অর্ডারটা তার উত্তরে থাকে না।
লুপটা যে ক্রমে চলা উচিত
- দখল।
POST /orders-pull। শরীরে ঐচ্ছিকlimit, ডিফল্ট একশো, সর্বোচ্চ পাঁচশো। পুরোনো অর্ডার আগে। - ইজারা, যদি আপনার worker ধীর হয়।
PATCH /orders/{id}/last-process-timeকোনো শরীর ছাড়াই এখনকার সময় বসিয়ে দেয়, আর তাতে অর্ডারটা আপনার নিজের পরের খোঁজ থেকে তিরিশ মিনিট আড়ালে চলে যায়। - ডেলিভারি।
POST /orders-updateলাইনগুলো নিয়ে, অথবা বড় কিছুর জন্যPOST /orders/{id}/accounts-bulk। - বন্ধ। ওই একই
orders-updateকলেইstatusহিসেবে completed যায়, বা কিছুটা পূরণ করে থাকলে partial।
প্রতিটা কলে একটা হেডার লাগে, আর দুটো রূপ গ্রহণ করা হয়। একটা হলো X-Api-Key, অন্যটা Authorization: Bearer। দুটোই সমান, তাই আপনার প্যানেল যে ধরনটা ইতিমধ্যে পাঠায় সেটাই রাখতে পারেন।
curl -X POST "https://api.hstockplus.com/api/admin/v2/orders-pull" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"limit": 50}'
চাবিটা বানানো যায় ড্যাশবোর্ডের API সেটিংসের পাতা থেকে। হেডার একেবারে না থাকলে ফেরত আসে API_KEY_REQUIRED, আর ভুল চাবি দিলে INVALID_API_KEY। দুটো আলাদা কোড আর দুটো আলাদা সমস্যা, তাই একসঙ্গে ধরবেন না।
তিরিশ মিনিটের জানালাটা একটা ইজারা, আর সেটা আপনাকেই নিতে হয়
দুটো তালিকার এন্ডপয়েন্টই এমন অর্ডার বাদ দেয় যার lastProcessTime গত তিরিশ মিনিটের ভেতরে। শুনতে মনে হয় ব্যবস্থাটা নিজে থেকেই দ্বিতীয়বার দেওয়া আটকায়। আটকায় না, কারণ ওই দুটো এন্ডপয়েন্টের একটাও ঘরটায় কিছু লেখে না।
গোটা ব্যবস্থায় ওই ঘরটায় লেখে কেবল একটা জিনিস, আর সেটা আপনি নিজে, PATCH /orders/{id}/last-process-time দিয়ে।
অর্থাৎ ঘরটা একটা সহযোগিতামূলক ইজারা। অর্ডারটা তুলে নেওয়ার সময় সময়টা বসিয়ে দিলে ওটা আধ ঘণ্টার জন্য আপনার পরের খোঁজ থেকে সরে যায়, আর ডেলিভারি করতে সময় লাগলে আর আপনার worker প্রতি মিনিটে খোঁজ করলে ঠিক এটাই আপনার দরকার। নির্দিষ্ট একটা সময় বসাতে চাইলে ISO সময় পাঠান, আর সঙ্গে সঙ্গে ছেড়ে দিতে চাইলে null। এন্ডপয়েন্টটা একেবারে ব্যবহার না করলে জানালাটা আপনার ক্ষেত্রে কখনও কাজই করবে না।
দুটোকে গুলিয়ে ফেলবেন না। orders-pull আপনাকে দুবার দখল করা থেকে বাঁচায় অবস্থার বদলটা দিয়ে। ইজারাটা দ্বিতীয় সমস্যার জন্য: আপনি অর্ডারটা নিয়েছেন, এখনও কাজ করছেন, আর ইতিমধ্যে ওটা আবার চোখের সামনে আসুক তা চান না।
অর্ডার পড়ার সময় দুটো ডিফল্ট মানুষকে চমকে দেয়
মিলিয়ে দেখা, হিসাব মেলানো আর ড্যাশবোর্ড বানানোর জন্য GET /orders এখনও ঠিক এন্ডপয়েন্ট। তার আগে দুটো কথা জেনে নিন।
- পেমেন্টের অবস্থার ডিফল্ট একটা নয়, দুটো।
paymentStatusপ্যারামিটার না দিলে আপনি পাবেন সেই অর্ডারগুলো যেগুলোcompletedঅথবাpartial। এখানে partial মানে টাকার কিছুটা ফেরত গেছে, আর ওই অর্ডারগুলো এখনও জীবন্ত কাজ। প্যারামিটারটা নিজে কমা দিয়ে আলাদা করা তালিকা নেয়, যেখানে pending, completed, failed, refunded, processing আর partial সবই চলে। - পণ্যের ধরন ঐচ্ছিক, আর একাধিক দেওয়া যায়। ম্যানুয়াল অর্ডার আনার জন্য ওটা বাধ্যতামূলক নয়। ওটা manual, inventory আর auto নেয়, কমা দিয়ে, তাই
productType=manual,autoবৈধ।
অর্ডারের অবস্থার ছাঁকনি নেয় pending, processing, completed, refunded আর error। partial ওই তালিকায় নেই, কারণ ওটা টাকার অবস্থা, অর্ডারের নয়। বাকি ছাঁকনিগুলো: entityType, subCategory, createdFrom আর createdTo, limit পাঁচশো পর্যন্ত, আর offset।
আপনার পাঠানো লাইনগুলোর সঙ্গে আসলে কী ঘটে
API নিয়ে সবচেয়ে বেশি অভিযোগ এখান থেকেই আসে, কারণ কিছু জমা হওয়ার আগে তিনটে রূপান্তর ঘটে আর উত্তরে তার একটাও দেখা যায় না।
- প্রতিটা উপাদান নতুন লাইনে ভাগ হয়ে যায়। অ্যারের একটা ঘরে গোটা একটা ফাইল ধরে রাখা যায়। তিনটে লাইন এক ঘরে পাঠানো আর তিনটে আলাদা ঘরে পাঠানো একই জিনিস। ফাঁকা লাইন আর আশপাশের ফাঁকা জায়গা বাদ পড়ে যায়।
- একই জিনিস দুবার থাকলে বাদ যায়, ছোট বড় হাতের অক্ষর না মিলিয়েই। এটা এক ব্যাচের ভেতরেও হয়, আর ওই অর্ডারে আগে যা দেওয়া হয়েছে তার সঙ্গেও হয়। ফলে সময় শেষ হয়ে যাওয়ার পর একই ব্যাচ আবার পাঠানো নিরাপদ, কিছুই দুবার যাবে না।
- অর্ডারের সংখ্যার বেশি লাইন চুপচাপ ফেলে দেওয়া হয়। যতটা জায়গা বাকি আছে ততটুকু রেখে বাকিটা কেটে দেওয়া হয়। কোনো ত্রুটি আসে না, তাই সংখ্যাটা নিজে ঠিক রাখুন।
লাইনের ভেতরে কী থাকবে সেটা পুরোপুরি আপনার ব্যাপার। প্ল্যাটফর্ম ওটাকে একটা আস্ত অস্বচ্ছ লেখা হিসেবে জমা রাখে আর কখনও ভেঙে পড়ে না। কোলন দিয়ে আলাদা করা জোড়া, পাইপ দিয়ে আলাদা করা তিনটে অংশ যার একটা দুই ধাপের যাচাইয়ের সংকেত, একটা টোকেন, একটা সেশনের গোছা, সবই কেবল লেখা।
এর মানেটা গুরুত্বপূর্ণ। আপনার তালিকার বিবরণে যে ধরনটার কথা লেখা আছে, ক্রেতা ঠিক সেটাই আশা করবেন, আর সেটা মেলানোর দায়িত্ব একমাত্র আপনার। ব্যবস্থাটা আপনার হয়ে কিছুই পরীক্ষা করে না।
আগে দেওয়া জিনিসের সঙ্গে মিলে গেলে সেটা বাদ পড়ে, ত্রুটি হয় না। তাই কয়েক দফায় ভাগ করে ডেলিভারি করা এখানে স্বীকৃত পদ্ধতি। যা আছে পাঠান, অর্ডারটাকে partial চিহ্নিত করুন, বাকিটা পরে পাঠান, তারপর completed করুন।
দুটো সীমা, আর তারা নিজেরাই বলে দেয় কতক্ষণ থামতে হবে
orders-update এর সঙ্গে প্রতিটা API চাবির জন্য একটা সর্বনিম্ন বিরতি বাঁধা আছে, ডিফল্টে দুশো মিলিসেকেন্ড, আর প্রতি ব্যবহারকারীর জন্য একসঙ্গে দুটোর বেশি অনুরোধ চলতে দেওয়া হয় না। যেকোনোটা ছাড়ালে HTTP 429 আসে, আর সঙ্গে যন্ত্রে পড়ার মতো একটা দেরির সংখ্যা:
{ "error": "RATE_LIMIT", "message": "Too many order update requests",
"retry_after_ms": 137 }
{ "error": "CONCURRENCY_LIMIT", "message": "Too many concurrent sync requests",
"retry_after_ms": 5000 }
retry_after_ms পড়ুন আর ঠিক ততক্ষণ ঘুমান। একটা বাঁধা দেরি ব্যবহার করলে সেটা দুশো মিলিসেকেন্ডের ক্ষেত্রে বড্ড ধীর হবে, আর একসঙ্গে চলার সীমার ক্ষেত্রে বড্ড দ্রুত।
এর সঙ্গে একটা তৃতীয় জিনিস আছে। একই অর্ডারে দুটো ডেলিভারি একসঙ্গে চেষ্টা করলে ORDER_DELIVERY_BUSY আসে, সেটাও একটা 429। এখানে ঠিক কাজ হলো একটু অপেক্ষা করে আবার একই অনুরোধ পাঠানো, অন্য কিছু পাঠানো নয়।
যে কোডগুলো আলাদা করে ধরা দরকার
API_KEY_REQUIREDআরINVALID_API_KEY: হেডার নেই বনাম হেডার আছে কিন্তু চেনা যাচ্ছে না।ORDER_NOT_FOUND: আইডিটা ক্যাশে নেই। সদ্য তৈরি হওয়া অর্ডারে অল্প সময়ের জন্য এটা আসতে পারে, তাই তৈরির কয়েক সেকেন্ড পরের 404 চিরস্থায়ী নাও হতে পারে।ACCESS_DENIED: ওই অর্ডারে আপনার নিজের কোনো লাইন নেই।PAYMENT_NOT_COMPLETED: টাকা আসেনি বা পুরোটা ফেরত গেছে। এখানে ডেলিভারি করা যাবে না আর আবার চেষ্টা করাও উচিত নয়।INVALID_ACCOUNTS, বার্তা "No valid accounts provided": আপনার পাঠানো অ্যারে ভেঙে শেষ পর্যন্ত কিছুই থাকেনি। সাধারণত একটা ফাঁকা লেখা বা লেখা নয় এমন কিছু।RATE_LIMIT,CONCURRENCY_LIMITআরORDER_DELIVERY_BUSY: থামুন, তারপর হুবহু একই অনুরোধ আবার পাঠান।
নিজের হিসাব রাখার দুটো ঘর
orders-update আপনার নিজের অর্ডারের সূত্র রাখার জন্য external_id নেয়, আর আপনার প্যানেল যদি ইতিমধ্যেই supplierOrderId নামে পাঠায় তাহলে সেটাও একই অর্থে গ্রহণ করা হয়।
আর external_price বলে একটা ঘর আছে, যেখানে অর্ডারটা আপনার নিজের উপরের দিকে কত খরচ হয়েছে সেটা রাখা যায়। ওই সংখ্যাটা কেবল আপনার নিজের হিসাবের জন্য, প্ল্যাটফর্মের কোনো যোগফলে ওটা ঢোকে না। null বা ফাঁকা লেখা পাঠালে আগে যা বসানো ছিল সেটাই থেকে যায়, তাই বারবার হালনাগাদ করলেও মুছে যাবে না।
ম্যানুয়াল মানে ধীর নয়, আর সংখ্যাটা চমকে দেয়
পণ্যের ধরন manual মানে অর্ডার আসার পর সরবরাহকারী মালটা তোলেন। ওটা ডেলিভারির গতির কোনো সেটিং নয়।
সাইটের সম্পন্ন হওয়া প্রতিটা ম্যানুয়াল পণ্যের অর্ডার ধরে হিসাব করলে, আর সেটা ছয় হাজারের বেশি অর্ডার, টাকা দেওয়া থেকে অর্ডার শেষ হওয়ার মাঝামাঝি সময়টা আধ মিনিটেরও কম। প্রায় তিন-চতুর্থাংশ শেষ হয়ে যায় দশ মিনিটের ভেতর। আর চল্লিশটার মধ্যে মোটামুটি একটা এক দিনের বেশি সময় নেয়।
এটা প্ল্যাটফর্মের কোনো কৃতিত্ব নয়। এর কারণ হলো, ম্যানুয়াল তালিকায় যাঁরা সত্যিই পরিমাণে বিক্রি করেন তাঁরা ড্যাশবোর্ডের দিকে তাকিয়ে বসে থাকেন না, তাঁরা এই API ছোট বিরতিতে খোঁজ করান। অর্থাৎ আপনার ইন্টিগ্রেশন যদি মিনিট যোগ করে, তাহলে আপনি স্বাভাবিক নন, আপনি ধীর লেজটা।
আর একটা ঘড়ি জেনে রাখুন যদি আপনি আইডির বদলে গ্রোথ সেবা বিক্রি করেন। গ্রোথ সেবার অর্ডারে ক্রেতা হাতে পাওয়ার মুহূর্ত থেকে ঠিক তিন দিন সময় পান, তালিকায় যা-ই লেখা থাক। আইডির তালিকা উল্টো নিয়মে চলে, ওখানে প্রতিটা তালিকার নিজের ওয়ারেন্টি থাকে।
একটা ভদ্র worker
const BASE = 'https://api.hstockplus.com/api/admin/v2';
const HEAD = { 'X-Api-Key': process.env.API_KEY, 'Content-Type': 'application/json' };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: HEAD,
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json().catch(() => ({}));
if (res.status === 429) {
await sleep(json.retry_after_ms ?? 1000);
return call(method, path, body);
}
if (!res.ok) throw Object.assign(new Error(json.message || res.status), { code: json.error });
return json;
}
async function tick() {
const { orders = [] } = await call('POST', '/orders-pull', { limit: 50 });
for (const order of orders) {
await call('PATCH', '/orders/' + order.id + '/last-process-time', {});
const accounts = await prepareCredentials(order);
await call('POST', '/orders-update', {
order: order.id,
status: accounts.length >= order.quantity ? 'completed' : 'partial',
accounts,
supplierOrderId: 'SUP-' + order.id,
});
}
}
এই লুপটা তিনটে জিনিস করে যেটা পুরোনো ধরনটা করত না। পড়ার বদলে দখল করে, নিজে দেরি আন্দাজ না করে সার্ভারের বলা দেরিটা মেনে নেয়, আর যেটা পুরো পূরণ করতে পারেনি সেটাকে সৎভাবে partial বলে, বন্ধ করে দেয় না।
প্রতিটা এন্ডপয়েন্টের পুরো তালিকা আছে সরবরাহকারীর API পাতায়। আর কোনো তালিকা manual হবে না inventory হবে, সেই সিদ্ধান্তটা নেওয়ার আগে পণ্য সাজানোর পাতাটা দেখে নিন, ওখানে দুটোর ফারাক সংখ্যা দিয়ে দেখানো আছে।



