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
Konstanta Lokasi Nilai Dampak 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 Copy [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 Copy $ 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;
Aspek Keterangan Penerima Member (pemilik akun via member_bm.member.user)Interval [7, 3, 1] hari sebelum expiredNotifikasi AdsAccountNotif::REMINDER + email email.subscription.reminder_expiryDedupe Cek notifications table — data->>'tipe' = 'reminder' & data->>'id' = $acc->id Method Notifications::send() — broadcast ke user notifications table
Email ke member, bukan admin Email reminder H-7/H-3/H-1 dikirim ke email member pemilik akun , bukan admin. Subject: “Pengingat : Masa Berlaku Langganan Anda”. View: email.subscription.reminder_expiry.
2. gracePeriod() — Grace notification 30 hari
php Copy $ expStart = now () -> toDateString ();
$ expEnd = now () -> addDays ( 30 ) -> toDateString ();
$ accounts = AdsAccount :: query ()
-> has ( ' member_bm.member.user ' )
-> whereDate ( ' expired ' , ' >= ' , $ expStart )
-> whereDate ( ' expired ' , ' <= ' , $ expEnd )
-> get ();
Aspek Keterangan Penerima Member Rentang expired >= today & expired <= today+30Notifikasi AdsAccountNotif::GRACEEmail email.subscription.reminder_graceSubject “Pengingat : Akun Iklan dalam Masa Tenggang”
Grace notification != grace period Grace notification (30 hari) adalah notifikasi informatif, bukan masa tenggang fungsional. Masa tenggang fungsional adalah EXPIRED_AFTER=4 hari setelah expired — akun masih aktif selama 4 hari setelah tanggal expired.
3. expired() — Auto-deactivation
php Copy $ 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;
Aspek Keterangan Trigger expired <= today - EXPIRED_AFTER(4) & is_active = trueAksi Facebook MyApi::post(config('api.account_rename')) — rename akun di Facebook via API engineAksi lokal $acc->update(['is_active' => false])Notifikasi AdsAccountNotif::EXPIRED ke memberError handling try/catch per-account — log warning, tidak menghentikan loop
notify:subscription-reminder (admin-side)
Bukan reminder expiry ke member notify:subscription-reminder bukan pengirim email H-7/H-3/H-1 ke member. Command ini mengirim reminder ke admin untuk dua kasus berbeda. Jangan tertukar dengan cron:ads-expired.
Command notify:subscription-reminder (app/Console/Commands/NotifySubscriptionReminderCommand.php) berjalan hourly via scheduler:
php Copy $ schedule -> command ( ' notify:subscription-reminder --interval=3600 ' )
-> hourly ()
-> withoutOverlapping ()
-> runInBackground ();
Dua kasus yang di-notify ke admin
Kasus Query Notifikasi Order paid belum diproses Order::where('flag','paid')->whereHas('subscribe', fn($q) => $q->needProcess())AdminSubscriptionReminderNotificationWithdraw belum selesai Order::whereHas('withdraw', fn($q) => $q->whereNull('completed_at'))AdminWithdrawReminderNotification
Aspek Keterangan Penerima Admin aktif (User::admin()->active()->get())Interval --interval=3600 — Cache TTL 1 jam per-order (dedupe)Cache key subscription_reminder_sent:{order_id} & withdraw_reminder_sent:{order_id}Channel In-app notification + broadcast (implement ShouldBroadcast)
Method pendukung AdsAccount
Method Lokasi Fungsi 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 96 Filter akun dummy (expired null, stock_ads_account_id null) scopeWhere_member($q, $member_id)line 68 Filter by member_bm.member_id getGraceExpiredAttribute()line 176 Return expired + EXPIRED_AFTER format Y-m-d getGraceLimitAttribute()line 191 Carbon::parse(expired)->addDays(EXPIRED_AFTER)getDaysUntilExpiredAttribute()line 222 Selisih hari ke expired; jika <= EXPIRED_BEFORE trigger reminder
Scheduler entries terkait
Dari app/Console/Kernel.php:
Command Frekuensi Fungsi 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
Template Lokasi view Trigger email.subscription.reminder_expiryresources/views/email/subscription/reminder_expiry.blade.phpcron:ads-expired — H-7/H-3/H-1email.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 Copy # 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