diff --git a/commerce/types.ts b/commerce/types.ts index 81967c7be..8d2b2fe9a 100644 --- a/commerce/types.ts +++ b/commerce/types.ts @@ -409,7 +409,29 @@ export interface Person extends Omit { image?: ImageObject[] | null; /** The Tax / Fiscal ID of the organization or person, e.g. the TIN in the US or the CIF/NIF in Spain. */ taxID?: string; + /** The telephone number. */ + telephone?: string; + /** The birth date of the person. */ + birthDate?: string; + /** User's corporate name */ + corporateName?: string; + /** User's corporate document */ + corporateDocument?: string; + /** User's corporate trade name */ + tradeName?: string; + /** User's business phone */ + businessPhone?: string; + /** Whether the user is a corporation or not */ + isCorporate?: boolean; + /** Custom fields */ + customFields?: CustomFields[]; +} + +interface CustomFields { + key: string; + value: string; } + // NON SCHEMA.ORG Compliant. Should be removed ASAP export interface Author extends Omit { "@type": "Author"; diff --git a/vtex/actions/profile/newsletterProfile.ts b/vtex/actions/profile/newsletterProfile.ts new file mode 100644 index 000000000..1f14b6403 --- /dev/null +++ b/vtex/actions/profile/newsletterProfile.ts @@ -0,0 +1,60 @@ +import { AppContext } from "../../mod.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; + +interface NewsletterInput { + email: string; + isNewsletterOptIn: boolean; +} + +const newsletterProfile = async ( + props: NewsletterInput, + req: Request, + ctx: AppContext, +): Promise => { + const { io } = ctx; + const { cookie } = parseCookie(req.headers, ctx.account); + + if (!props?.email) { + console.error("User profile not found or email is missing:", props.email); + return null; + } + + const mutation = ` + mutation SubscribeNewsletter($email: String!, $isNewsletterOptIn: Boolean!) { + subscribeNewsletter(email: $email, isNewsletterOptIn: $isNewsletterOptIn) + @context(provider: "vtex.store-graphql@2.x") + } + `; + + const variables = { + email: props.email, + isNewsletterOptIn: props.isNewsletterOptIn, + }; + + try { + await io.query<{ subscribeNewsletter: boolean }, unknown>( + { + query: mutation, + operationName: "SubscribeNewsletter", + variables, + }, + { + headers: { + cookie, + }, + }, + ); + + const result = await ctx.invoke("vtex/loaders/user.ts"); + const newsletterField = result?.customFields?.find((field) => + field.key === "isNewsletterOptIn" + ); + + return newsletterField?.value === "true"; + } catch (error) { + console.error("Error subscribing to newsletter:", error); + return null; + } +}; + +export default newsletterProfile; diff --git a/vtex/actions/profile/updateProfile.ts b/vtex/actions/profile/updateProfile.ts new file mode 100644 index 000000000..17c4f724b --- /dev/null +++ b/vtex/actions/profile/updateProfile.ts @@ -0,0 +1,93 @@ +import { AppContext } from "../../mod.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; +import { Person } from "../../../commerce/types.ts"; +import type { User } from "../../loaders/user.ts"; + +export interface UserMutation { + firstName?: string; + lastName?: string; + email?: string; + homePhone?: string | null; + gender?: string | null; + birthDate?: string | null; + corporateName?: string | null; + tradeName?: string | null; + businessPhone?: string | null; + isCorporate?: boolean; +} + +const updateProfile = async ( + props: UserMutation, + req: Request, + ctx: AppContext, +): Promise => { + const { io } = ctx; + const { cookie } = parseCookie(req.headers, ctx.account); + + if (!props?.email) { + console.error("User profile not found or email is missing:", props.email); + return null; + } + const mutation = ` + mutation UpdateProfile($input: ProfileInput!) { + updateProfile(fields: $input) @context(provider: "vtex.store-graphql") { + cacheId + firstName + lastName + birthDate + gender + homePhone + businessPhone + document + email + tradeName + corporateName + corporateDocument + stateRegistration + isCorporate + } + } + `; + + try { + const { updateProfile: updatedUser } = await io.query< + { updateProfile: User }, + { input: UserMutation } + >( + { + query: mutation, + operationName: "UpdateProfile", + variables: { + input: { + ...props, + email: props.email, + }, + }, + }, + { headers: { cookie } }, + ); + + return { + "@id": updatedUser?.userId ?? updatedUser.id, + email: updatedUser.email, + givenName: updatedUser?.firstName, + familyName: updatedUser?.lastName, + taxID: updatedUser?.document?.replace(/[^\d]/g, ""), + gender: updatedUser?.gender === "female" + ? "https://schema.org/Female" + : "https://schema.org/Male", + telephone: updatedUser?.homePhone, + birthDate: updatedUser?.birthDate, + corporateName: updatedUser?.tradeName, + corporateDocument: updatedUser?.corporateDocument, + businessPhone: updatedUser?.businessPhone, + isCorporate: updatedUser?.isCorporate, + customFields: updatedUser?.customFields, + }; + } catch (error) { + console.error("Error updating user profile:", error); + return null; + } +}; + +export default updateProfile; diff --git a/vtex/loaders/orders/order.ts b/vtex/loaders/orders/order.ts new file mode 100644 index 000000000..9489b04f4 --- /dev/null +++ b/vtex/loaders/orders/order.ts @@ -0,0 +1,35 @@ +import { RequestURLParam } from "../../../website/functions/requestToParam.ts"; +import { AppContext } from "../../mod.ts"; +import { Order } from "../../utils/types.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; + +export interface Props { + slug: RequestURLParam; +} + +export default async function loader( + props: Props, + req: Request, + ctx: AppContext, +): Promise { + const { vcsDeprecated } = ctx; + const { cookie } = parseCookie(req.headers, ctx.account); + + const { slug } = props; + + const response = await vcsDeprecated["GET /api/oms/user/orders/:orderId"]( + { orderId: slug }, + { + headers: { + cookie, + }, + }, + ); + + if (response.ok) { + const order = await response.json(); + return order; + } + + return null; +} diff --git a/vtex/loaders/user.ts b/vtex/loaders/user.ts index 174398e71..07e710b32 100644 --- a/vtex/loaders/user.ts +++ b/vtex/loaders/user.ts @@ -2,7 +2,7 @@ import { Person } from "../../commerce/types.ts"; import { AppContext } from "../mod.ts"; import { parseCookie } from "../utils/vtexId.ts"; -interface User { +export interface User { id: string; userId: string; email: string; @@ -11,6 +11,13 @@ interface User { profilePicture?: string; gender?: string; document?: string; + homePhone?: string; + birthDate?: string; + corporateDocument?: string; + tradeName?: string; + businessPhone?: string; + isCorporate?: boolean; + customFields?: { key: string; value: string }[]; } async function loader( @@ -26,7 +33,7 @@ async function loader( } const query = - "query getUserProfile { profile { id userId email firstName lastName profilePicture gender document }}"; + `query getUserProfile { profile(customFields: "isNewsletterOptIn") { id userId email firstName lastName profilePicture gender document homePhone birthDate corporateDocument tradeName businessPhone isCorporate customFields { key value } }}`; try { const { profile: user } = await io.query<{ profile: User }, null>( @@ -35,14 +42,21 @@ async function loader( ); return { - "@id": user.userId ?? user.id, + "@id": user?.userId ?? user.id, email: user.email, - givenName: user.firstName, - familyName: user.lastName, + givenName: user?.firstName, + familyName: user?.lastName, taxID: user?.document?.replace(/[^\d]/g, ""), - gender: user.gender === "f" + gender: user?.gender === "female" ? "https://schema.org/Female" : "https://schema.org/Male", + telephone: user?.homePhone, + birthDate: user?.birthDate, + corporateName: user?.tradeName, + corporateDocument: user?.corporateDocument, + businessPhone: user?.businessPhone, + isCorporate: user?.isCorporate, + customFields: user?.customFields, }; } catch (_) { return null; diff --git a/vtex/manifest.gen.ts b/vtex/manifest.gen.ts index d50f9fdc5..b4a5d4077 100644 --- a/vtex/manifest.gen.ts +++ b/vtex/manifest.gen.ts @@ -23,11 +23,13 @@ import * as $$$$$$$$$17 from "./actions/masterdata/createDocument.ts"; import * as $$$$$$$$$18 from "./actions/newsletter/subscribe.ts"; import * as $$$$$$$$$19 from "./actions/notifyme.ts"; import * as $$$$$$$$$20 from "./actions/payments/delete.ts"; -import * as $$$$$$$$$21 from "./actions/review/submit.ts"; -import * as $$$$$$$$$22 from "./actions/sessions/delete.ts"; -import * as $$$$$$$$$23 from "./actions/trigger.ts"; -import * as $$$$$$$$$24 from "./actions/wishlist/addItem.ts"; -import * as $$$$$$$$$25 from "./actions/wishlist/removeItem.ts"; +import * as $$$$$$$$$21 from "./actions/profile/newsletterProfile.ts"; +import * as $$$$$$$$$22 from "./actions/profile/updateProfile.ts"; +import * as $$$$$$$$$23 from "./actions/review/submit.ts"; +import * as $$$$$$$$$24 from "./actions/sessions/delete.ts"; +import * as $$$$$$$$$25 from "./actions/trigger.ts"; +import * as $$$$$$$$$26 from "./actions/wishlist/addItem.ts"; +import * as $$$$$$$$$27 from "./actions/wishlist/removeItem.ts"; import * as $$$$0 from "./handlers/sitemap.ts"; import * as $$$0 from "./loaders/cart.ts"; import * as $$$1 from "./loaders/categories/tree.ts"; @@ -52,22 +54,23 @@ import * as $$$19 from "./loaders/masterdata/searchDocuments.ts"; import * as $$$20 from "./loaders/navbar.ts"; import * as $$$21 from "./loaders/options/productIdByTerm.ts"; import * as $$$22 from "./loaders/orders/list.ts"; -import * as $$$23 from "./loaders/paths/PDPDefaultPath.ts"; -import * as $$$24 from "./loaders/paths/PLPDefaultPath.ts"; -import * as $$$25 from "./loaders/payments/info.ts"; -import * as $$$26 from "./loaders/payments/userPayments.ts"; -import * as $$$27 from "./loaders/product/extend.ts"; -import * as $$$28 from "./loaders/product/extensions/detailsPage.ts"; -import * as $$$29 from "./loaders/product/extensions/list.ts"; -import * as $$$30 from "./loaders/product/extensions/listingPage.ts"; -import * as $$$31 from "./loaders/product/extensions/suggestions.ts"; -import * as $$$32 from "./loaders/product/wishlist.ts"; -import * as $$$33 from "./loaders/proxy.ts"; -import * as $$$34 from "./loaders/sessions/info.ts"; -import * as $$$35 from "./loaders/user.ts"; -import * as $$$36 from "./loaders/wishlist.ts"; -import * as $$$37 from "./loaders/workflow/product.ts"; -import * as $$$38 from "./loaders/workflow/products.ts"; +import * as $$$23 from "./loaders/orders/order.ts"; +import * as $$$24 from "./loaders/paths/PDPDefaultPath.ts"; +import * as $$$25 from "./loaders/paths/PLPDefaultPath.ts"; +import * as $$$26 from "./loaders/payments/info.ts"; +import * as $$$27 from "./loaders/payments/userPayments.ts"; +import * as $$$28 from "./loaders/product/extend.ts"; +import * as $$$29 from "./loaders/product/extensions/detailsPage.ts"; +import * as $$$30 from "./loaders/product/extensions/list.ts"; +import * as $$$31 from "./loaders/product/extensions/listingPage.ts"; +import * as $$$32 from "./loaders/product/extensions/suggestions.ts"; +import * as $$$33 from "./loaders/product/wishlist.ts"; +import * as $$$34 from "./loaders/proxy.ts"; +import * as $$$35 from "./loaders/sessions/info.ts"; +import * as $$$36 from "./loaders/user.ts"; +import * as $$$37 from "./loaders/wishlist.ts"; +import * as $$$38 from "./loaders/workflow/product.ts"; +import * as $$$39 from "./loaders/workflow/products.ts"; import * as $$$$$$0 from "./sections/Analytics/Vtex.tsx"; import * as $$$$$$$$$$0 from "./workflows/events.ts"; import * as $$$$$$$$$$1 from "./workflows/product/index.ts"; @@ -97,22 +100,23 @@ const manifest = { "vtex/loaders/navbar.ts": $$$20, "vtex/loaders/options/productIdByTerm.ts": $$$21, "vtex/loaders/orders/list.ts": $$$22, - "vtex/loaders/paths/PDPDefaultPath.ts": $$$23, - "vtex/loaders/paths/PLPDefaultPath.ts": $$$24, - "vtex/loaders/payments/info.ts": $$$25, - "vtex/loaders/payments/userPayments.ts": $$$26, - "vtex/loaders/product/extend.ts": $$$27, - "vtex/loaders/product/extensions/detailsPage.ts": $$$28, - "vtex/loaders/product/extensions/list.ts": $$$29, - "vtex/loaders/product/extensions/listingPage.ts": $$$30, - "vtex/loaders/product/extensions/suggestions.ts": $$$31, - "vtex/loaders/product/wishlist.ts": $$$32, - "vtex/loaders/proxy.ts": $$$33, - "vtex/loaders/sessions/info.ts": $$$34, - "vtex/loaders/user.ts": $$$35, - "vtex/loaders/wishlist.ts": $$$36, - "vtex/loaders/workflow/product.ts": $$$37, - "vtex/loaders/workflow/products.ts": $$$38, + "vtex/loaders/orders/order.ts": $$$23, + "vtex/loaders/paths/PDPDefaultPath.ts": $$$24, + "vtex/loaders/paths/PLPDefaultPath.ts": $$$25, + "vtex/loaders/payments/info.ts": $$$26, + "vtex/loaders/payments/userPayments.ts": $$$27, + "vtex/loaders/product/extend.ts": $$$28, + "vtex/loaders/product/extensions/detailsPage.ts": $$$29, + "vtex/loaders/product/extensions/list.ts": $$$30, + "vtex/loaders/product/extensions/listingPage.ts": $$$31, + "vtex/loaders/product/extensions/suggestions.ts": $$$32, + "vtex/loaders/product/wishlist.ts": $$$33, + "vtex/loaders/proxy.ts": $$$34, + "vtex/loaders/sessions/info.ts": $$$35, + "vtex/loaders/user.ts": $$$36, + "vtex/loaders/wishlist.ts": $$$37, + "vtex/loaders/workflow/product.ts": $$$38, + "vtex/loaders/workflow/products.ts": $$$39, }, "handlers": { "vtex/handlers/sitemap.ts": $$$$0, @@ -142,11 +146,13 @@ const manifest = { "vtex/actions/newsletter/subscribe.ts": $$$$$$$$$18, "vtex/actions/notifyme.ts": $$$$$$$$$19, "vtex/actions/payments/delete.ts": $$$$$$$$$20, - "vtex/actions/review/submit.ts": $$$$$$$$$21, - "vtex/actions/sessions/delete.ts": $$$$$$$$$22, - "vtex/actions/trigger.ts": $$$$$$$$$23, - "vtex/actions/wishlist/addItem.ts": $$$$$$$$$24, - "vtex/actions/wishlist/removeItem.ts": $$$$$$$$$25, + "vtex/actions/profile/newsletterProfile.ts": $$$$$$$$$21, + "vtex/actions/profile/updateProfile.ts": $$$$$$$$$22, + "vtex/actions/review/submit.ts": $$$$$$$$$23, + "vtex/actions/sessions/delete.ts": $$$$$$$$$24, + "vtex/actions/trigger.ts": $$$$$$$$$25, + "vtex/actions/wishlist/addItem.ts": $$$$$$$$$26, + "vtex/actions/wishlist/removeItem.ts": $$$$$$$$$27, }, "workflows": { "vtex/workflows/events.ts": $$$$$$$$$$0, diff --git a/vtex/utils/client.ts b/vtex/utils/client.ts index 46be6e682..c252ce705 100644 --- a/vtex/utils/client.ts +++ b/vtex/utils/client.ts @@ -8,6 +8,7 @@ import { LegacyFacets, LegacyProduct, LegacySort, + Order, OrderForm, PageType, PortalSuggestion, @@ -247,6 +248,9 @@ export interface VTEXCommerceStable { "GET /api/oms/user/orders": { response: Userorderslist; }; + "GET /api/oms/user/orders/:orderId": { + response: Order; + }; } export interface SP { diff --git a/vtex/utils/types.ts b/vtex/utils/types.ts index 0fedd1893..c662603a4 100644 --- a/vtex/utils/types.ts +++ b/vtex/utils/types.ts @@ -62,11 +62,12 @@ export interface ClientProfileData { export interface ClientPreferencesData { locale: string; - optinNewsLetter: null; + optinNewsLetter: boolean; } export interface ItemMetadata { - items: ItemMetadataItem[]; + items?: ItemMetadataItem[]; + Items?: ItemMetadataItemUppercase[]; } export interface ItemMetadataItem { @@ -82,6 +83,19 @@ export interface ItemMetadataItem { assemblyOptions: AssemblyOption[]; } +export interface ItemMetadataItemUppercase { + Id: string; + Seller: string; + Name: string; + SkuName: string; + ProductId: string; + RefId: string; + Ean: null | string; + ImageUrl: string; + DetailUrl: string; + AssemblyOptions: AssemblyOption[]; +} + export interface AssemblyOption { id: Name; name: Name; @@ -1282,6 +1296,7 @@ export interface Order { invoiceOutput: string[]; isAllDelivered: boolean; isAnyDelivered: boolean; + itemMetadata: ItemMetadata; items: OrderFormItem[]; lastChange: string; lastMessageUnread: string; @@ -1304,6 +1319,79 @@ export interface Order { workflowInRetry: boolean; } +export interface OrderItem { + affiliateId: string; + allowCancellation: boolean; + allowEdition?: boolean; + authorizedDate: string; + callCenterOperatorData: string; + cancelReason: string; + cancellationData: { + RequestedByUser: boolean; + RequestedBySystem: boolean; + RequestedBySellerNotification: boolean; + RequestedByPaymentNotification: boolean; + Reason: string; + CancellationDate: string; + }; + cancellationRequests?: null; + changesAttachment: string; + checkedInPickupPointId: string | null; + clientPreferencesData: ClientPreferencesData; + clientProfileData: ClientProfileData | null; + // deno-lint-ignore no-explicit-any + commercialConditionData?: any | null; + creationDate: string; + // deno-lint-ignore no-explicit-any + customData?: Record | null; + followUpEmail?: string; + giftRegistryData?: GiftRegistry | null; + hostname: string; + // deno-lint-ignore no-explicit-any + invoiceData: any | null; + invoicedDate: string | null; + isCheckedIn: boolean; + isCompleted: boolean; + itemMetadata: ItemMetadata; + items: OrderFormItem[]; + lastChange: string; + lastMessage: string; + marketingData: OrderFormMarketingData | null; + marketplace: Marketplace; + marketplaceItems: []; + marketplaceOrderId: string; + marketplaceServicesEndpoint: string; + merchantName: string; + // deno-lint-ignore no-explicit-any + openTextField: any | null; +} + +interface Marketplace { + baseURL: string; + // deno-lint-ignore no-explicit-any + isCertified?: any | null; + name: string; +} + +interface OrderFormMarketingData { + utmCampaign?: string; + utmMedium?: string; + utmSource?: string; + utmiCampaign?: string; + utmiPart?: string; + utmipage?: string; + marketingTags?: string | string[]; +} + +interface GiftRegistry { + attachmentId: string; + giftRegistryId: string; + giftRegistryType: string; + giftRegistryTypeName: string; + addressId: string; + description: string; +} + export interface Orders { facets: Facet[]; list: Order[];