Files
tiennm99 0310a4d900 ci: add prettier + svelte-check + GitHub Actions workflow
- scripts: format/format:check/check - prettier with tab/single-quote/100-cols, prettier-plugin-svelte - svelte-check via jsconfig.json + @types/node - .github/workflows/ci.yml runs format:check + check + build on push/PR with placeholder env vars (build doesn't connect to Upstash) - reflowed docs/*.md to the new prettier style
2026-05-24 16:25:52 +07:00

113 lines
5.9 KiB
Markdown

# Tech Stack — sepay-playground
User-fixed stack. Research confirms feasibility.
## Runtime / Framework
- **SvelteKit** (latest stable, 2.x) running on **Svelte 5** (runes available — `$state`, `$derived`, `$effect`).
- **JavaScript** (not TypeScript). Use **JSDoc** for type hints where it pays for itself (Redis client, SePay payload shapes). `jsconfig.json` for editor IntelliSense + `checkJs: false` to stay loose.
- **Package manager: pnpm** (`pnpm-lock.yaml` committed, `packageManager` field in `package.json`, `.npmrc` with `shamefully-hoist=false`).
- **Node.js runtime** for all server routes (incl. webhook). No edge runtime — keeps things uniform and avoids Upstash REST cold-path edge cases.
## UI
- **Tailwind CSS v4** (PostCSS plugin + `@import "tailwindcss"` in `app.css`).
- **shadcn-svelte** (https://shadcn-svelte.com) — direct Svelte port of shadcn/ui, copy-paste components into `src/lib/components/ui`. Install only what's used: `button`, `input`, `label`, `card`, `badge`, `alert`. Powered by `bits-ui` under the hood.
- `lucide-svelte` for icons (pulled by shadcn-svelte).
- `mode-watcher` for light/dark mode (shadcn-svelte standard).
## Storage
- **Upstash Redis** via `@upstash/redis` (framework-agnostic; works fine in SvelteKit).
- Env: `UPSTASH_REDIS_REST_URL`, `UPSTASH_REDIS_REST_TOKEN` — canonical names for `Redis.fromEnv()`.
- Auto-serializes JSON values → store plain objects.
- `set(key, val, { nx: true })` for idempotent webhook dedup.
- `set(key, val, { ex: 86400 })` — orders TTL 24h (demo).
## Payments
- **SePay** VietQR.
- QR image: `https://qr.sepay.vn/img?acc=<ACC>&bank=<BANK>&amount=<N>&des=<CODE>&template=compact`. No server-side QR lib.
- Webhook: SePay POSTs to `/api/webhooks/sepay` with `Authorization: Apikey <SEPAY_WEBHOOK_API_KEY>`.
- Order code surfaces in payload `code` (auto-extracted by SePay's dashboard prefix rule); fallback regex on `content`.
- Dedup on payload `id` (stable across retries — up to 7 retries / 5h).
- Respond `200 {"success":true}` within 30s.
## Deployment
- **Vercel** via **`@sveltejs/adapter-vercel`** (auto-detected by Vercel when present in `svelte.config.js`).
- No `vercel.json` needed.
- Env vars: `vercel env add ...` per-environment, `vercel env pull .env.local` for dev.
- Local public URL for SePay during dev: **ngrok** (`ngrok http 5173` — SvelteKit's default dev port).
## Project Layout (target)
```
sepay-playground/
├── src/
│ ├── app.css # Tailwind import + tokens
│ ├── app.html
│ ├── lib/
│ │ ├── server/
│ │ │ ├── redis.js # Redis.fromEnv() client
│ │ │ ├── orders.js # createOrder / getOrder / markPaid
│ │ │ └── sepay.js # buildQrUrl, verifyWebhookAuth
│ │ ├── components/
│ │ │ ├── ui/ # shadcn-svelte components
│ │ │ ├── PayForm.svelte
│ │ │ ├── PayAwaiting.svelte
│ │ │ └── PayPaid.svelte
│ │ └── types.js # JSDoc typedefs (Order, SepayWebhookPayload)
│ └── routes/
│ ├── +page.svelte # /
│ ├── pay/
│ │ ├── +page.svelte # /pay (state machine: form → awaiting → paid)
│ │ └── +page.server.js # form action: createOrder
│ └── api/
│ ├── orders/[code]/+server.js # GET status (polled by /pay)
│ ├── webhooks/sepay/+server.js # POST receiver
│ └── dev/simulate-webhook/+server.js # gated by !PROD
├── static/
├── svelte.config.js # adapter-vercel
├── vite.config.js
├── tailwind.config.js (or v4 inline) + postcss.config.js
├── jsconfig.json
├── .npmrc
├── .env.example
├── package.json # packageManager: pnpm@...
└── pnpm-lock.yaml
```
## Env Vars
| Name | Source | Notes |
| -------------------------- | ------------------------------------ | ---------------------------------------------- |
| `SEPAY_API_TOKEN` | SePay dashboard | Reserved per user spec; unused by current flow |
| `SEPAY_WEBHOOK_API_KEY` | SePay dashboard | Compared against `Authorization: Apikey <...>` |
| `SEPAY_ACCOUNT_NUMBER` | SePay-bound bank account | Used in QR URL |
| `SEPAY_BANK_CODE` | qr.sepay.vn/banks.json | e.g. `MBBank`, `Vietcombank`, `ACB` |
| `UPSTASH_REDIS_REST_URL` | Upstash console / Vercel integration | |
| `UPSTASH_REDIS_REST_TOKEN` | Upstash console / Vercel integration | |
| `SEPAY_ORDER_PREFIX` | local config | Defaults `SEVQR` (VietinBank-compatible) |
All loaded via SvelteKit's `$env/static/private` (server-only — never leaks to client).
## Out of Scope (demo)
- Real auth / users
- Multi-currency (VND only)
- Refunds, settlement reconciliation
- Production observability beyond `console.log` + Redis state
## Confirmed decisions (vs. prior Next.js draft)
- SvelteKit replaces Next.js — Vercel still primary deploy target via official adapter.
- JavaScript replaces TypeScript — JSDoc typedefs cover the boundaries (Redis values, webhook payload).
- pnpm replaces npm — lockfile committed, `packageManager` field set.
- shadcn-svelte replaces shadcn/ui — same design system, Svelte port.
- Everything else (Upstash, SePay endpoints, env vars, ngrok-for-dev) unchanged.
## Open Question
- Include a dev-only `POST /api/dev/simulate-webhook` (gated by `!import.meta.env.PROD`) to test paid-state without a real transfer. **Recommendation: yes.**