Merge pull request #5 from Car-Part-Planet/CPP-152

Order Confirmation
This commit is contained in:
Teodor Raykov
2024-07-01 23:06:15 +03:00
committed by GitHub
42 changed files with 2429 additions and 406 deletions

View File

@@ -390,6 +390,7 @@ export async function isLoggedIn(request: NextRequest, origin: string) {
}
newHeaders.set('x-shop-customer-token', `${customerTokenValue}`);
console.log('Customer Token', customerTokenValue);
return NextResponse.next({
request: {
// New request headers

View File

@@ -0,0 +1,17 @@
const addressFragment = /* GraphQL */ `
fragment Address on CustomerAddress {
id
address1
address2
firstName
lastName
provinceCode: zoneCode
city
zip
countryCodeV2: territoryCode
company
phone: phoneNumber
}
`;
export default addressFragment;

View File

View File

@@ -0,0 +1,56 @@
const orderMetafieldsFragment = /* GraphQL */ `
fragment OrderMetafields on Order {
warrantyStatus: metafield(namespace: "custom", key: "warranty_status") {
value
id
key
}
warrantyActivationDeadline: metafield(
namespace: "custom"
key: "warranty_activation_deadline"
) {
value
id
key
}
warrantyActivationOdometer: metafield(
namespace: "custom"
key: "warranty_activation_odometer"
) {
value
id
key
}
warrantyActivationInstallation: metafield(
namespace: "custom"
key: "warranty_activation_installation"
) {
value
id
key
}
warrantyActivationSelfInstall: metafield(
namespace: "custom"
key: "warranty_activation_self_install"
) {
value
id
key
}
warrantyActivationVIN: metafield(namespace: "custom", key: "warranty_activation_vin") {
value
id
key
}
warrantyActivationMileage: metafield(namespace: "custom", key: "warranty_activation_mileage") {
value
id
key
}
orderConfirmation: metafield(namespace: "custom", key: "customer_confirmation") {
value
}
}
`;
export default orderMetafieldsFragment;

View File

@@ -0,0 +1,38 @@
const orderTransactionFragment = /* GraphQL */ `
fragment OrderTransaction on OrderTransaction {
id
processedAt
paymentIcon {
id
url
altText
}
paymentDetails {
... on CardPaymentDetails {
last4
cardBrand
}
}
transactionAmount {
presentmentMoney {
...Price
}
}
giftCardDetails {
last4
balance {
...Price
}
}
status
kind
transactionParentId
type
typeDetails {
name
message
}
}
`;
export default orderTransactionFragment;

View File

@@ -1,4 +1,8 @@
import addressFragment from './address';
import lineItemFragment from './line-item';
import orderMetafieldsFragment from './order-metafields';
import orderTrasactionFragment from './order-transaction';
import priceFragment from './price';
const orderCard = /* GraphQL */ `
fragment OrderCard on Order {
@@ -16,69 +20,44 @@ const orderCard = /* GraphQL */ `
}
}
totalPrice {
amount
currencyCode
...Price
}
subtotal {
...Price
}
totalShipping {
...Price
}
totalTax {
...Price
}
shippingLine {
title
originalPrice {
...Price
}
}
lineItems(first: 20) {
nodes {
...LineItem
}
}
shippingAddress {
...Address
}
billingAddress {
...Address
}
transactions {
...OrderTransaction
}
...OrderMetafields
}
${lineItemFragment}
`;
export const orderMetafields = /* GraphQL */ `
fragment OrderMetafield on Order {
id
warrantyStatus: metafield(namespace: "custom", key: "warranty_status") {
value
id
key
}
warrantyActivationDeadline: metafield(
namespace: "custom"
key: "warranty_activation_deadline"
) {
value
id
key
}
warrantyActivationOdometer: metafield(
namespace: "custom"
key: "warranty_activation_odometer"
) {
value
id
key
}
warrantyActivationInstallation: metafield(
namespace: "custom"
key: "warranty_activation_installation"
) {
value
id
key
}
warrantyActivationSelfInstall: metafield(
namespace: "custom"
key: "warranty_activation_self_install"
) {
value
id
key
}
warrantyActivationVIN: metafield(namespace: "custom", key: "warranty_activation_vin") {
value
id
key
}
warrantyActivationMileage: metafield(namespace: "custom", key: "warranty_activation_mileage") {
value
id
key
}
}
${addressFragment}
${priceFragment}
${orderTrasactionFragment}
${orderMetafieldsFragment}
`;
export default orderCard;

View File

@@ -0,0 +1,8 @@
const priceFragment = /* GraphQL */ `
fragment Price on MoneyV2 {
amount
currencyCode
}
`;
export default priceFragment;

View File

@@ -38,8 +38,7 @@ import { getCustomerQuery } from './queries/customer';
import { getMenuQuery } from './queries/menu';
import { getMetaobjectQuery, getMetaobjectsQuery } from './queries/metaobject';
import { getFileQuery, getImageQuery, getMetaobjectsByIdsQuery } from './queries/node';
import { getCustomerOrderQuery, getOrderMetafieldsQuery } from './queries/order';
import { getCustomerOrderMetafieldsQuery, getCustomerOrdersQuery } from './queries/orders';
import { getCustomerOrdersQuery } from './queries/orders';
import { getPageQuery, getPagesQuery } from './queries/page';
import {
getProductQuery,
@@ -64,6 +63,7 @@ import {
Metaobject,
Money,
Order,
OrderConfirmationContent,
Page,
PageInfo,
Product,
@@ -86,10 +86,10 @@ import {
ShopifyImageOperation,
ShopifyMenuOperation,
ShopifyMetaobject,
ShopifyMetaobjectOperation,
ShopifyMetaobjectsOperation,
ShopifyMoneyV2,
ShopifyOrder,
ShopifyOrderMetafield,
ShopifyPage,
ShopifyPageOperation,
ShopifyPagesOperation,
@@ -109,6 +109,7 @@ import {
UploadInput,
WarrantyStatus
} from './types';
import getCustomerOrderQuery from './queries/order';
const domain = process.env.SHOPIFY_STORE_DOMAIN
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, 'https://')
@@ -185,7 +186,7 @@ export async function shopifyFetch<T>({
}
}
async function adminFetch<T>({
async function shopifyAdminFetch<T>({
headers,
query,
variables,
@@ -313,7 +314,7 @@ export async function shopifyCustomerFetch<T>({
}
}
const removeEdgesAndNodes = (array: Connection<any>) => {
const removeEdgesAndNodes = <T = any>(array: Connection<T>) => {
return array.edges.map((edge) => edge?.node);
};
@@ -439,7 +440,7 @@ const reshapeImages = (images: Connection<Image>, productTitle: string) => {
const flattened = removeEdgesAndNodes(images);
return flattened.map((image) => {
const filename = image.url.match(/.*\/(.*)\..*/)[1];
const filename = (image.url.match(/.*\/(.*)\..*/) || [])[1];
return {
...image,
altText: image.altText || `${productTitle} - ${filename}`
@@ -531,8 +532,7 @@ function reshapeOrders(orders: ShopifyOrder[]): any[] | Promise<Order[]> {
}
function reshapeOrder(shopifyOrder: ShopifyOrder): Order {
const reshapeAddress = (address?: ShopifyAddress): Address | undefined => {
if (!address) return undefined;
const reshapeAddress = (address: ShopifyAddress): Address => {
return {
address1: address.address1,
address2: address.address2,
@@ -547,8 +547,7 @@ function reshapeOrder(shopifyOrder: ShopifyOrder): Order {
};
};
const reshapeMoney = (money?: ShopifyMoneyV2): Money | undefined => {
if (!money) return undefined;
const reshapeMoney = (money: ShopifyMoneyV2): Money => {
return {
amount: money.amount || '0.00',
currencyCode: money.currencyCode || 'USD'
@@ -619,23 +618,38 @@ function reshapeOrder(shopifyOrder: ShopifyOrder): Order {
totalShipping: reshapeMoney(shopifyOrder.totalShipping),
totalTax: reshapeMoney(shopifyOrder.totalTax),
totalPrice: reshapeMoney(shopifyOrder.totalPrice),
createdAt: shopifyOrder.createdAt
createdAt: shopifyOrder.createdAt,
shippingMethod: {
name: shopifyOrder.shippingLine?.title,
price: reshapeMoney(shopifyOrder.shippingLine.originalPrice)!
},
warrantyActivationDeadline: shopifyOrder.warrantyActivationDeadline,
warrantyStatus: shopifyOrder.warrantyStatus,
warrantyActivationInstallation: shopifyOrder.warrantyActivationInstallation,
warrantyActivationMileage: shopifyOrder.warrantyActivationMileage,
warrantyActivationOdometer: shopifyOrder.warrantyActivationOdometer,
warrantyActivationSelfInstall: shopifyOrder.warrantyActivationSelfInstall,
warrantyActivationVIN: shopifyOrder.warrantyActivationVIN,
orderConfirmation: shopifyOrder.orderConfirmation
};
if (shopifyOrder.customer) {
order.customer = reshapeCustomer(shopifyOrder.customer);
}
if (shopifyOrder.shippingLine) {
order.shippingMethod = {
name: shopifyOrder.shippingLine?.title,
price: reshapeMoney(shopifyOrder.shippingLine.originalPrice)!
};
}
return order;
}
export function reshapeOrderConfirmationPdf(
metaobject: ShopifyMetaobject
): OrderConfirmationContent {
return {
body: metaobject.fields.find((field) => field.key === 'body')?.value || '',
logo: metaobject.fields.find((field) => field.key === 'logo')?.reference.image!,
color: metaobject.fields.find((field) => field.key === 'color')?.value || '#000000'
};
}
export async function createCart(): Promise<Cart> {
const res = await shopifyFetch<ShopifyCreateCartOperation>({
query: createCartMutation,
@@ -874,6 +888,31 @@ export async function getMetaobjects(type: string) {
return reshapeMetaobjects(removeEdgesAndNodes(res.body.data.metaobjects));
}
export async function getAllMetaobjects(type: string) {
const allMetaobjects: Metaobject[] = [];
let hasNextPage = true;
let after: string | undefined;
while (hasNextPage) {
const res = await shopifyFetch<ShopifyMetaobjectsOperation>({
query: getMetaobjectsQuery,
tags: [TAGS.collections, TAGS.products],
variables: { type, after }
});
const metaobjects = reshapeMetaobjects(removeEdgesAndNodes(res.body.data.metaobjects));
for (const metaobject of metaobjects) {
allMetaobjects.push(metaobject);
}
hasNextPage = res.body.data.metaobjects.pageInfo?.hasNextPage || false;
after = res.body.data.metaobjects.pageInfo?.endCursor;
}
return allMetaobjects;
}
export async function getMetaobjectsByIds(ids: string[]) {
if (!ids.length) return [];
@@ -895,10 +934,7 @@ export async function getMetaobject({
id?: string;
handle?: { handle: string; type: string };
}) {
const res = await shopifyFetch<{
data: { metaobject: ShopifyMetaobject };
variables: { id?: string; handle?: { handle: string; type: string } };
}>({
const res = await shopifyFetch<ShopifyMetaobjectOperation>({
query: getMetaobjectQuery,
variables: { id, handle }
});
@@ -906,6 +942,15 @@ export async function getMetaobject({
return res.body.data.metaobject ? reshapeMetaobjects([res.body.data.metaobject])[0] : null;
}
export async function getOrderConfirmationContent(): Promise<OrderConfirmationContent> {
const res = await shopifyFetch<ShopifyMetaobjectOperation>({
query: getMetaobjectQuery,
variables: { handle: { handle: 'order-confirmation-pdf', type: 'order_confirmation_pdf' } }
});
return reshapeOrderConfirmationPdf(res.body.data.metaobject);
}
export async function getPage(handle: string): Promise<Page> {
const res = await shopifyFetch<ShopifyPageOperation>({
query: getPageQuery,
@@ -1064,7 +1109,7 @@ export const getImage = async (id: string): Promise<Image> => {
};
export const stageUploadFile = async (params: UploadInput) => {
const res = await adminFetch<ShopifyStagedUploadOperation>({
const res = await shopifyAdminFetch<ShopifyStagedUploadOperation>({
query: createStageUploads,
variables: { input: [params] }
});
@@ -1080,7 +1125,7 @@ export const uploadFile = async ({ url, formData }: { url: string; formData: For
};
export const createFile = async (params: FileCreateInput) => {
const res = await adminFetch<ShopifyCreateFileOperation>({
const res = await shopifyAdminFetch<ShopifyCreateFileOperation>({
query: createFileMutation,
variables: { files: [params] }
});
@@ -1103,7 +1148,7 @@ export const updateOrderMetafields = async ({
validMetafields.find(({ key }) => (Array.isArray(field) ? field.includes(key) : key === field))
);
const response = await adminFetch<ShopifyUpdateOrderMetafieldsOperation>({
const response = await shopifyAdminFetch<ShopifyUpdateOrderMetafieldsOperation>({
query: updateOrderMetafieldsMutation,
variables: {
input: {
@@ -1125,59 +1170,6 @@ export const updateOrderMetafields = async ({
return response.body.data.orderUpdate.order.id;
};
export const getOrdersMetafields = async (): Promise<{ [key: string]: ShopifyOrderMetafield }> => {
const customer = await getCustomer();
const res = await adminFetch<{
data: {
customer: {
orders: {
nodes: Array<
{
id: string;
} & ShopifyOrderMetafield
>;
};
};
};
variables: {
id: string;
};
}>({
query: getCustomerOrderMetafieldsQuery,
variables: { id: customer.id },
tags: [TAGS.orderMetafields]
});
return res.body.data.customer.orders.nodes.reduce(
(acc, order) => ({
...acc,
[order.id]: order
}),
{} as { [key: string]: ShopifyOrderMetafield }
);
};
export const getOrderMetafields = async (orderId: string): Promise<ShopifyOrderMetafield> => {
const res = await adminFetch<{
data: {
order: {
id: string;
} & ShopifyOrderMetafield;
};
variables: {
id: string;
};
}>({
query: getOrderMetafieldsQuery,
variables: { id: `gid://shopify/Order/${orderId}` },
tags: [TAGS.orderMetafields]
});
const order = res.body.data.order;
return order;
};
export const getFile = async (id: string) => {
const res = await shopifyFetch<{
data: {

View File

@@ -1,6 +1,6 @@
export const getMetaobjectsQuery = /* GraphQL */ `
query getMetaobjects($type: String!) {
metaobjects(type: $type, first: 200) {
query getMetaobjects($type: String!, $after: String) {
metaobjects(type: $type, first: 200, after: $after) {
edges {
node {
id
@@ -16,6 +16,10 @@ export const getMetaobjectsQuery = /* GraphQL */ `
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
@@ -30,6 +34,14 @@ export const getMetaobjectQuery = /* GraphQL */ `
... on Metaobject {
id
}
... on MediaImage {
image {
url
altText
height
width
}
}
}
key
value

View File

@@ -1,8 +1,11 @@
import addressFragment from '../fragments/address';
import lineItemFragment from '../fragments/line-item';
import { orderMetafields } from '../fragments/order';
import orderMetafieldsFragment from '../fragments/order-metafields';
import orderTrasactionFragment from '../fragments/order-transaction';
import priceFragment from '../fragments/price';
// NOTE: https://shopify.dev/docs/api/customer/latest/queries/customer
export const getCustomerOrderQuery = /* GraphQL */ `
const getCustomerOrderQuery = /* GraphQL */ `
query getCustomerOrderQuery($orderId: ID!) {
customer {
emailAddress {
@@ -95,60 +98,7 @@ export const getCustomerOrderQuery = /* GraphQL */ `
...Price
}
}
}
fragment OrderTransaction on OrderTransaction {
id
processedAt
paymentIcon {
id
url
altText
}
paymentDetails {
... on CardPaymentDetails {
last4
cardBrand
}
}
transactionAmount {
presentmentMoney {
...Price
}
}
giftCardDetails {
last4
balance {
...Price
}
}
status
kind
transactionParentId
type
typeDetails {
name
message
}
}
fragment Price on MoneyV2 {
amount
currencyCode
}
fragment Address on CustomerAddress {
id
address1
address2
firstName
lastName
provinceCode: zoneCode
city
zip
countryCodeV2: territoryCode
company
phone: phoneNumber
...OrderMetafields
}
fragment Fulfillment on Fulfillment {
@@ -220,13 +170,10 @@ export const getCustomerOrderQuery = /* GraphQL */ `
}
}
${lineItemFragment}
${addressFragment}
${priceFragment}
${orderTrasactionFragment}
${orderMetafieldsFragment}
`;
export const getOrderMetafieldsQuery = /* GraphQL */ `
query getOrderMetafields($id: ID!) {
order(id: $id) {
...OrderMetafield
}
}
${orderMetafields}
`;
export default getCustomerOrderQuery;

View File

@@ -1,5 +1,4 @@
import customerDetailsFragment from '../fragments/customer-details';
import { orderMetafields } from '../fragments/order';
const customerFragment = `#graphql
`;
@@ -14,16 +13,3 @@ export const getCustomerOrdersQuery = `#graphql
${customerFragment}
${customerDetailsFragment}
`;
export const getCustomerOrderMetafieldsQuery = /* GraphQL */ `
query getCustomerOrderMetafields($id: ID!) {
customer(id: $id) {
orders(first: 20, sortKey: PROCESSED_AT, reverse: true) {
nodes {
...OrderMetafield
}
}
}
}
${orderMetafields}
`;

View File

@@ -3,6 +3,7 @@ export type Maybe<T> = T | null;
export type Connection<T> = {
edges: Array<Edge<T>>;
pageInfo?: PageInfo;
};
export type Edge<T> = {
@@ -141,18 +142,18 @@ export type Order = {
fulfillments: Fulfillment[];
transactions: Transaction[];
lineItems: LineItem[];
shippingAddress?: Address;
billingAddress?: Address;
shippingAddress: Address;
billingAddress: Address;
/** the price of all line items, excluding taxes and surcharges */
subtotal?: Money;
totalShipping?: Money;
totalTax?: Money;
totalPrice?: Money;
shippingMethod?: {
subtotal: Money;
totalShipping: Money;
totalTax: Money;
totalPrice: Money;
shippingMethod: {
name: string;
price: Money;
};
};
} & ShopifyOrderMetafield;
export type ShopifyOrder = {
id: string;
@@ -181,7 +182,7 @@ export type ShopifyOrder = {
requiresShipping: boolean;
shippingLine: ShopifyShippingLine;
note: string | null;
};
} & ShopifyOrderMetafield;
type ShopifyShippingLine = {
title: string;
@@ -372,16 +373,30 @@ export type ShopifyMetaobject = {
value: string;
reference: {
id: string;
image?: Image;
};
}>;
};
export type ShopifyMetafield = {
id: string;
namespace: string;
key: string;
value: string;
};
export type Metaobject = {
id: string;
type: string;
[key: string]: string;
};
export type OrderConfirmationContent = {
logo: Image;
body: string;
color: string;
};
export type TransmissionType = 'Automatic' | 'Manual';
export type Product = Omit<
@@ -665,7 +680,7 @@ export type ShopifyImageOperation = {
export type ShopifyMetaobjectsOperation = {
data: { metaobjects: Connection<ShopifyMetaobject> };
variables: { type: string };
variables: { type: string; after?: string };
};
export type ShopifyPagesOperation = {
@@ -675,8 +690,8 @@ export type ShopifyPagesOperation = {
};
export type ShopifyMetaobjectOperation = {
data: { nodes: ShopifyMetaobject[] };
variables: { ids: string[] };
data: { metaobject: ShopifyMetaobject };
variables: { id?: string; handle?: { handle: string; type: string } };
};
export type ShopifyProductOperation = {
@@ -858,20 +873,15 @@ export enum WarrantyStatus {
LimitedActivated = 'Limited Activation'
}
export type OrderMetafieldValue<T = string> = {
value: T;
id: string;
key: string;
};
export type ShopifyOrderMetafield = {
warrantyStatus: OrderMetafieldValue | null;
warrantyActivationDeadline: OrderMetafieldValue | null;
warrantyActivationOdometer: OrderMetafieldValue | null;
warrantyActivationInstallation: OrderMetafieldValue | null;
warrantyActivationSelfInstall: OrderMetafieldValue | null;
warrantyActivationVIN: OrderMetafieldValue | null;
warrantyActivationMileage: OrderMetafieldValue | null;
orderConfirmation: ShopifyMetafield | null;
warrantyStatus: ShopifyMetafield | null;
warrantyActivationDeadline: ShopifyMetafield | null;
warrantyActivationOdometer: ShopifyMetafield | null;
warrantyActivationInstallation: ShopifyMetafield | null;
warrantyActivationSelfInstall: ShopifyMetafield | null;
warrantyActivationVIN: ShopifyMetafield | null;
warrantyActivationMileage: ShopifyMetafield | null;
};
export type File = {

View File

@@ -5,7 +5,7 @@ export const carPartPlanetColor = {
muted: '#E6CCB7'
},
content: {
subtle: '#9ca3af', // gray-400
subtle: '#d1d5db', // gray-300
DEFAULT: '#6b7280', // gray-500
emphasis: '#374151', // gray-700
strong: '#111827', // gray-900

View File

@@ -3,6 +3,34 @@ import { ReadonlyURLSearchParams } from 'next/navigation';
import { twMerge } from 'tailwind-merge';
import { Menu } from './shopify/types';
export function cx(...args: ClassValue[]) {
return twMerge(clsx(...args));
}
export const focusInput = [
// base
'focus:ring-2',
// ring color
'focus:ring-blue-200 focus:dark:ring-blue-700/30',
// border color
'focus:border-blue-500 focus:dark:border-blue-700'
];
export const hasErrorInput = [
// base
'ring-2',
// border color
'border-red-500 dark:border-red-700',
// ring color
'ring-red-200 dark:ring-red-700/30'
];
export const focusRing = [
// base
'outline outline-offset-2 outline-0 focus-visible:outline-2',
// outline color
'outline-blue-500 dark:outline-blue-500'
];
export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyURLSearchParams) => {
const paramsString = params.toString();
const queryString = `${paramsString.length ? '?' : ''}${paramsString}`;