Catalog database
catalog menyimpan definisi barang/jasa dan aturan presentasi atau harga. Ownership tetap ditentukan oleh organizationId dari runner.
Pola CRUD yang benar:
- Validasi session dan membership di runner.
- Validasi category berada pada scope organisasi yang sama.
- Tulis product di catalog.
- Kembalikan
idcatalog sebagai reference untuk commerce/inventory.
{
"name": "Product name",
"description": "Description",
"sellingPrice": 125000,
"categoryId": "<catalog category id>",
"organizationId": "<runner organization id>"
}Architectural principles:
- Multi-tenant via scalar organizationId references.
- Cross-database references remain scalar IDs.
- Product is the generic sellable catalog item.
- ServiceDefinition describes a sellable service.
- Service is an operational service/work record.
- Pricing is separated from Product.
- Promotions are separated into rules, conditions, targets, benefits, and usage.
- Historical/transactional records keep snapshots.
- Archive state is separate from business lifecycle.
- Json is used only as an extension point, not as the core model.
Catalog mendefinisikan apa yang dapat dijual; Pricing mendefinisikan berapa harganya; Promotion mendefinisikan bagaimana harga dapat berubah; Service mendefinisikan pekerjaan yang terjadi; snapshot menyimpan fakta historis pada saat transaksi.
Fundamental
Schema ini sengaja lebih besar daripada schema awal, tetapi setiap penambahan mempunyai boundary yang jelas.
1. Product benar-benar menjadi Catalog Item
Tidak ada lagi:
sellingPrice
purchasePrice
discountRules
laborCost
itemTypeProduct hanya menjawab:
Apa yang kita jual?
Bagaimana catalog menampilkannya?
Apa klasifikasinya?
Apa bentuk/varian/komponennya?Jenis barang ditentukan oleh:
kind ProductKindsehingga satu catalog dapat berisi:
physical
digital
service
bundle
subscription
otherSebuah jasa seperti:
Oil Change
Brake Service
AC Cleaning
Laptop Repairtetap dapat menjadi Product dengan:
kind = servicedan detail jasa diperluas melalui:
Product
βββ ServiceDefinitionGood to knows:
- Product is the generic sellable catalog item.
It can represent:
- physical goods
- digital goods
- services
- bundles
- subscription products
- other sellable items
Product does NOT own pricing.
Product does NOT own inventory.
Product does NOT own orders.
Product does NOT own customers.- Product component: Represents bundle/composite products.
parentProduct:
the bundle.
componentProduct:
the product contained by the bundle.Product Category
Product can belong to multiple categories.
- Explicit many-to-many relation.
- This is preferable to a single categoryId because a product
- can legitimately appear in multiple catalog categories.
Product Variant
Variants belong directly to Product.
Variant attributes remain Json because the shape can differ considerably between domains:
{
"color": "red",
"size": "XL"
}
{
"storage": "512GB",
"ram": "32GB"
}
{
"engine": "150cc"
}2. Pricing benar-benar dipisahkan
Tidak lagi:
sellingPrice Intmelainkan:
PriceList
β
βββ Price
βββ Product
βββ ProductVariantDengan ini Aori bisa punya:
Retail
Engine Oil = 100.000
Wholesale
Engine Oil = 85.000
Member
Engine Oil = 90.000
Partner
Engine Oil = 88.000tanpa mengotori Product.
Bahkan quantity tier:
1β4 = 100.000
5β9 = 95.000
10+ = 90.000bisa ditangani minQuantity / maxQuantity.
3. Service tidak lagi merupakan model tertentu
Field seperti:
itemType
itemSubtype
itemMerk
itemRegNumsepenuhnya hilang.
Sekarang:
subject ServiceSubjectSnapshotsehingga satu sistem dapat menangani:
vehicle
equipment
device
property
person
location
otherdan detailnya bersifat generic.
Misalnya service motor:
{
"type": "vehicle",
"manufacturer": "Honda",
"model": "CB150R",
"variant": "StreetFire",
"year": 2024,
"registration": "B 1234 XYZ"
}Service laptop:
{
"type": "device",
"manufacturer": "Apple",
"model": "MacBook Pro",
"variant": "14-inch",
"serialNumber": "ABC123"
}Tidak diperlukan perubahan schema.
4. ServiceDefinition dan Service sengaja dipisahkan
Product
"Oil Change"
β
βββ ServiceDefinition
duration = 60 min
components = Oil + Filterberbeda dengan:
Service
#SRV-2026-000123
customer = ...
subject = Honda CB150R
status = completedYang pertama adalah catalog definition.
Yang kedua adalah operational execution.
Dengan demikian master service dapat diubah tanpa mengubah histori service yang pernah terjadi.
5. ServiceItem adalah snapshot
Misalnya saat service:
Engine Oil
price = 75.000Kemudian enam bulan kemudian product berubah menjadi:
Engine Oil
price = 95.000record service lama tetap:
ServiceItem
name = Engine Oil
pricePerUnit = 75.000Ini jauh lebih penting daripada sekadar relation productId.
Relation ke Product hanya membantu navigasi/current reference; snapshot adalah historical truth.
6. ServiceCharge menggantikan laborCost
Daripada:
laborCost Intkita sekarang punya:
Service
βββ ServiceItem
β βββ Oil
β βββ Filter
β
βββ ServiceCharge
βββ Labor
βββ Diagnostic
βββ Installation
βββ DeliverySehingga model tidak mengasumsikan bahwa satu-satunya biaya jasa adalah labor.
Good to knows:
- Service definition: This is the catalog definition of a service.
Example:
"Oil Change"
"Brake Service"
"Laptop Repair"
"AC Cleaning"It is attached to a Product whose kind = service.
This model is NOT the actual execution/transaction. The actual execution is Service.
- Service definition component: Default products/materials associated with a service offering.
Example:
Oil Change
-> Engine Oil x 1
-> Oil Filter x 1
This is a recommendation/default, not the actual consumption.
Actual service usage is represented by ServiceItem.Operational/service execution record.
This is deliberately generic. It is NOT modeled around automotive fields.
Customer/member/organization IDs are external references. There are intentionally no Prisma relations to those entities.
Service subject snapshot
This replaces domain-specific fields such as: itemType itemSubtype itemMerk itemRegNum
The same model can represent:
- vehicle
- laptop
- AC
- machine
- property
- equipment
- etc.
Service Item
Product/material actually used during service.
ProductId is optional because a product can later be removed from the catalog while historical service data remains valid.
Name, SKU and price are snapshots.
Service Charge
Generic non-product service cost.
This replaces a single: laborCost Int
Examples:
- Labor
- Diagnostic
- Installation
- Delivery
- Travel
7. Category sekarang many-to-many
Sebelumnya:
Product
βββ categoryIdSekarang:
Product
βββ Motorcycle
βββ Maintenance
βββ Featuredmelalui:
ProductCategoryIni lebih realistis untuk catalog profesional.
Sengaja menggunakan explicit relation model karena pola many-to-many perlu dibuat eksplisit dalam skenario MongoDB Prisma ketika membutuhkan kontrol relation/action seperti ini. (Prisma)
8. ProductVariant tidak lagi hierarchy
Schema lama:
ProductVariant
βββ parent
βββ childrensebenarnya bukan representasi variant product yang normal.
Sekarang:
Product
βββ Variant A
βββ Variant B
βββ Variant Cdan fleksibilitas atribut diletakkan di:
attributes Json?Contoh:
{
"color": "black",
"size": "XL",
"storage": "256GB"
}9. Bundle memiliki model sendiri
Product dengan:
kind = bundledapat mempunyai:
ProductComponentcontoh:
Service Package A
βββ Engine Oil x1
βββ Oil Filter x1
βββ Labor Package x1Ini membuka jalan untuk bundle/composite product tanpa membuat Promotion menjadi tempat untuk menyimpan struktur product.
10. Promotion menjadi mini rule engine
Strukturnya:
Promotion
β
βββ PromotionRule
β β
β βββ PromotionCondition[]
β β
β βββ PromotionTarget[]
β β
β βββ PromotionBenefit[]
β
βββ PromotionUsage[]Artinya:
Promotionmenjawab:
campaign apa?
PromotionRulemenjawab:
kapan berlaku?
PromotionTargetmenjawab:
berlaku untuk apa?
PromotionBenefitmenjawab:
customer mendapatkan apa?
PromotionUsagemenjawab:
apa yang benar-benar terjadi?
Ini jauh lebih sehat daripada DiscountRule tunggal yang berisi semua konsep sekaligus.
Good to knows:
Promotion Targets
Defines WHAT the rule applies to.
IDs are optional because target type determines which one should be populated.
Application validation is required here.
Promotion is the campaign/container.
Rules explain when/how the promotion applies. Targets explain what it applies to. Benefits explain what the customer gets. Usage explains what actually happened.
Promotion Rules
A promotion may have multiple rules.
Example:
Promotion:
"Weekend Promo"
Rule 1:
if subtotal >= 500000
-> 10%
Rule 2:
if customer group = member
-> additional 5%Promotion Usage
This is an immutable-ish ledger of actual promotion usage.
It replaces the weak:
DiscountQuota {
quota
usedQuota
}model.
A ledger answers:
- which promotion was used?
- which customer?
- which transaction?
- which service?
- which rule?
- which benefit?
- how much discount?
- when?
11. PromotionCondition sengaja fleksibel
Misalnya:
{
"type": "order_subtotal",
"operator": "greater_than_or_equal",
"numberValue": 500000
}atau:
{
"type": "sales_channel",
"operator": "equals",
"value": "mobile"
}atau:
{
"type": "first_purchase",
"operator": "equals",
"booleanValue": true
}Maka promotion engine dapat berkembang tanpa setiap kondisi baru memaksa perubahan puluhan field pada Promotion.
Good to knows:
The typed fields allow common rules without turning the entire promotion engine into Json.
Application code must validate which value field is valid for a given condition type.
12. Promotion Benefit mendukung lebih dari sekadar diskon persen
Sekarang tersedia:
percentage
fixed_amount
fixed_price
buy_x_get_y
free_shipping
customJadi:
10% offRp 50.000 offharga menjadi Rp 75.000buy 2 get 1bahkan benefit khusus di masa depan dapat memakai:
customtanpa merombak keseluruhan Promotion model.
Good to knows:
Defines WHAT the customer receives.
Keeping Benefit separate from Rule allows a rule to produce more than one commercial outcome.
Example:
Buy 2 Oil
-> 10% off
-> free FilterOne rule can have two benefits.
13. DiscountQuota (sengaja dihilangkan)
Gunakan:
PromotionUsagesebagai ledger, daripada:
usedQuota = 173Karena dari ledger dapat menjawab:
Siapa yang menggunakan?
Kapan?
Transaksi apa?
Service apa?
Rule mana?
Benefit mana?
Berapa discount?Counter seperti usedQuota masih dapat dibuat sebagai cache/denormalized counter di application layer bila diperlukan untuk performance.
MongoDB + Prisma mendukung transaction, tetapi concurrency untuk quota tetap harus ditangani dengan benar di application/service layer; schema sendiri tidak akan menyelesaikan race condition penggunaan quota. Prisma juga mencatat bahwa MongoDB memakai Prisma-managed referential integrity secara default dan cycle perlu diputus dengan NoAction. (Prisma)
14. Archive tidak sama dengan status
Contoh Product:
status = discontinued
archivedAt = nullberbeda dengan:
status = inactive
archivedAt = 2026-08-29Begitu pula Promotion:
status = paused
archivedAt = nullberbeda dengan:
status = archived
archivedAt = ...Ini membuat lifecycle jauh lebih mudah dikembangkan.
Price
Represents a price entry for Product or ProductVariant.
Quantity tiers are supported.
Effective periods allow historical/future pricing:
2026-01-01 -> 100000
2026-03-01 -> 110000Overlapping validity periods should be prevented by application-level business validation.
Price List
Pricing is intentionally outside Product.
Examples:
- Retail
- Wholesale
- Member
- Partner
- Corporate
- Cost
Tax
Tax calculation itself belongs to transaction/tax logic. Product only declares classification metadata.
Merchandising
These are deliberately separate from ProductStatus. A product may be active + featured + published simultaneously.
Batasan Ketergantungan
Model catalog membutuhkan beberapa relasi lain seperti:
Organization
Customer
Member
Employee
Order
Cart
Payment
Inventory
Warehouse
Shipment
Review
Accountingyang tidak berada dalam schema ini.
Karena architecture memang menggunakan database boundaries.
Contohnya:
organizationId String
customerId String
memberId String
transactionId Stringtetap scalar reference.
Itu bukan "relation yang kurang". Itu boundary yang disengaja.
Satu prinsip yang sangat penting untuk implementasi
Schema ini sudah matang di level data model, tetapi beberapa invariant tidak realistis dipaksakan seluruhnya lewat Prisma schema. Itu harus menjadi domain validation di service layer.
Contohnya:
Product.kind = service
β ServiceDefinition seharusnya adaPromotionTarget.type = product
β productId harus adaPromotionBenefit.type = percentage
β percentage harus adaPromotionBenefit.type = buy_x_get_y
β buyQuantity
β freeQuantity
β buyProductId
β freeProductId
harus validPrice.minQuantity <= Price.maxQuantityPrice.validFrom < Price.validToPromotion.startAt < Promotion.endAtProduct.kind = bundle
β ProductComponent dapat digunakanIni memang seharusnya berada di domain/application layer, bukan dipaksa menjadi sekumpulan field nullable yang rumit.
Bentuk domain akhirnya
Secara keseluruhan, sekarang dapat diasumsikan Catalog DB memiliki boundary:
CATALOG
β
βββ BRAND
β
βββ CATEGORY
β
βββ PRODUCT
β βββ PRODUCT CATEGORY
β βββ PRODUCT VARIANT
β βββ PRODUCT COMPONENT
β
βββ SERVICE DEFINITION
β βββ SERVICE DEFINITION COMPONENT
β
βββ PRICING
β βββ PRICE LIST
β βββ PRICE
β
βββ SERVICE OPERATIONS
β βββ SERVICE
β βββ SERVICE ITEM
β βββ SERVICE CHARGE
β
βββ PROMOTION ENGINE
βββ PROMOTION
βββ PROMOTION RULE
βββ PROMOTION CONDITION
βββ PROMOTION TARGET
βββ PROMOTION BENEFIT
βββ PROMOTION USAGEIni menjadi fondasi yang jauh lebih layak untuk AORI sebagai SaaS platform, bukan lagi "schema aplikasi satu layanan yang dibuat generic".
Beberapa perubahan adalah breaking changes secara strukturalβterutama Product.sellingPrice, Product.categoryId, Product.discountRules, Service.item*, laborCost, dan seluruh DiscountRule/DiscountQuota.