Systems Hub
শুরুর গল্প

আমার ১০০ জন শিক্ষক আছে
সবাইকে কি একই সেবা দেবো?

শিক্ষক ১ একটু বড় কোর্স করেন — তাকে বেশি Storage দেওয়া উচিত।
শিক্ষক ২ ছোট কোর্স — তাকে কম দাম দিতে চাই। কিভাবে করবো?

👨‍🏫
শিক্ষক রহিম
Live Class + ৫০ GB Storage
মাসে ৫,০০০ টাকা
👩‍🏫
শিক্ষিকা সাবিনা
Live Class + ১০ GB Storage
মাসে ২,৫০০ টাকা
👨‍💼
শিক্ষক করিম
শুধু PDF Upload
মাসে ৫০০ টাকা
❓ সমস্যা কি?
প্রতিটা শিক্ষকের জন্য আলাদা কোড লিখলে → কোড হবে জগাখিচুড়ি, maintain করা অসম্ভব।
দরকার একটা flexible service management system — এটাই AddOn Service!
Common Mistakes

সবাই যে ৫টা ভুল করে

Hardcoded Pricing
Teacher.price = 5000 সরাসরি কোডে লিখে রাখা। দাম বদলাতে হলে deploy করতে হয়! 😱
একটাই Plan সবার জন্য
"সবাইকে একই সেবা দেবো" — এটা scaling এর সাথে কাজ করে না। ১০০ শিক্ষক মানে ১০০ রকম চাহিদা।
No Offering Flow
সরাসরি assign করা হয়, teacher এর consent নেওয়া হয় না। পরে billing নিয়ে ঝামেলা হয়।
Single Price Type
শুধু MONTHLY রাখে। কিন্তু কেউ PER_CLASS চায়, কেউ PER_STUDENT — flexibility নেই।
No Default Fallback
Custom offer না থাকলে কি হবে? কোনো fallback নেই → system crash বা wrong price দেখায়।
Core Concept

AddOn Service আসলে কি?

ভাবুন এটাকে একটা Menu Card হিসেবে।
SuperAdmin menu তৈরি করে। Teacher অর্ডার করে।

📋
Service Catalog
Live Class, Storage, AI Tools, Bandwidth — এগুলো হলো AddOnService। একবার বানাও, বারবার ব্যবহার করো।
🎯
Targeted Offering
শিক্ষক X-এর জন্য নির্দিষ্ট service নির্দিষ্ট price-এ offer করো। অন্যরা এটা দেখতেই পাবে না।
Teacher Accepts
Teacher offer দেখে নিজে price tier বেছে নেয় → ACCEPTED হয়। Mutual agreement!
🍽️ Real-world analogy: একটা Restaurant এর menu আছে (AddOnService)। Customer (Teacher) order করে (Offering)। Waiter confirm করে (Selection)। Bill আসে selected price অনুযায়ী।
Architecture

৪টা Table — কিভাবে সংযুক্ত?

TABLE 1
AddOnService
id, name, description, code, isActive
e.g. "Live Class", "Storage 50GB"
TABLE 2
CourseDefaultService
courseId + serviceId
Course-এর সব teacher-এর জন্য default
TABLE 3
CourseAdminServiceOffering
adminId + courseId + serviceId
status: OFFERED / ACCEPTED
নির্দিষ্ট teacher-এর custom offer
TABLE 4
ServiceOfferingPrice
MONTHLY / PER_CLASS / PER_STUDENT
amount, minQty, maxQty
একটা offer-এ অনেক price tier
TABLE 5
ServiceOfferingSelection
offeringId + selectedPriceId + selectedAt
Teacher যে price tier বেছে নিলো
Complete Flow

শুরু থেকে শেষ — Step by Step

SuperAdmin: Service বানাও
POST /add-on-services → name="Live Class", code=101
Unique code দিয়ে identify করা যায়
SuperAdmin Only
SuperAdmin: Default service set করো
CourseDefaultService → courseId + serviceId
এই course-এর সব teacher automatically এই service পাবে
Auto Apply
SuperAdmin: Custom offer করো
CourseAdminServiceOffering → adminId + courseId + serviceId
Status: OFFERED (pending teacher response)
Custom Deal
SuperAdmin: Price tier যোগ করো
একটা offer-এ multiple prices → MONTHLY ৫০০০, PER_CLASS ২০০
Teacher বেছে নেবে কোনটা চায়
Flexible
Teacher: Accept করো, Price বেছে নাও
Selection তৈরি হয় → status ACCEPTED → decidedAt recorded
Transaction-এ atomically হয়
✓ Done!
Smart Design

Default + Custom — দুটো Layer

🏠
Default Service
✅ Course-এর সব teacher automatically পায়
✅ SuperAdmin একবার set করে, সবাই পায়
✅ isDefault price — standard flat rate
✅ Teacher কোনো action নিতে হয় না
📌 Example: "Basic Live Class Access" → সব teacher-এই আছে
💎
Custom Offering
🎯 নির্দিষ্ট teacher-এর জন্য
🎯 Custom negotiated price
🎯 Multiple price tier দেওয়া যায়
🎯 Teacher নিজে accept করে
📌 Example: রহিম ভাই-এর জন্য "50GB Storage" special rate
🔀 Smart Merge Logic
// Default service আছে, কিন্তু custom offer নেই? const defaultOnlyOfferings = allDefaultServices ?.filter(d => !offerings.find(o => o.courseAdmin.courseId === d.courseId && o.serviceId === d.serviceId // custom নেই, তাহলে default দেখাও )) ?.map(d => ({ id: null, status: 'DEFAULT', ...d })) // merged offerings = custom + default fallback
Pricing System

১৫+ Price Type — Ultimate Flexibility

এটাই সিস্টেমের সবচেয়ে শক্তিশালী অংশ — যেকোনো business model support করে।

Time Based
MONTHLY — মাসে মাসে
YEARLY — বার্ষিক
PER_HOUR / PER_MINUTE
Usage Based
PER_CLASS — ক্লাস প্রতি
PER_STUDENT — ছাত্র প্রতি
PER_API_CALL
PER_USER
Storage / Data
PER_MB / PER_GB / PER_TB
BANDWIDTH
PER_COURSE
ONE_TIME / FIXED
💡 minQty / maxQty দিয়ে Volume Discount
Pro Feature
একই service-এ ৩টা tier রাখা যায়:
1-50 student → ৳200/student  |  51-200 student → ৳150/student  |  200+ student → ৳100/student
Code Deep Dive

Offering Offer করা — Validation Chain

✅ 3-Step Validation before offer:
const createCourseAdminServiceOfferIntoDb = async (payload) => { const { courseId, adminId, serviceId } = payload // Check 1: Service exist করে? const service = await prisma .addOnService.findFirst({ where: { id: serviceId, isDeleted: false, isActive: true } }) if (!service) throw AppErrors(404, 'Not found') // Check 2: Admin exist করে? // Check 3: Course exist করে? // সব ঠিক থাকলে offer create return prisma.courseAdminServiceOffering .create({ data: { courseId, adminId, serviceId } }) }
🔒 Teacher Accept = Transaction:
const result = await prisma.$transaction( async (tx) => { // Step 1: Selection তৈরি করো const selection = await tx .courseAdminServiceOfferingSelection .create({ data: { offeringId, selectedPriceId } }) // Step 2: Offering status update করো await tx.courseAdminServiceOffering.update({ where: { id: offeringId }, data: { status: 'ACCEPTED', decidedAt: new Date() } }) return selection })
💡 Transaction মানে — দুটো কাজ একসাথে হয়। একটা ব্যর্থ হলে দুটোই rollback। Data inconsistency শূন্য!
Production Grade

Unique Code Auto-Generate — Race Condition Safe

🤔 সমস্যা: একসাথে ২টা service create হলে কি হবে?
দুইজন একই সময়ে last code=101 পড়ে → দুইজনই 102 দিয়ে চেষ্টা করে → Duplicate! System crash!
// Retry loop দিয়ে race condition handle const createAddOneServiceIntoDb = async (payload) => { const { name, description } = payload for (let attempt = 0; attempt < 3; attempt++) { const last = await prisma.addOnService.findFirst({ orderBy: { createdAt: 'desc' }, select: { code: true } }) const nextCode = Number(last?.code ?? 100) + 1 try { return await prisma.addOnService.create({ data: { name, description, code: `${nextCode}` } }) } catch (e) { if (e?.code === 'P2002') continue // Unique constraint → retry! } } }
P2002 Error
Prisma duplicate key error → retry loop
Max 3 Attempts
3 বার চেষ্টার পরেও না হলে graceful fail
DB Unique Index
code field-এ unique constraint → last line of defense
Data Aggregation

formatTeacherServices — Cost Calculator

একটা function যা সব teacher-এর সব service-এর total monthly cost বের করে।

Price Type → Cost Bucket
switch (price.type) { case 'MONTHLY': totalMonthlyCost += amount; break case 'YEARLY': totalYearlyCost += amount; break case 'ONE_TIME': totalOneTimeCost += amount; break case 'PER_CLASS': // qty-based requiresCalculation = true; break }
Normalized Monthly Cost:
// সব কিছু monthly-তে convert করো const totalMonthly = totalMonthlyCost // direct + totalYearlyCost / 12 // yearly ÷ 12 + totalOneTimeCost / 12 // one-time ÷ 12
সব ধরনের price-কে apples-to-apples compare করা যায়!
📊 Output Summary (per teacher):
5
Total Services
3
Default Services
2
Custom Offerings
৳8,500
Monthly Cost
2
Selected
BDT
Currency
Teacher Experience

Teacher কি দেখে — Dashboard View

আমার Services Dashboard
3 Active
1 Pending
ServicePriceTypeStatus
Live Class Access ৳2,500/মাস DEFAULT ● Active
50GB Storage ৳500/মাস CUSTOM ● Accepted
AI Tools Suite ৳200/ক্লাস CUSTOM ⏳ Offered
Bandwidth Pack ৳1,000 (fixed) DEFAULT ● Active
⏳ "AI Tools Suite" — Offered অবস্থায়:
Teacher দুটো option পাচ্ছে:
Option A: ৳200/class (PER_CLASS)
Option B: ৳3,000/month (MONTHLY)
Teacher decide করবে কোনটা তার জন্য সুবিধাজনক।
✅ Accepted হলে:
• selectedPriceId record হয়
• decidedAt timestamp save হয়
• status = ACCEPTED
• Billing system এই data দিয়ে invoice generate করে
Scalability

কিভাবে Scale করে?

১০০ teacher থেকে ১০,০০০ teacher — same system, no rewrite।

🗂
Pagination সব জায়গায়
buildQueryOptions → skip/take/orderBy। ১০,০০০ record-ও smooth। limit max 1000 enforce করা আছে।
2-Query Pattern
আগে teacherIds বের করো, তারপর সব offerings একসাথে fetch। N+1 query problem নেই।
🗃
In-memory grouping
offeringsByTeacherAndCourse Map তৈরি করো, DB-তে join না করে। Fast O(n) lookup।
🌐
Hostname-based filtering
একই codebase দিয়ে multiple tenant। buildHostNameCondition() দিয়ে auto-filter।
📈 N+1 Problem বনাম এই System
❌ Wrong way:
teachers.forEach(t => DB.query(t.id)) → 100 DB calls!

✅ Right way:
1. Get teacherIds → 1 query
2. Get all offerings WHERE adminId IN [...teacherIds] → 1 query
3. Group in memory → 0 queries
Total: 2 queries for 100 teachers!
Soft Delete সব জায়গায়
isDeleted: true — কোনো data সত্যিকার delete হয় না। Audit trail থাকে। কোনো FK violation নেই।
DB Schema

Database Schema — Visual Map

AddOnService
id String @id @default(cuid()) name String description String? code String @unique isActive Boolean @default(true) isDeleted Boolean @default(false)
CourseDefaultService
id String courseId String // FK → Course serviceId String // FK → AddOnService isActive Boolean @default(true) isDeleted Boolean @default(false) // prices → DefaultServicePrice[]
CourseAdminServiceOffering
id String adminId String // FK → Admin (Teacher) courseId String // FK → Course serviceId String // FK → AddOnService status Enum // OFFERED | ACCEPTED expiresAt DateTime? decidedAt DateTime?
OfferingPrice + Selection
// Price: offeringId String type PriceType // MONTHLY|PER_CLASS... amount Decimal minQty, maxQty Int? // Selection: offeringId String @unique selectedPriceId String
Recap

AddOn Service — সব কিছু এক নজরে

📋
Service Catalog
AddOnService = reusable menu items। একবার বানাও, বারবার ব্যবহার করো।
🎯
Targeted Offering
প্রতিটা teacher-কে আলাদা deal অফার করা যায়। তারা accept করে নিজের price বেছে নেয়।
💰
15+ Price Types
MONTHLY, PER_CLASS, PER_GB, PER_STUDENT — যেকোনো business model support।
🏠
Default Fallback
Custom offer না থাকলে CourseDefaultService থেকে auto-serve। কখনো empty হয় না।
🏆 Production-Grade Features
✅ Transaction দিয়ে atomic accept
✅ Race condition safe code generation
✅ N+1 query problem নেই (2 queries)
✅ In-memory grouping for speed
✅ Soft delete everywhere
✅ Hostname-based multi-tenant
✅ Pagination + max limit enforced
✅ isRunning / expiresAt check
🎬 এই System দিয়ে কি করা যায়?
১ লাখ teacher × ১০ service × ৫ price tier
= ৫০ লাখ combinations
সব একই codebase দিয়ে!
1 / 14