Skip to main content

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 canonical promo-push-broadcast-proposal.md.

1. Identitas Service

AtributNilai
Port127.0.0.1:8095 (loopback)
Main DBdb_kesles_merchant_marketing (schema marketing)
VM dir/home/enalfarid/kesles_merchant/merchant_marketing
Binarymarketing-service
systemd unitmarketing-service.service
Health/ReadyGET /health (liveness), GET /ready (readiness + DB ping)
Logsjournalctl -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_schedules dengan rate limit max 3 active schedule per hari WIB; compute next_fire_at saat 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_logs lebih dari 90 hari.

3. Endpoint

Public (tanpa auth)

MethodPathTujuan
GET/public/marketing/promoHalaman promo (banner + items) tanpa auth

Authed (JWT Bearer)

MethodPathTujuan
GET/marketing/promoHalaman promo untuk user terautentikasi
POST/marketing/promo/claimKlaim promo oleh user

Internal (header X-Internal-API-Key)

MethodPathTujuan
GET/POST/internal/promo-schedulesList / 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}/logsAudit log delivery per-user
GET/PUT/PATCH/internal/promo-page-settingsSingleton settings halaman promo
GET/POST/internal/promo-itemsList / create item promo
GET/PATCH/PUT/DELETE/internal/promo-items/{id}Detail / update / soft delete item
PATCH/internal/promo-items/reorderBatch reorder (drag-and-drop)
GET/internal/promo-redemption-reportLaporan 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 pathMethodTarget marketing_service
/api/dashboard/marketing/promo-schedules*proxy/internal/promo-schedules*
/api/dashboard/marketing/promo-pageproxy/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

KomponenFileFungsi
Dispatcherinternal/worker/dispatcher.goPoll ready schedule (SELECT FOR UPDATE SKIP LOCKED), resolve audience, cek prefs/rate-limit, insert-before-send (status pendingsent dalam tx yang sama), recompute next_fire_at
Next-fire computeinternal/worker/nextfire.goHitung next_fire_at untuk 5 schedule_type
FCM clientinternal/worker/fcmclient.goHTTP 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

EnvDefaultCatatan
APP_ENVdevelopmentSet production di VM
APP_PORT8095Port loopback
POSTGRES_DSNWajib; DSN db_kesles_merchant_marketing
JWT_SECRETWajib; verifikasi JWT user
JWT_ISSUERkesles-merchant-authIssuer claim
INTERNAL_NOTIFICATION_API_KEYShared secret /internal/*; wajib di production
FCM_SERVICE_BASE_URLhttp://127.0.0.1:8093Base URL firebase_service untuk worker
PROMO_WORKER_ENABLEDfalseSet true untuk aktifkan dispatcher worker

6. Security Hardening (konfigurasi saat ini)

  • Internal key authsubtle.ConstantTimeCompare untuk header X-Internal-API-Key (cegah timing side-channel). Key kosong di production = fail-fast saat boot.
  • HTTP timeoutsReadTimeout/WriteTimeout 30s, IdleTimeout 60s.
  • Max body — request body di-cap 512 KB untuk semua route.
  • Security headersX-Content-Type-Options, X-Frame-Options: DENY, Referrer-Policy, Content-Security-Policy: default-src 'none', HSTS, Permissions-Policy.
  • Panic recovery — middleware withRecovery tangkap panic handler → 500, tidak crash proses.
  • DB poolMaxOpenConns=25, MaxIdleConns=5, ConnMaxLifetime=5m, ConnMaxIdleTime=5m; driver pgx/v5 via database/sql stdlib; ping fail-fast saat init.
  • Graceful shutdownShutdown(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):

TabelIsi
promo_page_settingsSingleton settings halaman promo (banner header + tips)
promo_itemsItem promo (banner card konten)
user_promo_claimsCatatan klaim promo per-user
promo_schedulesJadwal promo push
promo_send_logsLog 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

SymptomCauseFix
target_count=0 di log firenotification.fcm_push_tokens kosong / firebase_service tidak reachableCek GET /internal/fcm/tokens/active?limit=1 di firebase_service return tokens
Worker tidak claim schedulenext_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