Faturalama Davranışını Değiştirme

1.6k görüntülenme Markdown

Neyin, ne kadara ve ne zaman faturalanacağını, faturalama motorunu düzenleyerek değil onun açtığı dikişleri kullanarak ayarlayın.

Genel Bakış

Faturalama bir zincirdir: yenilemenin vakti gelir, fiyat çıkarılır, ödenmemiş fatura yazılır. Hizmeti ancak ödeme ilerletir. Her halkanın kendi genişletme noktası vardır. Yanlış halka, doğru toplamlı ama hiç ilerlemeyen bir vade üretir.

Her şeyi bir arada tutan tek kural: fatura kesmek hiçbir şeyi uzatmaz. Vade ödemede, ayrı bir işleyicide, fatura kalemine damgalanan döneme göre ilerler.

Ön Koşullar

  • Kanca dinleyicilerine aşinalık: buradaki neredeyse her dikiş bir dinleyicidir. Referans sözleşmesini de bilin. Bu filtrelerin çoğu size değeri döndürmeniz için değil değiştirmeniz için verir.
  • Gerçekten yenileyebileceğiniz bir test hizmeti. Kilitli ve canlı fiyat farkı ancak gerçek bir satırda görünür.
  • "Hangi halkayı değiştiriyorum" sorusunun net cevabı. Seçenekler: faturalama kararı, tutar, fatura belgesi ya da ödeme.

Yapı

Tekrarlayan zincir ve dikişleri.

AdımNe olurDikişiniz
KeşifZamanlanmış bir görev, yenileme tarihi gelen hizmetleri bulurTek bir hedefi gerekçesiyle atlayabilen bir kapı
FiyatlamaBirim fiyat, miktar, vergi muafiyeti ve indirimler çözülürFiyat sonucunun tamamı üzerinde bir filtre
FaturaÖdenmemiş fatura ve kalemleri, yenilenen dönem damgasıyla yazılırYük, kalem açıklaması ve toplamlar üzerinde filtreler
ÖdemeKalem olayı vadeyi ilerletir ve döngüyü hizmete geri yazarUzatmadan sonra ateşlenen bir olay

Metrik faturalama, birinciyi besleyen ikinci bir zincirdir. Kullanım saatlik toplanır; kapanan ay ya kendi faturası olur ya da bir sonraki yenileme faturasına alt kalem olur.

Adım Adım

Dikişi Seçin

  1. Değişikliğinizi anlatan cümleyi yazın ve içindeki nesneyi bulun. "Bu müşteriyi faturalama" keşiftir. "Yüzde 10 daha az al" fiyatlamadır. "Bir satır ekle" faturadır. "Sistemimize yenilendiğini söyle" ödemedir.
  2. Seçtiğiniz noktanın her yol için ateşlenip ateşlenmediğine bakın. Yenileme fiyatı filtresi ateşlenir; bir operatör ekranındaki filtre ateşlenmez.
  3. Yönü doğrulayın. Referansla çalışan bir filtre yerinde değişiklik bekler ve ne döndürdüğünüzü yok sayar.
  4. Hiçbiri uymuyorsa yardımcıyı düzenlemeyin. İhtiyacınız olan noktada jenerik bir kanca isteyin ve kendi koşulunuzu dinleyicinize koyun.

Bir Şeyin Fiyatını Değiştirin

  1. Yenileme tutarı filtresini dinleyin. Çözülmüş sonuç dizisini referansla, ayrıca hedef tipini, hedef satırını ve müşterinin fatura verisini alırsınız.
  2. Toplamı değil birim tutarı değiştirin. Miktar, vergiler ve indirimler sizin değerinizin etrafında uygulanır, yani yanlış alanı çarpmak iki kez saydırır.
  3. Sonuçtaki fiyat kaynağına saygı gösterin. Sayının satırdaki donmuş bir fiyattan mı yoksa ürünün canlı fiyatından mı geldiğini söyler. Biri için anlamlı olan indirim çoğu zaman diğeri için yanlıştır.
  4. Elde hesapla değil gerçek bir yenilemeyle doğrulayın: dahil vergi filtrenizden sonra ayıklanır.

Bir Yenilemeyi Atlayın ya da Yönlendirin

  1. Tek bir yenilemeyi durdurmak için oluşturma kapısından boş olmayan bir gerekçe dizesi döndürün. Görev kendini iptal edilmiş bildirir, gerekçe olarak dizeniz kaydedilir.
  2. Yenilemeleri kalıcı durdurmak için hizmete atlama bayrağını basın. Keşif adımı onu okur, işaretli hizmet hiç kuyruklanmaz.
  3. Hizmetin kendi döngüsünden farklı bir dönemi faturalamak için dönemi yenileme giriş noktasına geçirin. Hizmet satırına dokunmayın. Motor istenen dönemi canlı fiyatlar ve dönemi faturaya yazar. Ödeme o dönem kadar uzatır, saklı döngü yerinde kalır.
  4. Vadeyi kendiniz asla ilerletmeyin. Hizmet, arkasında ödenmiş bir fatura olmadan uzar ve bir sonraki koşu onu yeniden faturalar.

Paraya Tepki Verin

  1. "Hizmet uzatıldı" için yenileme olayını dinleyin. Yeni vade yazıldıktan sonra ateşlenir ve uzatmadan önceki anlık görüntüyü iki tarihle birlikte verir.
  2. "Bir fatura el değiştirdi" için, yeniden yüklenmiş faturayı ve önceki durumu veren durum değişikliği olayını dinleyin.
  3. Fatura oluşturma olayını ödeme yerine kullanmayın. Ödenmemiş bir fatura hiç ödenmeyebilir.
  4. Dinleyiciyi idempotent yapın. Operatör bir durumu yeniden yazabilir, bir ödeme farklı yollardan iki kez kaydedilebilir.

Referans

Giriş Noktaları

coremio/helpers/invoices.php
// Yenilemenin TEK giriş noktası. $target_type 'service' ya da 'addon'.
// ÖDENMEMİŞ bir fatura yazar ve döner; vadeye dokunmaz.
public static function process_renewal(string $target_type, int $target_id, array $opts = []): array;

// Hedef ve müşteri çözüldükten sonra yukarıdaki tarafından çağrılır.
public static function generate_renewal(string $target_type, array $target, array $user_data, array $opts = []): int|false;

// Fiyat. period ve period_time'ı $target ÜZERİNDEN okur; farklı bir dönemi
// fiyatlamanın yeni bir metot değil değiştirilmiş bir hedef vermek olmasının sebebi budur.
public static function calculate_renewal_amount(string $target_type, array $target, array $user_data, array $opts = []): array;

// 'already-invoiced' arkasındaki yinelenme sorgusu. Dönem anahtarın parçasıdır.
public static function has_renewal(string $target_type, int $target_id, string $duedate, string $period = ''): bool;

// Üründeki her faturanın tek INSERT noktası.
public static function create(array $data): int;
public static function add_item(array $data): int;

// Ara toplam, vergi, komisyon ve toplamı yeniden hesaplar. Önizleme için $persist = false.
public static function recalculate_totals(int $invoiceId, bool $persist = true): array|false;

// Durum geçişi ve tüm yan etkileri.
public static function change_status(int $invoiceId, string $status, array $options = []): bool;
Invoices::process_renewal() Üründeki her yenileme yolu ondan geçer: zamanlanmış görev, operatör butonu, müşterinin kendi yenileme işlemi ve API. Yenileme davranışını burada genişletmek dördüne birden ulaşır.
opts.period ve opts.period_time Hizmetin kendi döneminden başkasını faturalar. Kabul edilen değerler hour, day, week, month ve year. Hizmetin kendi döngüsünü geçmek etkisizdir; farklı biri fiyatlamayı canlıya çevirir ve faturayı, ödemede saklı döngü korunacak şekilde işaretler.
opts.duedate Yenilenen dönemi ezer; kaçırılmış eski bir dönemi faturalamak için birikmiş fatura yolunun kullandığı şey budur.
opts.source, opts.is_catchup İşe damgalanan köken; varsayılanı zamanlanmış koşudur. Bir yenilemeyi kendi kodunuzdan sürdüğünüzde bunları verin ki kayıt nereden geldiğini söylesin.
opts.run_hook, opts.dispatch_notify İkisi de varsayılan olarak true. Faturayı, hedef başına değil sonunda bir kez duyurup bildirecek daha büyük bir akışın parçası olarak üretiyorsanız false yapın.
Invoices::calculate_renewal_amount() Dönemi kendisine verilen hedef dizisinden okur, yani farklı bir dönemi fiyatlamak, satırın dönemi değiştirilmiş bir kopyasını vermek demektir. Bunun için ayrı bir metot yoktur. İsteğe bağlı $opts, hizmet detayı sayfasının kullandığı ve yalnız gösterime ait display_own_currency anahtarını taşır. Fatura fiyatlarken boş bırakın.
Invoices::recalculate_totals() İkinci argümanı false geçmek yazmadan hesaplar; bir raporun belgeye dokunmadan güncel rakamı almasının yolu budur.
Dönüş değeri Her zaman bir dizi. success artı invoice_id; hiçbir şey faturalanmadıysa bu null'dır ve reason nedenini taşır: already-invoiced, skip-renewal-invoice-flag, recurring-cycles-limit-reached, invalid-period ve kardeşleri. Atlama da bir başarıdır, o yüzden fatura id'sine göre dallanın.

Fiyat ve Belge Dikişleri

filter:invoice.renewal_amount Referansla, fiyat sonucunun tamamı üzerinde: amount (birim fiyat), quantity, currency, taxexempt, additional_taxes, discounts, pricing_source ve period_time. Bağlam: hedef tipi, hedef satırı ve müşterinin fatura verisi. İndirimler çözüldükten sonra, dahil vergi ayıklanmadan önce ateşlenir. Diziyi yerinde değiştirin; dönüş kullanılmaz.
filter:invoice.renewal_description Kalemin metni, referansla, yerelleştirilmiş dönem aralığı kurulduktan sonra ve kalem eklenmeden önce. Bağlam hedef tipi ve hedef satırıdır. Metni yerinde değiştirin; dönüş kullanılmaz.
filter:invoice.create_payload Üründeki her faturanın ekleme verisi, referansla, satır yazılmadan önce: sepet, yenileme, yükseltme, metrik ve API. Buradaki en geniş dikiş ve fazla geniş tutulması en kolay olanı. Veriyi yerinde değiştirin; dönüş kullanılmaz.
filter:invoice.totals Hesaplanmış subtotal, tax, additional_tax, pmethod_commission, total ve discounts, referansla, saklanmadan önce. Fatura satırı ve kalemler değiştirilemez gelir. Her yeniden hesaplamada ateşlenir; buna kalem eklemek ve ödeme kaydetmek de dahildir. Toplamları yerinde değiştirin; dönüş kullanılmaz.
filter:invoice.late_fee_amount Hesaplanan ücret, referansla; bağlam olarak fatura ve ücret döngüsü. Değer sizden sonra yeniden yuvarlanır ve sıfır ya da altındaki bir sonuç ücreti tümüyle atlar. Ücreti yerinde değiştirin; dönüş kullanılmaz.
filter:order.cart_totals Para sayfasının diğer yarısı: sepet özeti, referansla; fiyatlanmış kalemler, ara toplam, uygulanan kuponlar ve vergi bağlamıyla. Hem ziyaretçinin gördüğü önizlemeyi hem de gerçekten kaydedilen siparişi besler, yani buradaki bir dinleyici belirlenimci olmak zorundadır. Özeti yerinde değiştirin; dönüş kullanılmaz.

Karar ve Olay Dikişleri

gate:invoice.create Yenileme görevinde, giriş noktası çağrılmadan hemen önce; hedef tipi, hedef id'si ve vade tarihiyle ateşlenir. Boş olmayan bir dize döndürmek reddeder: iş kendini iptal edilmiş bildirir ve dizeniz kayıtlı gerekçe olur. Boş ya da null döndürmek devam eder.
gate:invoice.client_pay Müşterinin ödeme denemesini reddeder. Argümanlara dikkat: müşteri id'si faturanın sahibidir ve paylaşım bağlantısıyla yapılan ödemede kimliği belli bir ödeyen yoktur; bunu dördüncü argüman söyler. Boş olmayan bir dize döndürmek ödemeyi engeller: komisyon kaydedilmez, bakiye düşmez. Boş ya da null devam eder.
action:service.renewed Gerçek "uzatıldı" sinyali; yeni vade yazıldıktan sonra ateşlenir. Argümanlar: hizmet id'si, uzatmadan önceki anlık görüntü, yeni vade ve eskisi. Fatura üretilirken ve herhangi bir ödemeden önce ateşlenen tekrarlama-limiti duyurusuyla karıştırmayın. Dönüş yoksayılır.
action:invoice.status_changed Durum geçişinin sonunda, yazma, gelir kaydı ve geçmiş girdisi bittikten sonra ateşlenir. Argümanlar: yeniden yüklenmiş fatura, yeni durum, önceki durum ve geçiş seçenekleri. Dönüş yoksayılır.
action:invoice.created Artık bir fatura belgesi vardır. Paranın el değiştirdiği hakkında hiçbir şey söylemez ve bunu bir satış sayan entegrasyon, hiç gelmeyebilecek bir geliri kaydeder. Dönüş yoksayılır.
action:invoice.renewal_generated Yukarıdaki satırın yalnız yenilemelere özgü dar ikizi. Her sepet işlemini de duymadan tekrarlayan faturalamayı duymak istediğinizde kullanışlıdır. Dönüş yoksayılır.

Vade Gerçekte Nasıl İlerler

İki aday tarih üretilir; seçim yalnız operatörün ayarına değil hizmetin durumuna da bağlıdır.

Hizmet durumuYeni vadeNeden
Aktif, henüz vadesi geçmemişAyar ne derse desin kalemin dönem sonuErken ödeyen asla gün kaybetmemeli
Aktif ama vadesi geçmişOperatörün ayarına göreGecikmenin affedilip affedilmeyeceğine operatör karar verir
Askıya alınmışOperatörün ayarına göreAynı karar, aynı anahtar
Başka her şeyŞimdi artı dönemEski döngü artık anlamlı değil
Daha eski ödenmemiş bir yenileme varZorla kalemin dönem sonuBirikmiş faturayı ödemek kapsadığı ayları atlamamalı
Saatlik döngüZorla şimdi artı dönemSaatlik bir dönem sonu, ödendiğinde çoktan geçmiştir

Sonuç ardından asla geriye gidemeyecek şekilde sınırlanır. Fatura hizmetin kendi döneminden başkası için üretildiyse ödeme vadeyi o dönem kadar ilerletir. Saklı döngü, tutar ve para birimi olduğu gibi kalır.

Kullanım Bazlı Faturalama

coremio/helpers/Metrics.php
// Bir dönemin aşım fiyatı. $scheme 'per_unit', 'volume' ya da 'graduated';
// $pricing kademe haritası; $ccode ise PARA BİRİMİ KODUDUR, çünkü kademeler
// çevrilmez, para birimi başına fiyatlanır.
public static function calculate(string $scheme, float $billable, array $pricing, string $ccode): float;

// Kullanım eksi dahil hak, sıfırda taban yapılmış.
public static function get_billable(float $usage, float $included): float;

// Saatlik anlık görüntülerden ayın rakamı. Metrik dönemler son okumaya değil
// ortalamaya göre faturalanır.
public static function monthly_average(array $snapshots): float;
Metrics::calculate() Kullanımın tüm fiyatlama motoru, tek bir çağrıda. Müşteri sayfayı bunu tahmin için canlı, kapanış görevi ise gerçek rakam için kullanır; ikisinin uyuşmasının sebebi budur.
Yapılandırma nerede yaşar Üründe değil, hizmet satırında JSON anlık görüntü olarak, metrik anahtarı başına bir giriş. Hem zamanlanmış görevler hem müşteri sayfaları o anlık görüntüyü okur, yani bir ürünün metrik tanımını değiştirmek var olan bir hizmetin faturasını geriye dönük değiştirmez.
Metriği kapatmak faturasını iptal etmez Açıkken kaydedilen kullanım, ne olursa olsun dönem sonunda faturalanır. Yalnız toplama durur. Bu yüzden kapanış görevi, metriği açık olan hizmetlerin yanı sıra kullanımı olan hizmetleri de tarar.
Ödenmemiş kilidi hizmet genelidir Bir hizmetteki herhangi bir metriğin faturalanmış ve ödenmemiş bir faturası varken o hizmette hiçbir metrik açılamaz. Kapatmak serbest kalır ve fatura ödendiğinde hiçbir şey kendiliğinden yeniden açılmaz.
Vadesi geçen kullanım kendini kapatır Kullanım faturası vadesinden sonra ödenmemiş kalan bir metrik, tarihten bir gün sonra otomatik kapatılır ve hizmet iki gün sonra ayrı bir görevle askıya alınır. Otomatik kapatma, müşterinin nedenini görebilmesi için hizmete bir gerekçe kaydeder.
Tek fatura ya da alt kalem Kapanan bir dönem, hizmetin vadesi bir aydan fazla uzaktaysa kendi faturası olur; değilse bekler ve bir sonraki yenileme faturasına alt kalem olarak biner. Yıllık bir hizmetin aylık kullanım faturaları alırken aylık bir hizmetin tek birleşik belge almasının sebebi budur.
Faturayı iptal etmek dönemi geri alır Bir kullanım faturasını silmek, iade etmek ya da iptal etmek onun faturalama satırını faturasız bekleyen duruma döndürür ve zincir onu bir sonraki turda yeniden faturalar. O satırları elle temizlemeyin.

Örnek

Yenilemelerde bir sadakat indirimi, bir atlama kuralı ve dış bir sisteme haber veren ödeme tarafı.

coremio/hooks/acme-billing.php
// 1. FİYAT. Her şey referansla gelir; BİRİM tutarı değiştirin, çünkü adet,
//    vergiler ve indirimler onun etrafında uygulanır.
Hook::add('filter:invoice.renewal_amount', 20, function (&$result, $target_type, $target, $user_data) {
    if ($target_type !== 'service') return;

    $uid = (int) ($target['owner_id'] ?? 0);
    if ($uid <= 0) return;

    $years = (int) (User::getInfo($uid, ['acme_loyalty_years'])['acme_loyalty_years'] ?? 0);
    if ($years < 3) return;

    // Bu müşteriyle dondurulmuş bir fiyat üzerinde anlaşılmış; ona bir kez daha indirim
    // uygulamak, operatörün bilerek yaptığı bir anlaşmayı bozar.
    if (($result['pricing_source'] ?? '') !== 'live') return;

    $result['amount'] = round((float) ($result['amount'] ?? 0) * 0.9, 4);

    // İndirim listesi, faturanın müşteriye gösterdiği şeydir. Onu atlamak, üzerinde
    // hiçbir açıklama olmayan daha ucuz bir fatura üretir.
    $result['discounts']['acme_loyalty'] = [
        'label' => Language::gc('admin/invoices/acme-loyalty-label'),
        'rate'  => 10,
    ];
});

// 2. KARAR. Boş olmayan bir dize reddeder; iş onu gerekçe olarak kaydeder.
Hook::add('gate:invoice.create', 10, function ($target_type, $target_id, $duedate) {
    if ($target_type !== 'service') return '';

    $service = Services::get((int) $target_id, 'id,options');
    $opts    = $service['options'] ?? [];

    // Göç hâlindeki bir hizmet, yerine oturana kadar faturalanmamalıdır.
    if ((int) ($opts['acme_migrating'] ?? 0) === 1) return 'acme-migration-in-progress';

    return '';
});

// 3. TEPKİ. Vade yazıldıktan SONRA ateşlenir, yani uzatma sinyali budur.
//    $service, taşınmadan ÖNCEKİ anlık görüntüdür: eski değerleri ondan okuyun.
Hook::add('action:service.renewed', 30, function ($serviceId, $service, $newDuedate, $oldDuedate) {
    Utility::HttpRequest([
        'url'  => 'https://crm.example.com/renewals',
        'type' => 'POST',
        'data' => [
            'service' => (int) $serviceId,
            'from'    => (string) $oldDuedate,
            'to'      => (string) $newDuedate,
            'cycle'   => (string) ($service['period'] ?? ''),
        ],
    ]);
});

Bir yenilemeyi kendiniz sürmek; hizmette saklı olan dönem yerine müşterinin seçtiği dönem için.

kendi kodunuzdan yenileme
$result = Invoices::process_renewal('service', 5001, [
    // Hizmet aylık olarak saklanıyor olsa da iki yıl faturalayın. Fiyatlama yalnız bu
    // fatura için canlıya geçer ve ödeme iki yıl uzatır; saklı aylık döngü ile onun
    // dondurulmuş tutarı ise olduğu gibi kalır.
    'period'      => 'year',
    'period_time' => 2,
    'source'      => 'acme-portal',
]);

// Atlamada invoice_id null olur ve atlama bir başarısızlık DEĞİLDİR: dallanmayı id üzerinden yapın.
if (($result['invoice_id'] ?? null) === null) {
    // already-invoiced | skip-renewal-invoice-flag | recurring-cycles-limit-reached
    // | invalid-period | service-not-found | user-data-unavailable | ...
    throw new Exception('Renewal not issued: ' . (string) ($result['reason'] ?? 'unknown'));
}

// Fatura vardır ve ÖDENMEMİŞTİR. Hizmete dair henüz hiçbir şey değişmemiştir.
$invoiceId = (int) $result['invoice_id'];

Aynı paranın okuma tarafı; bir rapor ya da mutabakat işi için. Kontrol, bir belgenin varlığında değil durum geçişindedir.

sonucu geri okumak
Hook::add('action:invoice.status_changed', 10, function ($invoice, $status, $old_status, $options) {
    // Yalnız ödenmiş durumuna GEÇİŞ bir satıştır; tekrar eden bir yazma değildir.
    if ($status !== 'paid' || $old_status === 'paid') return;

    $id = (int) ($invoice['id'] ?? 0);

    // Saklı bir rakama güvenmek yerine yeniden hesaplayın: operatör, fatura kesildikten
    // sonra kalemleri düzenlemiş olabilir. false = önizleme, hiçbir şey kalıcı olmaz.
    $totals = Invoices::recalculate_totals($id, false);

    AcmeLedger::record([
        'invoice'  => $id,
        'customer' => (int) ($invoice['user_id'] ?? 0),
        'currency' => (string) ($invoice['currency'] ?? ''),
        'net'      => (float) ($totals['subtotal'] ?? 0),
        'tax'      => (float) ($totals['tax'] ?? 0),
        'gross'    => (float) ($totals['total'] ?? 0),
        'method'   => (string) ($options['pmethod'] ?? ''),
    ]);
});

Tuzaklar

Fatura kesmek hizmeti uzatmak değildir

Yenileme giriş noktası ödenmemiş bir fatura yazar ve durur. Fatura oluşturmayı yenileme olayı sayan bir entegrasyon, hiç ödenmemiş hizmetleri uzatır.

Donmuş fiyat yalnız kendi döngüsü için donmuştur

Kilitli fiyat, üzerinde anlaşılan döngüye aittir. Onu farklı bir döneme uygulamak, bir yıl için aylık tutarı tahsil eder. Başka bir dönemi fiyatlamak, o çağrı için hesabı canlıya çevirmek demektir; yenileme giriş noktası bir dönem geçtiğinizde bunu zaten yapar.

En geniş dikişler sandığınızdan sık ateşlenir

Fatura yükü filtresi her faturayı görür; sepet, yükseltme ve kullanım dahil. Toplamlar filtresi her yeniden hesaplamada ateşlenir. Dar bir koşulu olmayan bir dinleyici satırını tekrar tekrar ekler.

Atlama da bir başarıdır

Reddedilen bir çağrı yine başarı döndürür; fatura id'si null olur ve bir gerekçe gelir. Gerekçeler: bu dönem için zaten faturalanmış, atlama bayrağı basılı, tekrarlama limiti dolmuş. Başarıya değil fatura id'sine bakın.

Kullanım ortalanır ve kademeleri para birimi başınadır

Kapanan bir metrik dönem, son okumadan değil saatlik anlık görüntülerin ortalamasından fiyatlanır. Kademe tablosu para birimi koduyla anahtarlanır, yani kademe girdisi olmayan para birimi sıfıra fiyatlanır.

Faydalı oldu mu?

Geri bildiriminiz için teşekkürler!

Hâlâ Yardıma mı İhtiyacınız Var?

Yukarıda bulamadığınız her şey için destek ekibimiz her zaman yanınızda.