logistics-development
Logistics Adapter Development Guide (物流 Adapter 開發指南)
Overview
本指南說明如何基於 @rytass/logistics 基礎套件開發新的物流服務適配器。
Base Package Architecture
@rytass/logistics (Base)
├── LogisticsService<T> # 核心服務介面
├── LogisticsInterface<T> # 配置介面
├── LogisticsBaseStatus # 基礎狀態類型 ('DELIVERED' | 'DELIVERING' | 'SHELVED')
├── LogisticsStatus<T> # 泛型狀態類型
├── LogisticsTraceResponse # 追蹤結果
├── LogisticsStatusHistory # 狀態歷史
├── LogisticsErrorInterface # 錯誤介面
├── LogisticsError # 統一錯誤類別
└── ErrorCode # 錯誤代碼列舉
Core Interfaces
LogisticsService
interface LogisticsService<T extends LogisticsInterface<LogisticsStatus<T>>> {
trace(request: string): Promise<LogisticsTraceResponse<T>[]>;
trace(request: string[]): Promise<LogisticsTraceResponse<T>[]>;
}
LogisticsInterface
interface LogisticsInterface<T = LogisticsBaseStatus> {
reference?: T;
url: string;
}
type LogisticsBaseStatus = 'DELIVERED' | 'DELIVERING' | 'SHELVED';
Response Types
interface LogisticsTraceResponse<K extends LogisticsInterface<LogisticsStatus<K>>> {
logisticsId: string;
statusHistory: LogisticsStatusHistory<K['reference']>[];
}
interface LogisticsStatusHistory<T> {
date: string;
status: T;
}
interface LogisticsErrorInterface {
readonly code: string;
readonly message?: string;
}
Implementing a New Adapter
Step 1: Define Status Types
// my-logistics-adapter/src/typings.ts
export type MyLogisticsStatus =
| 'DELIVERED'
| 'DELIVERING'
| 'SHELVED'
| 'PENDING'
| 'CANCELLED';
export interface MyLogisticsInterface<T> extends LogisticsInterface<T> {
apiKey: string;
apiSecret: string;
ignoreNotFound?: boolean;
}
Step 2: Create Status Map
// my-logistics-adapter/src/constants.ts
export const MyLogisticsStatusMap: Record<string, MyLogisticsStatus> = {
'已送達': 'DELIVERED',
'配送中': 'DELIVERING',
'待取': 'SHELVED',
'待處理': 'PENDING',
'已取消': 'CANCELLED',
};
Step 3: Implement Service Class
// my-logistics-adapter/src/my-logistics.service.ts
import { LogisticsService, LogisticsTraceResponse, LogisticsError, ErrorCode } from '@rytass/logistics';
import axios from 'axios';
export class MyLogisticsService<T extends MyLogisticsInterface<LogisticsStatus<T>>>
implements LogisticsService<T> {
constructor(private readonly configuration: T) {}
async trace(logisticsIds: string | string[]): Promise<LogisticsTraceResponse<T>[]> {
const ids = Array.isArray(logisticsIds) ? logisticsIds : [logisticsIds];
return Promise.all(ids.map(id => this.getLogisticsStatus(id)));
}
private async getLogisticsStatus(trackingId: string): Promise<LogisticsTraceResponse<T>> {
try {
const response = await axios.get(`${this.configuration.url}/track/${trackingId}`, {
headers: {
'X-API-Key': this.configuration.apiKey,
'X-API-Secret': this.configuration.apiSecret,
},
});
if (!response.data.success) {
if (this.configuration.ignoreNotFound) {
return { logisticsId: trackingId, statusHistory: [] };
}
throw new LogisticsError(ErrorCode.NOT_FOUND_ERROR, `Tracking ${trackingId} not found`);
}
return {
logisticsId: trackingId,
statusHistory: this.mapStatusHistory(response.data.history),
};
} catch (error) {
if (error instanceof LogisticsError) throw error;
if (axios.isAxiosError(error)) {
if (error.response?.status === 403) {
throw new LogisticsError(ErrorCode.PERMISSION_DENIED, 'Invalid API credentials');
}
if (error.response?.status === 400) {
throw new LogisticsError(ErrorCode.INVALID_PARAMETER, 'Invalid tracking number');
}
}
throw new LogisticsError(ErrorCode.NOT_IMPLEMENTED, 'Unknown error');
}
}
private mapStatusHistory(history: any[]): LogisticsStatusHistory<T['reference']>[] {
return history.map(item => ({
date: item.timestamp,
status: MyLogisticsStatusMap[item.status] || 'DELIVERING',
}));
}
}
Step 4: Export Default Configuration
// my-logistics-adapter/src/index.ts
export * from './typings';
export * from './constants';
export * from './my-logistics.service';
export const MyLogistics: MyLogisticsInterface<MyLogisticsStatus> = {
url: 'https://api.mylogistics.com/v1',
apiKey: '',
apiSecret: '',
ignoreNotFound: false,
};
Error Handling
ErrorCode Reference
| Code | Constant | Usage |
|---|---|---|
| 999 | NOT_IMPLEMENTED | 未實現的功能 |
| 101 | NOT_FOUND_ERROR | 找不到追蹤號碼 |
| 102 | PERMISSION_DENIED | API 認證失敗 |
| 103 | INVALID_PARAMETER | 無效的參數 |
Best Practices
// 1. 使用統一的 LogisticsError
throw new LogisticsError(ErrorCode.NOT_FOUND_ERROR, 'Custom message');
// 2. 支援 ignoreNotFound 選項
if (this.configuration.ignoreNotFound) {
return { logisticsId: id, statusHistory: [] };
}
// 3. 適當的 HTTP 錯誤映射
if (response.status === 403) {
throw new LogisticsError(ErrorCode.PERMISSION_DENIED);
}
Testing Guidelines
// __tests__/my-logistics.spec.ts
import { MyLogisticsService, MyLogistics } from '../src';
import { LogisticsError, ErrorCode } from '@rytass/logistics';
describe('MyLogisticsService', () => {
const service = new MyLogisticsService({
...MyLogistics,
apiKey: 'test-key',
apiSecret: 'test-secret',
});
it('should trace single package', async () => {
const result = await service.trace('TRACK-001');
expect(result).toHaveLength(1);
expect(result[0].logisticsId).toBe('TRACK-001');
});
it('should trace multiple packages', async () => {
const result = await service.trace(['TRACK-001', 'TRACK-002']);
expect(result).toHaveLength(2);
});
it('should handle not found with ignoreNotFound', async () => {
const serviceWithIgnore = new MyLogisticsService({
...MyLogistics,
ignoreNotFound: true,
});
const result = await serviceWithIgnore.trace('INVALID');
expect(result[0].statusHistory).toHaveLength(0);
});
});
Package Structure
my-logistics-adapter/
├── src/
│ ├── index.ts
│ ├── typings.ts
│ ├── constants.ts
│ └── my-logistics.service.ts
├── __tests__/
│ └── my-logistics.spec.ts
├── package.json
├── tsconfig.build.json
└── README.md
Publishing Checklist
- 實現
LogisticsService介面 - 定義完整的狀態類型
- 實現錯誤處理
- 支援批量追蹤
- 撰寫單元測試
- 更新 README
- 遵循
@rytass/logistics-adapter-*命名規範
More from rytass/utils
payment-adapters
Taiwan payment integration (台灣支付整合). Use when working with ECPay (綠界), NewebPay (藍新), HwaNan Bank (華南銀行), CTBC (中信), iCash Pay, or Happy Card payment services. Covers credit card (信用卡), virtual account (虛擬帳號), ATM, CVS payment (超商代碼/條碼), card binding (卡片綁定), installments (分期付款), recurring payments (訂閱付款), and NestJS integration.
7storage-development
Development guide for @rytass/storages base package (儲存基底套件開發指南). Use when creating new storage adapters (新增儲存 adapter), understanding base interfaces, or extending storage functionality. Covers StorageInterface, Storage class, file converters (檔案轉換器), hash algorithms (雜湊演算法), and implementation patterns.
7wms-react-components
|
7sms-adapters
Taiwan SMS integration (台灣簡訊整合). Use when working with Every8D (每日簡訊 / 互動資通) or other Taiwan SMS service providers. Covers single SMS (單則簡訊), batch messaging (批次發送), multi-target messaging (多目標發送), Taiwan mobile number validation (台灣手機號碼驗證), delivery tracking (發送追蹤), and NestJS integration. Keywords - SMS, 簡訊, Every8D, 每日簡訊, 互動資通, Interactive Communications, mobile number, 手機號碼, batch send, 批次發送, message delivery, 訊息發送
6storage-adapters
File storage adapters (檔案儲存適配器). Use when working with AWS S3, Google Cloud Storage (GCS), Cloudflare R2, Azure Blob, or local file storage (本地儲存). Covers file upload (檔案上傳), download (下載), presigned URLs (簽署 URL), batch operations (批量操作), and NestJS integration.
5cms-react
|
5