/rates
// import { layeredRates, type PriceVersion } from "pricemeter/rates";const rows: PriceVersion[] = [ { id: "sms-list", meter: "sms", scope: "list", dims: { country: "*" }, effectiveFrom: 0, rate: flat(50_000) }, { id: "sms-list-tr", meter: "sms", scope: "list", dims: { country: "TR" }, effectiveFrom: 0, rate: flat(20_000) }, { id: "sms-pro-tr", meter: "sms", scope: "plan:pro", dims: { country: "TR" }, effectiveFrom: Date.parse("2026-09-01"), rate: flat(18_000) }, { id: "sms-acc1", meter: "sms", scope: "account:acc_1", dims: { country: "DE" }, effectiveFrom: 0, rate: flat(30_000) }, { id: "sms-acc1-tr-off", meter: "sms", scope: "account:acc_1", dims: { country: "TR" }, effectiveFrom: 0, removed: true },];
const getRate = layeredRates<{ accountId: string; plan: string }>({ loadRows: (meter) => rows.filter((r) => r.meter === meter), // your DB query scopes: (ctx) => [`account:${ctx.accountId}`, `plan:${ctx.plan}`, "list"], // most specific first cacheMs: 60_000,});
await getRate("sms", { country: "DE" }, ctx, at); // { rate: sms-acc1 (30000) } account rowawait getRate("sms", { country: "TR" }, ctx, at); // { rate: sms-pro-tr (18000) } account row removed → planawait getRate("sms", { country: "US" }, ctx, at); // { rate: sms-list (50000) } wildcard in listpricemeter/rates isteğe bağlı bir fiyat tablosudur ve hazır bir getRate üretir. Modül testindeki dikişi “getRate üretmek”tir; çekirdek onu görmez.
Çözümleme
Bölüm başlığı “Çözümleme”resolveLayered({ rows, scopes, dims, at, meter }) şu sırayla seçer:
- Scope sırası:
scopes(ctx)’in döndüğü dizi, en özelden en genele (account:…>plan:…>list). Scope adlarının anlamı yoktur; sırayı sen verirsin. - Scope içinde en özel eşleşen satır: boyut başına tam eşleşme bir puan; eksik anahtar ya da
"*"her şeyle eşleşir ama puan getirmez. - Eşitlikte en geç
effectiveFrom(≤at). - Kazanan satır
removed: trueise bu scope düşer, sıradakine geçilir. Hiçbir scope’ta satır yoksanull→no_price.
Kazanan tarifenin id’si yoksa satırın id’si verilir; usage satırındaki rateId böylece fiyat tablosundaki satıra işaret eder.
| API | Ne yapar |
|---|---|
layeredRates({ loadRows, scopes, cacheMs?, usedSoFar?, now? }) |
Hazır getRate. loadRows(meter, ctx) senin sorgun; (meter, scopes) başına cacheMs kadar önbellek. usedSoFar(meter, dims, ctx, at, rate) verilirse cevaba eklenir |
resolveLayered({ rows, scopes, dims, at, meter? }) |
Saf çözümleyici; kendi getRate’inin içinde kullan |
validateVersion(row) |
Bir fiyat satırını (tarifesiyle birlikte) doğrular; admin UI kaydetmeden önce |
applyDiscount, scaleTiers, withPolicy |
Bir tarifeden yeni bir tarife türeten saf yardımcılar |
const list: Rate = { id: "api", model: "graduated", tiers: [{ from: 0, unitPriceMicroUsd: 1_000 }, { from: 10_000, unitPriceMicroUsd: 800 }] };applyDiscount({ rate: list, percent: 15 }); // prices × 0.85, id "api-15pct"scaleTiers({ rate: list, factor: 0.5 }); // the cheaper tier starts at 5,000withPolicy({ rate: list, policy: { rounding: "cumulative" } });applyDiscount birim fiyatları ve sabit ücretleri en yakın mikro birime yuvarlar ve id’ye -{percent}pct ekler (CRM indirimi, B19). scaleTiers kademe eşiklerini çarpar (kurumsal hesap indirimli kademeye daha erken ulaşır).
/rates fiyatın kendisini bilir, usedSoFar’ı bilmez: sayaç senin deponda. Örnekler: SaaS koltuk layeredRates + döngü penceresi; mesajlaşma resolveLayered + rota maliyeti yedeği.
Bunu kullanmadan nasıl yapardım
Bölüm başlığı “Bunu kullanmadan nasıl yapardım”getRate bir fonksiyondur; fiyat tablon zaten varsa onu doğrudan sorgula:
// Without /rates: a getRate over your own table, in whatever order your product needs.const byHand = async (meter: string, dims: { country: string }, c: typeof ctx, t: number): Promise<RateAnswer | null> => { for (const scope of [`account:${c.accountId}`, `plan:${c.plan}`, "list"]) { const candidates = rows .filter((r) => r.meter === meter && r.scope === scope && r.effectiveFrom <= t) .filter((r) => r.dims?.country === dims.country || r.dims?.country === "*") .sort((a, b) => Number(b.dims?.country !== "*") - Number(a.dims?.country !== "*") || b.effectiveFrom - a.effectiveFrom); const best = candidates[0]; if (best && !best.removed) return { rate: { ...best.rate!, id: best.id } }; } return null;};Özellik tablosu, joker, sürüm ve “kaldırıldı” kuralları ürününe göre değişir; bu modül yalnızca yaygın bir biçimini verir. Tablon farklı bir sırayla çözülüyorsa (ör. kanal önce, hesap sonra) kendi getRate’ini yaz; kütüphane farkı görmez.