Lewati ke konten utama

Email Service — Architecture

Status: ✅ email_service FULLY DONE — Phase 3 Soak ✅ 14/14 CLOSED 2026-06-07 · Phase 4 ✅ DEPLOYED & VERIFIED 2026-05-29 15:50 WIB (ACCELERATED)

services/email_service adalah service standalone Go untuk pengiriman email transaksional. Per Phase 4, core_api/internal/email/ DIHAPUS — single path: emailservice.Client → HTTP langsung ke service ini. Mirror obligation EXPIRED.

Service Info​

FieldValue
Pathservices/email_service/
Port127.0.0.1:8094 (loopback — hanya dipanggil dari core_api di VM yang sama)
RuntimeGo
AuthX-Internal-API-Key header
SMTP providerGmail smtp.gmail.com:587 · dev: MailPit localhost:1025
VM folder/home/enalfarid/kesles_merchant/merchant_email/
Systemd unitemail-service.service · User=enalfarid · Restart=on-failure

Production Traffic Flow (Phase 4 — single path)​

mobile / dashboard user → core_api endpoint
└→ emailServiceClient.SendXxx() (emailservice.Client — Phase 4)
└→ HTTP POST http://127.0.0.1:8094/internal/email/send
└→ email_service handleSend
├→ store.Insert (status=queued)
├→ sender.SendXxx (SMTP via Gmail)
├→ store.MarkSent (status=sent + sent_at)
└→ return 202 {message_id, status:sent}

Tidak ada fallback. Service down = email tidak terkirim. core_api/internal/email/ DIHAPUS Phase 4 2026-05-29.


Endpoints​

GET /health​

Liveness — no auth. Return 200 selalu selama process up.

{
"service": "email-service",
"status": "ok",
"smtp_configured": true,
"postgres_configured": true,
"app_env": "production"
}

GET /ready​

Readiness — no auth. Verifikasi SMTP configured + Postgres ping. Return 503 kalau salah satu fail.

{
"service": "email-service",
"status": "ready",
"checks": { "smtp": "ok", "postgres": "ok" },
"app_env": "production"
}

POST /internal/email/send​

Kirim email via template + persist audit row. Auth: X-Internal-API-Key.

Request​

{
"template": "otp",
"to_email": "user@example.com",
"payload": { "code": "847291" }
}

Response​

HTTPBodyKondisi
202{"status":"sent","template":"...","message_id":"<uuid>"}Sukses
400{"error":"validation_failed"}template / to_email kosong
400{"error":"invalid_payload"}Body bukan JSON valid
401{"error":"unauthorized"}API key salah atau header tidak ada
502{"status":"error","error":"send_failed","message_id":"<uuid>"}SMTP transport error
503{"error":"smtp_not_configured"}Env SMTP belum diisi

message_id selalu ada di response (termasuk saat 502) — bisa dipakai untuk lookup via /internal/email/messages/{id}.


POST /internal/email/log​

INSERT audit row tanpa eksekusi SMTP. Phase 1 dual-write: core_api masih kirim record saat fallback ke direct. Auth: X-Internal-API-Key.

{
"template": "otp",
"to_email": "user@example.com",
"subject": "Kode Verifikasi Email Kesles Merchant",
"payload": { "code": "***" },
"status": "direct_sent",
"provider": "direct_core_api"
}

Response 201 {"status":"logged","message_id":"<uuid>"}. status default queued; kalau status=direct_sent, endpoint langsung set sent_at.


GET /internal/email/messages/{id}​

Lookup status email by UUID. Auth: X-Internal-API-Key. Return 404 kalau tidak ditemukan, 503 kalau POSTGRES_DSN kosong.

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"template": "otp",
"to_email": "user@example.com",
"subject": "Kode Verifikasi Email Kesles Merchant",
"delivery_status": "sent",
"provider": "smtp_gmail",
"created_at": "2026-05-25T03:30:00Z",
"sent_at": "2026-05-25T03:30:01Z"
}

Templates​

6 template tersedia. HTML dirender dari services/email_service/internal/email/templates/*.html (embed.FS — bukan inline di sender.go).

otp​

Subject: Kode Verifikasi Email Kesles Merchant

Payload fieldTypeKeterangan
codestring6-digit OTP code

Caller: mobile OTP, dashboard OTP, profile email verify.


password_reset​

Subject: Permintaan Reset Password Kesles Merchant

Payload fieldTypeKeterangan
display_namestringNama user — fallback "Pengguna"
reset_urlstringDeep link reset password berisi token — valid 1 jam

device_login_alert​

Subject: Notifikasi Login Baru Kesles Merchant

Payload fieldTypeKeterangan
device_namestringNama device — fallback "device baru"
cancel_urlstringURL batalkan sesi baru — valid 10 menit

mobile_user_welcome​

Subject: Selamat Datang di Kesles Merchant

Payload fieldTypeKeterangan
display_namestringNama user — fallback "Pengguna"
phonestringNomor HP terdaftar

dashboard_user_welcome​

Subject: Selamat Datang di Kesles Merchant Dashboard

Payload fieldTypeKeterangan
display_namestringNama user — fallback "Pengguna"
role_labelstringRole badge ("Admin", "Finance", dst) — opsional
dashboard_urlstringURL login dashboard — fallback https://kesles.com/merchant/dashboard

staff_invitation (baru 2026-05-25)​

Subject: Anda Diundang Bergabung di {merchant_name}

Template kontekstual untuk undangan staf — menggantikan template otp generik yang sebelumnya dipakai untuk caller staff_invite. Tone personal, mention merchant + inviter.

Payload fieldTypeKeterangan
merchant_namestringNama merchant yang mengundang
inviter_namestringNama admin yang kirim undangan
rolestringRole yang diassign (mis. "Staff", "Admin")
codestringOTP code undangan
app_urlstringDeep link ke halaman penerimaan undangan
catatan
Perubahan slug staff_invite

Slug staff_invite di Dispatcher tidak berubah. Yang berubah: method di-rename SendOTPStaffInvite → SendStaffInvitation dengan signature payload-based, dan template di payload dari "otp" → "staff_invitation". Invitee sekarang menerima email berisi nama merchant + inviter + role, bukan "Kode Verifikasi Email" generik.


Caller Integration (core_api — Phase 4)​

Phase 4 DONE 2026-05-29. Dispatcher, audit_adapter, internal/email/sender.go sudah DIHAPUS dari core_api. Tidak ada lagi EMAIL_TRANSPORT* atau SMTP_* di core_api.

Call pattern (8 wrapper method)​

s.emailServiceClient.SendOTP(ctx, toEmail, code)
s.emailServiceClient.SendPasswordReset(ctx, toEmail, displayName, resetURL)
s.emailServiceClient.SendOTPDashboard(ctx, toEmail, code)
s.emailServiceClient.SendOTPProfile(ctx, toEmail, code)
s.emailServiceClient.SendDeviceLoginAlert(ctx, toEmail, deviceName, cancelURL)
s.emailServiceClient.SendStaffInvitation(ctx, toEmail, merchantName, inviterName, role, code, appURL)
s.emailServiceClient.SendMobileUserWelcome(ctx, toEmail, displayName, phone)
s.emailServiceClient.SendDashboardUserWelcome(ctx, toEmail, displayName, roleLabel, dashboardURL)

Env minimal core_api (Phase 4)​

EnvValueKeterangan
EMAIL_SERVICE_BASE_URLhttp://127.0.0.1:8094URL loopback ke email_service
EMAIL_SERVICE_API_KEY(secret)Harus identik dengan INTERNAL_NOTIFICATION_API_KEY di email_service

EMAIL_TRANSPORT*, EMAIL_AUDIT, SMTP_* sudah DIHAPUS dari core_api.


Environment Variables (email_service)​

APP_ENV=production # development | staging | production
APP_PORT=8094

INTERNAL_NOTIFICATION_API_KEY= # wajib; harus identik dengan EMAIL_SERVICE_API_KEY di core_api

# Postgres — wajib Phase 1+
POSTGRES_DSN= # kosong = /log + /messages return 503; /send tetap jalan

# SMTP
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_NAME="Kesles Merchant"
SMTP_FROM_EMAIL=

# Retry (Phase 2+)
EMAIL_RETRY_MAX=3
EMAIL_RETRY_INITIAL_BACKOFF_MS=500
EMAIL_RETRY_MAX_BACKOFF_MS=10000
validateConfig() production guard

Di APP_ENV=production, service akan gagal startup kalau INTERNAL_NOTIFICATION_API_KEY, SMTP_USERNAME, atau SMTP_FROM_EMAIL kosong. Guard ini mencegah deploy production tanpa credential lengkap.

POSTGRES_DSN graceful degradation

Kalau POSTGRES_DSN kosong: /internal/email/send tetap jalan dan kirim SMTP tapi tidak persist ke DB. /internal/email/log dan /internal/email/messages/{id} return 503.


Database Schema​

Migration: merchant_database/db_kesles_merchant/migrations/v1/029_notification_email_messages.sql

notification.email_messages​

Audit log per email send. Pola identik dengan notification.whatsapp_messages.

KolomTypeKeterangan
iduuidPrimary key — caller-generated
templatetextotp / password_reset / device_login_alert / mobile_user_welcome / dashboard_user_welcome / staff_invitation
to_emailtextPenerima
subjecttextSubject email
payloadjsonbInput params (code, displayName, resetURL, dst)
delivery_statustextqueued / sent / failed / retrying
providertextsmtp_gmail (via email_service) / direct_core_api (fallback)
provider_message_idtextKosong untuk SMTP — REST provider isi ini
created_attimestamptz—
sent_attimestamptzSet saat MarkSent()
last_errortextSet saat MarkFailed()

Index: (delivery_status, created_at) · (to_email, created_at DESC).

Cara cek distribusi status di prod:

SELECT delivery_status, count(*) FROM notification.email_messages GROUP BY 1;

notification.email_send_attempts​

Per-attempt detail untuk retry policy (Phase 2+). 1 email_message bisa punya N attempts.

KolomTypeKeterangan
message_iduuid FK→ email_messages.id CASCADE DELETE
attempt_noint1-based
statustextsuccess / error
error_summarytext—

Telemetry​

Semua send emit slog JSON event email_send:

{
"event": "email_send",
"template": "otp",
"to_masked": "j***@example.com",
"duration_ms": 1240,
"success": true,
"error_class": "ok",
"attempt_no": 1
}

error_class enum: ok / gmail_throttle / gmail_blocked / smtp_auth / smtp_timeout / unknown.

PII protection: email di-mask (john.doe@x → j***@x) — tidak pernah masuk journalctl plain.

store.go juga emit slog warn saat DB write gagal:

  • email_insert_failed — INSERT row gagal
  • email_mark_sent_failed — MarkSent gagal
  • email_mark_failed_failed — MarkFailed gagal

Aggregator harian: scripts/email-telemetry-daily.sh.


Phase Soak Metrics (Phase 3) — ✅ 14/14 CLOSED 2026-06-07​

Phase 3 soak: 2026-05-25 → 2026-06-07. PASSED 14 hari clean. 14 hari berturut: 100% smtp_gmail, 0 failed, 0 fallback, 0 throttle.

MetricDay 1 (2026-05-25)Day 13 (2026-06-06)Threshold
Latency p503.2s2.8s< 5s ✅
Latency p954.0s3.8s< 5s ✅
Error rate0%0%< 1% ✅
Fallback events000 ✅
Gmail throttle000 ✅

Acceptance Phase 3: 14 hari clean — 0 crash, 0 fallback, error rate < 1%, latency p95 < 5s. ACHIEVED.


Extraction Phases​

PhaseStatusDetail
−1 Telemetri✅ DONEslog email_send + classifyEmailErr + maskEmail + aggregator sh live
0 Service Standalone✅ DONE6/6 deliverable — 5 endpoint, 6 template, unit test 608 baris, embed.FS refactor, validateConfig guard
1 Persistence Dual-Write✅ DONEEMAIL_AUDIT=service di prod sejak 2026-05-24 — audit_adapter + /log endpoint
2 Send via Feature Flag✅ DONEEMAIL_TRANSPORT=service global flip 2026-05-25 11:21 — 8/8 caller via email_service
3 Soak Production 14 Hari✅ 14/14 CLOSEDMulai 2026-05-25 · Selesai 2026-06-07 · 100% smtp_gmail, 0 failed/fallback/throttle
4 Cleanup core_api✅ DEPLOYED & VERIFIEDAccelerated 2026-05-29 15:50 · 1.431 LOC dihapus · 8 wrapper method ditambah

Mirror obligation: ✅ EXPIRED PERMANEN (Phase 4 DONE 2026-05-29). core_api/internal/email/ sudah dihapus. Edit template/logic email hanya di services/email_service/.


Dev — Local Setup​

# 1. Start MailPit (dev SMTP catcher)
mailpit # Web UI: localhost:8025 · SMTP: localhost:1025

# 2. Set env
export APP_ENV=development
export APP_PORT=8094
export SMTP_HOST=localhost
export SMTP_PORT=1025
export SMTP_FROM_EMAIL=test@kesles.com
export INTERNAL_NOTIFICATION_API_KEY=test-key-dev

# 3. Run
cd services/email_service && go run ./cmd/server

# 4. Test send
curl -X POST http://localhost:8094/internal/email/send \
-H "X-Internal-API-Key: test-key-dev" \
-H "Content-Type: application/json" \
-d '{"template":"otp","to_email":"dev@test.com","payload":{"code":"123456"}}'

# 5. Cek email di MailPit
open http://localhost:8025

Deployment (VM Production)​

# Build binary (dari local)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" \
-o services/email_service/dist/email-service \
./services/email_service/cmd/server

# Deploy ke VM
scp -P 2262 services/email_service/dist/email-service \
enalfarid@ptikn3-vm.cluster-vps.dalang.io:/home/enalfarid/kesles_merchant/merchant_email/

ssh -p 2262 enalfarid@ptikn3-vm.cluster-vps.dalang.io \
"systemctl restart email-service"

# Verify
ssh -p 2262 enalfarid@ptikn3-vm.cluster-vps.dalang.io \
"curl -fsS http://127.0.0.1:8094/health"
# expect: {"service":"email-service","status":"ok",...}

# Monitor logs
ssh -p 2262 enalfarid@ptikn3-vm.cluster-vps.dalang.io \
"sudo journalctl -u email-service -f"

  • Service Topology
  • Dashboard Admin API — bulk email path (workaround pre-Phase 0, refactor candidate post Phase 4)
  • Database Schema — notification schema