feat: Projekt-Scaffold + Core Utilities — Result Pattern, hashEmail, Zod-Schemas, Campaign-Typen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 08:11:54 +00:00
commit b63ce058a0
10 changed files with 185 additions and 0 deletions

19
src/lib/crypto.test.ts Normal file
View File

@@ -0,0 +1,19 @@
import { describe, it, expect } from 'vitest'
import { hashEmail } from './crypto'
describe('hashEmail', () => {
it('gibt SHA256-Hash zurück', () => {
const hash = hashEmail('test@example.com')
expect(hash).toHaveLength(64)
expect(hash).toMatch(/^[a-f0-9]+$/)
})
it('normalisiert E-Mail vor Hash (lowercase)', () => {
expect(hashEmail('Test@Example.COM')).toBe(hashEmail('test@example.com'))
})
it('gibt nie die Klartext-E-Mail zurück', () => {
const hash = hashEmail('test@example.com')
expect(hash).not.toContain('@')
})
})

5
src/lib/crypto.ts Normal file
View File

@@ -0,0 +1,5 @@
import { createHash } from 'crypto'
export function hashEmail(email: string): string {
return createHash('sha256').update(email.toLowerCase().trim()).digest('hex')
}

16
src/lib/result.test.ts Normal file
View File

@@ -0,0 +1,16 @@
import { describe, it, expect } from 'vitest'
import { ok, err } from './result'
describe('Result', () => {
it('ok wraps data', () => {
const r = ok('hello')
expect(r.ok).toBe(true)
if (r.ok) expect(r.data).toBe('hello')
})
it('err wraps error', () => {
const r = err(new Error('fail'))
expect(r.ok).toBe(false)
if (!r.ok) expect(r.error.message).toBe('fail')
})
})

6
src/lib/result.ts Normal file
View File

@@ -0,0 +1,6 @@
export type Result<T, E = Error> =
| { ok: true; data: T }
| { ok: false; error: E }
export const ok = <T>(data: T): Result<T> => ({ ok: true, data })
export const err = <E = Error>(error: E): Result<never, E> => ({ ok: false, error })

21
src/lib/validation.ts Normal file
View File

@@ -0,0 +1,21 @@
import { z } from 'zod'
export const CreateCampaignSchema = z.object({
name: z.string().min(1).max(255),
subject: z.string().min(1).max(998),
htmlBody: z.string().min(1),
plainBody: z.string().min(1),
})
export const UpdateCampaignSchema = CreateCampaignSchema.partial()
export const ScheduleCampaignSchema = z.union([
z.object({ type: z.literal('immediate') }),
z.object({ type: z.literal('once'), scheduledAt: z.coerce.date() }),
z.object({ type: z.literal('cron'), cronExpression: z.string().min(9) }),
])
export const RecipientSchema = z.union([
z.object({ listId: z.string().uuid(), segmentId: z.null().optional() }),
z.object({ segmentId: z.string().uuid(), listId: z.null().optional() }),
])

48
src/types/index.ts Normal file
View File

@@ -0,0 +1,48 @@
export type CampaignStatus =
| 'draft'
| 'scheduled'
| 'sending'
| 'sent'
| 'paused'
| 'cancelled'
export type TriggerType = 'cron' | 'event'
export interface Campaign {
id: string
name: string
subject: string
htmlBody: string
plainBody: string
status: CampaignStatus
scheduledAt: Date | null
cronExpression: string | null
createdAt: Date
updatedAt: Date
}
export interface CampaignRecipient {
id: string
campaignId: string
listId: string | null
segmentId: string | null
}
export interface CampaignTrigger {
id: string
campaignId: string
triggerType: TriggerType
triggerValue: string
}
export interface CampaignAnalytics {
campaignId: string
sent: number
opens: number
clicks: number
bounces: number
unsubscribes: number
openRate: number
clickRate: number
bounceRate: number
}