# Admin API 명세서 — Subscriptions

## 공통 사항

| 항목 | 값 |
|---|---|
| Base URL | `https://{apim서비스명}.azure-api.net/admin` |
| 인증 | `Ocp-Apim-Subscription-Key` 헤더 (Admin Product 전용 구독키) |
| Content-Type | `application/json` |
| 대상 리소스 | Azure APIM 자신의 `subscriptions` (ARM 리소스를 그대로 관리/노출) |

모든 오퍼레이션은 ARM 호출이 실패하면 **ARM이 반환한 상태 코드와 본문을 그대로 전달**합니다 (자체적으로 오류를 감추지 않음).

---

## 1. `GET /subscriptions` — 구독 목록 조회

| 항목 | 내용 |
|---|---|
| Method | `GET` |
| Path | `/subscriptions` |
| Query Parameter | `view` (선택, `raw` \| `filtered` \| `summary`, 기본값 `filtered`) |

### `view` 값별 차이

| 값 | 의미 |
|---|---|
| `raw` | ARM 원본 그대로 (id, type, ownerId 등 전부 포함) |
| `filtered` (기본값) | 불필요한 필드(`id`, `type`, `properties.ownerId`) 제거, `scope`는 마지막 세그먼트만 표시 |
| `summary` | `name`, `state`, `scope`만 담은 최소 형태 |

> 구독키(primary/secondary key) 조회는 이 목록 API와 별개로 처리할 예정입니다 (추후 별도 오퍼레이션으로 추가).

### 응답 (200)

`view=raw`: ARM이 반환하는 형태 그대로 (`id`, `type`, `properties.ownerId` 포함)

`view=filtered`:

```json
{
  "value": [
    {
      "name": "svc-0001-example",
      "properties": {
        "displayName": "예시 파트너 구독",
        "state": "active",
        "scope": "starter",
        "createdDate": "2026-01-10T02:00:00Z",
        "expirationDate": "2026-12-31T00:00:00Z"
      }
    }
  ],
  "nextLink": "..."
}
```

`view=summary`: `name`, `properties.state`, `properties.scope`만 포함

```json
{ "value": [ { "name": "svc-0001-example", "state": "active", "scope": "starter" } ] }
```

### 오류

| 상태 코드 | 원인 |
|---|---|
| 4xx/5xx | ARM 원본 오류 그대로 전달 |

---

## 2. `PATCH /subscriptions/{id}` — 구독 항목 수정

| 항목 | 내용 |
|---|---|
| Method | `PATCH` |
| Path | `/subscriptions/{id}` |
| `{id}` | 수정할 구독의 이름(고유 식별자) |

### 요청 body — 변경하고 싶은 필드만 포함 (부분 업데이트)

| 필드 | 타입 | 수정 가능 여부 |
|---|---|---|
| `scope` | string | ✅ |
| `displayName` | string | ✅ |
| `state` | string (enum, 아래 표 참고) | ✅ |
| `stateComment` | string | ✅ |
| `allowTracing` | boolean | ✅ |
| `expirationDate` | string (ISO 8601) | ✅ (기록용, 자동 차단 아님) |
| `createdDate`, `startDate`, `endDate`, `notificationDate` | - | ❌ 전송해도 무시됨 |

허용된 필드가 하나도 없으면 ARM에 요청을 보내지 않고 **400**을 즉시 반환합니다.

### `state` 값 상세

| 값 | 의미 | 이 상태에서 구독키로 API 호출 가능? | 비고 |
|---|---|---|---|
| `active` | 정상 활성 | ✅ 가능 | 실제 서비스 중인 정상 상태 |
| `suspended` | 관리자가 일시 차단 | ❌ 불가 | 결제 연체, 이상 트래픽 등으로 즉시 차단할 때 사용. 나중에 `active`로 되돌리면 재사용 가능 |
| `submitted` | 승인 대기 중 | ❌ 불가 | 개발자가 구독을 요청했지만 아직 승인/거절되지 않은 상태 (Product의 "승인 필요" 옵션 사용 시 발생) |
| `rejected` | 승인 거부됨 | ❌ 불가 | 관리자가 `submitted` 요청을 거부한 상태. 이때 `stateComment`에 거부 사유를 남기는 것이 일반적 |
| `cancelled` | 취소됨 | ❌ 불가 | 구독자 또는 관리자가 명시적으로 취소. 되돌리려면 다시 `active`로 변경 필요 |
| `expired` | 만료 처리됨 | ❌ 불가 | `expirationDate`가 지났다고 **자동으로** 이 상태가 되는 것이 아니라, 누군가(관리자 또는 별도 스케줄러)가 **직접 이 값으로 바꿔야** 실제로 반영됨 |

**중요:** `expirationDate`를 설정하는 것과 `state`를 `expired`로 바꾸는 것은 **완전히 별개의 동작**입니다. `expirationDate`는 기록/알림 목적일 뿐이라 그 날짜가 지나도 APIM이 자동으로 키를 막지 않습니다. 실제로 특정 날짜에 접근을 차단하려면, 그 날짜에 맞춰 이 `PATCH` API를 호출해서 `state`를 `suspended` 또는 `expired`로 바꿔주는 **별도의 스케줄러(Azure Function 등)**가 필요합니다.

### 응답

| 상태 코드 | 원인 |
|---|---|
| 200 | 수정 성공, 수정된 구독 정보 반환 (GET과 동일한 형태, `view` 파라미터 지원) |
| 400 | 허용된 필드가 하나도 없음 |
| 4xx/5xx (그 외) | ARM 원본 오류 그대로 전달 (예: 존재하지 않는 `{id}` → 404) |

### 예시

```
PATCH /admin/subscriptions/svc-0001-example
{
  "state": "suspended",
  "stateComment": "결제 연체로 임시 차단"
}
```

---

## 3. `PUT /subscriptions/{id}` — 구독 신규 등록

| 항목 | 내용 |
|---|---|
| Method | `PUT` |
| Path | `/subscriptions/{id}` |
| `{id}` (= `name`) | **네이밍 규칙**: `svc-####-영문명` (숫자 4자리 + 하이픈 + 영문으로 시작하는 이름) — 정규식: `^svc-\d{4}-[a-zA-Z][a-zA-Z0-9-]*$` |

### 요청 body

| 필드 | 필수 여부 | 설명 |
|---|---|---|
| `displayName` | ✅ 필수 | 구독 표시 이름 |
| `expirationDate` | ✅ 필수 | ISO 8601 형식 (예: `2026-12-31T00:00:00Z`). 유효한 날짜 형식인지 검증됨 |
| `scope` | 선택 | 비어있으면 스코프 없는 standalone 구독으로 생성. 값이 있으면 **실제로 존재하는 리소스인지 ARM에 확인 후 생성** (`/products/{id}`, `/apis/{id}` 등). `/apis`(전체)는 항상 존재하는 것으로 간주해 검증을 건너뜀 |
| `state` | 선택 | 기본값 `active`. 지정 시 6개 enum 값 중 하나여야 함 (위 표 참고) |
| `primaryKey` | 선택 | 지정 시 해당 값을 그대로 키로 사용. 미지정 시 ARM이 자동 생성 |
| `secondaryKey` | 선택 | 위와 동일 |

### 검증 순서

1. **`{id}` 네이밍 규칙** 검사 → 실패 시 즉시 400
2. **`displayName` 존재 여부** → 없으면 400
3. **`expirationDate` 존재 및 형식** → 없거나 파싱 불가하면 400
4. **`state` 값 유효성** (지정한 경우) → 6개 enum에 없으면 400
5. 위 1~4가 모두 통과해야 **`scope` 존재 여부 확인** 단계로 진행 (지정된 경우만, ARM에 실제 조회)
6. **중복 체크** — 동일한 `{id}`의 구독이 이미 존재하면 생성하지 않고 **409 Conflict** 반환
7. 모든 검증 통과 시 ARM에 실제 생성 요청

여러 항목이 동시에 잘못된 경우, 1~4번 검증은 **한 번에 모아서** `errors` 배열로 반환합니다.

### 내부 동작: 생성이 2단계로 이루어지는 이유

Azure ARM의 구독 **생성** API(`SubscriptionCreateParameters`)는 `displayName`, `scope`, `state`, `ownerId`, `primaryKey`, `secondaryKey`, `allowTracing`만 받고 **`expirationDate`를 받지 않습니다.** `expirationDate`는 오직 **수정(PATCH)** API에만 있는 속성입니다. 그래서 이 오퍼레이션은 내부적으로:

1. `PUT`으로 구독을 먼저 생성 (`expirationDate` 제외)
2. 생성 성공 직후 `PATCH`로 `expirationDate`만 별도 설정

두 단계로 처리합니다. 클라이언트 입장에서는 한 번의 `PUT` 요청으로 보이고, 응답에도 `expirationDate`가 정상 반영된 최종 상태가 내려갑니다.

- 1단계(`PUT`)가 실패하면 → 구독 자체가 생성되지 않은 것이므로 ARM 오류를 그대로 반환
- 1단계는 성공했는데 2단계(`PATCH`)가 실패하면 → **구독은 이미 생성된 상태**이므로 이를 감추지 않고 **207**과 함께 `warning` 필드로 실패 사유를 알려줍니다 (이 경우 별도로 `PATCH /subscriptions/{id}`를 호출해서 `expirationDate`를 다시 설정해주셔야 합니다)

### 응답

| 상태 코드 | 원인 |
|---|---|
| 200 또는 201 | 생성 성공 (`expirationDate`까지 정상 반영된 최종 상태 반환) |
| 207 | 구독 생성은 성공했지만 `expirationDate` 설정(2단계 PATCH)에 실패 — `warning` 필드 확인 후 별도로 PATCH 재시도 필요 |
| 400 | 입력값 검증 실패 (`errors` 배열) 또는 `scope`가 존재하지 않음 |
| 409 | 동일한 `{id}`의 구독이 이미 존재함 |
| 4xx/5xx (그 외) | ARM 원본 오류 그대로 전달 |

### 예시

**성공 요청:**
```
PUT /admin/subscriptions/svc-0001-example
{
  "displayName": "예시 파트너 구독",
  "scope": "/products/starter",
  "expirationDate": "2026-12-31T00:00:00Z"
}
```

**검증 실패 응답 예시 (400):**
```json
{
  "errors": [
    "name must match pattern svc-####-EnglishName (e.g. svc-0001-example)",
    "expirationDate is required"
  ]
}
```

**존재하지 않는 scope 지정 시 (400):**
```json
{ "error": "scope does not exist: /products/no-such-product" }
```

**이미 존재하는 `{id}`로 재요청 시 (409):**
```json
{ "error": "Subscription already exists: svc-0001-example" }
```
