Marketing Service
Marketing Service adalah service Go standalone yang menangani halaman promo (banner + item promo + klaim user), penjadwalan promo push notification, dan dispatcher worker yang mengirim push lewat firebase_service. Service berjalan di production sebagai sumber tunggal data marketing (db_kesles_merchant_marketing).
Dokumen pendamping
infrastruktur_kesles_merchant.md§5.1 + plan canonicalpromo-push-broadcast-proposal.md.
1. Identitas Service
| Atribut | Nilai |
|---|---|
| Port | 127.0.0.1:8095 (loopback) |
| Main DB | db_kesles_merchant_marketing (schema marketing) |
| VM dir | /home/enalfarid/kesles_merchant/merchant_marketing |
| Binary | marketing-service |
| systemd unit | marketing-service.service |
| Health/Ready | GET /health (liveness), GET /ready (readiness + DB ping) |
| Logs | journalctl -u marketing-service |
Cross-service hanya lewat HTTP: worker dispatcher memanggil firebase_service (http://127.0.0.1:8093) untuk resolve token aktif dan kirim bulk push. Tidak ada query langsung lintas-domain DB.
2. Tanggung Jawab
- Halaman promo mobile — serve banner header, tips, dan daftar item promo (public + authed) serta proses klaim promo per-user.
- CRUD promo content untuk operator — settings halaman + item promo (create/update/delete/reorder) via endpoint internal yang di-proxy dashboard_api.
- Penjadwalan promo push — CRUD
promo_schedulesdengan rate limit max 3 active schedule per hari WIB; computenext_fire_atsaat status transition draft→active. - Dispatcher worker — goroutine poll yang fire schedule yang sudah jatuh tempo, resolve audience, cek prefs/rate-limit user, kirim push lewat firebase_service, log per-user delivery, lalu recompute jadwal berikutnya.
- Retention — loop background harian yang purge
promo_send_logslebih dari 90 hari.
3. Endpoint
Public (tanpa auth)
| Method | Path | Tujuan |
|---|---|---|
GET | /public/marketing/promo | Halaman promo (banner + items) tanpa auth |
Authed (JWT Bearer)
| Method | Path | Tujuan |
|---|---|---|
GET | /marketing/promo | Halaman promo untuk user terautentikasi |
POST | /marketing/promo/claim | Klaim promo oleh user |
Internal (header X-Internal-API-Key)
| Method | Path | Tujuan |
|---|---|---|
GET/POST | /internal/promo-schedules | List / create promo schedule (rate limit 3 active/hari) |
GET/PATCH/DELETE | /internal/promo-schedules/{id} | Detail / update / set status / delete schedule |
GET | /internal/promo-schedules/{id}/logs | Audit log delivery per-user |
GET/PUT/PATCH | /internal/promo-page-settings | Singleton settings halaman promo |
GET/POST | /internal/promo-items | List / create item promo |
GET/PATCH/PUT/DELETE | /internal/promo-items/{id} | Detail / update / soft delete item |
PATCH | /internal/promo-items/reorder | Batch reorder (drag-and-drop) |
GET | /internal/promo-redemption-report | Laporan redemption promo |
Endpoint /internal/* hanya ter-register jika INTERNAL_NOTIFICATION_API_KEY di-set; di production key wajib ada (fail-fast saat boot).
4. Routing & Integrasi
Trafik mobile lewat nginx route /merchant/marketing-api/ → localhost:8095 (kesles.com.conf). Endpoint operator dashboard di-proxy oleh dashboard_api sebagai thin proxy ke endpoint /internal/* marketing_service, menambahkan header X-Internal-API-Key.
| Dashboard path | Method | Target marketing_service |
|---|---|---|
/api/dashboard/marketing/promo-schedules* | proxy | /internal/promo-schedules* |
/api/dashboard/marketing/promo-page | proxy | /internal/promo-page-settings |
/api/dashboard/marketing/promo-items* | proxy | /internal/promo-items* |
dashboard_api membaca base URL dari MARKETING_SERVICE_BASE_URL (default http://127.0.0.1:8095) + shared INTERNAL_NOTIFICATION_API_KEY. Operator UI di Flutter: panel jadwal_kirim_promo_panel.dart (Marketing → Jadwal Kirim Promo).
Dispatcher worker
| Komponen | File | Fungsi |
|---|---|---|
| Dispatcher | internal/worker/dispatcher.go | Poll ready schedule (SELECT FOR UPDATE SKIP LOCKED), resolve audience, cek prefs/rate-limit, insert-before-send (status pending→sent dalam tx yang sama), recompute next_fire_at |
| Next-fire compute | internal/worker/nextfire.go | Hitung next_fire_at untuk 5 schedule_type |
| FCM client | internal/worker/fcmclient.go | HTTP client ke firebase_service (/internal/fcm/tokens/active, /internal/fcm/send-bulk) |
Worker di-gate PROMO_WORKER_ENABLED=true (default OFF, opt-in eksplisit). Saat aktif, dispatcher poll dengan interval default; retention loop jalan terpisah (purge harian).
5. Config / Env
| Env | Default | Catatan |
|---|---|---|
APP_ENV | development | Set production di VM |
APP_PORT | 8095 | Port loopback |
POSTGRES_DSN | — | Wajib; DSN db_kesles_merchant_marketing |
JWT_SECRET | — | Wajib; verifikasi JWT user |
JWT_ISSUER | kesles-merchant-auth | Issuer claim |
INTERNAL_NOTIFICATION_API_KEY | — | Shared secret /internal/*; wajib di production |
FCM_SERVICE_BASE_URL | http://127.0.0.1:8093 | Base URL firebase_service untuk worker |
PROMO_WORKER_ENABLED | false | Set true untuk aktifkan dispatcher worker |
6. Security Hardening (konfigurasi saat ini)
- Internal key auth —
subtle.ConstantTimeCompareuntuk headerX-Internal-API-Key(cegah timing side-channel). Key kosong di production = fail-fast saat boot. - HTTP timeouts —
ReadTimeout/WriteTimeout30s,IdleTimeout60s. - Max body — request body di-cap 512 KB untuk semua route.
- Security headers —
X-Content-Type-Options,X-Frame-Options: DENY,Referrer-Policy,Content-Security-Policy: default-src 'none', HSTS,Permissions-Policy. - Panic recovery — middleware
withRecoverytangkap panic handler → 500, tidak crash proses. - DB pool —
MaxOpenConns=25,MaxIdleConns=5,ConnMaxLifetime=5m,ConnMaxIdleTime=5m; driverpgx/v5viadatabase/sqlstdlib; ping fail-fast saat init. - Graceful shutdown —
Shutdown(ctx)tutup HTTP server + DB pool.
7. Schema DB
Database db_kesles_merchant_marketing, schema marketing (5 tabel, sole source of truth — tidak ada lagi marketing.* di db_kesles_merchant):
| Tabel | Isi |
|---|---|
promo_page_settings | Singleton settings halaman promo (banner header + tips) |
promo_items | Item promo (banner card konten) |
user_promo_claims | Catatan klaim promo per-user |
promo_schedules | Jadwal promo push |
promo_send_logs | Log delivery per-user (audit) |
schedule_type: one_time / specific_date / daily / weekly / monthly.
Status delivery (promo_send_logs.status): pending / sent / failed / invalid_token / skipped_pref / skipped_rate_limit / skipped_quiet_hours.
promo_send_logs punya UNIQUE (schedule_id, user_id, fired_at) untuk idempotency, dan di-purge otomatis setelah 90 hari oleh retention loop.
Migrations: merchant_database/db_kesles_merchant_marketing/migrations/v1/ — 001_initial_schema.sql (settings + items + claims), 002_promo_schedules.sql (schedules + send_logs).
8. Operator Runbook — Dispatcher Worker
Aktifkan worker
echo "PROMO_WORKER_ENABLED=true" >> /home/enalfarid/kesles_merchant/merchant_marketing/.env.production
echo "FCM_SERVICE_BASE_URL=http://127.0.0.1:8093" >> /home/enalfarid/kesles_merchant/merchant_marketing/.env.production
sudo systemctl restart marketing-service
Verify hidup:
sudo journalctl -u marketing-service -n 20 -o cat | grep -E 'promo_dispatcher|promo_worker'
promo_dispatcher_start = aktif; promo_worker_disabled = env PROMO_WORKER_ENABLED tidak true.
Audit log delivery per-schedule
curl -X GET "http://127.0.0.1:8095/internal/promo-schedules/<SCHED_ID>/logs" \
-H "X-Internal-API-Key: $INTERNAL_NOTIFICATION_API_KEY"
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
target_count=0 di log fire | notification.fcm_push_tokens kosong / firebase_service tidak reachable | Cek GET /internal/fcm/tokens/active?limit=1 di firebase_service return tokens |
| Worker tidak claim schedule | next_fire_at IS NULL (schedule masih draft) | PATCH status active agar handler compute next_fire_at |
9. Reference
- Plan canonical:
promo-push-broadcast-proposal.md - Architecture:
infrastruktur_kesles_merchant.md§5.1 - Sibling:
firebase-service-status.md(notification site) — broadcast worker di service tetangga