Files
sase.tr/packages/config/src/index.ts
Sase Dev 6a0b36a7a0 feat: replace Iyzico with Stripe + single-page subscription stepper
Subscription/checkout flow rewritten end-to-end. The plan-card "Devam Et"
button silently wiped brand selection on re-click and the actual proceed
button lived offscreen — fixed by collapsing plan/brands/payment/confirm
into a single vertical stepper with one sticky CTA.

Backend
- Stripe Hosted Checkout (`/payments/stripe/checkout`) + webhook
  (`/payments/stripe/webhook`, raw body) replacing the stubbed Iyzico
  module. Webhook activates subscription on `checkout.session.completed`,
  expires the pending subscription on cancel/expire so users can retry.
- New `bank_accounts` table — multiple rows, single `is_active` enforced
  by a partial unique index. Admin CRUD under `/admin/bank-accounts`
  with multipart QR upload to MinIO; atomic `/activate` swap in a
  transaction; `GET /payments/bank-info` returns the active row.
- `payments` gains `stripe_session_id`, `stripe_payment_intent_id`,
  `bank_account_id`. EFT flow now reads the active bank account at
  payment time and stores the FK for reconciliation.
- Env: `IYZICO_*` removed, `STRIPE_*` added (validated by zod schema).
- `main.ts` `rawBody: true` for Stripe signature verification.
- Drizzle 0003 snapshot id collision fixed (VIEW-only migration shared
  prevId with 0002, blocking new generates).

Frontend
- `/dashboard/subscription` rewritten as a 4-step vertical stepper with
  step-aware sticky bottom CTA; plan re-selection is idempotent and
  preserves brand state. `/dashboard/subscription/pay` deleted; Stripe
  returns to the same page via `?stripe=success|cancelled` and the UI
  polls `/subscriptions/me` until the webhook activates the row.
- New components: `bank-transfer-card.tsx` (DB-driven IBAN + Kolay Adres
  + uploaded QR image + receipt upload) and `stripe-checkout-button.tsx`.
- Active subscription view, trial onboarding/urgency banner, downgrade
  and cancel dialogs preserved.
- TR/EN i18n: new `subscription.steps.*`, `subscription.stickyCta.*`,
  `payment.stripe.*`, `payment.bank.*`; provider label updated.

PostHog: `method: "iyzico"` → `"stripe"`; new events `iban_copied`,
`kolay_adres_copied`, `qr_viewed`, `eft_initiated`,
`stripe_redirect_returned`.

Deploy runs `db:migrate` which applies 0004_hot_quicksilver
(additive: new table + nullable columns; safe to apply on prod).
Operator must add `STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`,
`STRIPE_WEBHOOK_SECRET` to env and create the first
`bank_accounts` row via the admin endpoint before the bank tab works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:42:28 +00:00

110 lines
3.5 KiB
TypeScript

import { z } from "zod";
export const envSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
PORT: z.coerce.number().default(4000),
DATABASE_URL: z.string().url(),
REDIS_HOST: z.string().default("127.0.0.1"),
REDIS_PORT: z.coerce.number().default(6379),
REDIS_PASSWORD: z.string(),
BETTER_AUTH_SECRET: z.string().min(32),
BETTER_AUTH_URL: z.string().url(),
GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),
MINIO_ENDPOINT: z.string(),
MINIO_ACCESS_KEY: z.string(),
MINIO_SECRET_KEY: z.string(),
MINIO_BUCKET_NAME: z.string().default("sase-schemas"),
MINIO_PUBLIC_URL: z.string(),
MINIO_USE_SSL: z
.string()
.transform((v) => v === "true")
.default("false"),
CORS_ORIGIN: z.string().default("http://localhost:3000"),
STRIPE_SECRET_KEY: z.string().optional(),
STRIPE_PUBLISHABLE_KEY: z.string().optional(),
STRIPE_WEBHOOK_SECRET: z.string().optional(),
STRIPE_SUCCESS_URL: z
.string()
.url()
.default("http://localhost:3000/dashboard/subscription?stripe=success"),
STRIPE_CANCEL_URL: z
.string()
.url()
.default("http://localhost:3000/dashboard/subscription?stripe=cancelled"),
PL24_BASE_URL: z.string().optional(),
PL24_COMPANY_CODE: z.string().optional(),
PL24_USERNAME: z.string().optional(),
PL24_PASSWORD: z.string().optional(),
PL24_COMPANY_CODE_2: z.string().optional(), // de-708171
PL24_USERNAME_2: z.string().optional(),
PL24_PASSWORD_2: z.string().optional(),
PL24_PROXY_DE: z.string().optional(), // http://user:pass@gw.dataimpulse.com:10000
EMEX_USERNAME: z.string().optional(),
EMEX_PASSWORD: z.string().optional(),
// Parts-Catalogs (Playwright JWT capture + DataImpulse proxy)
PCAT_USE_PROXY: z.string().default("true"),
PCAT_PROXY_HOST: z.string().default("gw.dataimpulse.com"),
PCAT_PROXY_USER: z.string().optional(),
PCAT_PROXY_PASS: z.string().optional(),
ML_PREDICTION_ENABLED: z
.string()
.transform((v) => v === "true")
.default("false"),
// OpenRouter (used by scripts/emex-translate-bootstrap.ts → DeepSeek V3)
OPENROUTER_API_KEY: z.string().optional(),
// Postal Email
POSTAL_API_URL: z.string().url().optional(),
POSTAL_API_KEY: z.string().optional(),
POSTAL_FROM_ADDRESS: z.string().email().default("noreply@sase.tr"),
POSTAL_FROM_NAME: z.string().default("Sase.tr"),
// OpenTelemetry
OTEL_ENABLED: z
.string()
.transform((v) => v === "true")
.default("false"),
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().optional(),
OTEL_EXPORTER_OTLP_HEADERS: z.string().optional(),
OTEL_SERVICE_NAME: z.string().default("sase-api"),
OTEL_TRACE_SAMPLE_RATE: z.coerce.number().min(0).max(1).default(1.0),
// PostHog — product analytics (server-side)
POSTHOG_API_KEY: z.string().optional(),
POSTHOG_HOST: z.string().url().default("https://t.sase.tr"),
// Sentry — error tracking
SENTRY_DSN: z.string().url().optional(),
// Changelog automation (token-authed internal endpoint for Fusion webhook)
CHANGELOG_AUTOMATION_TOKEN: z.string().min(32).optional(),
});
export type Env = z.infer<typeof envSchema>;
export function validateEnv(
env: Record<string, unknown> = process.env as Record<string, unknown>,
): Env {
const result = envSchema.safeParse(env);
if (!result.success) {
const formatted = result.error.format();
console.error("Environment validation failed:");
console.error(JSON.stringify(formatted, null, 2));
throw new Error("Invalid environment variables");
}
return result.data;
}