Stripe-ভিত্তিক ব্যবহার-আধারিত বিলিং: একটি ব্যবহারিক ডেটা মডেল
Stripe-ভিত্তিক ব্যবহার-আধারিত বিলিংয়ের জন্য পরিষ্কার ইভেন্ট স্টোরেজ এবং রিকনসিলিয়েশন দরকার। একটি সরল স্কিমা, ওয়েবহুক ফ্লো, ব্যাকফিল ও ডাবল-কাউন্টিং সমাধান শিখুন।

আপনি যা বাস্তবে বানাচ্ছেন (এবং কেন এটি ভেঙে পড়ে)\n\nব্যবহার-ভিত্তিক বিলিং শোনায় সহজ: একটি গ্রাহক কী ব্যবহার করেছে মাপুন, মূল্যগুণ করুন, এবং পিরিয়ড শেষে চার্জ করুন। বাস্তবে, আপনি একটি ছোট হিসাব রাখার সিস্টেম বানাচ্ছেন। এটি সঠিক থাকতে হবে এমন পরিস্থিতিতেও যখন ডেটা দেরিতে আসে, বারবার আসে, বা কখনও আসে না।\n\nবেশিরভাগ ব্যর্থতা চেকআউট বা ড্যাশবোর্ডে ঘটে না। তারা ঘটে মিটারিং ডেটা মডেলে। যদি আপনি আত্মবিশ্বাসের সঙ্গে উত্তর দিতে না পারেন, "এই ইনভয়েসের জন্য কোন ব্যবহার ইভেন্টগুলো গণনা করা হয়েছে এবং কেন", তাহলে শেষপর্যন্ত আপনি অতিরিক্ত চার্জ, কম চার্জ, বা বিশ্বাস হারাবেন।\n\nব্যবহার বিলিং সাধারণত কয়েকটি পূর্বানুমেয় উপায়ে ভেঙে পড়ে: আউটেজের পরে ইভেন্ট হারিয়ে যায়, রিট্রাইগুলো ডুপ্লিকেট তৈরি করে, দেরিতে আসা ইভেন্টগুলো মোট গণনা করার পরে দেখা যায়, বা বিভিন্ন সিস্টেমের মধ্যে সামঞ্জস্য নেই এবং আপনি পার্থক্য মিলিয়ে নিতে পারেন না।\n\nStripe মূল্য নির্ধারণ, ইনভয়েস, ট্যাক্স, এবং সংগ্রহে চমৎকার। কিন্তু Stripe আপনার প্রোডাক্টের র কUsage কাঁচা ডেটা জানে না যতক্ষণ না আপনি সেটি পাঠান। এর ফলে একটি সোর্স-অফ-ট্রুথ সিদ্ধান্ত গ্রহণ করতে হয়: Stripe কি লেজার, নাকি আপনার ডাটাবেস লেজার যা Stripe প্রতিফলিত করে?\n\nঅনেক টিমের জন্য, সবচেয়ে নিরাপদ বিভাজন হলো:\n\n- আপনার ডাটাবেস কাঁচা ব্যবহার ইভেন্ট এবং তাদের লাইফসাইকেলের জন্য সোর্স-অফ-ট্রুথ।\n- Stripe হলো যা বাস্তবে ইনভয়েস ও পেমেন্ট হয়েছে তার সোর্স-অফ-ট্রুথ।\n\nউদাহরণ: আপনি "API কল" ট্র্যাক করেন। প্রতিটি কল একটি স্থির ইউনিক কী সহ একটি ব্যবহার ইভেন্ট তৈরি করে। ইনভয়েস সময়, আপনি শুধুমাত্র উপযুক্ত ইভেন্টগুলো টালি করেন যেগুলো এখনও বিল করা হয়নি, তারপর Stripe ইনভয়েস আইটেম তৈরি বা আপডেট করেন। যদি ইনজেশন রিট্রাই বা একটি ওয়েবহুক দুবার আসে, আইডেম্পটেনসি নিয়ম ডুপ্লিকেটকে নিরীহ করে তোলে।\n\n## টেবিল ডিজাইন করার আগে সিদ্ধান্ত যা নেওয়া দরকার\n\nটেবিল তৈরি করার আগে সেই সংজ্ঞাগুলো নির্ধারণ করুন যা পরে বিলিং কি ভাবে ব্যাখ্যা যোগ্য থাকবে তা নির্ধারণ করে। বেশিরভাগ "রহস্যময় ইনভয়েস বাগ" অস্পষ্ট নিয়ম থেকে আসে, খারাপ SQL থেকে নয়।\n\nপ্রথমে আপনি যা চার্জ করেন সেটির ইউনিট ঠিক করুন। এমন কিছু বেছে নিন যা মাপতে সহজ এবং তর্ক করতে কঠিন। "API কল" রিট্রাই, ব্যাচ অনুরোধ, এবং ব্যর্থতার কারণে জটিল হয়ে যায়। "মিনিট" ওভারল্যাপের সমস্যায় পড়ে। "GB"-এর ক্ষেত্রে একটি পরিষ্কার ভিত্তি (GB বনাম GiB) এবং মাপার পদ্ধতি (গড় বনাম পিক) লাগবে।\n\nপরবর্তী, সীমানা (boundaries) নির্ধারণ করুন। আপনার সিস্টেমকে জানাতে হবে একটি ইভেন্ট কোন উইন্ডোর অংশ। ব্যবহার প্রতি ঘন্টা, প্রতিদিন, প্রতি বিলিং পিরিয়ড, নাকি গ্রাহক অ্যাকশনের ভিত্তিতে গণ্য হচ্ছে? গ্রাহক মাঝেমধ্যে আপগ্রেড করলে কি উইন্ডো ভাগ করবেন নাকি পুরো মাসে এক মূল্য প্রয়োগ করবেন? এই সিদ্ধান্তগুলো ইভেন্ট গ্রুপিং এবং মোট ব্যাখ্যা করার নিয়ম নির্ধারণ করে।\n\nএছাড়াও কোন সিস্টেম কোন তথ্যের মালিক হবে তা ঠিক করুন। Stripe-সহ একটি সাধারণ প্যাটার্ন হলো: আপনার অ্যাপ কাঁচা ইভেন্ট এবং উৎপন্ন টোটালগুলোর মালিক, আর Stripe ইনভয়েস ও পেমেন্ট স্ট্যাটাসের মালিক। এই পদ্ধতি তখন ভাল কাজ করে যখন আপনি ইতিহাস নীরবে সম্পাদনা করেন না। আপনি সংশোধনগুলো নতুন এন্ট্রির মাধ্যমে রেকর্ড করবেন এবং মূল রেকর্ড অটুট রাখবেন।\n\nকয়েকটি অননেযোগ্য নিয়ম/schema সহজ রাখলে সাহায্য করে:\n\n- Traceability: প্রতিটি বিলে করা ইউনিট সংরক্ষিত ইভেন্টের সাথে টেকনিক্যালি যোগ করা যায়।\n- Auditability: আপনি কয় মাস পরে কেন চার্জ হয়েছে তা ব্যাখ্যা করতে পারবেন।\n- Reversibility: ভুলগুলো স্পষ্ট অ্যাডজাস্টমেন্ট দিয়ে ঠিক করা যায়।\n- Idempotency: একই ইনপুট দুইবার গোনা যাবে না।\n- Clear ownership: প্রতিটি ফ্যাক্টের একটি নির্দিষ্ট সিস্টেম মালিকানাধীন।\n\nউদাহরণ: আপনি যদি "পাঠানো মেসেজ" এর জন্য বিল করেন, তবে সিদ্ধান্ত নিন রিট্রাইগুলো গণ্য হবে কি না, ব্যর্থ ডেলিভারি গোনা হবে কি না, এবং কোন টাইমস্ট্যাম্প জয়ী (ক্লায়েন্ট সময় বনাম সার্ভার সময়)। লিখে রাখুন, তারপর ইভেন্ট ফিল্ড ও ভ্যালিডেশনে এনকোড করুন, কারো স্মৃতিতে না রেখে।\n\n## ব্যবহার ইভেন্টের জন্য একটি সহজ ডেটা মডেল\n\nব্যবহার-ভিত্তিক বিলিং সহজ যখন আপনি ব্যবহারকে হিসাবরক্ষার মত ধরে নেন: কাঁচা তথ্য append-only, এবং টোটাল ডেরাইভড। এই একটাই সিদ্ধান্ত বেশিরভাগ বিতর্ক রোধ করে কারণ আপনি সবসময় দেখাতে পারবেন যে একটি সংখ্যা কোথা থেকে এসেছে।\n\nএকটি ব্যবহারিক শুরু পয়েন্টে পাঁচটি মূল টেবিল থাকে (নাম ভিন্ন হতে পারে):\n\n- customer: অভ্যন্তরীণ customer id, Stripe customer id, স্ট্যাটাস, বেসিক মেটাডাটা।\n- subscription: অভ্যন্তরীণ subscription id, Stripe subscription id, প্রত্যাশিত প্ল্যান/দাম, start/end টাইমস্ট্যাম্প।\n- meter: আপনি কি মাপেন (API কল, সীট, স্টোরেজ GB-ঘন্টা)। একটি স্থিতিশীল meter key, ইউনিট, এবং এটি কিভাবে অ্যাগ্রিগেট করে (sum, max, unique) রাখবেন।\n- usage_event: প্রতিটি পরিমাপিত অ্যাকশনের জন্য একটি সারি। customer_id, subscription_id (যদি জানা থাকে), meter_id, quantity, occurred_at (ঘটনার সময়), received_at (আপনি যখন ইনগেস্ট করেছেন), source (app, batch import, partner), এবং একটি স্থিতিশীল external key ডেডুপ জন্য রাখুন।\n- usage_aggregate: উৎপন্ন টোটাল, সাধারণত customer + meter + টাইম বাল্কেট (দিন বা ঘণ্টা) এবং বিলিং পিরিয়ড অনুযায়ী। সংযুক্ত সাম অঙ্ক এবং একটি version বা last_event_received_at রাখুন পুনঃহিসাব সমর্থনে।\n\nusage_event অপরিবর্তনীয় (immutable) রাখুন। পরে যদি কোনো ভুল খুঁজে পান, তখন ইতিহাস সম্পাদনা করার পরিবর্তে একটি প্রতিশোধী (compensating) ইভেন্ট লিখুন (উদাহরণস্বরূপ, বাতিলের জন্য -3 সীট)।\n\nঅডিট ও বিতর্কের জন্য কাঁচা ইভেন্ট সংরক্ষণ করুন। যদি আপনি অনন্তকাল সংরক্ষণ না করতে পারেন, তবে অন্তত আপনার বিলিং লুকব্যাক উইন্ডো এবং রিফান্ড/বিতর্ক উইন্ডো পর্যন্ত রাখুন।\n\nউৎপন্ন টোটাল আলাদা রাখুন। অ্যাগ্রিগেটস ইনভয়েস ও ড্যাশবোর্ডের জন্য দ্রুত, তবে ধরনের ডিসপোজেবল। আপনি যে কোনো সময়ে, ব্যাকফিল সহ, usage_event থেকে usage_aggregate পুনর্নির্মাণ করতে সক্ষম হওয়া উচিত।\n\n## আইডেম্পটেনসি এবং ইভেন্ট লাইফসাইকেল স্টেটসমূহ\n\nব্যবহার ডেটা গোলমেলে। ক্লায়েন্টরা রিট্রাই করে, কিউগুলো ডুপ্লিকেট ডেলিভারি দেয়, এবং Stripe ওয়েবহুক অর্ডারবিহীনভাবে এসে যেতে পারে। যদি আপনার ডাটাবেস প্রমাণ করতে না পারে "এই ব্যবহার ইভেন্টটি আগে থেকেই গণনা করা হয়েছে", তাহলে শেষমেশ আপনি দুইবার বিল করবেন।\n\nপ্রতিটি ব্যবহার ইভেন্টকে একটি স্থিতিশীল, নির্ধারনযোগ্য event_id দিন এবং এর উপর ইউনিকনেস জোরদার করুন। কেবল auto-increment id-র ওপর নির্ভর করবেন না। একটি ভাল event_id ব্যবসায়িক অ্যাকশনের ওপর নির্ভর করে তৈরি করা উচিত, যেমন customer_id + meter + source_record_id (বা customer_id + meter + timestamp_bucket + sequence)। একই অ্যাকশন পুনরায় পাঠালে একই event_id তৈরি হবে এবং ইনসার্ট নিরাপদভাবে নো-অপ হবে।\n\nআইডেম্পটেনসি আপনার প্রতিটি ইনজেস্ট পাথকে কভার করতে হবে, শুধুমাত্র পাবলিক API নয়। SDK কল, ব্যাচ ইমপোর্ট, ওয়ার্কার জব, এবং ওয়েবহুক প্রসেসর—সবকিছুকে রিট্রাই করা হতে পারে। একটি নিয়ম রাখুন: ইনপুট রিট্রাই হতে পারে এমনটি হলে, সেটির জন্য একটি idempotency কী ডাটাবেসে সংরক্ষণ করে টোটাল বদলানোর আগে চেক করতে হবে।\n\nএকটি সহজ লাইফসাইকেল স্টেট মডেল রিট্রাইকে নিরাপদ করে এবং সাপোর্ট সহজ করে। এটিকে স্পষ্ট রাখুন, এবং ব্যর্থ হলে কারণও সংরক্ষণ করুন:\n\n- received: সংরক্ষণ করা হয়েছে, এখনও যাচাই করা হয়নি\n- validated: স্কিমা, গ্রাহক, মিটার, এবং টাইম-উইন্ডো নিয়ম পাস করেছে\n- posted: বিলিং পিরিয়ড টোটালে গণনা করা হয়েছে\n- rejected: স্থায়ীভাবে উপেক্ষিত (একটি কারণ কোডসহ)\n\nউদাহরণ: আপনার ওয়ার্কার ভ্যালিডেট করার পরে ক্র্যাশ করে কিন্তু পোস্ট করার আগে। রিট্রাইতে এটি একই event_id-এর validated স্টেটে পায় এবং তারপর posted-এ চলে যায় কোনো দ্বিতীয় ইভেন্ট তৈরি না করে।\n\nStripe ওয়েবহুকগুলোর জন্যও একই প্যাটার্ন ব্যবহার করুন: Stripe event.id সংরক্ষণ করুন এবং একবারই প্রসেসেড হিসেবে চিহ্নিত করুন, যাতে ডুপ্লিকেট ডেলিভারি নিরপেক্ষ থাকে।\n\n## পদক্ষেপ-বাই-পদক্ষেপ: মিটারিং ইভেন্ট অন্তর্গত করা (ingest) সম্পূর্ণ ফ্লো\n\nপ্রতিটি মিটারিং ইভেন্টকে অর্থের মতো বিবেচনা করুন: এটাকে যাচাই করুন, কাঁচা রেকর্ড রাখুন, তারপর উৎস থেকে টোটাল ডেরাইভ করুন। এতে রিট্রাই বা দেরিতে ডেটা আসা হলেও বিলিং পূর্বানুমানযোগ্য থাকে।\n\n### একটি নির্ভরযোগ্য ইনজেশন ফ্লো\n\nপ্রতিটি ইনকামিং ইভেন্ট যাচাই করুন টোটাল স্পর্শ করার আগে। ন্যূনতম প্রয়োজন: একটি স্থিতিশীল গ্রাহক শনাক্তকারী, একটি মিটার নাম, একটি সংখ্যাসূচক পরিমাণ, একটি টাইমস্ট্যাম্প, এবং idempotency-র জন্য একটি ইউনিক ইভেন্ট কী।\n\nকাঁচা ইভেন্ট প্রথমে লিখুন, এমনকি পরে অ্যাগ্রিগেট করার পরিকল্পনা থাকলেও। ঐ কাঁচা রেকর্ডই আপনি পুনরায় প্রক্রিয়াকরণ করবেন, অডিট করবেন, এবং ভুল ঠিক করার সময় ব্যবহার করবেন।\n\nএকটি নির্ভরযোগ্য ফ্লো দেখাবে এইভাবে:\n\n- ইভেন্ট গ্রহণ, বাধ্যতামূলক ফিল্ড যাচাই, ইউনিটগুলো নর্মালাইজ করুন (উদাহরণস্বরূপ সেকেন্ড বনাম মিনিট)।\n- ইভেন্ট কী-কে ইউনিক কনস্ট্রেইন্ট হিসেবে ব্যবহার করে কাঁচা ব্যবহার ইভেন্ট সারি ইনসার্ট করুন।\n- প্রতিদিন বা বিলিং পিরিয়ড বাল্কেটে অ্যাগ্রিগেট করে পরিমাণ যোগ করুন।\n- যদি আপনি Stripe-এ ব্যবহার রিপোর্ট করেন, যে তথ্য পাঠিয়েছেন তা (মিটার, মুল্য, পিরিয়ড, এবং Stripe রেসপন্স আইডেন্টিফায়ার) রেকর্ড করুন।\n- অস্বাভাবিকতা (rejected ইভেন্ট, ইউনিট কনভার্সন, দেরিতে আগমন) অডিট লগ করুন।\n\nঅ্যাগ্রিগেশন পুনরাবৃত্তিযোগ্য রাখুন। একটি সাধারণ পদ্ধতি হলো: কাঁচা ইভেন্ট এক ট্রানজেকশনে ইনসার্ট করে তারপর একটি জব enqueue করা যা বাল্কেট আপডেট করে। যদি জবটি দুইবার চালান হয়, সেটি সনাক্ত করবে যে কাঁচা ইভেন্টটি ইতিমধ্যেই প্রয়োগ করা হয়েছে।\n\nযখন একজন গ্রাহক জিজ্ঞেস করে কেন তাদের বিল 12,430 API কল দেখাচ্ছে, আপনাকে সেই বিলিং উইন্ডোতে অন্তর্ভুক্ত কাঁচা ইভেন্টগুলোর সঠিক সেট দেখাতে সক্ষম হতে হবে।\n\n## Stripe ওয়েবহুককে আপনার ডাটাবেসের সাথে মিলানো (Reconciling)\n\nওয়েবহুক হলো Stripe যা বাস্তবে করেছে তার রসিদ। আপনার অ্যাপ ড্রাফট তৈরি করে এবং ব্যবহার পাঠাতে পারে, কিন্তু ইনভয়েসের অবস্থা তখনই বাস্তব যখন Stripe বলে।\n\nঅধিকাংশ টিম কয়েকটি ওয়েবহুক টাইপের ওপরই মনোযোগ দেয় যেগুলো বিলিং ফলাফলে প্রভাব ফেলে:\n\n- invoice.created, invoice.finalized, invoice.paid, invoice.payment_failed\n- customer.subscription.created, customer.subscription.updated, customer.subscription.deleted\n- checkout.session.completed (যদি আপনি Checkout-এ সাবস্ক্রিপশন শুরু করে থাকেন)\n\nপ্রতিটি ওয়েবহুক সংরক্ষণ করুন। কাঁচা পেএলোড রাখুন এবং যখন এটি আসছিলো তখন যা পর্যবেক্ষণ করেছেন তা সংরক্ষণ করুন: Stripe event.id, event.created, আপনার সিগনেচার ভেরিফিকেশন ফলাফল, এবং আপনার সার্ভারের গ্রহণ টাইমস্ট্যাম্প। এই ইতিহাস ডিবাগিং বা "কেন আমাকে চার্জ করা হয়েছে?" জবাব দেওয়ার সময় মূল্যবান।\n\nএকটি শক্ত, idempotent রিকনসিলিয়েশন প্যাটার্ন দেখাবে এইভাবে:\n\n1. stripe_webhook_events টেবিলে একটি ইউনিক কনস্ট্রেইন্ট সহ ওয়েবহুক ইনসার্ট করুন, event_id-এর উপর।\n2. যদি ইনসার্ট ব্যর্থ হয়, এটি রিট্রাই; থামুন।\n3. সিগনেচার যাচাই করুন এবং পাশ/ফেল রেকর্ড করুন।\n4. ইভেন্ট প্রসেস করার সময় Stripe ID (customer, subscription, invoice) দিয়ে আপনার অভ্যন্তরীণ রেকর্ডগুলো দেখুন।\n5. স্টেটচেঞ্জটি কেবল তখন প্রয়োগ করুন যখন এটি রেকর্ডকে এগিয়ে নেয়।\n\nআউট-অফ-অর্ডার ডেলিভারি স্বাভাবিক। "ম্যাক্স স্টেট জয়েস" নিয়ম এবং টাইমস্ট্যাম্প ব্যবহার করুন: কখনো কোনো রেকর্ডকে আগের অবস্থায় নামান না।\n\nউদাহরণ: আপনি invoice.paid পান invoice in_123-এর জন্য, কিন্তু আপনার অভ্যন্তরীণ ইনভয়েস রো এখনও নেই। একটি রো তৈরি করুন যেটি "Stripe থেকে দেখা হয়েছে" হিসেবে চিহ্নিত থাকবে, তারপর পরে Stripe customer ID ব্যবহার করে সঠিক অ্যাকাউন্টের সঙ্গে এটিকে যুক্ত করুন। এতে আপনার লেজার কনসিস্টেন্ট থাকে ডাবল প্রসেসিং ছাড়াই।\n\n## ব্যবহার টোটাল থেকে ইনভয়েস লাইন আইটেম পর্যন্ত\n\nকাঁচা ব্যবহারকে ইনভয়েস লাইনে পরিণত করা মূলত সময়কাঠামো ও সীমানার বিষয়। নির্ধারণ করুন আপনি কি রিয়েল-টাইম টোটাল চান (ড্যাশবোর্ড, স্পেন্ড অ্যালার্ট) নাকি শুধুমাত্র বিলিং সময়ে। অনেক টিম উভয় করে: ইভেন্ট ক্রমাগত লিখে রাখে, ইনভয়েস-রেডি টোটাল শিডিউলড জব দিয়ে কম্পিউট করে।\n\nআপনার ব্যবহার উইন্ডো Stripe-র বিলিং পিরিয়ডের সাথে সাযুজ্য রাখুন। ক্যালেন্ডার মাস অনুমান করবেন না। সাবস্ক্রিপশন আইটেমের বর্তমান বিলিং পিরিয়ড স্টার্ট ও এন্ড ব্যবহার করুন, তারপর কেবল সেই উইন্ডোর মধ্যে পড়া ইভেন্টগুলোর সমষ্টি নিন। টাইমস্ট্যাম্প UTC-তে রাখুন এবং বিলিং উইন্ডোও UTC-তে রাখুন।\n\nইতিহাস অমোচনীয় রাখুন। পরে যদি ভুল পাওয়া যায়, পুরানো ইভেন্ট সম্পাদনা করবেন না বা পূর্বের টোটাল পুনঃলিখন করবেন না। একটি অ্যাডজাস্টমেন্ট রেকর্ড তৈরি করুন যা মূল উইন্ডোকে নির্দেশ করে এবং পরিমাণ যোগ বা বিয়োগ করে। এটি অডিটে সহজ এবং ব্যাখ্যা রাখতে সুবিধা দেয়।\n\nপরিকল্পনা পরিবর্তন ও প্রোরেশন হল জায়গা যেখানে ট্রেসেবিলিটি প্রায়ই হারায়। গ্রাহক মাঝপথে প্ল্যান বদলালে ব্যবহারকে সাব-উইন্ডোতে ভাগ করুন যাতে প্রতিটি দাম যেখানে সক্রিয় ছিল সেই সময়সীমার সাথে মিলিয়ে যায়। আপনার ইনভয়েস দুইটি ব্যবহার লাইনে (বা একটি লাইন প্লাস একটি অ্যাডজাস্টমেন্ট) দেখাতে পারে, প্রত্যেকটি নির্দিষ্ট মূল্য ও সময়সীমার সঙ্গে যুক্ত।\n\nএকটি ব্যবহারিক ফ্লো:\n\n- Stripe থেকে ইনভয়েস উইন্ডো (period start এবং end) টেনে নিন।\n- ঐ উইন্ডো ও প্রাইস অনুযায়ী উপযুক্ত ব্যবহার ইভেন্টগুলো অ্যাগ্রিগেট করুন।\n- ব্যবহার টোটাল ও যেকোনো অ্যাডজাস্টমেন্ট থেকে ইনভয়েস লাইন আইটেম জেনারেট করুন।\n- একটি calculation run id সংরক্ষণ করুন যাতে পরবর্তীতে সংখ্যাগুলি পুনরায় প্রজনন করা যায়।\n\n## ব্যাকফিল এবং দেরি ডেটা—বিশ্বাস ভাঙানো ছাড়াই\n\nদেরিতে আসা ব্যবহার ডেটা স্বাভাবিক। ডিভাইস অফলাইন হয়, ব্যাচ জব দেরি করে, পার্টনার ফাইল পুনরায় পাঠায়, এবং লগস আউটেজের পরে রিপ্লে হয়। মূল বিষয় হলো ব্যাকফিলকে একটি সংশোধন কাজ হিসেবে দেখা, সংখ্যাগুলো "ফিট করানো" হিসেবে নয়।\n\nস্পষ্টভাবে উল্লেখ করুন ব্যাকফিল কোথা থেকে আসতে পারে (অ্যাপ্লিকেশন লগ, ওয়্যারহাউস এক্সপোর্ট, পার্টনার সিস্টেম)। প্রতিটি ইভেন্টে সোর্স রেকর্ড করুন যাতে আপনি ব্যাখ্যা করতে পারেন কেন এটি দেরিতে এলো।\n\nব্যাকফিল করলে দুইটি টাইমস্ট্যাম্প রাখুন: যখন ব্যবহার হয়েছে (যা আপনি বিল করতে চান) এবং যখন আপনি এটি ইনজেস্ট করেছেন। ইভেন্টটিকে ব্যাকফিল হিসেবে ট্যাগ করুন, কিন্তু ইতিহাস ওভাররাইট করবেন না।\n\nদেল্টা প্রয়োগ করার বদলে কাঁচা ইভেন্ট থেকে টোটাল পুনর্নির্মাণ পছন্দ করুন। রেপ্লে হলো বাগ থেকে পুনরুদ্ধারের উপায় बिना অনুমান করার। যদি আপনার পাইপলাইন idempotent হয়, আপনি একটি দিন, এক সপ্তাহ, বা পুরো বিলিং পিরিয়ড পুনরায় চালাতে পারেন এবং একই টোটাল পাবেন।\n\nএকবার ইনভয়েস তৈরি হয়ে গেলে, সংশোধন করার নীতিগুলো স্পষ্ট রাখুন:\n\n- যদি ইনভয়েস ফাইনাল না হয়, ফাইনালাইজেশনের আগে পুনঃহিসাব করুন এবং টোটাল আপডেট করুন।\n- যদি এটি ফাইনাল এবং আন্ডারবিলড হয়, একটি অ্যাড-অন ইনভয়েস ইস্যু করুন (বা নতুন ইনভয়েস আইটেম যোগ করুন) স্পষ্ট বর্ণনাসহ।\n- যদি এটি ফাইনাল এবং ওভারবিল্ড হয়, একটি ক্রেডিট নোট ইস্যু করুন এবং মূল ইনভয়েস রেফারেন্স করুন।\n- সংশোধন এড়াতে ব্যবহারকে অন্য পিরিয়ডে সরাবেন না।\n- সংশোধনের একটি সংক্ষিপ্ত কারণ সংরক্ষণ করুন (পার্টনার রিসেন্ড, দেরি লগ ডেলিভারি, বাগ ফিক্স)।\n\nউদাহরণ: একটি পার্টনার জানায় যে জানুয়ারি 28-29-এর অনুপস্থিত ইভেন্টগুলো ফেব্রুয়ারি 3-এ পাঠিয়েছে। আপনি সেগুলো insert করেন occurred_at জানুয়ারিতে, ingested_at ফেব্রুয়ারিতে, এবং সোর্স হিসেবে "partner" রাখেন। জানুয়ারির ইনভয়েস ইতিমধ্যেই পেইড ছিল, তাই আপনি অনুপস্থিত ইউনিটগুলোর জন্য একটি ছোট অ্যাড-অন ইনভয়েস তৈরি করবেন এবং রিকনসিলিয়েশন রেকর্ডের পাশে কারণ সংরক্ষণ করবেন।\n\n## ডাবল-কাউন্টিং-এর কারণ হওয়া সাধারণ ভুলগুলো\n\nডাবল-কাউন্টিং ঘটে যখন সিস্টেম মনে করে "একটি মেসেজ এসেছে" মানেই "অ্যাকশন হয়েছে"। রিট্রাই, দেরি ওয়েবহুক, এবং ব্যাকফিলের সাথে, আপনাকে গ্রাহকের অ্যাকশনকে আপনার প্রসেসিং থেকে আলাদা করতে হবে।\n\nসাধারণ অপরাধীগুলো:\n\n- রিট্রাইগুলো নতুন ব্যবহার হিসেবে দেখা। যদি প্রতিটি ইভেন্টে একটি স্থিতিশীল অ্যাকশন আইডি (request_id, message_id) না থাকে এবং ডাটাবেস ইউনিকনেস জোর না করে, আপনি দুইবার গণনা করবেন।\n- ইভেন্ট টাইম ও প্রসেসিং টাইম মিশ্রিত করা। ingest সময় অনুযায়ী রিপোর্ট করলে দেরিতে আসা ইভেন্ট ভুল পিরিয়ডে আসে, পরে রেপ্লে-তে আবার গণনা হয়ে যায়।\n- কাঁচা ইভেন্ট মুছে ফেলা বা ওভাররাইট করা। যদি কেবল চলমান টোটাল রাখা হয়, আপনি প্রমাণ করতে পারবেন না কী ঘটেছিল, এবং রি-প্রসেসিং টোটাল বাড়িয়ে দিতে পারে।\n- ওয়েবহুক অর্ডার ধরে নেওয়া। ওয়েবহুক ডুপ্লিকেট, আউট-অফ-অর্ডার, বা আংশিক স্টেট প্রতিনিধিত্ব করতে পারে। Stripe অবজেক্ট আইডি অনুযায়ী reconcile করুন এবং একটি "ইতিমধ্যে প্রক্রিয়াকৃত" গার্ড রাখুন।\n- ক্যান্সেলেশন, রিফান্ড, এবং ক্রেডিট স্পষ্টভাবে মডেল না করা। আপনি যদি কেবল ব্যবহার যোগ করেন এবং নেতিবাচক অ্যাডজাস্টমেন্ট রেকর্ড না করেন, তাহলে পরে আপনি ইমপোর্ট করে বা অন্যভাবে ঠিক করার চেষ্টা করলে আবারও গোনা হতে পারে।\n\nউদাহরণ: আপনি লগ করেন "10 API কল" এবং পরে আউটেজের কারণে 2 কল ক্রেডিট দেন। যদি আপনি একই দিনে পুরো দিনের ব্যবহার আবার পাঠান এবং ক্রেডিটও প্রয়োগ করেন, গ্রাহক দেখতে পায় 18 কল (10 + 10 - 2) বরং সঠিক 8 কল।\n\n## লাইভ যাওয়ার আগে দ্রুত চেকলিস্ট\n\nবাস্তব গ্রাহকদের জন্য ব্যবহার-ভিত্তিক বিলিং চালু করার আগে অল্প কিছু পরীক্ষায় নিয়ে যান যা সস্তা বিলিং বাগগুলো আটকায়। বেশিরভাগ ব্যর্থতা "Stripe সমস্যা" নয়—এগুলো ডেটা সমস্যা: ডুপ্লিকেট, অনুপস্থিত দিন, এবং নীরব রিট্রাই।\n\nচেকলিস্ট সংক্ষিপ্ত ও প্রয়োগযোগ্য রাখুন:\n\n- ব্যবহার ইভেন্টে ইউনিকনেস নিশ্চিত করুন (উদাহরণ: event_id-এ ইউনিক কনস্ট্রেইন্ট) এবং একটি id কৌশলে প্রবল প্রতিশ্রুতি রাখুন।\n- প্রতিটি ওয়েবহুক সংরক্ষণ করুন, তার সিগনেচার যাচাই করুন, এবং idempotentভাবে প্রসেস করুন।\n- কাঁচা ব্যবহার অনুচ্ছেদ হিসাবে আচরণ করুন—ইভেন্টগুলোকে অপরিবর্তিত রাখুন। সংশোধন করুন অ্যাডজাস্টমেন্ট দিয়ে (positive বা negative), সম্পাদনা করে নয়।\n- একটি দৈনিক reconciliation জব চালান যা অভ্যন্তরীণ টোটাল (প্রতি গ্রাহক, প্রতি মিটার, প্রতি দিন) এবং Stripe বিলিং স্টেট তুলনা করে।\n- গ্যাপ ও অস্বাভাবিকতার জন্য অ্যালার্ট যোগ করুন: অনুপস্থিত দিন, নেতিবাচক টোটাল, হঠাৎ উত্থান, অথবা "ইভেন্ট ইনজেস্ট করা হয়েছে" এবং "ইভেন্ট বিল করা হয়েছে"-এর মধ্যে বড় ফারাক।\n\nএকটি সহজ পরীক্ষা: একটি গ্রাহক বেছে নিন, শেষ 7 দিনের জন্য ইনজেশন পুনরায় চালান, এবং নিশ্চিত করুন টোটাল পরিবর্তন করে না। পরিবর্তন হলে আপনার কাছে এখনও idempotency বা ব্যাকফিল সমস্যা আছে।\n\n## উদাহরণ দৃশ্য: ব্যবহার ও ইনভয়েসের একটি বাস্তবসম্মত মাস\n\nএকটি ছোট সাপোর্ট টিম একটি কাস্টমার পোর্টাল ব্যবহার করে যা প্রতি কথোপকথন $0.10 চার্জ করে। তারা Stripe-এ ব্যবহার-ভিত্তিক বিলিং বিক্রয় করে, কিন্তু বিশ্বাস আসে তখনই যখন ডেটা গোলমেলে হলে কি ঘটে তা স্বচ্ছ থাকে।\n\nমার্চ 1-এ গ্রাহক একটি নতুন বিলিং পিরিয়ড শুরু করে। প্রতবার একটি এজেন্ট কথোপকথন বন্ধ করলে আপনার অ্যাপ একটি ব্যবহার ইভেন্ট এমিট করে:\n\n- event_id: আপনার অ্যাপ থেকে একটি স্থিতিশীল UUID\n- customer_id এবং subscription_item_id\n- quantity: 1 কথোপকথন\n- occurred_at: ক্লোজ টাইম\n- ingested_at: প্রথমবার দেখা সময়\n\nমার্চ 3-এ একটি ব্যাকগ্রাউন্ড ওয়ার্কার টাইমআউটের পরে রিট্রাই করে এবং একই কথোপকথন আবার পাঠায়। event_id ইউনিক হওয়ায় দ্বিতীয় ইনসার্ট নো-অপ হয়ে যায় এবং টোটাল পরিবর্তিত হয় না।\n\nমধ্যমাসে, Stripe ইনভয়েস প্রিভিউ এবং পরে ফাইনালাইজড ইনভয়েসের ওয়েবহুক পাঠায়। আপনার ওয়েবহুক হ্যান্ডলার stripe_event_id, type, এবং received_at সংরক্ষণ করে, এবং কেবল আপনার ডাটাবেস ট্রানজেকশন কমিট হওয়ার পরে প্রসেসড হিসেবে চিহ্নিত করে। যদি ওয়েবহুক দুবার ডেলিভারি হয়, দ্বিতীয়টি উপেক্ষা করা হয় কারণ stripe_event_id ইতিমধ্যেই আছে।\n\nমার্চ 18-এ আপনি একটি মোবাইল ক্লায়েন্ট থেকে দেরিতে আসা ব্যাচ ইমপোর্ট পান যেটি মার্চ 17-এর 35 কথোপকথন রাখে। ঐ ইভেন্টগুলোর occurred_at পুরানো হলেও এগুলো বৈধ। সিস্টেম সেগুলো ইনসার্ট করে, মার্চ 17-এর দৈনিক টোটাল পুনঃগণনা করে, এবং অতিরিক্ত ব্যবহার পরের ইনভয়েসে ধরা পড়ে কারণ এটি এখনও খোলা বিলিং পিরিয়ডের মধ্যে ছিল।\n\nমার্চ 22-এ আপনি আবিষ্কার করেন একটি কথোপকথন দুইবার রেকর্ড হয়েছে কারণ একটি বাগ দুইটি ভিন্ন event_id তৈরি করেছে। ইতিহাস মুছে ফেলার বদলে আপনি একটি অ্যাডজাস্টমেন্ট ইভেন্ট লিখেন quantity = -1 এবং কারণ উল্লেখ করেন "duplicate detected"। এতে অডিট ট্রেইল অটুট থাকে এবং ইনভয়েস পরিবর্তন ব্যাখ্যাযোগ্য হয়।\n\n## পরবর্তী পদক্ষেপ: নিরাপদভাবে ইমপ্লিমেন্ট, মনিটর, এবং ইটরেট করুন\n\nছোট থেকে শুরু করুন: একটি মিটার, একটি প্ল্যান, একটি গ্রাহক সেগমেন্ট যেটি আপনি ভালোভাবে বুঝেন। লক্ষ্য হলো সহজ সামঞ্জস্য — আপনার সংখ্যাগুলো Stripe-র সাথে মাস থেকে মাসে মেলে, কোনো অপ্রত্যাশিত ঘটনা ছাড়াই।\n\n### ছোট করে বানান, তারপর শক্ত করুন\n\nপ্র্যাকটিক্যাল প্রথম রোলআউট:\n\n- একটি ইভেন্ট শেপ সংজ্ঞায়িত করুন (কি কাটা হচ্ছে, কোন ইউনিটে, কোন সময়ে)।\n- প্রতিটি ইভেন্ট ইউনিক idempotency কী ও স্পষ্ট স্ট্যাটাস সহ সংরক্ষণ করুন।\n- ইনভয়েস ব্যাখ্যাযোগ্য করার জন্য দৈনিক (বা ঘন্টার) টোটালে অ্যাগ্রিগেট করুন।\n- Stripe-র বিরুদ্ধে reconcile শিডিউল করে চালান, কেবল রিয়েল-টাইম নয়।\n- ইনভয়েস হওয়ার পরে পিরিয়ডটি ক্লোজ করা হিসেবে ধরুন এবং দেরি ইভেন্টগুলো অ্যাডজাস্টমেন্ট পথ দিয়ে নিয়ে যান।\n\nকোনো-কোড টুল ব্যবহার করলেও, যদি আপনি অবৈধ স্টেটগুলোকে অসম্ভব করে দেন (ইউনিক কনস্ট্রেইন্ট, ফরেন কিজ, এবং গ্রহণ করা কাঁচা ইভেন্ট আপডেট না করা), তখন শক্ত ডেটা ইন্টিগ্রিটি বজায় রাখাটা সম্ভব।\n\n### ভবিষ্যত বাঁচাতে মনিটরিং\n\nশুরুতেই সহজ অডিট স্ক্রিন যোগ করুন। যখন কেউ জিজ্ঞেস করবে, "এই মাস আমার বিল বেশি কেন?" প্রথমবারেই এগুলোই মূল্য দেয়। দরকারী ভিউগুলির মধ্যে: কাস্টমার ও পিরিয়ড অনুযায়ী ইভেন্ট সার্চ, প্রতিদিনের প্রতি-পিরিয়ড টোটাল, ওয়েবহুক প্রসেসিং স্ট্যাটাস, এবং ব্যাকফিল/অ্যাডজাস্টমমেন্টের সঙ্গী তথ্য কে/কখন/কেন।\n\nআপনি যদি AppMaster (appmaster.io) দিয়ে এটি বাস্তবায়ন করে থাকেন, তবে মডেল সহজেই মানায়: Data Designer-এ কাঁচা ইভেন্ট, অ্যাগ্রিগেট, এবং অ্যাডজাস্টমেন্ট ডিফাইন করুন, তারপর Business Processes দিয়ে idempotent ingestion, শিডিউলড aggregation, এবং webhook reconciliation বাস্তবায়ন করুন। আপনি তখনও একটি বাস্তব লেজার ও অডিট ট্রেইল পাবেন, সব প্লাম্বিং নিজে লিখে না করেও।\n\nপ্রথম মিটার স্থিতিশীল হলে পরেরটি যোগ করুন। একই লাইফসাইকেল নীতি, একই অডিট টুল, এবং একই অভ্যাস বজায় রাখুন: একসঙ্গে এক জিনিস পরিবর্তন করুন, তারপর শুরু থেকে শেষ পর্যন্ত যাচাই করুন।
প্রশ্নোত্তর
ইটিকে ছোট একটি খাতাবহির মত ভাবুন। সমস্যা কার্ড চার্জ করা নয়; বরং সেটা হচ্ছে কীভাবে এমন একটি সঠিক ও ব্যাখ্যাযোগ্য রেকর্ড রাখা হবে যা দেখায় কোন ইভেন্টগুলো গণনা করা হয়েছে — বিশেষত যখন ইভেন্ট দেরিতে আসে, বারবার আসে বা সংশোধন প্রয়োজন হয়।
একটি নিরাপদ ডিফল্ট হলো: আপনার ডাটাবেস কাঁচা ব্যবহার ইভেন্ট এবং তাদের স্ট্যাটাসের উৎস; আর Stripe হলো যে ইনভয়েস এবং পেমেন্ট ফলাফল حقیقت অনুসারে জানায়। এই বিভাজনটি বিলিংকে ট্রেসেবল রাখে, Stripe-এর পেমেন্ট, ট্যাক্স এবং সংগ্রহ পরিচালনার সুবিধা ব্যবহার করে।
এটি এমনভাবে বানান যাতে এটি স্থিতিশীল এবং নির্ধারনযোগ্য—যাতে রিট্রাই করলে একই আইডি ফিরে আসে। সাধারণত এটি ব্যবসায়িক অ্যাকশনের উপর ভিত্তি করে থাকে, যেমন কাস্টমার আইডি + মিটার কী + সোর্স রেকর্ড আইডি, যাতে একই অ্যাকশন পুনঃপ্রেরণ করলে সেটি একটি নিরাপদ নো-অপ হয়ে যায়।
গ্রহণকৃত ব্যবহার ইভেন্টগুলি মুছবেন বা সরাসরি সম্পাদনা করবেন না। একটি সমন্বয়কারী (compensating) অ্যাডজাস্টমেন্ট ইভেন্ট লিখুন (প্রয়োজনে নেগেটিভ পরিমাণসহ) এবং মূল রেকর্ড অটুট রাখুন, যাতে পরে ইতিহাস ব্যাখ্যা করা সহজ হয়।
কাঁচা ব্যবহার ইভেন্টগুলো append-only রাখুন, এবং অ্যাগ্রিগেটস আলাদা রাখুন যাতে প্রয়োজনে পুনর্নির্মাণ করা যায়। অ্যাগ্রিগেটস দ্রুততার জন্য থাকে; কাঁচা ইভেন্টগুলো ডিসপিউট ও অডিটের জন্য অপরিহার্য।
অন্তত দুটি টাইমস্ট্যাম্প রাখুন: কখন এটি ঘটেছিল (occurred_at) এবং কখন আপনি তা গ্রহণ করেছেন (ingested_at), এবং সোর্স ট্যাগ রাখুন। যদি ইনভয়েস এখনও ফাইনাল না হয়, ফাইনালাইজেশনের আগে পুনঃহিসাব করুন; যদি ফাইনাল হয়, তাহলে এটি স্পষ্ট একটি সংশোধনী হিসেবে ধরুন (অ্যাড-অন চার্জ বা ক্রেডিট) এবং কোনোভাবে গোপনভাবে পিরিয়ড পরিবর্তন করবেন না।
প্রতিটি ওয়েবহুক পেএলোড সংরক্ষণ করুন এবং Stripe-র event id-কে ইউনিক কী হিসেবে ব্যবহার করে idempotent প্রসেসিং বাধ্য করুন। ওয়েবহুক সাধারণত ডুপ্লিকেট বা আউট-অফ-অর্ডার ডেলিভারি হতে পারে, তাই হ্যান্ডলারকে কেবল সেই স্টেটচেঞ্জগুলোই প্রয়োগ করতে হবে যেগুলো রেকর্ডকে এগিয়ে নিয়ে যায়।
Stripe-র বিলিং পিরিয়ডের শুরু ও শেষ ব্যবহার করুন এবং যখন দাম পরিবর্তিত হয় তখন ব্যবহারকে সাব-উইন্ডোতে ভাগ করুন। প্রতিটি ইনভয়েস লাইন একটি নির্দিষ্ট সময়সীমা এবং মূল্য যাচাইযোগ্যভাবে নিয়ে থাকা উচিত যেন টোটাল ব্যাখ্যাযোগ্য থাকে।
একটি ক্যালকুলেশন রান আইডি বা সমতুল্য মেটাডাটা সংরক্ষণ করুন যাতে আপনি পরবর্তীতে টোটাল পুনরায় তৈরি করতে পারেন। একই উইন্ডো আবার চালালে যদি টোটাল পরিবর্তন হয়, তাহলে সম্ভবত idempotency বা লাইফসাইকেল স্টেট-সম্পর্কিত সমস্যা আছে।
Data Designer-এ কাঁচা ব্যবহার ইভেন্ট, অ্যাগ্রিগেট, অ্যাডজাস্টমেন্ট এবং ওয়েবহুক ইনবক্স টেবিল মডেল করুন, তারপর Business Processes-এ ইনজেস্ট ও reconcile ইমপ্লিমেন্ট করুন এবং idempotency-এর জন্য ইউনিক কনস্ট্রেইন্ট ব্যবহার করুন। এতেই আপনি এক অডিটেবল লেজার এবং শিডিউলড reconciliation তৈরি করতে পারবেন, হাতের লেখা প্লাম্বিং ছাড়াই।


