package usecase import ( "context" "time" "gateway/internal/model/member" memberconfig "gateway/internal/model/member/config" "gateway/internal/model/member/domain/enum" domrepo "gateway/internal/model/member/domain/repository" domusecase "gateway/internal/model/member/domain/usecase" notifenum "gateway/internal/model/notification/domain/enum" domnotif "gateway/internal/model/notification/domain/usecase" ) type verificationUseCase struct { otp domusecase.OTPUseCase notifier domnotif.NotifierUseCase profile domrepo.ProfileRepository rates domrepo.VerifyRateStore config memberconfig.Config } // VerificationUseCaseParam wires VerificationUseCase. type VerificationUseCaseParam struct { OTP domusecase.OTPUseCase Notifier domnotif.NotifierUseCase Profile domrepo.ProfileRepository Rates domrepo.VerifyRateStore Config memberconfig.Config } // MustVerificationUseCase constructs VerificationUseCase. func MustVerificationUseCase(param VerificationUseCaseParam) domusecase.VerificationUseCase { return &verificationUseCase{ otp: param.OTP, notifier: param.Notifier, profile: param.Profile, rates: param.Rates, config: param.Config.Defaults(), } } func (uc *verificationUseCase) StartEmailVerify(ctx context.Context, tenantID, uid, target, locale string) (*domusecase.OTPChallengeDTO, error) { return uc.startVerify(ctx, tenantID, uid, target, locale, enum.VerifyKindEmail, enum.OTPPurposeBusinessEmail, notifenum.ChannelEmail, notifenum.NotifyVerifyEmail) } func (uc *verificationUseCase) ConfirmEmailVerify(ctx context.Context, tenantID, uid, challengeID, code string) error { target, err := uc.otp.Verify(ctx, &domusecase.VerifyOTPRequest{ TenantID: tenantID, UID: uid, ChallengeID: challengeID, Code: code, Purpose: enum.OTPPurposeBusinessEmail, }) if err != nil { return err } return uc.profile.SetBusinessEmailVerified(ctx, tenantID, uid, target) } func (uc *verificationUseCase) StartPhoneVerify(ctx context.Context, tenantID, uid, target, locale string) (*domusecase.OTPChallengeDTO, error) { return uc.startVerify(ctx, tenantID, uid, target, locale, enum.VerifyKindPhone, enum.OTPPurposeBusinessPhone, notifenum.ChannelSMS, notifenum.NotifyVerifyPhone) } func (uc *verificationUseCase) ConfirmPhoneVerify(ctx context.Context, tenantID, uid, challengeID, code string) error { target, err := uc.otp.Verify(ctx, &domusecase.VerifyOTPRequest{ TenantID: tenantID, UID: uid, ChallengeID: challengeID, Code: code, Purpose: enum.OTPPurposeBusinessPhone, }) if err != nil { return err } return uc.profile.SetBusinessPhoneVerified(ctx, tenantID, uid, target) } func (uc *verificationUseCase) startVerify( ctx context.Context, tenantID, uid, target, locale string, kind enum.VerifyKind, purpose enum.OTPPurpose, channel notifenum.Channel, notifyKind notifenum.NotifyKind, ) (*domusecase.OTPChallengeDTO, error) { if tenantID == "" || uid == "" || target == "" { return nil, errb.InputMissingRequired("tenant_id, uid and target are required") } if uc.notifier == nil { return nil, errb.SysInternal("notifier is not configured") } if uc.rates == nil { return nil, errb.SysInternal("verify rate store is not configured") } if err := uc.checkRateLimits(ctx, tenantID, uid, kind); err != nil { return nil, err } dto, code, err := uc.otp.Generate(ctx, &domusecase.GenerateOTPRequest{ TenantID: tenantID, UID: uid, Purpose: purpose, Target: target, }) if err != nil { return nil, err } _, err = uc.notifier.Send(ctx, &domnotif.SendRequest{ TenantID: tenantID, UID: uid, Channel: channel, Kind: notifyKind, Target: target, Locale: locale, Data: map[string]any{"code": code, "expires_in": dto.ExpiresIn}, IdempotencyKey: dto.ChallengeID, DoNotPersistBody: true, Severity: notifenum.SeverityInfo, }) if err != nil { if invErr := uc.otp.Invalidate(ctx, dto.ChallengeID); invErr != nil { return nil, errb.SysInternal("invalidate otp after send failure").WithCause(invErr) } return nil, err } return dto, nil } func (uc *verificationUseCase) checkRateLimits(ctx context.Context, tenantID, uid string, kind enum.VerifyKind) error { cooldown := time.Duration(uc.config.OTP.ResendCooldownSeconds) * time.Second ok, err := uc.rates.TryResendLock(ctx, member.GetVerifyRateRedisKey(tenantID, uid, kind.String()), cooldown) if err != nil { return errb.SysInternal("verify rate check failed").WithCause(err) } if !ok { return errb.ResInvalidState("verification resend cooldown").WithCause(member.ErrResendCooldown) } count, err := uc.rates.IncrDaily(ctx, member.GetVerifyDailyRedisKey(tenantID, uid, kind.String()), 25*time.Hour) if err != nil { return errb.SysInternal("verify daily limit check failed").WithCause(err) } if count > int64(uc.config.OTP.DailyVerifyLimit) { return errb.ResInsufficientQuota("daily verification limit exceeded").WithCause(member.ErrDailyLimit) } return nil }