Files
moeka-project/server/apps/api
37c837e502 refactor(api): extract payment CORE to support other payment providers [1/2] (#2335)
## Why

Stripe checkout used Stripe-only tables. This extracts a shared payment
CORE. New checkout writes `payment_order`. Old Stripe tables stay so
in-progress Sessions can still settle.

This PR is [1/2]. [#2368](https://github.com/moeru-ai/airi/pull/2368) is
[2/2]. That PR archives leftover Stripe tables after in-progress
Sessions finish or expire.

## Changes

- Add `payment_order` and `provider_account`.
- Copy `stripe_checkout_session` into `payment_order`.
- Copy `stripe_customer` into `provider_account`.
- Keep `stripe_*` tables and `user_flux.stripe_customer_id`.
- New checkout writes `payment_order` and stores
`metadata.payment_order_id`.
- Webhook resolves new Sessions by `metadata.payment_order_id`.
- Webhook resolves older Sessions by `provider_order_id`, then by a
leftover `stripe_checkout_session` row.
- That leftover-row lookup covers Sessions opened before this deploy,
and rows written while 0023 runs.
[#2368](https://github.com/moeru-ai/airi/pull/2368) deletes it after
those Sessions finish or expire.

## Test plan

- [x] `pnpm exec vitest run
server/apps/api/src/services/domain/payment/tests/payment.test.ts
server/apps/api/src/routes/stripe`
- [x] `pnpm -F @proj-airi/api-server typecheck`
- [x] `git diff --check`

## Visual changes

No user-visible changes.

## Open: settle after account deletion

Account deletion stamps `payment_order.deletedAt`. A Checkout Session
that is still open can still pay after that stamp. This PR keeps `main`
behavior. `settle` loads the row by id, including a soft-deleted row,
then marks it `paid` and credits Flux.

Follow-up policy: if `deletedAt` is set, skip. Do not update the archive
row. Do not credit Flux. Do not insert a live `provider_account`. Stripe
remains the payment record. Skip matches the soft-delete rule: a deleted
row is gone, not write it again.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: RainbowBird <git@luoling.moe>
2026-09-12 17:43:10 +08:00
..
2026-08-26 20:13:10 +08:00
2026-08-26 20:13:10 +08:00

@proj-airi/api-server

Project AIRI's resource API. Authentication is a separate workspace app at server/apps/auth; this package does not instantiate Better Auth or expose auth/OIDC routes.

Responsibilities

  • Hono business APIs and WebSocket endpoints.
  • Characters, chats, providers, Flux, Stripe, model routing, and billing.
  • PostgreSQL migration ownership for the currently shared database. Drizzle reads the checked-in drizzle/ journal and SQL files at startup.
  • Redis cache, configuration KV, and cross-instance Pub/Sub.
  • Local verification of Auth-issued OIDC JWTs through public JWKS.

Payment

src/services/domain/payment owns pack grant and payment_order rows. CORE exposes openPending, bindProcessorOrder, abandon, settle, and deleteAllForUser. Checkout and package list live in the Stripe adapter on /api/v1/stripe/*. CORE never sees a raw processor event. The adapter maps the processor result onto a ClaimReceipt, then calls settle.

Run locally

pnpm -F @proj-airi/api-server dev
pnpm -F @proj-airi/api-server typecheck
pnpm -F @proj-airi/api-server exec vitest run
pnpm -F @proj-airi/api-server build

Run the complete local backend from the repository root:

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.

Service boundaries

  • 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.

Railway

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 for the complete cross-service variable and migration contract.