# 型別

> SDK 的 TypeScript 型別——與 API v1 相同的 JSON 結構，欄位名保持不變。

URL: https://developers.vimine.network/zh-hant/sdk/types/

這些型別由 `@vimine/sdk`（以及 `/server`）匯出，與 OpenAPI 文件一一對應：欄位名就是 JSON 中的欄位名。完整的結構見 [API 參考](https://developers.vimine.network/api/)；這裡列出主要的型別。

## 權限與狀態

```ts
type Scope =
    | 'profile:read' | 'profile:write'
    | 'membership:read' | 'membership:request' | 'statuto:accept' | 'network:manage'
    | 'memberships:read' | 'verify:read'
    | 'identity:signup' | 'enroll:write' | 'checkins:write' | 'webhooks:manage'
    | 'events:read' | 'registrations:read' | 'registrations:write';

type MembershipStatus = 'pending' | 'active' | 'suspended' | 'revoked' | 'rejected';
/** 會員卡此刻的效力。 */
type MembershipVerdict = 'valid' | 'expired' | 'not_yet_valid' | 'statuto_required' | 'inactive';
```

## 會員

```ts
interface Me {
    sub: string;                  // 在您的協會中的識別碼（vms_…）
    email: string;
    email_verified: boolean;
    first_name: string | null;
    last_name: string | null;
    phone: string | null;
    dob: string | null;           // YYYY-MM-DD
    gender: 'M' | 'F' | null;     // 僅用於統計：不與稅號比對
    birthplace: string | null;    // 市鎮（在義大利出生）或城市，自由文字（在國外出生）
    birth_country: string | null; // ISO 3166-1 alpha-2
    codicefiscale: string | null; // 可選：一個人可以沒有稅號
    codicefiscale_check: CodiceFiscaleCheck | null;   // 當事人的身份識別方式；null = 尚未識別
    locale: Locale | null;        // 當事人選擇的語言；null = 從未選擇
    updated_at: string | null;
}

/** Vimine 支援的一種語言。API 在接受語言的地方也接受帶地區的標籤（`en-GB` → `en`，`zh-TW` → `zh-Hant`），並將其歸併。 */
type Locale = 'it' | 'en' | 'zh-Hans' | 'zh-Hant' | 'fr' | 'es' | 'de' | 'pt-BR';
interface LocaleParam { locale?: Locale | string }   // 讀取介面的 ?locale=——參見“語言”指南

/** 一個人如何被識別：透過稅號，或者透過出生日期、出生地和出生國——兩者不能都沒有。 */
type CodiceFiscaleCheck = 'matched' | 'mismatch_confirmed' | 'absent';

interface MeUpdate {              // PATCH /v1/me
    phone?: string | null;
    dob?: string | null;
    gender?: 'M' | 'F' | null;
    birthplace?: string | null;
    birth_country?: string | null;            // ISO 3166-1 alpha-2
    codicefiscale?: string | null;            // 可以之後再補充，或重新設為 null
    codicefiscale_confirmed?: boolean;        // 唯寫：當事人確認一個不一致的稅號（在 `confirmable: true` 之後）
    locale?: Locale | null;                   // null 清除所選語言；Vimine 不支援的語言傳回 422 `unsupported`
}
```

## 會員卡

```ts
interface Membership {
    id: string;
    status: MembershipStatus;
    verdict: MembershipVerdict;
    subdate: string | null;
    issued_at: string | null;
    expires_at: string | null;
    membership_type: { uid: string; cod: string; name: string } | null;
    statuto: {
        current_version: number | null;
        current_sha256: string | null;
        accepted_version_id: string | null;
        accepted_at: string | null;
        pending: boolean;         // 會員接受章程之後，又發布了更新的章程
        overdue: boolean;         // ……而且重新接受的期限已過
    };
    pending_reasons: Array<'approval' | 'payment'>;   // 仍在等待的原因，按順序：先入會審核，再繳納會費
    payment_open: boolean;        // 現在可以繳納會費嗎？（獲准入會後才開放繳費）
    valid_from: string | null;    // 會員卡何時可以使用（協會設定的間隔）
    usable: boolean;              // 會員卡此刻是否可在入場時使用
    quota: { amount: number; currency: string; paid: boolean } | null;
    payment_instructions: string | null;
    expires_in_days: number | null;
    renewal: { window_open: boolean; requested_at: string | null };   // 自助續期：到期前 60 天開放
    network_opt_in: boolean;
    card: { token: string; nbf: number; exp: number } | null;   // QR Code的 vmt_… 權杖及其有效時段（unix 秒）
}

interface Card { token: string; nbf: number; exp: number; status: MembershipStatus }

interface MembershipRequest {     // POST /v1/me/membership
    membership_type?: string;     // uid；協會只提供一種類型時可省略
    accept_statuto: { version: number; sha256?: string };
}
type MembershipRequestResult =
    | { created: true; status: 'pending'; id: string }
    | { already_requested: true; status: MembershipStatus };

interface MembershipEvents {
    items: Array<{ from: MembershipStatus | null; to: MembershipStatus; reason: string | null; changed_at: string; channel: string }>;
    next_cursor: string | null;
}

interface StatutoInfo {
    uid: string;
    version: number;
    approvaldate: string | null;
    sha256: string | null;
    content: string | null;
    document_url: string | null;
    accepted: boolean;
    accepted_at: string | null;
    overdue: boolean;
}

interface Connection {
    client_id: string;
    name: string;
    kind: 'public' | 'native' | 'confidential' | 'first_party';
    scopes: Scope[];
    granted_at: string;
}
```

## 公開

```ts
interface Verification {
    status: 'active' | 'inactive';
    valid_until?: string;
    valid_from?: string;          // 當 reason 為 'not_yet_valid' 時：會員卡真實且有效，從此時起可以使用
    reason?: 'expired' | 'invalid' | 'not_yet_valid';
}

type AdmissionModel = 'board' | 'provisional' | 'automatic';   // 協會如何接納會員

interface AssociationPublicConfig {
    aps: {
        slug: string; name: string;
        picture_url: string | null;
        citta: string | null; provincia: string | null;
        enroll_enabled: boolean;
        accepting_requests: boolean;  // 入會已開放，並且此刻還能再容納一位會員
        invitation_only: boolean;     // 內測：只接納受邀請的人（accepting_requests 為 false）
        // 協會如何接納會員——請事先告知申請人。board：由理事會決定，之後繳納會費；
        // provisional：立即臨時入會，之後追認；automatic：條件一滿足即生效。
        // delay_hours：從獲准入會到會員卡可以使用之間的小時數（0 = 立即）
        admission: { model: AdmissionModel; delay_hours: number };
        approval_required: boolean;   // 已棄用：當 admission.model 為 'board' 時為 true——請讀取 `admission`
        payment_instructions: string | null;   // 如何繳納會費，在協會發布了說明時提供
        member_address: MemberAddressPolicy;   // 協會是否收集會員地址——對所有會員都收集，或對所有會員都不收集
        // 公共利益活動（《第三部門法典》第 5 條）：字母編號、表述、是否為主要活動
        activity_sectors: Array<{ cod: string; description: string; prevalent: boolean }>;
        // 根據官方名單已在 RUNTS 登記的資訊，否則為 null
        registry: {
            section: 'APS' | 'ODV' | 'EF' | 'IS' | 'RA' | 'SOMS' | 'ALTRI' | null;
            number: string | null; registered_on: string | null; checked_on: string | null;
        } | null;
    };
    membership_types: Array<{
        uid: string; cod: string; name: string; description: string | null;
        quota_amount: number | null; currency: string;
        expiry_model: 'solar_year' | 'rolling_12' | 'rolling_24' | 'prorated_solar' | 'custom';
        network_eligible: boolean;
    }>;
    statuto: { uid: string; version: number; approvaldate: string | null; sha256: string | null; content: string | null; document_url: string | null } | null;
}

type MemberAddressPolicy = 'none' | 'optional' | 'required';

interface FiscalCodeCheck {
    code: string; first_name?: string; last_name?: string; gender?: 'M' | 'F'; date?: string; place?: string;
    country?: string;             // 出生國：在國外出生的人，稅號中只包含國家，`place` 不參與比對
}
type FiscalCodeMismatch = 'lastName' | 'firstName' | 'gender' | 'date' | 'place';
type FiscalCodeHint = 'names_swapped';
interface FiscalCodeResult {
    valid: boolean;
    error: 'invalid' | 'mismatch' | null;     // 'invalid'：結構或檢查碼有誤；'mismatch'：與所傳送的資訊不一致
    mismatches: FiscalCodeMismatch[];
    hints: FiscalCodeHint[];
    birth_date: string | null;    // 稅號中包含的出生日期（YYYY-MM-DD）
    birth_country: string | null; // 稅號中包含的出生國（在義大利市鎮出生則為 IT）
}

interface CardKeys { keys: Array<{ kty: string; crv: string; x: string; kid: string; use: string; alg: string }> }
```

## 機密用戶端

```ts
interface MembershipStatusInfo {  // GET /v1/memberships/{id}/status
    id: string;
    status: MembershipStatus;
    verdict: MembershipVerdict;
    valid_until: string | null;
    membership_type: { cod: string } | null;
}

interface SignupInput {           // POST /v1/signup
    email: string;
    first_name: string;
    last_name: string;
    phone?: string | null;
    dob?: string | null;          // YYYY-MM-DD
    gender?: 'M' | 'F' | null;
    birthplace?: string | null;
    birth_country?: string | null;            // ISO 3166-1 alpha-2——對於在國外出生的人：稅號中只包含國家
    codicefiscale?: string | null;
    codicefiscale_confirmed?: boolean;        // 當事人確認一個不一致的稅號（422 `codicefiscale_mismatch` 且 `confirmable: true`）
    accept_terms?: LegalRef;                  // 在您的網站上接受的帳戶使用條款——可選，現行版本
    acknowledge_privacy?: LegalRef;           // 在您的網站上閱讀的平臺隱私權聲明——可選，規則相同
}
interface EnrollInput extends SignupInput {   // POST /v1/enroll
    codicefiscale?: string | null;            // 可選：稅號，或 dob + birthplace + birth_country，兩者不能都沒有
    address?: MemberAddress;                  // 戶籍地或住所地，在 `aps.member_address` 不為 `none` 時提供
    membership_type?: string;     // 只有一種類型時可省略
    accept_statuto: { version: number; sha256?: string };
    privacy_notice?: { version: number; sha256?: string };   // 協會發布了隱私權聲明時必填
}
interface MemberAddress {
    kind?: 'residence' | 'domicile';          // 預設 'residence'
    line: string;                 // 街道和門牌號
    postal_code?: string | null;
    city: string;
    province?: string | null;     // 僅限義大利地址（兩個字母）
    country?: string;             // ISO 3166-1 alpha-2，預設 'IT'
}
interface EnrollIdentification {  // 收到 409 identification_required 之後呼叫 POST /v1/enroll/confirm
    dob?: string; birthplace?: string; birth_country?: string; codicefiscale?: string; codicefiscale_confirmed?: boolean;
}
interface CardCheckinResult {     // 會員卡的 POST /v1/checkins
    kind: 'card';
    verdict: MembershipVerdict;
    status: MembershipStatus;
    membership_id: string;
    valid_until: string | null;
    valid_from: string | null;
    identity_check: IdentityCheck | null;     // 給入口值班人員的提示：不屬於 `verdict`，不限制任何操作
    checked_in_at: string;
}
/** 'pending'：尚需檢視證件；'verified'：已檢視；'flagged'：已檢視，但與資訊不符。 */
type IdentityCheck = 'pending' | 'verified' | 'flagged';
interface FieldError {            // `VimineError.errors` 中的一條記錄
    field?: string; code?: string; message?: string;
    mismatches?: FiscalCodeMismatch[]; hints?: FiscalCodeHint[]; confirmable?: boolean;   // 僅限 `codicefiscale_mismatch`
}
interface LegalRef { version: string; sha256?: string }      // accept_terms / acknowledge_privacy（可選，現行版本）
interface MyAcceptances {         // GET /v1/me/acceptances (legal:read)
    items: AcceptanceRecord[];    // at, action, channel, kind, title, version, sha256, rendered_sha256, locale, association
    pending: { platform: PendingDocument[]; privacy_notice: PendingDocument | null };
}
interface AcceptanceInput { kind: string; version: string | number; sha256?: string }   // POST /v1/me/acceptances (legal:accept)
interface Accepted { status: 'accepted' }     // 回應，始終相同
interface EnrollConfirmResult {  // POST /v1/enroll/confirm
    status: MembershipStatus;
    already: boolean;             // 已經有會員資格或申請
    pending_reasons: Array<'approval' | 'payment'>;   // 按順序：先入會審核，再繳納會費
    payment_open: boolean;
    valid_from: string | null;
    association: { name: string; slug: string };
    event?: EnrolledPlace | null; // 僅當連結來自從活動開啟的入會頁面時提供：名額的結果
}
interface EnrolledPlace {        // 隨入會申請一起申請的名額（“活動與報名”指南 › 尚未入會的人）
    slug: string; title: string; starts_at: string; timezone: string; place: string | null;
    registration_type: { uid: string; name: string; amount: number; currency: string };
    state: 'confirmed' | 'reserved' | 'held' | 'unavailable';   // held：申請本身在等待電子郵件地址確認
    waiting: Array<'membership' | 'contribution'>;               // `reserved` 名額在等待什麼
    reasons: string[];                                           // 為什麼是 `unavailable`（報名資格的原因）
    reserved_until: string | null;
    payment_instructions: string | null;
    already: boolean;                                            // 名額此前已被保留
}

interface SandboxMembers { slug: string; members: Array<{ email: string; name: string; status: MembershipStatus; expires_at: string | null; statuto_accepted: boolean }> }
interface SandboxReset { password: string; members: Array<{ email: string; name: string; status: MembershipStatus }> }
```

## OAuth

```ts
interface TokenResponse {
    access_token: string;
    token_type: 'Bearer';
    expires_in: number;
    refresh_token?: string;
    scope: string;
    sub?: string;                 // 用戶端權杖中沒有此欄位
}
```

## OpenAPI 文件的詞彙

`Spec` 由 `openapi/v1.json` 產生（`npm run spec:types`，在 CI 中檢查）：文件中的每個 schema 都對應一個型別，另有 `Spec.operations`（id → 方法和路徑）。上面的型別仍是文件化的公開介面；`Spec` 供直接串接 API 的開發者使用。

```ts
import type { Spec } from '@vimine/sdk';
let p: Spec.Problem; const [m, path] = Spec.operations.listMemberships;
```
