import EasyPost from '@easypost/api';
import { createLogger } from '../utils/logger';
import { withRetry } from '../utils/retry';
import { ExternalServiceError } from '../utils/errors';
const logger = createLogger();
export interface EasyPostAddress {
id?: string;
name?: string;
company?: string;
street1: string;
street2?: string;
city: string;
state: string;
zip: string;
country: string;
phone?: string;
email?: string;
}
export interface EasyPostParcel {
length: number; // inches
width: number; // inches
height: number; // inches
weight: number; // pounds
}
export interface EasyPostShipment {
id?: string;
to_address: EasyPostAddress;
from_address: EasyPostAddress;
parcel: EasyPostParcel;
customs_info?: any;
options?: any;
}
export interface EasyPostRate {
id: string;
mode: string;
service: string;
carrier: string;
rate: string;
currency: string;
delivery_days?: number;
delivery_date?: string;
delivery_date_guaranteed?: boolean;
}
export interface EasyPostLabel {
id: string;
object: string;
status: string;
tracking_code: string;
shipment_id: string;
rate: EasyPostRate;
label_url: string;
label_pdf_url?: string;
label_zpl_url?: string;
}
export interface EasyPostTracker {
id: string;
object: string;
mode: string;
tracking_code: string;
status: string;
signed_by?: string;
weight?: number;
est_delivery_date?: string;
shipment_id: string;
carrier: string;
tracking_details: any[];
carrier_detail?: any;
}
export class EasyPostClient {
private client: EasyPost;
private apiKey: string;
constructor() {
this.apiKey = process.env.EASYPOST_API_KEY || '';
if (!this.apiKey) {
throw new Error('EASYPOST_API_KEY environment variable is required');
}
this.client = new EasyPost(this.apiKey);
}
async createShipment(shipment: EasyPostShipment, idempotencyKey?: string): Promise<EasyPostShipment> {
try {
const headers: Record<string, string> = {};
if (idempotencyKey) {
headers['Idempotency-Key'] = idempotencyKey;
}
logger.info('Creating EasyPost shipment', { shipment });
return await withRetry(async () => {
const response = await this.client.Shipment.create(shipment, headers);
logger.info('EasyPost shipment created', { shipmentId: response.id });
return response;
}, { maxRetries: 3 });
} catch (error) {
logger.error('Failed to create EasyPost shipment', { error });
throw new ExternalServiceError('Failed to create shipment in EasyPost', error);
}
}
async getRates(shipmentId: string): Promise<EasyPostRate[]> {
try {
logger.info('Getting rates for shipment', { shipmentId });
return await withRetry(async () => {
const shipment = await this.client.Shipment.retrieve(shipmentId);
const rates = await shipment.getRates();
logger.info('Rates retrieved', { shipmentId, rateCount: rates.length });
return rates;
}, { maxRetries: 3 });
} catch (error) {
logger.error('Failed to get rates from EasyPost', { error, shipmentId });
throw new ExternalServiceError('Failed to get rates from EasyPost', error);
}
}
async buyLabel(shipmentId: string, rateId: string, idempotencyKey?: string): Promise<EasyPostLabel> {
try {
const headers: Record<string, string> = {};
if (idempotencyKey) {
headers['Idempotency-Key'] = idempotencyKey;
}
logger.info('Buying label for shipment', { shipmentId, rateId });
return await withRetry(async () => {
const shipment = await this.client.Shipment.retrieve(shipmentId);
const purchasedShipment = await shipment.buy(rateId, headers);
logger.info('Label purchased', {
shipmentId,
trackingCode: purchasedShipment.tracking_code,
labelUrl: purchasedShipment.postage_label?.label_url
});
return {
id: purchasedShipment.id,
object: purchasedShipment.object,
status: purchasedShipment.status,
tracking_code: purchasedShipment.tracking_code || '',
shipment_id: purchasedShipment.id,
rate: purchasedShipment.selected_rate as EasyPostRate,
label_url: purchasedShipment.postage_label?.label_url || ''
};
}, { maxRetries: 3 });
} catch (error) {
logger.error('Failed to buy label from EasyPost', { error, shipmentId, rateId });
throw new ExternalServiceError('Failed to buy label from EasyPost', error);
}
}
async trackShipment(trackingCode: string): Promise<EasyPostTracker> {
try {
logger.info('Tracking shipment', { trackingCode });
return await withRetry(async () => {
const tracker = await this.client.Tracker.create({ tracking_code: trackingCode });
logger.info('Tracking information retrieved', {
trackingCode,
status: tracker.status,
carrier: tracker.carrier
});
return tracker;
}, { maxRetries: 3 });
} catch (error) {
logger.error('Failed to track shipment in EasyPost', { error, trackingCode });
throw new ExternalServiceError('Failed to track shipment in EasyPost', error);
}
}
async refundLabel(shipmentId: string, idempotencyKey?: string): Promise<boolean> {
try {
const headers: Record<string, string> = {};
if (idempotencyKey) {
headers['Idempotency-Key'] = idempotencyKey;
}
logger.info('Requesting refund for shipment', { shipmentId });
return await withRetry(async () => {
const shipment = await this.client.Shipment.retrieve(shipmentId);
await shipment.refund(headers);
logger.info('Refund requested', { shipmentId });
return true;
}, { maxRetries: 3 });
} catch (error) {
logger.error('Failed to request refund from EasyPost', { error, shipmentId });
throw new ExternalServiceError('Failed to request refund from EasyPost', error);
}
}
}