package usecase import ( "fmt" libcrypto "gateway/internal/library/crypto" redislib "gateway/internal/library/redis" memberconfig "gateway/internal/model/member/config" domrepo "gateway/internal/model/member/domain/repository" domusecase "gateway/internal/model/member/domain/usecase" "gateway/internal/model/member/repository" ) // Module bundles member atomic primitives. Each entry is a single-purpose // usecase; composite flows (e.g. "send verification email then mark // business_email verified") are assembled at the logic / driver layer and // MUST NOT live inside another usecase. type Module struct { // OTP issues and verifies one-time codes (purpose-agnostic). OTP domusecase.OTPUseCase // TOTP is nil when Member.TOTP.SecretKEK is empty / invalid; downstream // code must gracefully degrade (e.g. fall back to SMS/email OTP at the // logic layer). TOTP domusecase.TOTPUseCase // Stores exposed for logic-layer orchestration (rate limit, profile // flips). They are intentionally surfaced so the logic layer can compose // atomic usecases with rate-limit + profile mutation without re-wiring. VerifyRate domrepo.VerifyRateStore Profile domrepo.ProfileRepository } // ModuleParam wires member module dependencies. type ModuleParam struct { Redis *redislib.Client Config memberconfig.Config // Profile is optional; defaults to memory repository. Profile domrepo.ProfileRepository // TOTPProfile is optional; defaults to memory repository. TOTPProfile domrepo.TOTPProfileRepository } // NewModuleFromParam builds member atomic usecases. // // TOTP is wired only when Member.TOTP.SecretKEK is provided; this lets local // dev / unit tests boot without a KMS-backed key while production deployments // fail loud when the operator forgets to configure it. func NewModuleFromParam(param ModuleParam) (*Module, error) { if param.Redis == nil || param.Redis.Zero() == nil { return nil, fmt.Errorf("member: redis is required") } otpStore := repository.NewRedisOTPChallengeStore(param.Redis) rateStore := repository.NewRedisVerifyRateStore(param.Redis) profile := param.Profile if profile == nil { profile = repository.NewMemoryProfileRepository() } cfg := param.Config.Defaults() mod := &Module{ OTP: MustOTPUseCase(OTPUseCaseParam{Store: otpStore, Config: cfg}), VerifyRate: rateStore, Profile: profile, } if cfg.TOTP.SecretKEK != "" { cipher, err := libcrypto.NewAESGCMFromString(cfg.TOTP.SecretKEK) if err != nil { return nil, fmt.Errorf("member: totp kek: %w", err) } totpProfile := param.TOTPProfile if totpProfile == nil { totpProfile = repository.NewMemoryTOTPProfileRepository() } mod.TOTP = MustTOTPUseCase(TOTPUseCaseParam{ Profile: totpProfile, Enroll: repository.NewRedisTOTPEnrollStore(param.Redis), Replay: repository.NewRedisTOTPReplayStore(param.Redis), Cipher: cipher, Config: cfg, }) } return mod, nil }