JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 3
  • Score
    100M100P100Q57514F
  • License MIT

Rytass Utils Logistics tcat adapter

Package Exports

  • @rytass/logistics-adapter-tcat
  • @rytass/logistics-adapter-tcat/index.cjs.js
  • @rytass/logistics-adapter-tcat/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@rytass/logistics-adapter-tcat) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Rytass Utils - TCAT Logistics Adapter

Comprehensive logistics tracking adapter for TCAT (Taiwan Cat), one of Taiwan's leading courier and logistics companies. Provides real-time package tracking with customizable status mapping and batch processing capabilities.

Features

  • Single package tracking by logistics ID
  • Batch tracking for multiple packages
  • Real-time delivery status updates
  • Customizable status mapping
  • Error handling for not found packages
  • HTML parsing from TCAT website
  • TypeScript type safety
  • Configurable ignore options

Installation

npm install @rytass/logistics-adapter-tcat
# or
yarn add @rytass/logistics-adapter-tcat

Basic Usage

Quick Start with Default Configuration

import { TCatLogisticsService, TCatLogistics } from '@rytass/logistics-adapter-tcat';

// Use default TCAT configuration
const logisticsService = new TCatLogisticsService(TCatLogistics);

// Track single package
const trackingResult = await logisticsService.trace('800978442950');
console.log('Package Status:', trackingResult[0].statusHistory);

// Track multiple packages
const multipleResults = await logisticsService.trace(['800978442950', '903404283301', '123456789012']);

multipleResults.forEach((result, index) => {
  console.log(`Package ${index + 1}:`, result.logisticsId);
  console.log('Current Status:', result.statusHistory[0]?.status);
  console.log('Last Update:', result.statusHistory[0]?.timestamp);
});

Default Status Types

The default configuration provides these standard logistics statuses:

  • PENDING - Package received by TCAT
  • IN_TRANSIT - Package in transit
  • OUT_FOR_DELIVERY - Package out for delivery
  • DELIVERED - Package delivered successfully
  • FAILED - Delivery attempt failed
  • RETURNED - Package returned to sender

Custom Configuration

Basic Custom Configuration

import { TCatLogisticsService, TCatLogisticsInterface } from '@rytass/logistics-adapter-tcat';

// Define custom status types
type CustomStatus = 'RECEIVED' | 'PROCESSING' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED';

const customLogistics: TCatLogisticsInterface<CustomStatus> = {
  ignoreNotFound: false, // Throw error if package not found
  url: 'https://www.t-cat.com.tw/Inquire/TraceDetail.aspx',
  statusMap: (htmlContent: string, logisticsId: string) => {
    // Custom logic to parse HTML and map to your status types
    const statusHistory = parseHTMLContent(htmlContent);

    return statusHistory.map(item => ({
      status: mapToCustomStatus(item.originalStatus) as CustomStatus,
      timestamp: new Date(item.datetime),
      location: item.location,
      description: item.description,
    }));
  },
};

const logisticsService = new TCatLogisticsService(customLogistics);

// Helper functions for custom mapping
function parseHTMLContent(html: string) {
  // Parse TCAT HTML response and extract tracking information
  // Implementation depends on TCAT website structure
  return [];
}

function mapToCustomStatus(originalStatus: string): CustomStatus {
  const statusMapping: Record<string, CustomStatus> = {
    已收件: 'RECEIVED',
    理貨中: 'PROCESSING',
    配送中: 'SHIPPED',
    已送達: 'DELIVERED',
    配送失敗: 'CANCELLED',
  };

  return statusMapping[originalStatus] || 'PROCESSING';
}

Advanced Custom Configuration

import { TCatLogisticsService, TCatLogisticsInterface } from '@rytass/logistics-adapter-tcat';
import * as cheerio from 'cheerio';

type DetailedStatus =
  | 'PICKUP_SCHEDULED'
  | 'PICKUP_COMPLETED'
  | 'SORTING_FACILITY'
  | 'IN_TRANSIT_TO_DESTINATION'
  | 'ARRIVED_AT_DESTINATION'
  | 'OUT_FOR_DELIVERY'
  | 'DELIVERY_ATTEMPTED'
  | 'DELIVERED'
  | 'PICKUP_READY'
  | 'RETURNED_TO_SENDER';

const advancedLogistics: TCatLogisticsInterface<DetailedStatus> = {
  ignoreNotFound: true, // Don't throw errors for not found packages
  url: 'https://www.t-cat.com.tw/Inquire/TraceDetail.aspx',
  statusMap: (htmlContent: string, logisticsId: string) => {
    const $ = cheerio.load(htmlContent);
    const statusHistory: any[] = [];

    // Parse TCAT tracking table
    $('table.trace-table tr').each((index, element) => {
      if (index === 0) return; // Skip header row

      const cells = $(element).find('td');
      if (cells.length >= 4) {
        const datetime = $(cells[0]).text().trim();
        const location = $(cells[1]).text().trim();
        const status = $(cells[2]).text().trim();
        const description = $(cells[3]).text().trim();

        statusHistory.push({
          status: mapDetailedStatus(status),
          timestamp: parseChineseDate(datetime),
          location: location,
          description: description,
          rawStatus: status,
        });
      }
    });

    return statusHistory.reverse(); // Most recent first
  },
};

function mapDetailedStatus(status: string): DetailedStatus {
  const detailedMapping: Record<string, DetailedStatus> = {
    預約取件: 'PICKUP_SCHEDULED',
    已取件: 'PICKUP_COMPLETED',
    理貨中心處理: 'SORTING_FACILITY',
    運輸中: 'IN_TRANSIT_TO_DESTINATION',
    到達營業所: 'ARRIVED_AT_DESTINATION',
    配送中: 'OUT_FOR_DELIVERY',
    配送失敗: 'DELIVERY_ATTEMPTED',
    已送達: 'DELIVERED',
    可自取: 'PICKUP_READY',
    退回寄件人: 'RETURNED_TO_SENDER',
  };

  return detailedMapping[status] || 'IN_TRANSIT_TO_DESTINATION';
}

function parseChineseDate(dateStr: string): Date {
  // Parse Chinese date format: "113/08/14 10:30"
  const parts = dateStr.split(' ');
  const datePart = parts[0].split('/');
  const timePart = parts[1]?.split(':') || ['0', '0'];

  // Convert ROC year to AD year (ROC year + 1911)
  const year = parseInt(datePart[0]) + 1911;
  const month = parseInt(datePart[1]) - 1; // JavaScript months are 0-indexed
  const day = parseInt(datePart[2]);
  const hour = parseInt(timePart[0]);
  const minute = parseInt(timePart[1]);

  return new Date(year, month, day, hour, minute);
}

const logisticsService = new TCatLogisticsService(advancedLogistics);

Configuration Options

TCatLogisticsInterface

Property Type Required Description
ignoreNotFound boolean Yes If true, don't throw errors for packages not found
url string Yes TCAT tracking URL endpoint
statusMap function Yes Function to parse HTML and map to custom status types

statusMap Function Signature

statusMap: (htmlContent: string, logisticsId: string) => StatusHistory[]

Where StatusHistory contains:

  • status: T - Your custom status type
  • timestamp: Date - When the status was recorded
  • location?: string - Location information
  • description?: string - Additional details

Error Handling

import { LogisticsError, ErrorCode } from '@rytass/logistics';

try {
  const result = await logisticsService.trace('INVALID_TRACKING_NUMBER');
} catch (error) {
  if (error instanceof LogisticsError) {
    switch (error.code) {
      case ErrorCode.NOT_FOUND_ERROR:
        console.log('Package not found in TCAT system');
        break;
      case ErrorCode.NETWORK_ERROR:
        console.log('Network connection failed');
        break;
      case ErrorCode.PARSING_ERROR:
        console.log('Failed to parse TCAT response');
        break;
      default:
        console.log('Unknown logistics error:', error.message);
    }
  }
}

Integration Examples

E-commerce Order Tracking

class OrderTrackingService {
  constructor(private logisticsService: TCatLogisticsService<any>) {}

  async updateOrderStatus(orderId: string, trackingNumber: string) {
    try {
      const trackingResult = await this.logisticsService.trace(trackingNumber);

      if (trackingResult.length > 0) {
        const latestStatus = trackingResult[0].statusHistory[0];

        await this.updateOrderInDatabase(orderId, {
          trackingNumber,
          currentStatus: latestStatus.status,
          lastUpdated: latestStatus.timestamp,
          location: latestStatus.location,
          statusHistory: trackingResult[0].statusHistory,
        });

        // Send notification to customer if delivered
        if (latestStatus.status === 'DELIVERED') {
          await this.notifyCustomerDelivery(orderId);
        }
      }
    } catch (error) {
      console.error(`Failed to update tracking for order ${orderId}:`, error);
    }
  }

  async batchUpdateTracking(orders: { orderId: string; trackingNumber: string }[]) {
    const trackingNumbers = orders.map(order => order.trackingNumber);

    try {
      const results = await this.logisticsService.trace(trackingNumbers);

      // Process results and update orders
      for (let i = 0; i < results.length; i++) {
        const order = orders[i];
        const trackingResult = results[i];

        if (trackingResult.statusHistory.length > 0) {
          await this.updateOrderInDatabase(order.orderId, {
            currentStatus: trackingResult.statusHistory[0].status,
            lastUpdated: trackingResult.statusHistory[0].timestamp,
            statusHistory: trackingResult.statusHistory,
          });
        }
      }
    } catch (error) {
      console.error('Batch tracking update failed:', error);
    }
  }

  private async updateOrderInDatabase(orderId: string, trackingData: any) {
    // Implementation depends on your database
  }

  private async notifyCustomerDelivery(orderId: string) {
    // Send email/SMS notification
  }
}

Logistics Dashboard

class LogisticsDashboard {
  constructor(private logisticsService: TCatLogisticsService<any>) {}

  async getTrackingSummary(trackingNumbers: string[]) {
    const results = await this.logisticsService.trace(trackingNumbers);

    const summary = {
      total: results.length,
      byStatus: {} as Record<string, number>,
      recentUpdates: [] as any[],
      issues: [] as any[],
    };

    results.forEach(result => {
      const latestStatus = result.statusHistory[0];

      if (latestStatus) {
        // Count by status
        summary.byStatus[latestStatus.status] = (summary.byStatus[latestStatus.status] || 0) + 1;

        // Collect recent updates (last 24 hours)
        const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
        if (latestStatus.timestamp > oneDayAgo) {
          summary.recentUpdates.push({
            logisticsId: result.logisticsId,
            status: latestStatus.status,
            timestamp: latestStatus.timestamp,
            location: latestStatus.location,
          });
        }

        // Identify potential issues
        if (latestStatus.status === 'DELIVERY_ATTEMPTED' || latestStatus.status === 'RETURNED_TO_SENDER') {
          summary.issues.push({
            logisticsId: result.logisticsId,
            issue: latestStatus.status,
            description: latestStatus.description,
          });
        }
      }
    });

    return summary;
  }

  async getDeliveryReport(trackingNumbers: string[], dateRange: { from: Date; to: Date }) {
    const results = await this.logisticsService.trace(trackingNumbers);

    const report = {
      totalPackages: results.length,
      delivered: 0,
      inTransit: 0,
      issues: 0,
      averageDeliveryTime: 0,
      deliveryTimes: [] as number[],
    };

    results.forEach(result => {
      const statusHistory = result.statusHistory;
      const firstStatus = statusHistory[statusHistory.length - 1]; // Oldest first
      const latestStatus = statusHistory[0]; // Most recent first

      if (latestStatus.status === 'DELIVERED') {
        report.delivered++;

        // Calculate delivery time
        if (firstStatus) {
          const deliveryTime = latestStatus.timestamp.getTime() - firstStatus.timestamp.getTime();
          report.deliveryTimes.push(deliveryTime);
        }
      } else if (latestStatus.status === 'IN_TRANSIT' || latestStatus.status === 'OUT_FOR_DELIVERY') {
        report.inTransit++;
      } else {
        report.issues++;
      }
    });

    // Calculate average delivery time
    if (report.deliveryTimes.length > 0) {
      const totalTime = report.deliveryTimes.reduce((sum, time) => sum + time, 0);
      report.averageDeliveryTime = totalTime / report.deliveryTimes.length;
    }

    return report;
  }
}

Scheduled Tracking Updates

class ScheduledTrackingService {
  constructor(
    private logisticsService: TCatLogisticsService<any>,
    private orderRepository: any,
  ) {}

  async runScheduledUpdate() {
    console.log('Starting scheduled tracking update...');

    try {
      // Get all active shipments
      const activeShipments = await this.orderRepository.getActiveShipments();

      if (activeShipments.length === 0) {
        console.log('No active shipments to track');
        return;
      }

      console.log(`Tracking ${activeShipments.length} shipments...`);

      // Process in batches to avoid overwhelming the service
      const batchSize = 10;
      for (let i = 0; i < activeShipments.length; i += batchSize) {
        const batch = activeShipments.slice(i, i + batchSize);
        await this.processBatch(batch);

        // Add delay between batches
        if (i + batchSize < activeShipments.length) {
          await this.delay(2000); // 2 second delay
        }
      }

      console.log('Scheduled tracking update completed');
    } catch (error) {
      console.error('Scheduled tracking update failed:', error);
    }
  }

  private async processBatch(shipments: any[]) {
    const trackingNumbers = shipments.map(s => s.trackingNumber);

    try {
      const results = await this.logisticsService.trace(trackingNumbers);

      for (let i = 0; i < results.length; i++) {
        const shipment = shipments[i];
        const trackingResult = results[i];

        await this.updateShipmentStatus(shipment, trackingResult);
      }
    } catch (error) {
      console.error('Batch processing failed:', error);
    }
  }

  private async updateShipmentStatus(shipment: any, trackingResult: any) {
    const latestStatus = trackingResult.statusHistory[0];

    if (!latestStatus) return;

    // Check if status changed
    if (shipment.currentStatus !== latestStatus.status) {
      await this.orderRepository.updateShipment(shipment.id, {
        currentStatus: latestStatus.status,
        lastUpdated: latestStatus.timestamp,
        statusHistory: trackingResult.statusHistory,
      });

      // Send notification for important status changes
      if (latestStatus.status === 'DELIVERED' || latestStatus.status === 'DELIVERY_ATTEMPTED') {
        await this.sendStatusNotification(shipment, latestStatus);
      }
    }
  }

  private async sendStatusNotification(shipment: any, status: any) {
    // Send email/SMS notification
  }

  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Usage with cron job
// import * as cron from 'node-cron';
//
// const trackingService = new ScheduledTrackingService(logisticsService, orderRepository);
//
// // Run every hour
// cron.schedule('0 * * * *', () => {
//   trackingService.runScheduledUpdate();
// });

Best Practices

Performance

  • Use batch tracking for multiple packages when possible
  • Implement caching to avoid unnecessary API calls
  • Add delays between requests to respect rate limits
  • Process tracking updates asynchronously

Error Handling

  • Always wrap tracking calls in try-catch blocks
  • Implement retry logic for network failures
  • Log errors for debugging and monitoring
  • Handle "not found" cases gracefully

Data Management

  • Store tracking history for audit trails
  • Update package status regularly but not excessively
  • Clean up old tracking data periodically
  • Index database fields used for tracking queries

User Experience

  • Provide meaningful status messages to customers
  • Send notifications for important status changes
  • Display estimated delivery dates when available
  • Offer manual refresh options for real-time updates

Testing

// Mock TCAT service for testing
const mockTCatLogistics: TCatLogisticsInterface<'TEST_STATUS'> = {
  ignoreNotFound: false,
  url: 'https://test.example.com',
  statusMap: (html: string, id: string) => {
    // Return mock data for testing
    return [
      {
        status: 'TEST_STATUS' as const,
        timestamp: new Date(),
        location: 'Test Location',
        description: 'Test package status',
      },
    ];
  },
};

const testService = new TCatLogisticsService(mockTCatLogistics);

// Test tracking
const testResult = await testService.trace('TEST123456');
console.log('Test result:', testResult);

License

MIT