diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..590b314 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +quote_type = single diff --git a/.env.example b/.env.example index 002a23f..df6b530 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,7 @@ DEBUG_MODE=true PORT=3000 VNPOST_TOKEN=xxx GHTK_TOKEN=xxx +GHN_TOKEN=xxx KIOTVIET_CLIENT_ID=xxx KIOTVIET_SECRET=xxx KIOTVIET_RETAILER=xxx diff --git a/src/commands/cli.ts b/src/commands/cli.ts index ded488a..f46bfce 100644 --- a/src/commands/cli.ts +++ b/src/commands/cli.ts @@ -2,10 +2,11 @@ import 'dotenv/config'; import { Command } from 'commander'; -import { ghtkCommand } from './ghtk.command'; -import { kiotvietCommand } from './kiotviet.command'; -import { vnpostCommand } from './vnpost.command'; +import { ghtkCommand } from './ghtk/ghtk.command'; +import { kiotvietCommand } from './kiotviet/kiotviet.command'; +import { vnpostCommand } from './vnpost/vnpost.command'; import { settingCommand } from './setting.command'; +import { ghnCommand } from './ghn/ghn.command'; if (process.env.NODE_ENV === 'production') console.debug = () => {}; @@ -30,6 +31,7 @@ program program.addCommand(settingCommand()); program.addCommand(ghtkCommand()); program.addCommand(vnpostCommand()); +program.addCommand(ghnCommand()); program.addCommand(kiotvietCommand()); program.showHelpAfterError('(add --help for additional information)'); program.showSuggestionAfterError(); diff --git a/src/commands/ghn/ghn.command.ts b/src/commands/ghn/ghn.command.ts new file mode 100644 index 0000000..74d278d --- /dev/null +++ b/src/commands/ghn/ghn.command.ts @@ -0,0 +1,64 @@ +import { Command } from 'commander'; +import { setEnvValue } from '../../util/env.util'; +import { UTC_TIME_FORMAT } from '../../config/constant'; +import { info, log } from '../../util/console'; +import { getGHNOrder, showOrders } from '../../services/ghn.service'; + +export const ghnCommand = (): Command => { + const ghn = new Command('ghn').description('manage order, get information,...'); + + ghn + .command('token') + .description('Set GHN token') + .option('-s, --set ', 'Save access token into .env') + .action(async (options) => { + try { + if (options.set) { + setEnvValue('GHN_TOKEN', options.set); + } + } catch (error) { + console.error(error.message); + } + }); + + ghn + .command('get') + .description('get order information') + .option('-c, --code ', 'order code') + .option('-d, --date ', 'created date') + .option('-f, --from ', 'from date') + .option('-t, --to ', 'to date') + .action(async (options) => { + if (options.code) { + const order = await getGHNOrder(options.code); + + if (order) { + info(order); + } else { + info(`❌ Can not find order with code: ${options.code}`); + } + } + + if (options.date || options.from || options.to) { + const { date, from, to } = options; + let fromPurchaseDate; + let toPurchaseDate; + if (date) { + fromPurchaseDate = toPurchaseDate = new Date( + `${date}${UTC_TIME_FORMAT}` + ); + } else { + fromPurchaseDate = from + ? new Date(`${from}${UTC_TIME_FORMAT}`) + : new Date(); + toPurchaseDate = to + ? new Date(`${to}${UTC_TIME_FORMAT}`) + : new Date(); + } + log(`From: ${fromPurchaseDate}, to: ${toPurchaseDate}`); + await showOrders(fromPurchaseDate, toPurchaseDate); + } + }); + + return ghn; +}; diff --git a/src/commands/ghtk.command.ts b/src/commands/ghtk/ghtk.command.ts similarity index 83% rename from src/commands/ghtk.command.ts rename to src/commands/ghtk/ghtk.command.ts index 6c4a54c..b9d80e4 100644 --- a/src/commands/ghtk.command.ts +++ b/src/commands/ghtk/ghtk.command.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; -import { setEnvValue } from '../util/env.util'; -import { getGHTKOrder } from '../services/ghtk.service'; -import { info } from '../util/console'; +import { setEnvValue } from '../../util/env.util'; +import { getGHTKOrder } from '../../services/ghtk.service'; +import { info } from '../../util/console'; export const ghtkCommand = (): Command => { const ghtk = new Command('ghtk').description('manage order, get information,...'); @@ -13,7 +13,7 @@ export const ghtkCommand = (): Command => { .action(async (options) => { try { if (options.set) { - setEnvValue('GHTK_TOKEN111', options.set); + setEnvValue('GHTK_TOKEN', options.set); } } catch (error) { console.error(error.message); diff --git a/src/commands/kiotviet.command.ts b/src/commands/kiotviet/kiotviet.command.ts similarity index 81% rename from src/commands/kiotviet.command.ts rename to src/commands/kiotviet/kiotviet.command.ts index 32c5210..afcff2f 100644 --- a/src/commands/kiotviet.command.ts +++ b/src/commands/kiotviet/kiotviet.command.ts @@ -1,8 +1,8 @@ import { Command, Option } from 'commander'; -import { listBranchesCommand } from './kiotviet/branches.command'; -import { createCustomersCommand } from './kiotviet/customers.command'; -import { getInvoiceCommand, syncInvoiceCommand } from './kiotviet/invoices.command'; -import { tokenCommand } from './kiotviet/token.command'; +import { listBranchesCommand } from './branches.command'; +import { createCustomersCommand } from './customers.command'; +import { getInvoiceCommand, syncInvoiceCommand } from './invoices.command'; +import { tokenCommand } from './token.command'; export const kiotvietCommand = (): Command => { const kiotviet = new Command('kiotviet').description('manage, sync invoice, order,...'); @@ -28,7 +28,7 @@ export const kiotvietCommand = (): Command => { customers .command('create') - .description('create kiotviet customers from ghtk, vnpost order') + .description('create kiotviet customers from ghtk, ghn vnpost order') .option('-d, --date ', 'Purchase Date') .addOption(new Option('-f, --from ', 'From Purchase Date').conflicts('date')) .addOption(new Option('-t, --to ', 'To Purchase Date').conflicts('date')) @@ -48,7 +48,7 @@ export const kiotvietCommand = (): Command => { invoices .command('sync') - .description('sync kiotviet invoice with ghtk, vnpost order') + .description('sync kiotviet invoice with ghtk, ghn, vnpost order') .option('-c, --code ', 'Kiotviet invoice code') .addOption(new Option('-d, --date ', 'Purchase Date').conflicts('code')) .addOption(new Option('-f, --from ', 'From Purchase Date').conflicts('code').conflicts('date')) diff --git a/src/commands/vnpost.command.ts b/src/commands/vnpost/vnpost.command.ts similarity index 90% rename from src/commands/vnpost.command.ts rename to src/commands/vnpost/vnpost.command.ts index d5df4d8..f3b9cae 100644 --- a/src/commands/vnpost.command.ts +++ b/src/commands/vnpost/vnpost.command.ts @@ -1,8 +1,8 @@ import { Command } from 'commander'; -import { setEnvValue } from '../util/env.util'; -import { UTC_TIME_FORMAT } from '../config/constant'; -import { getVNPostOrder, getVNPostOrderDetail, showOrders } from '../services/vnpost.service'; -import { info, log } from '../util/console'; +import { setEnvValue } from '../../util/env.util'; +import { UTC_TIME_FORMAT } from '../../config/constant'; +import { getVNPostOrder, getVNPostOrderDetail, showOrders } from '../../services/vnpost.service'; +import { info, log } from '../../util/console'; export const vnpostCommand = (): Command => { const vnpost = new Command('vnpost').description('manage order, get information,...'); diff --git a/src/config/ghn.ts b/src/config/ghn.ts new file mode 100644 index 0000000..5eca299 --- /dev/null +++ b/src/config/ghn.ts @@ -0,0 +1,9 @@ +export const ghn = { + baseShipUrl: 'https://online-gateway.ghn.vn/shiip/public-api', + baseOrderTrackingUrl: + 'https://online-gateway.ghn.vn/order-tracking/public-api', + searchOrder: '/v2/shipping-order/search', + getOrder: '/v2/shipping-order/detail', + trackingLogs: '/client/tracking-logs', + token: process.env.GHN_TOKEN, +}; diff --git a/src/dtos/order.dto.ts b/src/dtos/order.dto.ts index 5e6ebc7..580255a 100644 --- a/src/dtos/order.dto.ts +++ b/src/dtos/order.dto.ts @@ -1,6 +1,6 @@ export class Order { id: string; - statusCode: number; + statusCode?: number; status: string; fullName: string; phone: string; diff --git a/src/services/ghn.service.ts b/src/services/ghn.service.ts new file mode 100644 index 0000000..3a870d7 --- /dev/null +++ b/src/services/ghn.service.ts @@ -0,0 +1,107 @@ +import { info } from '../util/console'; +import { Order } from '../dtos/order.dto'; +import { Order as GHNOrder, getOrder as getGHNOrder, getTrackingLogs, searchOrder } from '../util/ghn.util'; + +const showOrders = async (fromPurchaseDate: Date, toPurchaseDate: Date) => { + try { + const orders: Order[] = await getOrders( + fromPurchaseDate.toISOString(), + toPurchaseDate.toISOString() + ); + info( + `🙌 Find ${ + orders?.length + } GHN orders from ${fromPurchaseDate.toLocaleDateString()} to ${toPurchaseDate.toLocaleDateString()}!` + ); + + orders.forEach(async (order, index) => { + info( + `-------------------- 🔰 GHN Order #${index + 1}: ${ + order.code + } 🔰 --------------------` + ); + showOrder(order); + }); + + info(`(Total: ${orders?.length} orders)`); + } catch (error) { + console.error(error.message); + } +}; + +const showOrder = (order: Order) => { + info('• Order Id: ' + order.id); + info('• Order Code: ' + order.code); + info('• Order delivery status: ' + order.status); + info('• Order products: ' + order.products); + info('• Order delivery date: ' + order.doneAt); +}; + +const getOrders = async ( + fromDate: string, + toDate: string +): Promise => { + return []; + // const data: OrderRequestDto = { + // ChildUserId: '', + // CreateTimeStart: fromDate, + // CreateTimeEnd: toDate, + // KeySearch: '', + // OrderByDescending: true, + // PageIndex: 0, + // PageSize: 1000, + // }; + + // const orders: VNPostListOrder = await searchOrder(data); + + // return orders?.Items?.map((item: VNPostOrder) => { + // const order = { + // id: item.Id, + // statusCode: item.OrderStatusId, + // status: item.OrderStatusName, + // fullName: item.ReceiverFullname, + // phone: item.ReceiverTel, + // codAmount: item.CodAmount, + // feeShip: Number(item.TotalFreightIncludeVat), + // products: item.PackageContent.substring( + // 0, + // item.PackageContent.indexOf('TMĐT') + // ), + // code: item.OrderCode, + // createdAt: new Date(item.CreateTime), + // doneAt: new Date(item.DeliveryTime), + // }; + + // return order; + // }); +}; + +const getOrder = async (orderCode: string): Promise => { + const order: GHNOrder = await getGHNOrder(orderCode); + if (order && !order.returnFee) { + const orderFromTrackingLogs = await getTrackingLogs(orderCode); + + order.statusName = orderFromTrackingLogs?.statusName; + order.returnFee = orderFromTrackingLogs?.returnFee; + order.mainServiceFee = orderFromTrackingLogs?.mainServiceFee; + order.totalFee = orderFromTrackingLogs?.totalFee; + } + + return { + id: order.id, + statusCode: undefined, + status: order.status, + fullName: order.toName, + phone: order.toPhone, + address: order.toAddress, + codAmount: order.codAmount, + feeShip: order.totalFee, + products: order.items.map((p) => p.name).join(','), + code: order.orderCode, + createdAt: order.orderDate, + doneAt: order.finishDate, + returnAt: order.returnTime, + } as Order; +}; + +export { getOrder as getGHNOrder, showOrders }; diff --git a/src/services/kiotviet.service.ts b/src/services/kiotviet.service.ts index 2e55565..072b371 100644 --- a/src/services/kiotviet.service.ts +++ b/src/services/kiotviet.service.ts @@ -5,6 +5,7 @@ import { kiotviet } from '../config/kiotviet'; import { KIOTVIET_DELIVERY_STATUS, KIOTVIET_INVOICE_STATUS } from '../config/constant'; import { getVNPostOrder, getVNPostOrderDetail, getVNPostOrders } from './vnpost.service'; import { Order } from '../dtos/order.dto'; +import { getGHNOrder } from './ghn.service'; const printInvoiceByCode = async (code: string) => { try { @@ -33,66 +34,44 @@ const syncInvoiceByCode = async (code: string) => { const syncInvoice = async (invoice: any, index = 0) => { try { const partnerDelivery = invoice.invoiceDelivery?.partnerDelivery; - const deliveryCode = invoice.invoiceDelivery?.deliveryCode; - if (partnerDelivery?.code !== kiotviet.partnerDelivery.GHTK && partnerDelivery?.code !== kiotviet.partnerDelivery.VNPOST) { - info(`🙃 Skip data sync for invoice ${invoice.code} because delivery partner is not be GHTK, GHN, VNPOST!`); + if ( + partnerDelivery?.code !== kiotviet.partnerDelivery.GHTK && + partnerDelivery?.code !== kiotviet.partnerDelivery.GHN && + partnerDelivery?.code !== kiotviet.partnerDelivery.VNPOST + ) { + info( + `🙃 Skip data sync for invoice ${invoice.code} because delivery partner is not be GHTK, GHN, VNPOST!` + ); return; } let data = {}; if (partnerDelivery?.code === kiotviet.partnerDelivery.GHTK) { - log(`-------------------- [GHTK Order #${index + 1}: ${deliveryCode}] --------------------`); - const ghtkOrder: Order = await getGHTKOrder(deliveryCode); - - if (!ghtkOrder) { - return; - } - - const deliveryDate = ghtkOrder.doneAt ?? undefined; - log('• Invoice status: ' + getInvoiceStatusText(invoice.status)); - log('• Order delivery status: ' + ghtkOrder.status); - log('• Order delivery date: ' + (deliveryDate || 'Time is not recorded')); - log('--'); - - const deliveryStatus = toGHTKDeliveryStatus(ghtkOrder.status); - data = { - ...data, - deliveryDetail: { - status: deliveryStatus, - price: ghtkOrder?.feeShip, - usingPriceCod: invoice.invoiceDelivery.usingPriceCod, - expectedDelivery: deliveryDate, - partnerDelivery: partnerDelivery - } - }; + data = await syncInvoiceByDelivery( + index, + invoice, + getGHTKOrder, + toGHTKDeliveryStatus + ); + } + + if (partnerDelivery?.code === kiotviet.partnerDelivery.GHN) { + data = await syncInvoiceByDelivery( + index, + invoice, + getGHNOrder, + toGHNDeliveryStatus + ); } if (partnerDelivery?.code === kiotviet.partnerDelivery.VNPOST) { - log(`-------------------- [VNPost Order #${index + 1}: ${deliveryCode}] --------------------`); - const vnpostOrder: Order = await getVNPostOrder(deliveryCode); - - if (!vnpostOrder) { - return; - } - - const deliveryDate = vnpostOrder.doneAt ?? undefined; - log('• Invoice status: ' + getInvoiceStatusText(invoice.status)); - log('• Order delivery status: ' + vnpostOrder.status); - log('• Order delivery date: ' + (deliveryDate || 'Time is not recorded')); - log('--'); - - const deliveryStatus = toVNPOSTDeliveryStatus(vnpostOrder.statusCode); - data = { - ...data, - deliveryDetail: { - status: deliveryStatus, - price: vnpostOrder?.feeShip, - usingPriceCod: invoice.invoiceDelivery.usingPriceCod, - expectedDelivery: deliveryDate, - partnerDelivery: partnerDelivery - } - }; + data = await syncInvoiceByDelivery( + index, + invoice, + getVNPostOrder, + toVNPostDeliveryStatus + ); } if (invoice.status === KIOTVIET_INVOICE_STATUS.PROCESSING || invoice.status === KIOTVIET_INVOICE_STATUS.COMPLETE) { @@ -106,6 +85,43 @@ const syncInvoice = async (invoice: any, index = 0) => { } }; +const syncInvoiceByDelivery = async ( + index: number, + invoice: any, + getOrder: (id: string) => Promise, + toDeliveryStatus: (status: any) => number, +) => { + const deliveryCode = invoice.invoiceDelivery?.deliveryCode; + + log( + `-------------------- [Order #${ + index + 1 + }: ${deliveryCode}] --------------------` + ); + const order: Order = await getOrder(deliveryCode); + + if (!order) { + return; + } + + const deliveryDate = order.doneAt ?? undefined; + log('• Invoice status: ' + getInvoiceStatusText(invoice.status)); + log('• Order delivery status: ' + order.status); + log('• Order delivery date: ' + (deliveryDate || 'Time is not recorded')); + log('--'); + + const deliveryStatus = toDeliveryStatus(order.status); + return { + deliveryDetail: { + status: deliveryStatus, + price: order?.feeShip, + usingPriceCod: invoice.invoiceDelivery.usingPriceCod, + expectedDelivery: deliveryDate, + partnerDelivery: invoice.invoiceDelivery?.partnerDelivery, + }, + }; +}; + const syncInvoices = async (status: number, fromPurchaseDate: Date, toPurchaseDate: Date) => { try { const invoices: Invoice[] = await getKVInvoices(status, fromPurchaseDate.toUTCString(), toPurchaseDate.toUTCString()); @@ -233,7 +249,19 @@ const toGHTKDeliveryStatus = (ghtkOrderStatus: string): number => { } }; -const toVNPOSTDeliveryStatus = (vnpostOrderStatus: number): number => { +const toGHNDeliveryStatus = (ghnOrderStatus: string): number => { + switch (ghnOrderStatus) { + case 'delivered': + return KIOTVIET_DELIVERY_STATUS.COMPLETE; + case 'return': + case 'returned': + return KIOTVIET_DELIVERY_STATUS.RETURNNING; + default: + return KIOTVIET_DELIVERY_STATUS.PROCESSING; + } +}; + +const toVNPostDeliveryStatus = (vnpostOrderStatus: number): number => { switch (vnpostOrderStatus) { case 70: // Thu gom thành công return KIOTVIET_DELIVERY_STATUS.TAKEN; diff --git a/src/util/ghn.util.ts b/src/util/ghn.util.ts new file mode 100644 index 0000000..a045c78 --- /dev/null +++ b/src/util/ghn.util.ts @@ -0,0 +1,134 @@ +import { Expose, Type } from 'class-transformer'; +import { ghn } from '../config/ghn'; +import { transformObject } from './transform'; + +export class Order { + @Expose({ name: '_id' }) + id: string; + + @Expose({ name: 'shop_id' }) + shopId: number; + + @Expose({ name: 'client_id' }) + clientId: number; + + @Expose({ name: 'to_name' }) + toName: string; + + @Expose({ name: 'to_phone' }) + toPhone: string; + + @Expose({ name: 'to_address' }) + toAddress: string; + + @Expose({ name: 'to_ward_code' }) + toWardCode: string; + + @Type(() => Item) + items: Item[]; + + @Expose({ name: 'cod_amount' }) + codAmount: number; + + @Expose({ name: 'order_code' }) + orderCode: string; + + @Expose({ name: 'status' }) + status: string; + + @Expose({ name: 'status_name' }) + statusName: string; + + @Expose({ name: 'order_date' }) + orderDate: Date; + + @Expose({ name: 'finish_date' }) + finishDate?: Date; + + @Expose({ name: 'return_time' }) + returnTime?: Date; + + @Expose({ name: 'main_service_fee' }) + mainServiceFee: number; + + @Expose({ name: 'return_fee' }) + returnFee: number; + + @Expose({ name: 'total_fee' }) + totalFee: number; +} + +class Item { + name: string; + quantity: number; +} + +export const searchOrder = async (id: string): Promise => { + const url = `${ghn.baseShipUrl}${ghn.searchOrder}`; + return []; +}; + +export const getOrder = async (orderCode: string): Promise => { + const url = `${ghn.baseShipUrl}${ghn.getOrder}`; + let options; + let response; + let dataResponse; + let result; + + try { + options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'token': ghn.token, + }, + body: JSON.stringify({ + order_code: orderCode + }), + }; + response = await fetch(url, options); + + if (response.status == 200) { + dataResponse = await response.json(); + result = transformObject(Order, dataResponse.data); + } else { + throw Error(`[GHN] ${await response.text()}`); + } + } catch (e) { + throw e; + } + + return result; +}; + +export const getTrackingLogs = async (orderCode: string): Promise => { + const url = `${ghn.baseOrderTrackingUrl}${ghn.trackingLogs}`; + let options; + let response; + let dataResponse; + let result; + + try { + options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + order_code: orderCode, + }), + }; + response = await fetch(url, options); + + if (response.status == 200) { + dataResponse = await response.json(); + result = transformObject(Order, dataResponse?.data?.order_info); + } else { + throw Error(`[GHN] ${await response.text()}`); + } + } catch (e) { + throw e; + } + + return result; +};