From d1fb32f89e697abe9c5422b018e154df149f9a08 Mon Sep 17 00:00:00 2001 From: Jamal Soueidan Date: Sat, 9 Dec 2023 21:03:26 +0300 Subject: [PATCH] feat(order): add service to get order by line-item for customer feat(order): add service to list orders for customer on specific year/month chore(order.schema.ts): reformat code for better readability feat(order.schema.ts): add types for OrderFulfillment, OrderRefundLineItem, and OrderRefund --- .../customer/services/order/get.spec.ts | 27 ++ src/functions/customer/services/order/get.ts | 145 ++++-- .../customer/services/order/list.spec.ts | 6 +- src/functions/customer/services/order/list.ts | 75 ++- src/functions/order/order.schema.ts | 429 +++++++++++------- src/functions/order/order.types.ts | 6 + 6 files changed, 451 insertions(+), 237 deletions(-) create mode 100644 src/functions/customer/services/order/get.spec.ts diff --git a/src/functions/customer/services/order/get.spec.ts b/src/functions/customer/services/order/get.spec.ts new file mode 100644 index 00000000..a5a4c246 --- /dev/null +++ b/src/functions/customer/services/order/get.spec.ts @@ -0,0 +1,27 @@ +import { OrderModel } from "~/functions/order/order.models"; +import { Order } from "~/functions/order/order.types"; +import { orderWithfulfillmentAndRefunds } from "~/functions/webhook/data-ordre-with-fullfilment-and-refunds"; +import { CustomerOrderServiceGet } from "./get"; +require("~/library/jest/mongoose/mongodb.jest"); + +describe("CustomerOrderServiceGet", () => { + it("should return order by line-item for customer", async () => { + const dumbData = Order.parse(orderWithfulfillmentAndRefunds); + const response = await OrderModel.create(dumbData); + + const lineItemId = response.line_items[0].id; + + const customerId = response.line_items[0].properties?.find( + (p) => p.name === "_customerId" + )?.value as number | undefined; + + const order = await CustomerOrderServiceGet({ + customerId: customerId || 0, + lineItemId, + }); + + expect(order.line_items.id).toEqual(lineItemId); + expect(order.fulfillments.length).toBe(1); + expect(order.refunds.length).toBe(1); + }); +}); diff --git a/src/functions/customer/services/order/get.ts b/src/functions/customer/services/order/get.ts index e474bc45..b4a8fe65 100644 --- a/src/functions/customer/services/order/get.ts +++ b/src/functions/customer/services/order/get.ts @@ -1,15 +1,17 @@ import { OrderModel } from "~/functions/order/order.models"; +import { NotFoundError } from "~/library/handler"; +import { CustomerOrderServiceListAggregate } from "./list"; export type CustomerOrderServiceGetProps = { customerId: number; - lineItem: number; + lineItemId: number; }; export const CustomerOrderServiceGet = async ({ customerId, - lineItem, + lineItemId, }: CustomerOrderServiceGetProps) => { - return OrderModel.aggregate([ + const orders = await OrderModel.aggregate([ { $match: { $and: [ @@ -19,7 +21,9 @@ export const CustomerOrderServiceGet = async ({ }, }, { - "line_items.id": lineItem, + line_items: { + $elemMatch: { id: lineItemId }, + }, }, ], }, @@ -34,69 +38,108 @@ export const CustomerOrderServiceGet = async ({ }, }, { - "line_items.id": lineItem, + "line_items.id": lineItemId, }, ], }, }, { $addFields: { - "line_items.refunds": { + refunds: { $map: { input: { $filter: { - input: { - $reduce: { - input: "$refunds", - initialValue: [], - in: { - $concatArrays: ["$$value", "$$this.refund_line_items"], + input: "$refunds", + as: "refund", + cond: { + $anyElementTrue: { + $map: { + input: "$$refund.refund_line_items", + as: "refund_line_item", + in: { + $eq: [ + "$$refund_line_item.line_item_id", + "$line_items.id", + ], + }, }, }, }, - as: "refund_line_item", - cond: { - $eq: ["$$refund_line_item.line_item_id", "$line_items._id"], - }, }, }, - as: "refund", + as: "filtered_refund", in: { - _id: "$$refund._id", - line_item_id: "$$refund.line_item_id", - location_id: "$$refund.location_id", - quantity: "$$refund.quantity", - restock_type: "$$refund.restock_type", - subtotal: "$$refund.subtotal", - subtotal_set: "$$refund.subtotal_set", - total_tax: "$$refund.total_tax", - total_tax_set: "$$refund.total_tax_set", + id: "$$filtered_refund.id", + admin_graphql_api_id: "$$filtered_refund.admin_graphql_api_id", + created_at: "$$filtered_refund.created_at", + note: "$$filtered_refund.note", + order_id: "$$filtered_refund.order_id", + processed_at: "$$filtered_refund.processed_at", + restock: "$$filtered_refund.restock", + total_duties_set: "$$filtered_refund.total_duties_set", + user_id: "$$filtered_refund.user_id", + order_adjustments: "$$filtered_refund.order_adjustments", + transactions: "$$filtered_refund.transactions", + duties: "$$filtered_refund.duties", + refund_line_items: { + $map: { + input: "$$filtered_refund.refund_line_items", + as: "refund_line_item", + in: { + id: "$$refund_line_item.id", + line_item_id: "$$refund_line_item.line_item_id", + location_id: "$$refund_line_item.location_id", + quantity: "$$refund_line_item.quantity", + restock_type: "$$refund_line_item.restock_type", + subtotal: "$$refund_line_item.subtotal", + subtotal_set: "$$refund_line_item.subtotal_set", + total_tax: "$$refund_line_item.total_tax", + total_tax_set: "$$refund_line_item.total_tax_set", + // Excluding the 'line_item' field + }, + }, + }, }, }, }, - }, - }, - { - $addFields: { - "line_items.fulfillments": { - $cond: { - if: { $eq: [{ $size: "$line_items.refunds" }, 0] }, - then: { + fulfillments: { + $map: { + input: { $filter: { - input: { - $reduce: { - input: "$fulfillments", - initialValue: [], - in: { $concatArrays: ["$$value", "$$this.line_items"] }, - }, - }, - as: "fulfillment_line_item", + input: "$fulfillments", + as: "fulfillment", cond: { - $eq: ["$$fulfillment_line_item._id", "$line_items._id"], + $anyElementTrue: { + $map: { + input: "$$fulfillment.line_items", + as: "fulfillment_line_item", + in: { + $eq: ["$$fulfillment_line_item.id", "$line_items.id"], + }, + }, + }, }, }, }, - else: [], + as: "fulfillment", + in: { + id: "$$fulfillment.id", + admin_graphql_api_id: "$$fulfillment.admin_graphql_api_id", + created_at: "$$fulfillment.created_at", + location_id: "$$fulfillment.location_id", + name: "$$fulfillment.name", + order_id: "$$fulfillment.order_id", + service: "$$fulfillment.service", + shipment_status: "$$fulfillment.shipment_status", + status: "$$fulfillment.status", + tracking_company: "$$fulfillment.tracking_company", + tracking_number: "$$fulfillment.tracking_number", + tracking_numbers: "$$fulfillment.tracking_numbers", + tracking_url: "$$fulfillment.tracking_url", + tracking_urls: "$$fulfillment.tracking_urls", + updated_at: "$$fulfillment.updated_at", + // Excluding the 'line_items' field + }, }, }, }, @@ -115,7 +158,21 @@ export const CustomerOrderServiceGet = async ({ cancelled_at: 1, note: 1, note_attributes: 1, + fulfillments: 1, + refunds: 1, }, }, ]); + + if (orders.length === 0) { + throw new NotFoundError([ + { + code: "custom", + message: "ORDER_NOT_FOUND", + path: ["lineItemId"], + }, + ]); + } + + return orders[0]; }; diff --git a/src/functions/customer/services/order/list.spec.ts b/src/functions/customer/services/order/list.spec.ts index 631228b4..82cdae9d 100644 --- a/src/functions/customer/services/order/list.spec.ts +++ b/src/functions/customer/services/order/list.spec.ts @@ -4,7 +4,7 @@ import { orderWithfulfillmentAndRefunds } from "~/functions/webhook/data-ordre-w import { CustomerOrderServiceList } from "./list"; require("~/library/jest/mongoose/mongodb.jest"); -describe("CustomerOrderService", () => { +describe("CustomerOrderServiceList", () => { it("should return orders for customer on specific year/month", async () => { const dumbData = Order.parse(orderWithfulfillmentAndRefunds); const response = await OrderModel.create(dumbData); @@ -15,8 +15,8 @@ describe("CustomerOrderService", () => { const orders = await CustomerOrderServiceList({ customerId: customerId || 0, - year: 2024, - month: 1, + year: 2023, + month: 12, }); expect(orders.length).toBe(1); diff --git a/src/functions/customer/services/order/list.ts b/src/functions/customer/services/order/list.ts index 462cdb1c..547fbbb4 100644 --- a/src/functions/customer/services/order/list.ts +++ b/src/functions/customer/services/order/list.ts @@ -1,4 +1,11 @@ import { OrderModel } from "~/functions/order/order.models"; +import { + Order, + OrderFulfillment, + OrderLineItem, + OrderRefund, + OrderRefundLineItem, +} from "~/functions/order/order.types"; export type CustomerOrderServiceListProps = { customerId: number; @@ -6,6 +13,19 @@ export type CustomerOrderServiceListProps = { month: number; }; +export type CustomerOrderServiceListAggregate = Omit< + Order, + "line_items" | "refunds" | "fulfillments" +> & { + line_items: OrderLineItem; + fulfillments: Array>; + refunds: Array< + Omit & { + refund_line_items: Array>; + } + >; +}; + export const CustomerOrderServiceList = async ({ customerId, year, @@ -14,7 +34,7 @@ export const CustomerOrderServiceList = async ({ const firstDayOfMonth = new Date(Date.UTC(year, month - 1, 1)); const lastDayOfMonth = new Date(Date.UTC(year, month, 0)); - return OrderModel.aggregate([ + return OrderModel.aggregate([ { $match: { $and: [ @@ -92,11 +112,11 @@ export const CustomerOrderServiceList = async ({ $anyElementTrue: { $map: { input: "$$refund.refund_line_items", - as: "refund_refund_line_items", + as: "refund_line_item", in: { $eq: [ - "$$refund_refund_line_items.line_item_id", - "$line_items._id", + "$$refund_line_item.line_item_id", + "$line_items.id", ], }, }, @@ -104,17 +124,38 @@ export const CustomerOrderServiceList = async ({ }, }, }, - as: "refund", + as: "filtered_refund", in: { - _id: "$$refund._id", - line_item_id: "$$refund.line_item_id", - location_id: "$$refund.location_id", - quantity: "$$refund.quantity", - restock_type: "$$refund.restock_type", - subtotal: "$$refund.subtotal", - subtotal_set: "$$refund.subtotal_set", - total_tax: "$$refund.total_tax", - total_tax_set: "$$refund.total_tax_set", + id: "$$filtered_refund.id", + admin_graphql_api_id: "$$filtered_refund.admin_graphql_api_id", + created_at: "$$filtered_refund.created_at", + note: "$$filtered_refund.note", + order_id: "$$filtered_refund.order_id", + processed_at: "$$filtered_refund.processed_at", + restock: "$$filtered_refund.restock", + total_duties_set: "$$filtered_refund.total_duties_set", + user_id: "$$filtered_refund.user_id", + order_adjustments: "$$filtered_refund.order_adjustments", + transactions: "$$filtered_refund.transactions", + duties: "$$filtered_refund.duties", + refund_line_items: { + $map: { + input: "$$filtered_refund.refund_line_items", + as: "refund_line_item", + in: { + id: "$$refund_line_item.id", + line_item_id: "$$refund_line_item.line_item_id", + location_id: "$$refund_line_item.location_id", + quantity: "$$refund_line_item.quantity", + restock_type: "$$refund_line_item.restock_type", + subtotal: "$$refund_line_item.subtotal", + subtotal_set: "$$refund_line_item.subtotal_set", + total_tax: "$$refund_line_item.total_tax", + total_tax_set: "$$refund_line_item.total_tax_set", + // Excluding the 'line_item' field + }, + }, + }, }, }, }, @@ -130,7 +171,7 @@ export const CustomerOrderServiceList = async ({ input: "$$fulfillment.line_items", as: "fulfillment_line_item", in: { - $eq: ["$$fulfillment_line_item._id", "$line_items._id"], + $eq: ["$$fulfillment_line_item.id", "$line_items.id"], }, }, }, @@ -139,7 +180,7 @@ export const CustomerOrderServiceList = async ({ }, as: "fulfillment", in: { - _id: "$$fulfillment._id", + id: "$$fulfillment.id", admin_graphql_api_id: "$$fulfillment.admin_graphql_api_id", created_at: "$$fulfillment.created_at", location_id: "$$fulfillment.location_id", @@ -154,12 +195,12 @@ export const CustomerOrderServiceList = async ({ tracking_url: "$$fulfillment.tracking_url", tracking_urls: "$$fulfillment.tracking_urls", updated_at: "$$fulfillment.updated_at", + // Excluding the 'line_items' field }, }, }, }, }, - { $project: { id: 1, diff --git a/src/functions/order/order.schema.ts b/src/functions/order/order.schema.ts index 67ae7ecf..a1bb6a7f 100644 --- a/src/functions/order/order.schema.ts +++ b/src/functions/order/order.schema.ts @@ -7,127 +7,169 @@ export interface IOrderDocument extends IOrder, Omit {} export interface IOrderModel extends Model {} -const PriceSetSchema = new Schema({ - shop_money: { - amount: String, - currency_code: String, - }, - presentment_money: { - amount: String, - currency_code: String, +const PriceSetSchema = new Schema( + { + shop_money: { + amount: String, + currency_code: String, + }, + presentment_money: { + amount: String, + currency_code: String, + }, }, -}); + { + autoIndex: false, + _id: false, + } +); -const TaxLineSchema = new Schema({ - channel_liable: Boolean, - price: String, - price_set: PriceSetSchema, - rate: Number, - title: String, -}); +const TaxLineSchema = new Schema( + { + channel_liable: Boolean, + price: String, + price_set: PriceSetSchema, + rate: Number, + title: String, + }, + { + autoIndex: false, + _id: false, + } +); -const DiscountAllocationSchema = new Schema({ - amount: String, - amount_set: PriceSetSchema, - discount_application_index: Number, -}); +const DiscountAllocationSchema = new Schema( + { + amount: String, + amount_set: PriceSetSchema, + discount_application_index: Number, + }, + { + autoIndex: false, + _id: false, + } +); -const DiscountCodeSchema = new Schema({ - code: String, - amount: String, - type: String, -}); +const DiscountCodeSchema = new Schema( + { + code: String, + amount: String, + type: String, + }, + { + autoIndex: false, + _id: false, + } +); -const AddressSchema = new Schema({ - _id: Number, - first_name: String, - address1: String, - phone: String, - city: String, - zip: String, - province: String, - country: String, - last_name: String, - address2: String, - company: String, - latitude: Number, - longitude: Number, - name: String, - country_code: String, - province_code: String, -}); +const AddressSchema = new Schema( + { + first_name: String, + address1: String, + phone: String, + city: String, + zip: String, + province: String, + country: String, + last_name: String, + address2: String, + company: String, + latitude: Number, + longitude: Number, + name: String, + country_code: String, + province_code: String, + }, + { + _id: false, + } +); -const CustomerSchema = new Schema({ - _id: { - type: Number, - index: true, +const CustomerSchema = new Schema( + { + id: { + type: Number, + required: true, + unique: true, + }, + email: String, + accepts_marketing: Boolean, + created_at: Date, + updated_at: Date, + first_name: String, + last_name: String, + state: String, + note: String, + verified_email: Boolean, + multipass_identifier: String, + tax_exempt: Boolean, + phone: String, + email_marketing_consent: {}, + sms_marketing_consent: {}, + tags: String, + currency: String, + accepts_marketing_updated_at: Date, + marketing_opt_in_level: String, + tax_exemptions: [String], + admin_graphql_api_id: String, + default_address: AddressSchema, }, - email: String, - accepts_marketing: Boolean, - created_at: Date, - updated_at: Date, - first_name: String, - last_name: String, - state: String, - note: String, - verified_email: Boolean, - multipass_identifier: String, - tax_exempt: Boolean, - phone: String, - email_marketing_consent: {}, - sms_marketing_consent: {}, - tags: String, - currency: String, - accepts_marketing_updated_at: Date, - marketing_opt_in_level: String, - tax_exemptions: [String], - admin_graphql_api_id: String, - default_address: AddressSchema, -}); + { + autoIndex: false, + _id: false, + } +); const propertySchema = new Schema( { - name: { type: String, required: true, index: true }, + name: { type: String, required: true, unqiue: true, index: true }, }, { discriminatorKey: "kind", _id: false } ); -const LineItemSchema = new Schema({ - _id: { - type: Number, - index: true, - }, - admin_graphql_api_id: String, - fulfillable_quantity: Number, - fulfillment_service: String, - fulfillment_status: String, - gift_card: Boolean, - grams: Number, - name: String, - price: String, - price_set: PriceSetSchema, - product_exists: Boolean, - product_id: Number, - properties: [propertySchema], - quantity: Number, - requires_shipping: Boolean, - sku: String, - taxable: Boolean, - title: String, - total_discount: String, - total_discount_set: PriceSetSchema, - variant_id: Number, - variant_inventory_management: String, - variant_title: String, - vendor: String, - tax_lines: [TaxLineSchema], - duties: [ - { - /* Schema definition if available */ +const LineItemSchema = new Schema( + { + id: { + type: Number, + required: true, + unique: true, }, - ], - discount_allocations: [DiscountAllocationSchema], -}); + admin_graphql_api_id: String, + fulfillable_quantity: Number, + fulfillment_service: String, + fulfillment_status: String, + gift_card: Boolean, + grams: Number, + name: String, + price: String, + price_set: PriceSetSchema, + product_exists: Boolean, + product_id: Number, + properties: [propertySchema], + quantity: Number, + requires_shipping: Boolean, + sku: String, + taxable: Boolean, + title: String, + total_discount: String, + total_discount_set: PriceSetSchema, + variant_id: Number, + variant_inventory_management: String, + variant_title: String, + vendor: String, + tax_lines: [TaxLineSchema], + duties: [ + { + /* Schema definition if available */ + }, + ], + discount_allocations: [DiscountAllocationSchema], + }, + { + autoIndex: false, + _id: false, + } +); const properties = LineItemSchema.path("properties"); @@ -156,80 +198,121 @@ properties.discriminator( }) ); -const FulfillmentSchema = new Schema({ - _id: Number, - admin_graphql_api_id: String, - created_at: Date, - location_id: Number, - name: String, - order_id: Number, - origin_address: {}, - receipt: {}, - service: String, - shipment_status: String, - status: String, - tracking_company: String, - tracking_number: String, - tracking_numbers: [String], - tracking_url: String, - tracking_urls: [String], - updated_at: Date, - line_items: [LineItemSchema], -}); +const FulfillmentSchema = new Schema( + { + id: { + type: Number, + required: true, + unique: true, + }, + admin_graphql_api_id: String, + created_at: Date, + location_id: Number, + name: String, + order_id: Number, + origin_address: {}, + receipt: {}, + service: String, + shipment_status: String, + status: String, + tracking_company: String, + tracking_number: String, + tracking_numbers: [String], + tracking_url: String, + tracking_urls: [String], + updated_at: Date, + line_items: [LineItemSchema], + }, + { + autoIndex: false, + _id: false, + } +); -const RefundLineItemSchema = new Schema({ - _id: Number, - line_item_id: { - type: Number, - index: true, +const RefundLineItemSchema = new Schema( + { + id: { + type: Number, + required: true, + unique: true, + }, + line_item_id: { + type: Number, + index: true, + }, + location_id: Number, + quantity: Number, + restock_type: String, + subtotal: String, + subtotal_set: PriceSetSchema, + total_tax: String, + total_tax_set: PriceSetSchema, + line_item: LineItemSchema, }, - location_id: Number, - quantity: Number, - restock_type: String, - subtotal: String, - subtotal_set: PriceSetSchema, - total_tax: String, - total_tax_set: PriceSetSchema, - line_item: LineItemSchema, -}); + { + autoIndex: false, + _id: false, + } +); -const RefundSchema = new Schema({ - _id: Number, - admin_graphql_api_id: String, - created_at: Date, - note: String, - order_id: Number, - processed_at: Date, - restock: Boolean, - total_duties_set: PriceSetSchema, - user_id: Number, - order_adjustments: [{}], - transactions: [{}], - refund_line_items: [RefundLineItemSchema], - duties: [{}], -}); +const RefundSchema = new Schema( + { + id: { + type: Number, + required: true, + unique: true, + }, + admin_graphql_api_id: String, + created_at: Date, + note: String, + order_id: Number, + processed_at: Date, + restock: Boolean, + total_duties_set: PriceSetSchema, + user_id: Number, + order_adjustments: [{}], + transactions: [{}], + refund_line_items: [RefundLineItemSchema], + duties: [{}], + }, + { + autoIndex: false, + _id: false, + } +); -const ShippingLineSchema = new Schema({ - _id: { - type: Number, - index: true, +const ShippingLineSchema = new Schema( + { + id: { + type: Number, + required: true, + unique: true, + }, + carrier_identifier: String, + code: String, + discounted_price: String, + discounted_price_set: PriceSetSchema, + phone: String, + price: String, + price_set: PriceSetSchema, + requested_fulfillment_service_id: String, + source: String, + title: String, + tax_lines: [{}], + discount_allocations: [{}], }, - carrier_identifier: String, - code: String, - discounted_price: String, - discounted_price_set: PriceSetSchema, - phone: String, - price: String, - price_set: PriceSetSchema, - requested_fulfillment_service_id: String, - source: String, - title: String, - tax_lines: [{}], - discount_allocations: [{}], -}); + { + autoIndex: false, + _id: false, + } +); export const OrdreMongooseSchema = new Schema({ - _id: Number, + id: { + type: Number, + required: true, + unique: true, + }, admin_graphql_api_id: String, app_id: Number, browser_ip: String, diff --git a/src/functions/order/order.types.ts b/src/functions/order/order.types.ts index 108ae4d0..0974bb8a 100644 --- a/src/functions/order/order.types.ts +++ b/src/functions/order/order.types.ts @@ -195,6 +195,8 @@ const FulfillmentZod = z.object({ ), }); +export type OrderFulfillment = z.infer; + const RefundLineItemZod = z.object({ id: z.number(), line_item_id: z.number(), @@ -208,6 +210,8 @@ const RefundLineItemZod = z.object({ line_item: LineItemZod, }); +export type OrderRefundLineItem = z.infer; + const RefundZod = z.object({ id: z.number(), admin_graphql_api_id: z.string(), @@ -242,6 +246,8 @@ const RefundZod = z.object({ ), // Define further if structure is known }); +export type OrderRefund = z.infer; + const ShippingLineZod = z.object({ id: z.number(), carrier_identifier: z.string().nullable(),