2026-05-20 09:32:22 +00:00
|
|
|
// Command notify-test runs one notification test by -method (point-and-shoot).
|
|
|
|
|
//
|
|
|
|
|
// make deps-up && make mongo-index
|
|
|
|
|
// make notify-test METHOD=email-send TO=you@example.com
|
|
|
|
|
// make notify-test METHOD=sms-send PHONE=0912345678 MOCK=1
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"gateway/internal/config"
|
|
|
|
|
redislib "gateway/internal/library/redis"
|
2026-05-20 13:03:59 +00:00
|
|
|
memberenum "gateway/internal/model/member/domain/enum"
|
2026-05-20 09:32:22 +00:00
|
|
|
dommember "gateway/internal/model/member/domain/usecase"
|
|
|
|
|
memberusecase "gateway/internal/model/member/usecase"
|
|
|
|
|
notifconfig "gateway/internal/model/notification/config"
|
|
|
|
|
"gateway/internal/model/notification/domain/enum"
|
|
|
|
|
domtpl "gateway/internal/model/notification/domain/template"
|
|
|
|
|
domusecase "gateway/internal/model/notification/domain/usecase"
|
|
|
|
|
notifusecase "gateway/internal/model/notification/usecase"
|
|
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"github.com/zeromicro/go-zero/core/conf"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
2026-05-20 13:03:59 +00:00
|
|
|
methodEmailSend = "email-send"
|
|
|
|
|
methodEmailEnqueue = "email-enqueue"
|
|
|
|
|
methodEmailIdempotency = "email-idempotency"
|
|
|
|
|
methodSMSSend = "sms-send"
|
|
|
|
|
methodSMSEnqueue = "sms-enqueue"
|
|
|
|
|
methodMemberEmail = "member-email"
|
|
|
|
|
methodMemberPhone = "member-phone"
|
|
|
|
|
methodAdminDLQ = "admin-dlq"
|
2026-05-20 09:32:22 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var validMethods = []string{
|
|
|
|
|
methodEmailSend,
|
|
|
|
|
methodEmailEnqueue,
|
|
|
|
|
methodEmailIdempotency,
|
|
|
|
|
methodSMSSend,
|
|
|
|
|
methodSMSEnqueue,
|
|
|
|
|
methodMemberEmail,
|
|
|
|
|
methodMemberPhone,
|
|
|
|
|
methodAdminDLQ,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
configFile = flag.String("f", "etc/gateway.dev.yaml", "config file")
|
|
|
|
|
method = flag.String("method", "", "test method (required): "+strings.Join(validMethods, ", "))
|
|
|
|
|
toEmail = flag.String("to", "", "recipient email")
|
|
|
|
|
phone = flag.String("phone", "", "recipient phone")
|
|
|
|
|
tenantID = flag.String("tenant", "notify-test", "tenant_id")
|
|
|
|
|
uid = flag.String("uid", "notify-test-uid", "uid")
|
|
|
|
|
mockOnly = flag.Bool("mock", false, "force mock email/SMS providers")
|
|
|
|
|
pollSec = flag.Int("poll", 45, "max seconds to wait for async delivery (enqueue methods)")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type env struct {
|
2026-05-20 13:03:59 +00:00
|
|
|
ctx context.Context
|
|
|
|
|
tenant string
|
|
|
|
|
uid string
|
|
|
|
|
to string
|
|
|
|
|
phone string
|
|
|
|
|
locale string
|
|
|
|
|
notifier domusecase.NotifierUseCase
|
|
|
|
|
// otp is the atomic primitive; this CLI plays the role of the future
|
|
|
|
|
// logic layer and orchestrates OTP.Generate + Notifier.Send inline.
|
|
|
|
|
otp dommember.OTPUseCase
|
|
|
|
|
admin domusecase.AdminNotifierUseCase
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
flag.Usage = func() {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Usage: notify-test -method <name> [options]\n\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Methods:\n")
|
|
|
|
|
for _, m := range validMethods {
|
|
|
|
|
fmt.Fprintf(os.Stderr, " %s\n", m)
|
|
|
|
|
}
|
|
|
|
|
fmt.Fprintf(os.Stderr, "\nExamples:\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " notify-test -method email-send -to you@example.com\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " notify-test -method email-enqueue -to you@example.com\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " notify-test -method sms-send -phone 0912345678\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " notify-test -method member-email -to you@example.com\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " notify-test -method admin-dlq\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " notify-test -method email-send -to t@e.com -mock\n")
|
|
|
|
|
flag.PrintDefaults()
|
|
|
|
|
}
|
|
|
|
|
flag.Parse()
|
|
|
|
|
|
2026-05-20 13:03:59 +00:00
|
|
|
code, err := run()
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintln(os.Stderr, err)
|
|
|
|
|
}
|
|
|
|
|
if code != 0 {
|
|
|
|
|
os.Exit(code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// run wires the requested method and returns (exitCode, error). Deferred
|
|
|
|
|
// cleanups inside run always execute before main calls os.Exit.
|
|
|
|
|
func run() (int, error) {
|
2026-05-20 09:32:22 +00:00
|
|
|
m := strings.TrimSpace(*method)
|
|
|
|
|
if m == "" {
|
|
|
|
|
flag.Usage()
|
2026-05-20 13:03:59 +00:00
|
|
|
return 2, fmt.Errorf("notify-test: -method is required")
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
if !isValidMethod(m) {
|
|
|
|
|
flag.Usage()
|
2026-05-20 13:03:59 +00:00
|
|
|
return 2, fmt.Errorf("notify-test: unknown method %q", m)
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
if err := validateArgs(m); err != nil {
|
2026-05-20 13:03:59 +00:00
|
|
|
return 2, fmt.Errorf("notify-test: %w", err)
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var c config.Config
|
|
|
|
|
conf.MustLoad(*configFile, &c)
|
|
|
|
|
if c.Mongo.Host == "" {
|
2026-05-20 13:03:59 +00:00
|
|
|
return 1, fmt.Errorf("notify-test: Mongo.Host is empty")
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
if c.Redis.Host == "" {
|
2026-05-20 13:03:59 +00:00
|
|
|
return 1, fmt.Errorf("notify-test: Redis.Host is empty")
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
if c.Notification.Email.From == "" && needsEmailFrom(m) {
|
2026-05-20 13:03:59 +00:00
|
|
|
return 1, fmt.Errorf("notify-test: Notification.Email.From is empty")
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
if *mockOnly {
|
|
|
|
|
forceMock(&c.Notification)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*pollSec+60)*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
rds, err := redislib.NewClient(c.Redis)
|
|
|
|
|
if err != nil {
|
2026-05-20 13:03:59 +00:00
|
|
|
return 1, fmt.Errorf("notify-test: redis: %w", err)
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mod, err := notifusecase.NewModuleFromParam(notifusecase.FactoryParam{
|
|
|
|
|
MongoConf: &c.Mongo,
|
|
|
|
|
Redis: rds,
|
|
|
|
|
Config: c.Notification,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
2026-05-20 13:03:59 +00:00
|
|
|
return 1, fmt.Errorf("notify-test: notification: %w", err)
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-20 13:03:59 +00:00
|
|
|
var otpUC dommember.OTPUseCase
|
2026-05-20 09:32:22 +00:00
|
|
|
if m == methodMemberEmail || m == methodMemberPhone {
|
2026-05-20 13:03:59 +00:00
|
|
|
memberMod, memErr := memberusecase.NewModuleFromParam(memberusecase.ModuleParam{
|
|
|
|
|
Redis: rds,
|
|
|
|
|
Config: c.Member,
|
2026-05-20 09:32:22 +00:00
|
|
|
})
|
2026-05-20 13:03:59 +00:00
|
|
|
if memErr != nil {
|
|
|
|
|
return 1, fmt.Errorf("notify-test: member: %w", memErr)
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
2026-05-20 13:03:59 +00:00
|
|
|
otpUC = memberMod.OTP
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e := &env{
|
2026-05-20 13:03:59 +00:00
|
|
|
ctx: ctx,
|
|
|
|
|
tenant: *tenantID,
|
|
|
|
|
uid: *uid,
|
|
|
|
|
to: *toEmail,
|
|
|
|
|
phone: *phone,
|
|
|
|
|
locale: c.Notification.DefaultLocale,
|
|
|
|
|
notifier: mod.Notifier,
|
|
|
|
|
otp: otpUC,
|
|
|
|
|
admin: mod.Admin,
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if m == methodEmailEnqueue || m == methodSMSEnqueue {
|
|
|
|
|
if mod.RetryWorker == nil {
|
2026-05-20 13:03:59 +00:00
|
|
|
return 1, fmt.Errorf("notify-test: retry worker not configured (need Redis)")
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
workerCtx, stop := context.WithCancel(context.Background())
|
|
|
|
|
go mod.RetryWorker.Run(workerCtx)
|
|
|
|
|
defer stop()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("method=%s email=%s sms=%s\n", m, strings.Join(emailProviders(&c.Notification), ","), strings.Join(smsProviders(&c.Notification), ","))
|
|
|
|
|
|
2026-05-20 13:03:59 +00:00
|
|
|
if runErr := runMethod(e, m); runErr != nil {
|
|
|
|
|
return 1, fmt.Errorf("FAIL: %w", runErr)
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
fmt.Println("OK")
|
2026-05-20 13:03:59 +00:00
|
|
|
return 0, nil
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runMethod(e *env, m string) error {
|
|
|
|
|
switch m {
|
|
|
|
|
case methodEmailSend:
|
|
|
|
|
return e.emailSend()
|
|
|
|
|
case methodEmailEnqueue:
|
|
|
|
|
return e.emailEnqueue()
|
|
|
|
|
case methodEmailIdempotency:
|
|
|
|
|
return e.emailIdempotency()
|
|
|
|
|
case methodSMSSend:
|
|
|
|
|
return e.smsSend()
|
|
|
|
|
case methodSMSEnqueue:
|
|
|
|
|
return e.smsEnqueue()
|
|
|
|
|
case methodMemberEmail:
|
|
|
|
|
return e.memberEmail()
|
|
|
|
|
case methodMemberPhone:
|
|
|
|
|
return e.memberPhone()
|
|
|
|
|
case methodAdminDLQ:
|
|
|
|
|
return e.adminDLQ()
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("unhandled method %q", m)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *env) emailSend() error {
|
|
|
|
|
dto, err := e.notifier.Send(e.ctx, &domusecase.SendRequest{
|
|
|
|
|
TenantID: e.tenant,
|
|
|
|
|
UID: e.uid,
|
|
|
|
|
Channel: enum.ChannelEmail,
|
|
|
|
|
Kind: enum.NotifyVerifyEmail,
|
|
|
|
|
Target: e.to,
|
|
|
|
|
Locale: e.locale,
|
|
|
|
|
Data: map[string]any{domtpl.VarCode: "123456", domtpl.VarExpiresIn: 300},
|
|
|
|
|
IdempotencyKey: uuid.NewString(),
|
|
|
|
|
DoNotPersistBody: true,
|
|
|
|
|
Severity: enum.SeverityInfo,
|
|
|
|
|
})
|
|
|
|
|
return reportSent(dto, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *env) emailEnqueue() error {
|
|
|
|
|
pending, err := e.notifier.Enqueue(e.ctx, &domusecase.SendRequest{
|
|
|
|
|
TenantID: e.tenant,
|
|
|
|
|
UID: e.uid,
|
|
|
|
|
Channel: enum.ChannelEmail,
|
|
|
|
|
Kind: enum.NotifyTenantWelcome,
|
|
|
|
|
Target: e.to,
|
|
|
|
|
Locale: e.locale,
|
|
|
|
|
Data: map[string]any{"tenant_name": "Test Corp"},
|
|
|
|
|
IdempotencyKey: uuid.NewString(),
|
|
|
|
|
DoNotPersistBody: false,
|
|
|
|
|
Severity: enum.SeverityInfo,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
final, err := waitSent(e.ctx, e.notifier, e.tenant, pending.ID, time.Duration(*pollSec)*time.Second)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("notification_id=%s provider=%s status=%s\n", final.ID, final.Provider, final.Status)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *env) emailIdempotency() error {
|
|
|
|
|
key := uuid.NewString()
|
|
|
|
|
first, err := e.notifier.Send(e.ctx, &domusecase.SendRequest{
|
|
|
|
|
TenantID: e.tenant,
|
|
|
|
|
UID: e.uid,
|
|
|
|
|
Channel: enum.ChannelEmail,
|
|
|
|
|
Kind: enum.NotifyVerifyEmail,
|
|
|
|
|
Target: e.to,
|
|
|
|
|
Locale: e.locale,
|
|
|
|
|
Data: map[string]any{domtpl.VarCode: "111111", domtpl.VarExpiresIn: 300},
|
|
|
|
|
IdempotencyKey: key,
|
|
|
|
|
Severity: enum.SeverityInfo,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
second, err := e.notifier.Send(e.ctx, &domusecase.SendRequest{
|
|
|
|
|
TenantID: e.tenant,
|
|
|
|
|
UID: e.uid,
|
|
|
|
|
Channel: enum.ChannelEmail,
|
|
|
|
|
Kind: enum.NotifyVerifyEmail,
|
|
|
|
|
Target: e.to,
|
|
|
|
|
Locale: e.locale,
|
|
|
|
|
Data: map[string]any{domtpl.VarCode: "222222", domtpl.VarExpiresIn: 300},
|
|
|
|
|
IdempotencyKey: key,
|
|
|
|
|
Severity: enum.SeverityInfo,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if first.ID != second.ID {
|
|
|
|
|
return fmt.Errorf("idempotency: expected same id, got %s vs %s", first.ID, second.ID)
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("notification_id=%s (replay ok)\n", first.ID)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *env) smsSend() error {
|
|
|
|
|
dto, err := e.notifier.Send(e.ctx, &domusecase.SendRequest{
|
|
|
|
|
TenantID: e.tenant,
|
|
|
|
|
UID: e.uid,
|
|
|
|
|
Channel: enum.ChannelSMS,
|
|
|
|
|
Kind: enum.NotifyVerifyPhone,
|
|
|
|
|
Target: e.phone,
|
|
|
|
|
Locale: e.locale,
|
|
|
|
|
Data: map[string]any{domtpl.VarCode: "123456", domtpl.VarExpiresIn: 300},
|
|
|
|
|
IdempotencyKey: uuid.NewString(),
|
|
|
|
|
DoNotPersistBody: true,
|
|
|
|
|
Severity: enum.SeverityInfo,
|
|
|
|
|
})
|
|
|
|
|
return reportSent(dto, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *env) smsEnqueue() error {
|
|
|
|
|
pending, err := e.notifier.Enqueue(e.ctx, &domusecase.SendRequest{
|
|
|
|
|
TenantID: e.tenant,
|
|
|
|
|
UID: e.uid,
|
|
|
|
|
Channel: enum.ChannelSMS,
|
|
|
|
|
Kind: enum.NotifyVerifyPhone,
|
|
|
|
|
Target: e.phone,
|
|
|
|
|
Locale: e.locale,
|
|
|
|
|
Data: map[string]any{domtpl.VarCode: "654321", domtpl.VarExpiresIn: 300},
|
|
|
|
|
IdempotencyKey: uuid.NewString(),
|
|
|
|
|
Severity: enum.SeverityInfo,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
final, err := waitSent(e.ctx, e.notifier, e.tenant, pending.ID, time.Duration(*pollSec)*time.Second)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("notification_id=%s provider=%s\n", final.ID, final.Provider)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 13:03:59 +00:00
|
|
|
// memberEmail demonstrates the logic-layer orchestration: generate an OTP
|
|
|
|
|
// challenge (atomic) and dispatch the verification email through Notifier
|
|
|
|
|
// (atomic). usecases never call each other — this driver is what the real
|
|
|
|
|
// logic handler will look like.
|
2026-05-20 09:32:22 +00:00
|
|
|
func (e *env) memberEmail() error {
|
2026-05-20 13:03:59 +00:00
|
|
|
return e.startMemberVerify(memberenum.OTPPurposeBusinessEmail, enum.ChannelEmail, enum.NotifyVerifyEmail, e.to)
|
2026-05-20 09:32:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *env) memberPhone() error {
|
2026-05-20 13:03:59 +00:00
|
|
|
return e.startMemberVerify(memberenum.OTPPurposeBusinessPhone, enum.ChannelSMS, enum.NotifyVerifyPhone, e.phone)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *env) startMemberVerify(purpose memberenum.OTPPurpose, channel enum.Channel, kind enum.NotifyKind, target string) error {
|
|
|
|
|
if e.otp == nil {
|
|
|
|
|
return fmt.Errorf("member OTP usecase not configured")
|
|
|
|
|
}
|
|
|
|
|
if target == "" {
|
|
|
|
|
return fmt.Errorf("target is empty")
|
|
|
|
|
}
|
|
|
|
|
dto, code, err := e.otp.Generate(e.ctx, &dommember.GenerateOTPRequest{
|
|
|
|
|
TenantID: e.tenant,
|
|
|
|
|
UID: e.uid,
|
|
|
|
|
Purpose: purpose,
|
|
|
|
|
Target: target,
|
|
|
|
|
})
|
2026-05-20 09:32:22 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-05-20 13:03:59 +00:00
|
|
|
if _, err := e.notifier.Send(e.ctx, &domusecase.SendRequest{
|
|
|
|
|
TenantID: e.tenant,
|
|
|
|
|
UID: e.uid,
|
|
|
|
|
Channel: channel,
|
|
|
|
|
Kind: kind,
|
|
|
|
|
Target: target,
|
|
|
|
|
Locale: e.locale,
|
|
|
|
|
Data: map[string]any{"code": code, "expires_in": dto.ExpiresIn},
|
|
|
|
|
IdempotencyKey: dto.ChallengeID,
|
|
|
|
|
DoNotPersistBody: true,
|
|
|
|
|
Severity: enum.SeverityInfo,
|
|
|
|
|
}); err != nil {
|
|
|
|
|
if invErr := e.otp.Invalidate(e.ctx, dto.ChallengeID); invErr != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "warn: invalidate otp after send failure: %v\n", invErr)
|
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("challenge_id=%s expires_in=%d\n", dto.ChallengeID, dto.ExpiresIn)
|
2026-05-20 09:32:22 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *env) adminDLQ() error {
|
|
|
|
|
if e.admin == nil {
|
|
|
|
|
return fmt.Errorf("admin notifier not configured")
|
|
|
|
|
}
|
|
|
|
|
entries, err := e.admin.ListDLQ(e.ctx, e.tenant, 10)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("dlq_count=%d\n", len(entries))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func reportSent(dto *domusecase.NotificationDTO, err error) error {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if dto.Status != enum.NotifyStatusSent {
|
|
|
|
|
return fmt.Errorf("status=%s last_error=%s", dto.Status, dto.LastError)
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("notification_id=%s provider=%s message_id=%s\n", dto.ID, dto.Provider, dto.ProviderMessageID)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func waitSent(ctx context.Context, notifier domusecase.NotifierUseCase, tenantID, notificationID string, timeout time.Duration) (*domusecase.NotificationDTO, error) {
|
|
|
|
|
deadline := time.Now().Add(timeout)
|
|
|
|
|
for time.Now().Before(deadline) {
|
|
|
|
|
dto, err := notifier.Get(ctx, tenantID, notificationID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
switch dto.Status {
|
|
|
|
|
case enum.NotifyStatusSent:
|
|
|
|
|
return dto, nil
|
|
|
|
|
case enum.NotifyStatusFailed, enum.NotifyStatusDropped:
|
|
|
|
|
return dto, fmt.Errorf("status=%s: %s", dto.Status, dto.LastError)
|
|
|
|
|
}
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return nil, ctx.Err()
|
|
|
|
|
case <-time.After(500 * time.Millisecond):
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("timeout after %s", timeout)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateArgs(m string) error {
|
|
|
|
|
switch m {
|
|
|
|
|
case methodEmailSend, methodEmailEnqueue, methodEmailIdempotency, methodMemberEmail:
|
|
|
|
|
if *toEmail == "" {
|
|
|
|
|
return fmt.Errorf("%s requires -to", m)
|
|
|
|
|
}
|
|
|
|
|
case methodSMSSend, methodSMSEnqueue, methodMemberPhone:
|
|
|
|
|
if *phone == "" {
|
|
|
|
|
return fmt.Errorf("%s requires -phone", m)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func needsEmailFrom(m string) bool {
|
|
|
|
|
switch m {
|
|
|
|
|
case methodEmailSend, methodEmailEnqueue, methodEmailIdempotency, methodMemberEmail:
|
|
|
|
|
return true
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isValidMethod(m string) bool {
|
|
|
|
|
for _, v := range validMethods {
|
|
|
|
|
if v == m {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func forceMock(cfg *notifconfig.Config) {
|
|
|
|
|
cfg.Email.SMTP.Enable = false
|
|
|
|
|
cfg.Email.SES.Enable = false
|
|
|
|
|
cfg.Email.Provider = notifconfig.ProviderMock
|
|
|
|
|
cfg.SMS.Mitake.Enable = false
|
|
|
|
|
cfg.SMS.Provider = notifconfig.ProviderMock
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func emailProviders(cfg *notifconfig.Config) []string {
|
|
|
|
|
var out []string
|
|
|
|
|
if cfg.Email.SMTP.Enable {
|
|
|
|
|
out = append(out, "smtp")
|
|
|
|
|
}
|
|
|
|
|
if cfg.Email.SES.Enable {
|
|
|
|
|
out = append(out, "ses")
|
|
|
|
|
}
|
|
|
|
|
if len(out) == 0 {
|
|
|
|
|
out = append(out, "mock")
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func smsProviders(cfg *notifconfig.Config) []string {
|
|
|
|
|
if cfg.SMS.Mitake.Enable {
|
|
|
|
|
return []string{"mitake"}
|
|
|
|
|
}
|
|
|
|
|
return []string{"mock"}
|
|
|
|
|
}
|