D
Engineering

Subscription Lifecycle

Siklus langganan sewa akun iklan Konco — reminder H-7/H-3/H-1 ke member via cron:ads-expired, grace notification 30D, auto-deactivation setelah EXPIRED_AFTER=4 hari. notify:subscription-reminder adalah admin-side untuk order unprocessed, bukan reminder expiry.

Konco mengelola siklus langganan sewa akun iklan Facebook otomatis lewat command terjadwal. Sistem membedakan reminder ke member (akhir masa sewa) dan reminder ke admin (order belum diproses) — dua alur yang sering tertukar.

Konstanta siklus

KonstantaLokasiNilaiDampak
AdsAccount::EXPIRED_BEFOREapp/Models/AdsAccount.php:267Hari sebelum expired untuk trigger reminder
AdsAccount::EXPIRED_AFTERapp/Models/AdsAccount.php:274Hari grace period sebelum auto-deactivation

Diagram lifecycle

text
[Active Subscription]
        │
        │  cron:ads-expired (every minute)
        │  cek expired == today + {7,3,1}
        ▼
[H-7 Reminder] ──► [H-3 Reminder] ──► [H-1 Reminder]
   email                email                email
   AdsAccountNotif      AdsAccountNotif      AdsAccountNotif
   ::REMINDER           ::REMINDER           ::REMINDER
        │
        │  Tanggal expired tercapai
        ▼
[Expired Date Reached]
        │
        │  cron:ads-expired cek expired di rentang
        │  today ... today+30 (grace notification)
        ▼
[Grace Notification 30D] (AdsAccountNotif::GRACE)
        │
        │  cron:ads-expired cek expired <= today - EXPIRED_AFTER(4)
        │  + is_active = true
        ▼
[Auto-Deactivation]
   ├─ MyApi::post(config('api.account_rename'), ...)
   ├─ $acc->update(['is_active' => false])
   └─ Notification::send(..., AdsAccountNotif::EXPIRED)
        │
        ▼
[Returned to Stock Pool]

cron:ads-expired — tiga fase

Command cron:ads-expired (app/Console/Commands/AdsExpired.php) berjalan every minute via app/Console/Kernel.php. Setiap eksekusi menjalankan tiga method berurutan:

1. reminders() — Reminder H-7 / H-3 / H-1 ke member

php
$daysList = [7, 3, 1];

foreach ($daysList as $days) &#123;
    $expDate = now()->addDays($days)->toDateString();

    $accounts = AdsAccount::query()
        ->has('member_bm.member.user')
        ->has('stock_ads_account')
        ->with(['member_bm.member.user', 'stock_ads_account'])
        ->whereDate('expired', $expDate)
        ->get();

    foreach ($accounts as $acc) &#123;
        // Dedupe via alreadyNotified() — cek tabel notifications
        if ($this->alreadyNotified($user->id, AdsAccount::class, $acc->id, AdsAccountNotif::REMINDER)) &#123;
            continue;
        &#125;

        Notification::send($user, new AdsAccountNotif($acc, AdsAccountNotif::REMINDER, ['days' => $days]));

        send_email([
            'to' => $user->email,
            'subject' => 'Pengingat : Masa Berlaku Langganan Anda',
            'view' => 'email.subscription.reminder_expiry',
            'data' => ['user' => $user, 'adsAccount' => $acc, 'days' => $days],
        ], false, true);
    &#125;
&#125;
AspekKeterangan
PenerimaMember (pemilik akun via member_bm.member.user)
Interval[7, 3, 1] hari sebelum expired
NotifikasiAdsAccountNotif::REMINDER + email email.subscription.reminder_expiry
DedupeCek notifications table — data->>'tipe' = 'reminder' & data->>'id' = $acc->id
MethodNotifications::send() — broadcast ke user notifications table

2. gracePeriod() — Grace notification 30 hari

php
$expStart = now()->toDateString();
$expEnd = now()->addDays(30)->toDateString();

$accounts = AdsAccount::query()
    ->has('member_bm.member.user')
    ->whereDate('expired', '>=', $expStart)
    ->whereDate('expired', '<=', $expEnd)
    ->get();
AspekKeterangan
PenerimaMember
Rentangexpired >= today & expired <= today+30
NotifikasiAdsAccountNotif::GRACE
Emailemail.subscription.reminder_grace
Subject“Pengingat : Akun Iklan dalam Masa Tenggang”

3. expired() — Auto-deactivation

php
$expDate = now()->subDays(AdsAccount::EXPIRED_AFTER)->toDateString();

$accounts = AdsAccount::query()
    ->has('member_bm.member.user')
    ->has('stock_ads_account')
    ->whereDate('expired', '<=', $expDate)
    ->where('is_active', true)
    ->get();

foreach ($accounts as $acc) &#123;
    // Rename akun di Facebook via API engine
    $api = MyApi::post(config('api.account_rename'), [
        'headers' => ['identity-key' => $acc->stock_ads_account->stock_bm->bm_id ?? null],
        'json' => ['account_id' => $acc->stock_ads_account->ads_id ?? null],
    ]);

    // Update status jadi expired
    $acc->update(['is_active' => false]);

    Notification::send($user, new AdsAccountNotif($acc, AdsAccountNotif::EXPIRED));
&#125;
AspekKeterangan
Triggerexpired <= today - EXPIRED_AFTER(4) & is_active = true
Aksi FacebookMyApi::post(config('api.account_rename')) — rename akun di Facebook via API engine
Aksi lokal$acc->update(['is_active' => false])
NotifikasiAdsAccountNotif::EXPIRED ke member
Error handlingtry/catch per-account — log warning, tidak menghentikan loop

notify:subscription-reminder (admin-side)

Command notify:subscription-reminder (app/Console/Commands/NotifySubscriptionReminderCommand.php) berjalan hourly via scheduler:

php
$schedule->command('notify:subscription-reminder --interval=3600')
    ->hourly()
    ->withoutOverlapping()
    ->runInBackground();

Dua kasus yang di-notify ke admin

KasusQueryNotifikasi
Order paid belum diprosesOrder::where('flag','paid')->whereHas('subscribe', fn($q) => $q->needProcess())AdminSubscriptionReminderNotification
Withdraw belum selesaiOrder::whereHas('withdraw', fn($q) => $q->whereNull('completed_at'))AdminWithdrawReminderNotification
AspekKeterangan
PenerimaAdmin aktif (User::admin()->active()->get())
Interval--interval=3600 — Cache TTL 1 jam per-order (dedupe)
Cache keysubscription_reminder_sent:&#123;order_id&#125; & withdraw_reminder_sent:&#123;order_id&#125;
ChannelIn-app notification + broadcast (implement ShouldBroadcast)

Method pendukung AdsAccount

MethodLokasiFungsi
scopeActive($q, $member_id = null)app/Models/AdsAccount.php:76-93Filter akun aktif: expired > today - EXPIRED_AFTER & is_active=true & stock_ads_account aktif
scopeDummy($q, $member_id = null)line 96Filter akun dummy (expired null, stock_ads_account_id null)
scopeWhere_member($q, $member_id)line 68Filter by member_bm.member_id
getGraceExpiredAttribute()line 176Return expired + EXPIRED_AFTER format Y-m-d
getGraceLimitAttribute()line 191Carbon::parse(expired)->addDays(EXPIRED_AFTER)
getDaysUntilExpiredAttribute()line 222Selisih hari ke expired; jika <= EXPIRED_BEFORE trigger reminder

Scheduler entries terkait

Dari app/Console/Kernel.php:

CommandFrekuensiFungsi
cron:ads-expiredeveryMinute()Reminder + grace + auto-deactivate (di atas)
expiry:ordereveryMinute()Batalkan order/invoice belum dibayar melewati batas waktu
notify:subscription-reminder --interval=3600hourly()Reminder ke admin — order unprocessed + withdraw

Email templates

TemplateLokasi viewTrigger
email.subscription.reminder_expiryresources/views/email/subscription/reminder_expiry.blade.phpcron:ads-expired — H-7/H-3/H-1
email.subscription.reminder_graceresources/views/email/subscription/reminder_grace.blade.phpcron:ads-expired — grace notification 30D

Testing lifecycle

Untuk testing tanpa menunggu expiry asli, gunakan dev command:

bash
# Set akun ke kondisi reminder H-7
php artisan dev:subscription-state reminder --email=user@example.com --days=7

# Set akun ke kondisi reminder H-3
php artisan dev:subscription-state reminder --email=user@example.com --days=3

# Set akun ke kondisi reminder H-1
php artisan dev:subscription-state reminder --email=user@example.com --days=1

# Set akun ke kondisi grace period
php artisan dev:subscription-state grace --email=user@example.com

Command ini (app/Console/Commands/DevSubscriptionState.php) memanipulasi tanggal expired akun untuk pengujian.

Langkah berikutnya