# Sub2ApiPay

**Language**: [中文](./README.md) | English (current)

Sub2ApiPay is a self-hosted payment gateway built for the [Sub2API](https://sub2api.com) platform. It supports four payment channels — **EasyPay** (aggregated Alipay/WeChat Pay), **Alipay** (official), **WeChat Pay** (official), and **Stripe** — with both pay-as-you-go balance top-up and subscription plans. Once a payment is confirmed, the system automatically calls the Sub2API management API to credit the user's balance or activate the subscription — no manual intervention required.

---

## Table of Contents

- [Features](#features)
- [Tech Stack](#tech-stack)
- [Quick Start](#quick-start)
- [Environment Variables](#environment-variables)
- [Deployment](#deployment)
- [Sub2API Integration](#sub2api-integration)
- [Payment Flow](#payment-flow)
- [API Endpoints](#api-endpoints)
- [Development](#development)

---

## Features

- **Four Payment Channels** — EasyPay aggregation, Alipay (official), WeChat Pay (official), Stripe
- **Dual Billing Modes** — Pay-as-you-go balance top-up + subscription plans
- **Auto Balance Credit** — Automatically calls Sub2API after payment verification, fully hands-free
- **Full Order Lifecycle** — Auto-expiry, user cancellation, admin cancellation, refunds
- **Limit Controls** — Per-transaction cap, daily per-user cap, daily per-channel global cap
- **Security** — Token auth, RSA2/MD5/Webhook signature verification, timing-safe comparison, full audit log
- **Responsive UI** — PC + mobile adaptive layout, dark/light theme, iframe embed support
- **Bilingual** — Automatic Chinese/English interface adaptation
- **Admin Panel** — Dashboard, order management (pagination/filtering/retry/refund), channel & subscription management

> **EasyPay Recommendation**: We personally recommend [ZPay](https://z-pay.cn/?uid=23808) as an EasyPay provider (referral link — feel free to remove). ZPay supports **individual users** (no business license) with a daily limit of ¥10,000; business license holders have no limit. Please evaluate the security, reliability, and compliance of any third-party payment provider on your own — this project does not endorse or guarantee any specific provider.

<details>
<summary>ZPay Registration QR Code</summary>

![ZPay Preview](./docs/zpay-preview.png)

</details>

---

## Tech Stack

| Category        | Technology                 |
| --------------- | -------------------------- |
| Framework       | Next.js 16 (App Router)    |
| Language        | TypeScript 5 + React 19    |
| Styling         | TailwindCSS 4              |
| ORM             | Prisma 7 (adapter-pg mode) |
| Database        | PostgreSQL 16              |
| Container       | Docker + Docker Compose    |
| Package Manager | pnpm                       |

---

## Quick Start

### Using Docker Hub Image (Recommended)

No Node.js or pnpm required on the server — just Docker.

```bash
mkdir -p /opt/sub2apipay && cd /opt/sub2apipay

# Download Compose file and env template
curl -O https://raw.githubusercontent.com/touwaeriol/sub2apipay/main/docker-compose.hub.yml
curl -O https://raw.githubusercontent.com/touwaeriol/sub2apipay/main/.env.example
cp .env.example .env

# Fill in required environment variables
nano .env

# Start (includes bundled PostgreSQL)
docker compose -f docker-compose.hub.yml up -d
```

### Build from Source

```bash
git clone https://github.com/touwaeriol/sub2apipay.git
cd sub2apipay
cp .env.example .env
nano .env
docker compose up -d --build
```

---

## Environment Variables

See [`.env.example`](./.env.example) for the full template.

### Core (Required)

| Variable                | Description                                                |
| ----------------------- | ---------------------------------------------------------- |
| `SUB2API_BASE_URL`      | Sub2API service URL, e.g. `https://sub2api.com`            |
| `SUB2API_ADMIN_API_KEY` | Sub2API admin API key                                      |
| `ADMIN_TOKEN`           | Admin panel access token (use a strong random string)      |
| `NEXT_PUBLIC_APP_URL`   | Public URL of this service, e.g. `https://pay.example.com` |

> `DATABASE_URL` is automatically injected by Docker Compose when using the bundled database.

### Payment Providers & Methods

**Step 1**: Declare which payment providers to load via `PAYMENT_PROVIDERS` (comma-separated):

```env
# Available: easypay, alipay, wxpay, stripe
# Example: EasyPay only
PAYMENT_PROVIDERS=easypay
# Example: Alipay + WeChat Pay + Stripe (official channels)
PAYMENT_PROVIDERS=alipay,wxpay,stripe
```

> **Alipay / WeChat Pay (official)** and **EasyPay** can coexist. Official channels connect directly to Alipay/WeChat Pay APIs with funds going straight to your merchant account and lower fees; EasyPay proxies payments through a third-party platform that forwards to official channels, with a lower barrier to entry. When using EasyPay, choose providers where funds are forwarded through official channels directly to your own account, rather than collected by a third party.

#### EasyPay (Alipay / WeChat Pay Aggregation)

Any payment provider compatible with the **EasyPay protocol** can be used.

| Variable              | Description                                                      |
| --------------------- | ---------------------------------------------------------------- |
| `EASY_PAY_PID`        | EasyPay merchant ID                                              |
| `EASY_PAY_PKEY`       | EasyPay merchant secret key                                      |
| `EASY_PAY_API_BASE`   | EasyPay API base URL                                             |
| `EASY_PAY_NOTIFY_URL` | Async callback URL: `${NEXT_PUBLIC_APP_URL}/api/easy-pay/notify` |
| `EASY_PAY_RETURN_URL` | Redirect URL after payment: `${NEXT_PUBLIC_APP_URL}/pay/result`  |
| `EASY_PAY_CID_ALIPAY` | Alipay channel ID (optional)                                     |
| `EASY_PAY_CID_WXPAY`  | WeChat Pay channel ID (optional)                                 |

#### Alipay (Official)

Direct integration with the Alipay Open Platform. Supports PC page payment (`alipay.trade.page.pay`) and mobile web payment (`alipay.trade.wap.pay`), automatically switching based on device type.

| Variable             | Description                                    |
| -------------------- | ---------------------------------------------- |
| `ALIPAY_APP_ID`      | Alipay application AppID                       |
| `ALIPAY_PRIVATE_KEY` | Application private key (content or file path) |
| `ALIPAY_PUBLIC_KEY`  | Alipay public key (content or file path)       |
| `ALIPAY_NOTIFY_URL`  | Async callback URL                             |
| `ALIPAY_RETURN_URL`  | Sync redirect URL (optional)                   |

#### WeChat Pay (Official)

Direct integration with WeChat Pay APIv3. Supports Native QR code payment and H5 payment, with mobile devices preferring H5 and auto-fallback to QR code.

| Variable              | Description                                     |
| --------------------- | ----------------------------------------------- |
| `WXPAY_APP_ID`        | WeChat Pay AppID                                |
| `WXPAY_MCH_ID`        | Merchant ID                                     |
| `WXPAY_PRIVATE_KEY`   | Merchant API private key (content or file path) |
| `WXPAY_CERT_SERIAL`   | Merchant certificate serial number              |
| `WXPAY_API_V3_KEY`    | APIv3 key                                       |
| `WXPAY_PUBLIC_KEY`    | WeChat Pay public key (content or file path)    |
| `WXPAY_PUBLIC_KEY_ID` | WeChat Pay public key ID                        |
| `WXPAY_NOTIFY_URL`    | Async callback URL                              |

#### Stripe

| Variable                 | Description                                 |
| ------------------------ | ------------------------------------------- |
| `STRIPE_SECRET_KEY`      | Stripe secret key (`sk_live_...`)           |
| `STRIPE_PUBLISHABLE_KEY` | Stripe publishable key (`pk_live_...`)      |
| `STRIPE_WEBHOOK_SECRET`  | Stripe webhook signing secret (`whsec_...`) |

> Stripe webhook endpoint: `${NEXT_PUBLIC_APP_URL}/api/stripe/webhook`
> Subscribe to: `payment_intent.succeeded`, `payment_intent.payment_failed`

### Business Rules

| Variable                         | Description                                       | Default                    |
| -------------------------------- | ------------------------------------------------- | -------------------------- |
| `MIN_RECHARGE_AMOUNT`            | Minimum amount per transaction (CNY)              | `1`                        |
| `MAX_RECHARGE_AMOUNT`            | Maximum amount per transaction (CNY)              | `1000`                     |
| `MAX_DAILY_RECHARGE_AMOUNT`      | Daily cumulative max per user (`0` = unlimited)   | `10000`                    |
| `MAX_DAILY_AMOUNT_ALIPAY`        | EasyPay Alipay channel daily global limit (opt.)  | Provider default           |
| `MAX_DAILY_AMOUNT_ALIPAY_DIRECT` | Alipay official channel daily global limit (opt.) | Provider default           |
| `MAX_DAILY_AMOUNT_WXPAY`         | WeChat Pay channel daily global limit (opt.)      | Provider default           |
| `MAX_DAILY_AMOUNT_STRIPE`        | Stripe channel daily global limit (opt.)          | Provider default           |
| `ORDER_TIMEOUT_MINUTES`          | Order expiry in minutes                           | `5`                        |
| `PRODUCT_NAME`                   | Product name shown on payment page                | `Sub2API Balance Recharge` |

### UI Customization (Optional)

Display a support contact image and description on the right side of the payment page.

| Variable             | Description                                                                     |
| -------------------- | ------------------------------------------------------------------------------- |
| `PAY_HELP_IMAGE_URL` | Help image URL — external URL or local path (see below)                         |
| `PAY_HELP_TEXT`      | Help text; use `\n` for line breaks, e.g. `Scan to add WeChat\nMon–Fri 9am–6pm` |

**Two ways to provide the image:**

- **External URL** (recommended — no Compose changes needed): any publicly accessible image link (CDN, OSS, image hosting).

  ```env
  PAY_HELP_IMAGE_URL=https://cdn.example.com/help-qr.jpg
  ```

- **Local file**: place the image in `./uploads/` and reference it as `/uploads/<filename>`.
  The directory must be mounted in `docker-compose.app.yml` (included by default):
  ```yaml
  volumes:
    - ./uploads:/app/public/uploads:ro
  ```
  ```env
  PAY_HELP_IMAGE_URL=/uploads/help-qr.jpg
  ```

> Clicking the help image opens it full-screen in the center of the screen.

### Docker Compose Variables

| Variable      | Description                      | Default                               |
| ------------- | -------------------------------- | ------------------------------------- |
| `APP_PORT`    | Host port mapping                | `3001`                                |
| `DB_PASSWORD` | PostgreSQL password (bundled DB) | `password` (**change in production**) |

---

## Deployment

### Option 1: Docker Hub Image + Bundled Database

Use `docker-compose.hub.yml` — the simplest deployment:

```bash
docker compose -f docker-compose.hub.yml up -d
```

Image: [`touwaeriol/sub2apipay:latest`](https://hub.docker.com/r/touwaeriol/sub2apipay)

### Option 2: Docker Hub Image + External Database

For existing PostgreSQL instances (shared with other services):

1. Set `DATABASE_URL` in `.env`
2. Use `docker-compose.app.yml` (app only, no DB):

```bash
docker compose -f docker-compose.app.yml up -d
```

### Option 3: Build from Source

For custom builds after modifications:

```bash
# On the build server
docker compose build
docker tag sub2apipay-app:latest touwaeriol/sub2apipay:latest
docker push touwaeriol/sub2apipay:latest

# On the deploy server
docker compose -f docker-compose.hub.yml pull
docker compose -f docker-compose.hub.yml up -d
```

### Reverse Proxy

The default host port is `3001` (configurable via `APP_PORT`). Use Nginx or Caddy as a reverse proxy with HTTPS:

```nginx
server {
    listen 443 ssl;
    server_name pay.example.com;

    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

### Database Migrations

Migrations run automatically on container startup via `prisma migrate deploy`. To run manually:

```bash
docker compose exec app npx prisma migrate deploy
```

---

## Sub2API Integration

Assuming this service is deployed at `https://pay.example.com`.

### User-Facing Pages

Configure the following URLs in the Sub2API admin panel under **Recharge Settings**, so users can navigate from Sub2API to the payment and order pages:

| Setting   | URL                                  | Description                                        |
| --------- | ------------------------------------ | -------------------------------------------------- |
| Payment   | `https://pay.example.com/pay`        | User top-up & subscription purchase page           |
| My Orders | `https://pay.example.com/pay/orders` | User views their own recharge/subscription history |

Sub2API **v0.1.88** and above will automatically append the following parameters — no manual query string needed:

| Parameter | Description                                       |
| --------- | ------------------------------------------------- |
| `user_id` | Sub2API user ID                                   |
| `token`   | User login token (required to view order history) |
| `theme`   | `light` (default) or `dark`                       |
| `lang`    | Interface language, `zh` (default) or `en`        |
| `ui_mode` | `standalone` (default) or `embedded` (for iframe) |

### Admin Panel

The admin panel is authenticated via the `token` URL parameter (set to the `ADMIN_TOKEN` environment variable). When integrating with Sub2API, just configure the paths — **no query parameters needed** — Sub2API will automatically append `token` and other parameters:

| Page          | URL                                           | Description                                                   |
| ------------- | --------------------------------------------- | ------------------------------------------------------------- |
| Overview      | `https://pay.example.com/admin`               | Aggregated entry with card-style navigation                   |
| Orders        | `https://pay.example.com/admin/orders`        | Filter by status, paginate, view details, retry/cancel/refund |
| Dashboard     | `https://pay.example.com/admin/dashboard`     | Revenue stats, order trends, payment method breakdown         |
| Channels      | `https://pay.example.com/admin/channels`      | Configure API channels & rates, sync from Sub2API             |
| Subscriptions | `https://pay.example.com/admin/subscriptions` | Manage subscription plans & user subscriptions                |

> **Tip**: When accessing directly (not via Sub2API), you need to manually append `?token=YOUR_ADMIN_TOKEN` to the URL. All admin pages share the same token — once you enter any page, you can navigate between modules via the sidebar.

---

## Payment Flow

```
User selects top-up / subscription plan
         │
         ▼
  Create Order (PENDING)
  ├─ Validate user status / pending orders / daily limit / channel limit
  └─ Call payment provider to get payment link
         │
         ▼
  User completes payment
  ├─ EasyPay       → QR code / H5 redirect
  ├─ Alipay (official) → PC page payment / H5 mobile web payment
  ├─ WeChat Pay (official) → Native QR code / H5 payment
  └─ Stripe        → Payment Element (PaymentIntent)
         │
         ▼
  Payment callback (RSA2 / MD5 / Webhook signature verified) → Order PAID
         │
         ▼
  Auto-call Sub2API recharge / subscription API
  ├─ Success → COMPLETED, balance credited / subscription activated
  └─ Failure → FAILED (admin can retry)
```

---

## API Endpoints

All API paths are prefixed with `/api`.

### Public API

User-facing endpoints, authenticated via `user_id` + `token` URL parameters.

| Method | Path                      | Description                                         |
| ------ | ------------------------- | --------------------------------------------------- |
| `GET`  | `/api/user`               | Get current user info                               |
| `GET`  | `/api/users/:id`          | Get specific user info                              |
| `POST` | `/api/orders`             | Create recharge / subscription order                |
| `GET`  | `/api/orders/:id`         | Query order details                                 |
| `POST` | `/api/orders/:id/cancel`  | User cancels pending order                          |
| `GET`  | `/api/orders/my`          | List current user's orders                          |
| `GET`  | `/api/channels`           | Get channel list (for frontend display)             |
| `GET`  | `/api/subscription-plans` | Get available subscription plans                    |
| `GET`  | `/api/subscriptions/my`   | Query current user's subscriptions                  |
| `GET`  | `/api/limits`             | Query recharge limits & payment method availability |

### Payment Callbacks

Called asynchronously by payment providers; signature verified before triggering credit flow.

| Method | Path                   | Description                    |
| ------ | ---------------------- | ------------------------------ |
| `GET`  | `/api/easy-pay/notify` | EasyPay async callback         |
| `POST` | `/api/alipay/notify`   | Alipay (official) callback     |
| `POST` | `/api/wxpay/notify`    | WeChat Pay (official) callback |
| `POST` | `/api/stripe/webhook`  | Stripe webhook callback        |

### Admin API

Authenticated via `token` parameter set to `ADMIN_TOKEN`.

| Method   | Path                                | Description                          |
| -------- | ----------------------------------- | ------------------------------------ |
| `GET`    | `/api/admin/orders`                 | Order list (paginated, filterable)   |
| `GET`    | `/api/admin/orders/:id`             | Order details (with audit log)       |
| `POST`   | `/api/admin/orders/:id/cancel`      | Admin cancels order                  |
| `POST`   | `/api/admin/orders/:id/retry`       | Retry failed recharge / subscription |
| `POST`   | `/api/admin/refund`                 | Issue refund                         |
| `GET`    | `/api/admin/dashboard`              | Dashboard (revenue stats, trends)    |
| `GET`    | `/api/admin/channels`               | Channel list                         |
| `POST`   | `/api/admin/channels`               | Create channel                       |
| `PUT`    | `/api/admin/channels/:id`           | Update channel                       |
| `DELETE` | `/api/admin/channels/:id`           | Delete channel                       |
| `GET`    | `/api/admin/subscription-plans`     | Subscription plan list               |
| `POST`   | `/api/admin/subscription-plans`     | Create subscription plan             |
| `PUT`    | `/api/admin/subscription-plans/:id` | Update subscription plan             |
| `DELETE` | `/api/admin/subscription-plans/:id` | Delete subscription plan             |
| `GET`    | `/api/admin/subscriptions`          | User subscription records            |
| `GET`    | `/api/admin/config`                 | Get system configuration             |
| `PUT`    | `/api/admin/config`                 | Update system configuration          |
| `GET`    | `/api/admin/sub2api/groups`         | Sync channel groups from Sub2API     |
| `GET`    | `/api/admin/sub2api/search-users`   | Search Sub2API users                 |

---

## Development

### Requirements

- Node.js 22+
- pnpm
- PostgreSQL 16+

### Local Setup

```bash
pnpm install
cp .env.example .env
# Edit .env with DATABASE_URL and other required values
pnpm prisma migrate dev
pnpm dev
```

### Commands

```bash
pnpm dev                      # Dev server with hot reload
pnpm build                    # Production build
pnpm test                     # Run tests
pnpm typecheck                # TypeScript type check
pnpm lint                     # ESLint
pnpm format                   # Prettier format

pnpm prisma generate          # Generate Prisma client
pnpm prisma migrate dev       # Create and apply migration (dev)
pnpm prisma migrate deploy    # Apply migrations (production)
pnpm prisma studio            # Visual database browser
```

---

## License

MIT
