logistics-development
Installation
SKILL.md
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-*命名規範
Related skills
More from rytass/utils
wms-module
|
24logistics-adapters
|
12invoice-adapters
Taiwan e-invoice integration (台灣電子發票整合). Use when working with ECPay (綠界), EZPay (藍新), BankPro (金財通), or Amego (光貿) invoice services. Covers issuing invoices (開立發票), voiding (作廢), allowances (折讓), and querying invoice data.
8payment-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.
7member-module
|
7quadrats-module
|
7