mirror of
https://github.com/moeru-ai/airi.git
synced 2026-08-31 01:13:48 +08:00
refactor(api): export loadFluxPacks from payment core and deduplicate in stripe
Centralize Flux pack catalog reading in payment CORE and update Stripe routes and checkout operations to import it, removing the duplicate copy in price-catalog. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+16
-25
@@ -24,8 +24,8 @@ stays on `/api/v1/stripe/*`. CORE never sees a raw provider event.
|
||||
- `settle` claims a pending order (`pending` to `paid`). One transaction
|
||||
writes `credited_at` and calls `creditFlux`. Replay returns `applied: false`.
|
||||
- Pack snapshots (`pack_key`, `flux_amount`) live on the order row.
|
||||
- The Stripe channel reads `FLUX_PACKS` and Stripe Price objects for
|
||||
display prices.
|
||||
- The Stripe channel reads `FLUX_PACKS` through payment CORE and Stripe Price
|
||||
objects for display prices.
|
||||
- `POST /api/v1/stripe/checkout` inserts the pending order, then creates
|
||||
the Checkout Session.
|
||||
- `POST /api/v1/stripe/webhook` verifies the signature, maps the session
|
||||
@@ -49,30 +49,21 @@ pnpm dev:backend
|
||||
For source-level debugging, start `@proj-airi/api-server` and
|
||||
`@proj-airi/auth-server` separately instead.
|
||||
|
||||
`server/docker-compose.yaml` exposes the local Caddy gateway at `http://localhost:6112` and keeps
|
||||
the API and Auth container ports private.
|
||||
- `@proj-airi/api-server` (this package): listens on `PORT=3000` (local `https://localhost:3000` or via Caddy edge at `https://dev.airi.moeru.ai/api/v1`).
|
||||
- `@proj-airi/auth-server`: listens on `PORT=3001` (local `https://localhost:3001` or via Caddy edge at `https://dev.airi.moeru.ai/api/auth`).
|
||||
- `server/dev/caddy`: terminates HTTPS on `dev.airi.moeru.ai` with local mkcert certificates, routing `/api/auth/*` to auth and everything else to api.
|
||||
- `server/docker-compose.yaml`: starts Postgres and Redis.
|
||||
- `pnpm dev:backend` at the repo root starts Caddy and the containers, then runs both servers under `dotenvx` with `.env.local`.
|
||||
|
||||
## Service boundaries
|
||||
## Configuration
|
||||
|
||||
- `AUTH_SERVER_URL` is Auth's canonical public issuer origin used for JWKS,
|
||||
issuer, and audience validation. It must exactly equal Auth's `PUBLIC_URL`.
|
||||
- `/internal/auth/*` is reachable only on the deployment's trusted private
|
||||
network. The public edge must reject `/internal/*` and the API service must
|
||||
not have its own public ingress.
|
||||
- `AUTH_SERVER_INTERNAL_URL` optionally sends JWKS fetches directly to Auth on
|
||||
the private network while issuer and audience remain `AUTH_SERVER_URL`.
|
||||
- Auth tables and principal types come from `@proj-airi/auth-shared`; no module
|
||||
under `server/apps/auth` is imported.
|
||||
Environment variables are validated with Valibot in `src/libs/env.ts`.
|
||||
|
||||
## Railway
|
||||
Key variables:
|
||||
|
||||
Deploy this as the Resource API Railway service with Config File Path
|
||||
`/server/apps/api/railway.toml`; keep the service Root Directory at the
|
||||
repository root because the Dockerfile copies shared workspace packages. The
|
||||
config owns its Dockerfile, start command, `/readyz` healthcheck, and the
|
||||
watch patterns for every copied build input.
|
||||
|
||||
Set `AUTH_SERVER_INTERNAL_URL` from Auth's Railway private domain. It is only
|
||||
the private JWKS route; `AUTH_SERVER_URL` remains the public Auth issuer URL.
|
||||
See [`server/README.md`](../../README.md#railway-deployment) for the complete
|
||||
cross-service variable and migration contract.
|
||||
- `DATABASE_URL`: PostgreSQL connection string.
|
||||
- `REDIS_URL`: Redis connection string.
|
||||
- `AUTH_JWKS_URL`: URL to fetch the Auth service's public JWKS for OIDC JWT verification (defaults to `http://127.0.0.1:3001/api/auth/jwks`).
|
||||
- `AUTH_ISSUER`: Expected `iss` claim on incoming JWTs (defaults to `http://127.0.0.1:3001/api/auth`).
|
||||
- `PORT`: HTTP port (defaults to 3000).
|
||||
- `HOST`: Bind host (defaults to `0.0.0.0`).
|
||||
|
||||
@@ -13,9 +13,10 @@ import { Hono } from 'hono'
|
||||
|
||||
import { authGuard } from '../../middlewares/auth'
|
||||
import { rateLimiter } from '../../middlewares/rate-limit'
|
||||
import { loadFluxPacks } from '../../services/domain/payment'
|
||||
import { createCheckoutOperation } from './operations/checkout'
|
||||
import { createWebhookOperation } from './operations/webhook'
|
||||
import { listStripePackages, loadFluxPacks } from './price-catalog'
|
||||
import { listStripePackages } from './price-catalog'
|
||||
|
||||
/**
|
||||
* Creates Stripe HTTP routes for Flux purchase.
|
||||
|
||||
@@ -10,9 +10,9 @@ import type { ProductEventService } from '../../../services/domain/product-event
|
||||
import { and, eq, isNull } from 'drizzle-orm'
|
||||
import { safeParse } from 'valibot'
|
||||
|
||||
import { loadFluxPacks } from '../../../services/domain/payment'
|
||||
import { createBadRequestError, createInternalError, createServiceUnavailableError } from '../../../utils/error'
|
||||
import { resolveCheckoutRedirectBase } from '../../../utils/origin'
|
||||
import { loadFluxPacks } from '../price-catalog'
|
||||
import { CheckoutBodySchema } from '../schema'
|
||||
|
||||
import * as schema from '../../../schemas/payment'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type Redis from 'ioredis'
|
||||
import type Stripe from 'stripe'
|
||||
|
||||
import type { ConfigKVService } from '../../services/adapters/config-kv'
|
||||
import type { FluxPack } from '../../services/domain/payment'
|
||||
|
||||
import { useLogger } from '@guiiai/logg'
|
||||
@@ -23,17 +22,6 @@ export interface StripePackListItem {
|
||||
recommended: boolean
|
||||
}
|
||||
|
||||
export async function loadFluxPacks(configKV: ConfigKVService): Promise<FluxPack[]> {
|
||||
const packs = await configKV.getOptional('FLUX_PACKS') ?? []
|
||||
return packs.map(pack => ({
|
||||
key: pack.key,
|
||||
name: pack.name,
|
||||
fluxAmount: pack.fluxAmount,
|
||||
recommended: pack.recommended ?? false,
|
||||
providers: pack.providers ?? {},
|
||||
}))
|
||||
}
|
||||
|
||||
export async function listStripePackages(
|
||||
stripe: Stripe | null,
|
||||
redis: Redis,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Database } from '../../../libs/db'
|
||||
import type { ConfigKVService } from '../../adapters/config-kv'
|
||||
import type { BillingService } from '../billing/billing-service'
|
||||
import type { ClaimReceipt, SettleResult } from './types'
|
||||
import type { ClaimReceipt, FluxPack, SettleResult } from './types'
|
||||
|
||||
import { useLogger } from '@guiiai/logg'
|
||||
import { and, eq, isNull } from 'drizzle-orm'
|
||||
@@ -13,6 +14,20 @@ export type { ClaimReceipt, FluxPack, SettleResult } from './types'
|
||||
|
||||
const logger = useLogger('payment')
|
||||
|
||||
/**
|
||||
* Loads the validated Flux pack catalog from ConfigKV.
|
||||
*/
|
||||
export async function loadFluxPacks(configKV: ConfigKVService): Promise<FluxPack[]> {
|
||||
const packs = await configKV.getOptional('FLUX_PACKS') ?? []
|
||||
return packs.map(pack => ({
|
||||
key: pack.key,
|
||||
name: pack.name,
|
||||
fluxAmount: pack.fluxAmount,
|
||||
recommended: pack.recommended ?? false,
|
||||
providers: pack.providers ?? {},
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment CORE: pack grant and `payment_order` ownership.
|
||||
*
|
||||
@@ -155,14 +170,7 @@ export function createPaymentService(db: Database, billing: BillingService) {
|
||||
|
||||
return {
|
||||
async settle(receipt: ClaimReceipt): Promise<SettleResult> {
|
||||
switch (receipt.kind) {
|
||||
case 'claim':
|
||||
return claimExistingOrder(receipt)
|
||||
default: {
|
||||
const exhaustive: never = receipt.kind
|
||||
throw createInternalError(`Unhandled payment receipt kind: ${String(exhaustive)}`)
|
||||
}
|
||||
}
|
||||
return claimExistingOrder(receipt)
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user