Skip to content

Commit

Permalink
carrier: Add kr.homepick
Browse files Browse the repository at this point in the history
  • Loading branch information
shlee322 committed Nov 23, 2024
1 parent 52ded3e commit e980b54
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/core/src/carrier-registry/DefaultCarrierRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { GoodsToLuck } from "../carriers/kr.goodstoluck";
import { CainiaoGlobal } from "../carriers/cn.cainiao.global";
import { Pantos } from "../carriers/kr.epantos";
import { LotteGlobal } from "../carriers/kr.lotte.global";
import { Homepick } from "../carriers/kr.homepick";

interface DefaultCarrierRegistryConfig {
carriers: Record<
Expand Down Expand Up @@ -69,7 +70,7 @@ class DefaultCarrierRegistry implements CarrierRegistry {
await this.register(new KoreaPost());
await this.register(new KoreaPostEMS());
await this.register(new GoodsToLuck());
await this.register(new CarrierAlias("kr.homepick", new Hanjin()));
await this.register(new Homepick());
await this.register(new Hanjin());
await this.register(new HonamLogis());
await this.register(new IlyangLogis());
Expand Down
41 changes: 41 additions & 0 deletions packages/core/src/carriers/kr.homepick/HomepickAPISchemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { z } from "zod";

const DeliveryResponseDataDeliveryOrderStatusHistoryListItemSchema = z.object({
statusDateTime: z.string(),
trackingStatus: z.string(),
trackingStatusName: z.string(),
tmsStatusName: z.string().nullable(),
location: z.string().nullable(),
contents: z.string().nullable(),
});

const DeliveryResponseDataDeliverySchema = z.object({
orderStatusHistoryList: z.array(
DeliveryResponseDataDeliveryOrderStatusHistoryListItemSchema
),
});

const DeliveryResponseDataSchema = z.object({
cancelTypeList: z.unknown(),
orderCancelType: z.string(),
delivery: DeliveryResponseDataDeliverySchema,
});

const DeliveryResponseSchema = z.object({
success: z.boolean(),
message: z.string().nullable(),
responseDateTime: z.string(),
data: DeliveryResponseDataSchema,
});

type DeliveryResponseDataDeliveryOrderStatusHistoryListItem = z.infer<
typeof DeliveryResponseDataDeliveryOrderStatusHistoryListItemSchema
>;
type DeliveryResponse = z.infer<typeof DeliveryResponseSchema>;

export {
DeliveryResponseSchema,
type DeliveryResponse,
DeliveryResponseDataDeliveryOrderStatusHistoryListItemSchema,
type DeliveryResponseDataDeliveryOrderStatusHistoryListItem,
};
168 changes: 168 additions & 0 deletions packages/core/src/carriers/kr.homepick/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { type Logger } from "winston";
import {
Carrier,
type CarrierTrackInput,
type TrackInfo,
type TrackEvent,
TrackEventStatusCode,
} from "../../core";
import { rootLogger } from "../../logger";
import { BadRequestError } from "../../core/errors";
import { DateTime } from "luxon";
import { type CarrierUpstreamFetcher } from "../../carrier-upstream-fetcher/CarrierUpstreamFetcher";
import {
type DeliveryResponse,
type DeliveryResponseDataDeliveryOrderStatusHistoryListItem,
DeliveryResponseSchema,
} from "./HomepickAPISchemas";

const carrierLogger = rootLogger.child({
carrierId: "kr.homepick",
});

class Homepick extends Carrier {
readonly carrierId = "kr.homepick";

public async track(input: CarrierTrackInput): Promise<TrackInfo> {
return await new HomepickTrackScraper(
this.upstreamFetcher,
input.trackingNumber
).track();
}
}

class HomepickTrackScraper {
private readonly logger: Logger;

constructor(
readonly upstreamFetcher: CarrierUpstreamFetcher,
readonly trackingNumber: string
) {
this.logger = carrierLogger.child({ trackingNumber });
}

public async track(): Promise<TrackInfo> {
const queryString = new URLSearchParams({
keyword: this.trackingNumber,
}).toString();

const response = await this.upstreamFetcher.fetch(
`https://www.homepick.com/user/api/delivery/universalInquiry?${queryString}`
);

const universalInquiryResponse = await response.json();

if (universalInquiryResponse.success === false) {
throw new BadRequestError(universalInquiryResponse.message);
}

const orderBoxId = universalInquiryResponse.data;
const deliveryResponse = await this.upstreamFetcher.fetch(
`https://www.homepick.com/user/api/delivery/${orderBoxId}`
);

const deliveryResponseBody: DeliveryResponse =
await deliveryResponse.json();
this.logger.debug("deliveryResponseBody", {
deliveryResponseBody,
});

const safeParseResult =
await DeliveryResponseSchema.strict().safeParseAsync(
deliveryResponseBody
);

if (!safeParseResult.success) {
this.logger.warn("deliveryResponseBody parse failed (strict)", {
error: safeParseResult.error,
deliveryResponseBody,
});
}

const events: TrackEvent[] = [];
for (const event of deliveryResponseBody.data.delivery
.orderStatusHistoryList) {
events.unshift(this.parseEvent(event));
}

return {
events,
sender: {
name: null,
location: null,
phoneNumber: null,
carrierSpecificData: new Map(),
},
recipient: {
name: null,
location: null,
phoneNumber: null,
carrierSpecificData: new Map(),
},
carrierSpecificData: new Map(),
};
}

private parseEvent(
event: DeliveryResponseDataDeliveryOrderStatusHistoryListItem
): TrackEvent {
return {
status: {
code: this.parseStatusCode(event.trackingStatus),
name: event.tmsStatusName ?? event.trackingStatusName ?? null,
carrierSpecificData: new Map(),
},
time: this.parseTime(event.statusDateTime),
location: {
name: event.location ?? null,
countryCode: "KR",
postalCode: null,
carrierSpecificData: new Map(),
},
contact: null,
description: event.contents ?? null,
carrierSpecificData: new Map(),
};
}

private parseStatusCode(status: string | null): TrackEventStatusCode {
switch (status) {
case "RECEIVED":
return TrackEventStatusCode.InformationReceived;
case "TERMINAL_IN":
return TrackEventStatusCode.InTransit;
case "MOVING":
return TrackEventStatusCode.InTransit;
case "DLV_START":
return TrackEventStatusCode.OutForDelivery;
case "COMPLETED":
return TrackEventStatusCode.Delivered;
}

this.logger.warn("Unexpected status code", {
status,
});

return TrackEventStatusCode.Unknown;
}

private parseTime(time: string | null): DateTime | null {
if (time === null) {
return null;
}

const result = DateTime.fromISO(time, { setZone: true });

if (!result.isValid) {
this.logger.warn("time parse error", {
inputTime: time,
invalidReason: result.invalidReason,
});
return result;
}

return result;
}
}

export { Homepick };

0 comments on commit e980b54

Please sign in to comment.