@evenicanpm/storefront-core 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/package.json +9 -4
  2. package/src/api-manager/README.md +52 -27
  3. package/src/api-manager/datasources/d365/d365-cart.datasource.ts +166 -17
  4. package/src/api-manager/datasources/d365/d365-invoice.datasource.ts +100 -0
  5. package/src/api-manager/datasources/d365/d365-order.datasource.ts +75 -18
  6. package/src/api-manager/datasources/d365/d365-product.datasource.ts +19 -1
  7. package/src/api-manager/datasources/d365/d365-user.datasource.ts +96 -22
  8. package/src/api-manager/datasources/d365/d365.datasource.ts +3 -0
  9. package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +14 -5
  10. package/src/api-manager/datasources/e4/e4.datasource.ts +9 -0
  11. package/src/api-manager/datasources/e4/middleware.ts +35 -0
  12. package/src/api-manager/datasources/e4/order/e4-order.datasource.ts +7 -4
  13. package/src/api-manager/datasources/e4/user/e4-user.datasource.ts +4 -0
  14. package/src/api-manager/index.ts +54 -3
  15. package/src/api-manager/lib/get-graphql-client.ts +13 -6
  16. package/src/api-manager/schemas/cart.schema.ts +10 -1
  17. package/src/api-manager/schemas/invoice.schema.ts +55 -0
  18. package/src/api-manager/schemas/order.schema.ts +13 -1
  19. package/src/api-manager/schemas/user.schema.ts +8 -0
  20. package/src/api-manager/services/create-query.ts +5 -56
  21. package/src/api-manager/services/invoice/queries/get-invoice-details.ts +18 -0
  22. package/src/api-manager/services/invoice/queries/get-invoices.ts +18 -0
  23. package/src/api-manager/services/order/queries/get-order-details.ts +1 -1
  24. package/src/api-manager/services/order/queries/get-orders.ts +6 -3
  25. package/src/api-manager/services/user/queries/get-customer-balance.ts +20 -0
  26. package/src/api-manager/types/Datasource.ts +21 -2
  27. package/src/auth/better-auth.ts +89 -0
  28. package/src/auth/signout.ts +5 -34
  29. package/src/cms/blocks/block-manager.tsx +18 -3
  30. package/src/cms/blocks/components/cta-banner.tsx +65 -0
  31. package/src/cms/blocks/components/hero-image.tsx +83 -0
  32. package/src/cms/blocks/index.tsx +2 -0
  33. package/src/cms/blocks/interfaces.ts +20 -0
  34. package/src/cms/draft-mode-badge.tsx +26 -0
  35. package/src/cms/queries.ts +81 -0
  36. package/src/components/flex-box/flex-box.tsx +2 -0
  37. package/src/components/header/__tests__/header.test.tsx +9 -2
  38. package/src/components/header/__tests__/user.test.tsx +34 -19
  39. package/src/components/header/components/user.tsx +70 -29
  40. package/src/components/mini-cart/mini-cart.tsx +3 -3
  41. package/src/components/wishlist-dialogs/add-to-wishlist/compound/add-to-wishlist-dialog.tsx +3 -1
  42. package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +4 -1
  43. package/src/hooks/use-nextauth-session.ts +10 -14
  44. package/src/pages/account/account-navigation.tsx +88 -60
  45. package/src/pages/account/account-routes.ts +52 -0
  46. package/src/pages/account/orders/order-history-filter-types.tsx +26 -0
  47. package/src/pages/account/orders/order-history-filters-panel.tsx +375 -0
  48. package/src/pages/account/orders/order-history-header.tsx +54 -0
  49. package/src/pages/account/orders/order-history-pagination.tsx +55 -0
  50. package/src/pages/account/orders/order-history-root.tsx +125 -0
  51. package/src/pages/account/orders/order-history-row.tsx +110 -0
  52. package/src/pages/account/orders/order-history-search-and-filter.tsx +449 -0
  53. package/src/pages/account/orders/order-history-search-bar.tsx +84 -0
  54. package/src/pages/account/orders/order-history-search-dropdown.tsx +53 -0
  55. package/src/pages/account/orders/order-history-sort.tsx +90 -0
  56. package/src/pages/account/orders/order-history-table.tsx +54 -0
  57. package/src/pages/account/orders/order-row.tsx +2 -2
  58. package/src/pages/account/orders/orders-drop-down-handler.tsx +19 -0
  59. package/src/pages/account/table-header.tsx +20 -0
  60. package/src/pages/account/wishlist/wishlist-item.tsx +1 -1
  61. package/src/pages/blog/blog-card.tsx +9 -3
  62. package/src/pages/blog/blog-detail-view.tsx +150 -0
  63. package/src/pages/blog/blog-list-view.tsx +59 -0
  64. package/src/pages/cart/__tests__/checkout-form.test.tsx +4 -2
  65. package/src/pages/cart/estimate-shipping.tsx +6 -5
  66. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +43 -21
  67. package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +144 -19
  68. package/src/pages/cms-page-view.tsx +53 -0
  69. package/src/pages/quickorder/order-upload.tsx +12 -6
  70. package/src/pages/quickorder/quick-order.tsx +25 -20
  71. package/src/api-manager/datasources/d365/utils/decode-jwt.ts +0 -10
  72. package/src/auth/auth-options.test.ts +0 -70
  73. package/src/auth/auth-options.ts +0 -101
  74. package/src/auth/next-auth-cookie-manager.ts +0 -87
  75. package/src/auth/providers/aadb2c-provider.ts +0 -32
  76. package/src/auth/providers/authentik-provider.ts +0 -24
  77. package/src/auth/providers/entra-external-id-provider.ts +0 -50
  78. package/src/auth/providers/index.ts +0 -16
  79. package/src/auth/providers/keycloak-provider.ts +0 -22
  80. package/src/auth/refresh-token.ts +0 -132
  81. package/src/auth/types/next-auth.d.ts +0 -38
@@ -26,11 +26,15 @@ import {
26
26
  updateProductListLinesAsync,
27
27
  readAsync as wishlistReadAsync,
28
28
  } from "@msdyn365-commerce/retail-proxy/dist/DataActions/ProductListsDataActions.g";
29
+ import { getCustomerBalanceAsync } from "@msdyn365-commerce/retail-proxy/dist/DataActions/StoreOperationsDataActions.g";
30
+ import { SpanStatusCode, trace } from "@opentelemetry/api";
29
31
  import jwt from "jsonwebtoken";
30
32
  import { uniq } from "lodash";
31
33
  import type {
32
34
  CopyCartToWishlistInput,
35
+ CustomerBalances,
33
36
  ExtendedWishlist,
37
+ GetCustomerBalanceInput,
34
38
  User,
35
39
  UserCounts,
36
40
  Wishlist,
@@ -51,6 +55,8 @@ function decodeIdTokenClaims(token: string): IdTokenClaims {
51
55
  return decoded as IdTokenClaims;
52
56
  }
53
57
 
58
+ const tracer = trace.getTracer("d365-user-datasource");
59
+
54
60
  class D365UserDatasource extends D365DatasourceBase implements UserApi {
55
61
  private getRetailContext(context: ICallerContext): IContext {
56
62
  return context as unknown as IContext;
@@ -93,34 +99,102 @@ class D365UserDatasource extends D365DatasourceBase implements UserApi {
93
99
  }
94
100
 
95
101
  async getUser(): Promise<User | null> {
96
- const context = this.context;
97
- if (!context.requestContext.user.isAuthenticated) return null;
98
- let customer: Customer;
99
- try {
100
- customer = await readAsync(this.getRetailContext(context), "");
101
- } catch {
102
- const token = decodeIdTokenClaims(context.requestContext.user.token);
103
- customer = await createCustomerAsync(this.getRetailContext(context), {
104
- AccountNumber: "",
105
- Email: Array.isArray(token.emails) ? token.emails[0] : token.email,
106
- FirstName: token.given_name,
107
- LastName: token.family_name || ".",
108
- });
109
- }
110
- const mappedAddresses = (customer.Addresses ?? []).map((a) => ({
111
- ...a,
112
- Id: a.RecordId?.toString() ?? "",
113
- }));
114
- return {
115
- ...customer,
116
- Addresses: mappedAddresses,
117
- } as User;
102
+ return tracer.startActiveSpan("d365.getUser", async (span) => {
103
+ try {
104
+ const context = this.context;
105
+ if (!context.requestContext.user.isAuthenticated) {
106
+ span.setAttributes({ "d365.user.authenticated": false });
107
+ span.setStatus({ code: SpanStatusCode.OK });
108
+ return null;
109
+ }
110
+
111
+ let customer: Customer;
112
+ try {
113
+ customer = await readAsync(this.getRetailContext(context), "");
114
+ } catch {
115
+ const token = decodeIdTokenClaims(context.requestContext.user.token);
116
+ customer = await createCustomerAsync(this.getRetailContext(context), {
117
+ AccountNumber: "",
118
+ Email: Array.isArray(token.emails) ? token.emails[0] : token.email,
119
+ FirstName: token.given_name,
120
+ LastName: token.family_name || ".",
121
+ });
122
+ }
123
+
124
+ const mappedAddresses = (customer.Addresses ?? []).map((a) => ({
125
+ ...a,
126
+ Id: a.RecordId?.toString() ?? "",
127
+ }));
128
+
129
+ span.setAttributes({
130
+ "d365.user.authenticated": true,
131
+ "d365.user.account_number": customer.AccountNumber ?? "",
132
+ "d365.user.email": customer.Email ?? "",
133
+ "d365.user.first_name": customer.FirstName ?? "",
134
+ "d365.user.last_name": customer.LastName ?? "",
135
+ "d365.user.customer_type": customer.CustomerTypeValue ?? "",
136
+ "d365.user.record_id": customer.RecordId?.toString() ?? "",
137
+ "d365.user.address_count": mappedAddresses.length,
138
+ });
139
+ span.setStatus({ code: SpanStatusCode.OK });
140
+
141
+ return {
142
+ ...customer,
143
+ Addresses: mappedAddresses,
144
+ } as User;
145
+ } catch (error) {
146
+ const message =
147
+ error instanceof Error ? error.message : JSON.stringify(error);
148
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
149
+ span.recordException(
150
+ error instanceof Error ? error : new Error(message),
151
+ );
152
+ throw new Error(`getUser failed: ${message}`);
153
+ } finally {
154
+ span.end();
155
+ }
156
+ });
118
157
  }
119
158
 
120
159
  async getUserCounts(): Promise<UserCounts> {
121
160
  throw new Error("Method not implemented.");
122
161
  }
123
162
 
163
+ async getCustomerBalance(
164
+ input: GetCustomerBalanceInput,
165
+ ): Promise<CustomerBalances> {
166
+ return tracer.startActiveSpan("d365.getCustomerBalance", async (span) => {
167
+ try {
168
+ const context = this.context;
169
+ const balances = await getCustomerBalanceAsync(
170
+ this.getRetailContext(context),
171
+ input.accountNumber,
172
+ input.invoiceAccountNumber,
173
+ );
174
+ span.setAttributes({
175
+ "d365.balance.account_number": input.accountNumber,
176
+ "d365.balance.credit_limit": balances.CreditLimit,
177
+ "d365.balance.invoice_account_balance":
178
+ balances.InvoiceAccountBalance,
179
+ "d365.balance.invoice_account_credit_limit":
180
+ balances.InvoiceAccountCreditLimit,
181
+ });
182
+ span.setStatus({ code: SpanStatusCode.OK });
183
+ return balances;
184
+ } catch (error) {
185
+ const message =
186
+ error instanceof Error ? error.message : JSON.stringify(error);
187
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
188
+ span.recordException(
189
+ error instanceof Error ? error : new Error(message),
190
+ );
191
+ throw new Error(`getCustomerBalance failed: ${message}`);
192
+ } finally {
193
+ span.end();
194
+ }
195
+ });
196
+ }
197
+
124
198
  async getWishlists(): Promise<Wishlist[]> {
125
199
  const context = this.context;
126
200
  const productListType = ProductListType.WishList;
@@ -18,6 +18,7 @@ import {
18
18
  } from "@evenicanpm/storefront-core/src/api-manager/types/Datasource";
19
19
  import type { ICallerContext } from "@msdyn365-commerce/retail-proxy";
20
20
  import { D365ProductDatasource } from ".";
21
+ import D365InvoiceDatasource from "./d365-invoice.datasource";
21
22
 
22
23
  export class D365Datasource extends Datasource implements PrimaryDatasource {
23
24
  user: UserApi;
@@ -28,6 +29,7 @@ export class D365Datasource extends Datasource implements PrimaryDatasource {
28
29
  organization: OrganizationApi;
29
30
  session: SessionApi;
30
31
  cart: D365CartDatasource;
32
+ invoice: D365InvoiceDatasource;
31
33
  constructor(name: string, context: ICallerContext) {
32
34
  super(name);
33
35
  this.cart = new D365CartDatasource(context);
@@ -38,6 +40,7 @@ export class D365Datasource extends Datasource implements PrimaryDatasource {
38
40
  this.organization = new D365OrganizationDatasource(context);
39
41
  this.session = new D365SessionDatasource(context);
40
42
  this.user = new D365UserDatasource(context);
43
+ this.invoice = new D365InvoiceDatasource(context);
41
44
  }
42
45
  // Datasources
43
46
  }
@@ -1,8 +1,8 @@
1
1
  "use server";
2
2
  import type { ICallerContext } from "@msdyn365-commerce/retail-proxy";
3
- import { cookies } from "next/headers";
3
+ import { cookies, headers as getHeaders } from "next/headers";
4
4
  import { getLocale } from "next-intl/server";
5
- import { manuallyRetrieveSessionCookie } from "@/auth/next-auth-cookie-manager";
5
+ import { auth } from "@/auth/better-auth";
6
6
 
7
7
  /**
8
8
  * Retrieves the D365 context from the "D365Context" cookie, enriches it with the current session token,
@@ -21,14 +21,23 @@ import { manuallyRetrieveSessionCookie } from "@/auth/next-auth-cookie-manager";
21
21
  export async function getContextFromCookie(): Promise<ICallerContext> {
22
22
  const cookieStore = await cookies();
23
23
  const d365Context = cookieStore.get("D365Context");
24
- const token = await manuallyRetrieveSessionCookie();
24
+ let accessToken = "";
25
+ try {
26
+ const tokens = await auth.api.getAccessToken({
27
+ headers: await getHeaders(),
28
+ body: { providerId: "entra-external-id" },
29
+ });
30
+ accessToken = tokens?.idToken || tokens?.accessToken || "";
31
+ } catch {
32
+ // User may not be authenticated — continue with empty token
33
+ }
25
34
  const locale = await getLocale();
26
35
  if (d365Context) {
27
36
  const context = JSON.parse(d365Context.value) as ICallerContext;
28
37
  context.requestContext.operationId = crypto.randomUUID();
29
38
  context.requestContext.user = {
30
- isAuthenticated: !!token?.accessToken,
31
- token: token?.accessToken ?? "",
39
+ isAuthenticated: !!accessToken,
40
+ token: accessToken,
32
41
  };
33
42
  context.requestContext.locale = locale;
34
43
  return context;
@@ -19,6 +19,15 @@ const E4Datasource: PrimaryDatasource = {
19
19
  product,
20
20
  organization,
21
21
  categories,
22
+ // To be implemented in future iteration
23
+ invoice: {
24
+ getInvoices: async () => {
25
+ throw new Error("Method not implemented.");
26
+ },
27
+ getInvoiceDetails: async () => {
28
+ throw new Error("Method not implemented.");
29
+ },
30
+ },
22
31
  };
23
32
 
24
33
  export default E4Datasource;
@@ -0,0 +1,35 @@
1
+ import createGraphQLClient from "@evenicanpm/storefront-core/src/lib/create-graphql-client";
2
+ import type { NextRequest, NextResponse } from "next/server";
3
+ import { parse, splitCookiesString } from "set-cookie-parser";
4
+
5
+ const setE4SessionCookies = async (
6
+ response: NextResponse,
7
+ request: NextRequest,
8
+ ) => {
9
+ const cookiesAlreadySet = request.cookies.get("storefront");
10
+ if (cookiesAlreadySet) return response;
11
+
12
+ const e4Api = await createGraphQLClient();
13
+
14
+ const session = await e4Api.e4ReadSessionCustomer();
15
+
16
+ const cookieString = session?.headers?.get("set-cookie");
17
+ if (!cookieString) return response;
18
+ const splitCookies = parse(
19
+ splitCookiesString(decodeURIComponent(cookieString)),
20
+ );
21
+
22
+ splitCookies.forEach((cookie) => {
23
+ response.cookies.set({
24
+ name: cookie.name,
25
+ value: cookie.value,
26
+ httpOnly: true,
27
+ expires: cookie.expires,
28
+ secure: true,
29
+ });
30
+ });
31
+
32
+ return response;
33
+ };
34
+
35
+ export default setE4SessionCookies;
@@ -5,17 +5,19 @@ import {
5
5
  import getGraphqlClient from "@evenicanpm/storefront-core/src/api-manager/lib/get-graphql-client";
6
6
  import type {
7
7
  GetSalesOrderDetailsInput,
8
+ GetSalesOrdersInput,
8
9
  SalesOrder,
9
- SalesOrderInput,
10
10
  } from "@evenicanpm/storefront-core/src/api-manager/schemas/order.schema";
11
11
  import type { OrderApi } from "@evenicanpm/storefront-core/src/api-manager/types/Datasource";
12
12
  import { merge } from "object-mapper";
13
13
 
14
14
  const order: OrderApi = {
15
- async getSalesOrders(input: SalesOrderInput): Promise<SalesOrder[]> {
15
+ async getSalesOrders(input: GetSalesOrdersInput): Promise<SalesOrder[]> {
16
16
  const e4Api = await getGraphqlClient(true);
17
17
 
18
- const { data, errors } = await e4Api.e4OrderListSM({ id: input?.id });
18
+ const { data, errors } = await e4Api.e4OrderListSM({
19
+ id: input?.orderSearchCriteria?.CustomerAccountNumber ?? "",
20
+ });
19
21
 
20
22
  if (errors) {
21
23
  throw new Error("Error getting orders");
@@ -30,9 +32,10 @@ const order: OrderApi = {
30
32
  input: GetSalesOrderDetailsInput,
31
33
  ): Promise<SalesOrder> {
32
34
  const e4Api = await getGraphqlClient(true);
35
+ if (!input?.transactionId) throw new Error("transactionId is required");
33
36
 
34
37
  const { data, errors } = await e4Api.e4OrderReadMD({
35
- id: input?.transactionId,
38
+ id: input.transactionId,
36
39
  });
37
40
 
38
41
  if (errors) {
@@ -138,6 +138,10 @@ const user: UserApi = {
138
138
  async copyCartToWishlist(_input: CopyCartToWishlistInput): Promise<Wishlist> {
139
139
  throw new Error("copyCartToWishlist not implemented");
140
140
  },
141
+
142
+ async getCustomerBalance(_input) {
143
+ throw new Error("getCustomerBalance not implemented");
144
+ },
141
145
  };
142
146
 
143
147
  export default user;
@@ -1,23 +1,74 @@
1
1
  import { D365Datasource } from "@evenicanpm/storefront-core/src/api-manager/datasources/d365/index";
2
2
  import { getContextFromCookie } from "@evenicanpm/storefront-core/src/api-manager/datasources/d365/utils/get-context-cookie";
3
3
  import E4Datasource from "@evenicanpm/storefront-core/src/api-manager/datasources/e4/e4.datasource";
4
- import type { Datasource } from "@evenicanpm/storefront-core/src/api-manager/types/Datasource";
4
+ import type {
5
+ Datasource,
6
+ DatasourceApis,
7
+ SecondaryDatasource,
8
+ } from "@evenicanpm/storefront-core/src/api-manager/types/Datasource";
9
+
10
+ const OVERRIDES_FILE = "./overrides.datasource.ts";
5
11
 
6
12
  /**
7
13
  * Returns the active commerce datasource based on the API_MANAGER_PRIMARY
8
14
  * environment variable. Defaults to "e4" when the variable is not set.
9
15
  *
16
+ * Supports project-level function overrides via
17
+ * src/extensions/api-manager-overrides/overrides.datasource.ts
18
+ *
10
19
  * Supported values: "e4" | "d365"
11
20
  */
12
21
  async function createApiManager(): Promise<Datasource> {
13
22
  const primary = process.env.API_MANAGER_PRIMARY ?? "e4";
14
23
 
24
+ let datasource: Datasource;
15
25
  if (primary === "d365") {
16
26
  const context = await getContextFromCookie();
17
- return new D365Datasource("d365", context);
27
+ datasource = new D365Datasource("d365", context);
28
+ } else {
29
+ datasource = E4Datasource;
30
+ }
31
+
32
+ const storefrontOverrides: { [key: string]: any } = {};
33
+
34
+ // @ts-expect-error
35
+ function importAll(r) {
36
+ r.keys().forEach((key: string) => {
37
+ storefrontOverrides[key] = r(key);
38
+ });
39
+ }
40
+
41
+ // Arguments must be literals not variables.
42
+ importAll(
43
+ // @ts-expect-error
44
+ require.context(
45
+ "@storefront/extensions/api-manager-overrides",
46
+ true,
47
+ /overrides\.datasource\.ts/,
48
+ ),
49
+ );
50
+
51
+ const projectOverrides: SecondaryDatasource | undefined =
52
+ storefrontOverrides[OVERRIDES_FILE]?.default;
53
+
54
+ if (projectOverrides) {
55
+ for (const entityName of Object.keys(projectOverrides) as Array<
56
+ keyof DatasourceApis
57
+ >) {
58
+ const entityOverrides = projectOverrides[entityName];
59
+ if (!entityOverrides) continue;
60
+
61
+ if (datasource[entityName]) {
62
+ // Merge override methods into the existing entity
63
+ Object.assign(datasource[entityName] as object, entityOverrides);
64
+ } else {
65
+ // Non-core entity added entirely from overrides
66
+ (datasource as any)[entityName] = entityOverrides;
67
+ }
68
+ }
18
69
  }
19
70
 
20
- return E4Datasource;
71
+ return datasource;
21
72
  }
22
73
 
23
74
  export default createApiManager;
@@ -2,9 +2,8 @@
2
2
 
3
3
  import { getSdk } from "@evenicanpm/storefront-core/src/api-manager/datasources/e4/graphqlRequestSdk";
4
4
  import { GraphQLClient } from "graphql-request";
5
- import { cookies } from "next/headers";
6
- import { getServerSession } from "next-auth/next";
7
- import authOptions from "@/auth/auth-options";
5
+ import { cookies, headers as getHeaders } from "next/headers";
6
+ import { auth } from "@/auth/better-auth";
8
7
 
9
8
  /**
10
9
  * Initializes and returns a cached GraphQL client
@@ -28,9 +27,17 @@ const getGraphQLClient = async (withAuth = false) => {
28
27
  }
29
28
 
30
29
  if (withAuth) {
31
- const session = await getServerSession(authOptions());
32
- if (session?.accessToken) {
33
- headers.Authorization = `Bearer ${session.accessToken}`;
30
+ try {
31
+ const tokens = await auth.api.getAccessToken({
32
+ headers: await getHeaders(),
33
+ body: { providerId: "entra-external-id" },
34
+ });
35
+ const accessToken = tokens?.idToken || tokens?.accessToken;
36
+ if (accessToken) {
37
+ headers.Authorization = `Bearer ${accessToken}`;
38
+ }
39
+ } catch {
40
+ // User may not be authenticated — continue without auth header
34
41
  }
35
42
  }
36
43
 
@@ -54,9 +54,10 @@ export const getCardAcceptPointInput = z.object({
54
54
  export const checkoutInput = z.object({
55
55
  id: z.string(),
56
56
  receiptEmail: z.string(),
57
- tokenizedPaymentCard: tokenizedPaymentCardSchema,
57
+ tokenizedPaymentCard: tokenizedPaymentCardSchema.optional(),
58
58
  currency: z.string(),
59
59
  amount: z.number(),
60
+ paymentMethod: z.enum(["card", "on-account"]).default("card"),
60
61
  });
61
62
 
62
63
  export const updateCartLineDeliveryModeInput = z.object({
@@ -82,6 +83,13 @@ const InventorySearchCartLinesCriteriaInput = z.object({
82
83
  allDimensionsSelected: z.boolean(),
83
84
  });
84
85
 
86
+ export const updateInvoiceLineInput = z.object({
87
+ cartId: z.string(),
88
+ cartLineId: z.string(),
89
+ price: z.number(),
90
+ version: z.number().optional(),
91
+ });
92
+
85
93
  // Typescript Types
86
94
  // export type Cart = z.infer<typeof cartSchema>;
87
95
  // export type CartLine = z.infer<typeof cartLineSchema>;
@@ -108,3 +116,4 @@ export type CheckoutInput = z.infer<typeof checkoutInput>;
108
116
  export type InventorySearchCartLinesCriteriaInput = z.infer<
109
117
  typeof InventorySearchCartLinesCriteriaInput
110
118
  >;
119
+ export type UpdateInvoiceLineInput = z.infer<typeof updateInvoiceLineInput>;
@@ -0,0 +1,55 @@
1
+ export interface InvoiceSearchCriteria {
2
+ InvoiceIds?: string[];
3
+ CustomerId?: string;
4
+ BusinessPartnerId?: string;
5
+ InvoiceTypeValues?: number[];
6
+ InvoicePaidStatusValues?: number[];
7
+ }
8
+
9
+ export interface GetInvoicesInput {
10
+ InvoiceSearchCriteria: InvoiceSearchCriteria;
11
+ }
12
+
13
+ export interface InvoiceDetailsSearchCriteria {
14
+ InvoiceId?: string;
15
+ }
16
+
17
+ export interface GetInvoicesDetailsInput {
18
+ InvoiceSearchCriteria: InvoiceDetailsSearchCriteria;
19
+ }
20
+
21
+ export interface SalesInvoiceLine {
22
+ ItemId?: string;
23
+ Description?: string;
24
+ Quantity?: number;
25
+ Price?: number;
26
+ NetAmount?: number;
27
+ LineDiscount?: number;
28
+ }
29
+
30
+ export interface Invoice {
31
+ Id?: string;
32
+ CustomerName?: string;
33
+ CustomerAccountNumber?: string;
34
+ TotalAmount?: number;
35
+ InvoiceDate?: Date;
36
+ InvoiceDueDate?: Date;
37
+ InvoicePaidStatusValue?: number;
38
+ AmountPaid?: number;
39
+ AmountBalance?: number;
40
+ CurrencyCode?: string;
41
+ }
42
+
43
+ export interface InvoiceDetails {
44
+ Id?: string;
45
+ AmountBalance?: number;
46
+ AmountPaid?: number;
47
+ DueDate?: Date;
48
+ SalesInvoiceLine?: SalesInvoiceLine[];
49
+ OrderedByCustomerName?: string;
50
+ Name?: string;
51
+ InvoiceDate?: Date;
52
+ InvoiceSubtotal?: number;
53
+ TotalTaxAmount?: number;
54
+ Amount?: number;
55
+ }
@@ -1,13 +1,17 @@
1
1
  import {
2
+ orderSearchCriteriaSchema,
3
+ pagingInfoSchema,
2
4
  salesLineSchema,
3
5
  salesOrderSchema,
4
6
  searchLocationSchema,
5
7
  simpleProductSchema,
8
+ sortColumnSchema,
6
9
  } from "@evenicanpm/storefront-core/src/api-manager/schemas/d365.schema";
7
10
  import z from "zod";
8
11
 
9
12
  const getSalesOrderDetailsInput = z.object({
10
- transactionId: z.string(),
13
+ transactionId: z.string().optional(),
14
+ salesId: z.string().optional(),
11
15
  searchLocation: searchLocationSchema.optional(),
12
16
  });
13
17
 
@@ -22,9 +26,17 @@ const extendedSalesOrderSchema = salesOrderSchema
22
26
  .omit({ SalesLines: true })
23
27
  .extend({ SalesLines: extendedSalesLineSchema.array().optional() });
24
28
 
29
+ const getSalesOrdersInput = z.object({
30
+ orderSearchCriteria: orderSearchCriteriaSchema,
31
+ sort: sortColumnSchema.optional(),
32
+ paging: pagingInfoSchema.optional(),
33
+ });
34
+
25
35
  export type GetSalesOrderDetailsInput = z.infer<
26
36
  typeof getSalesOrderDetailsInput
27
37
  >;
38
+ export type GetSalesOrdersInput = z.infer<typeof getSalesOrdersInput>;
39
+
28
40
  export type SalesOrderInput = z.infer<typeof salesOrderInput>;
29
41
  export type SalesOrder = z.infer<typeof extendedSalesOrderSchema>;
30
42
  export type SalesLine = z.infer<typeof extendedSalesLineSchema>;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type customerBalancesSchema,
2
3
  customerSchema,
3
4
  productListLineSchema,
4
5
  productListSchema,
@@ -17,6 +18,13 @@ export const extendedCustomerSchema = customerSchema.extend({
17
18
 
18
19
  export type User = z.infer<typeof extendedCustomerSchema>;
19
20
  export type UserCounts = z.infer<typeof userCountsSchema>;
21
+ export type CustomerBalances = z.infer<typeof customerBalancesSchema>;
22
+
23
+ export const getCustomerBalanceInput = z.object({
24
+ accountNumber: z.string(),
25
+ invoiceAccountNumber: z.string().optional(),
26
+ });
27
+ export type GetCustomerBalanceInput = z.infer<typeof getCustomerBalanceInput>;
20
28
 
21
29
  // Wishlist
22
30
 
@@ -7,14 +7,9 @@ import {
7
7
  useQuery,
8
8
  useSuspenseQuery,
9
9
  } from "@tanstack/react-query";
10
- import { Mutex } from "async-mutex";
11
- import jwt from "jsonwebtoken";
12
- import { getSession } from "next-auth/react";
13
- import { manuallyRetrieveSessionCookie } from "@/auth/next-auth-cookie-manager";
14
10
  import signOut from "@/auth/signout";
15
11
 
16
12
  const QUERY_ROUTE = "/api";
17
- const mutex = new Mutex();
18
13
 
19
14
  export type ApiPath<T extends keyof DatasourceApis = keyof DatasourceApis> = [
20
15
  T,
@@ -27,22 +22,6 @@ export type Query<TInput, TOutput> = ReturnType<
27
22
 
28
23
  // --- Shared helpers (no closure dependencies) ---
29
24
 
30
- const checkTokenExpiry = async (): Promise<boolean> => {
31
- let token: string | undefined;
32
- if (isServer) {
33
- const session = (await manuallyRetrieveSessionCookie()) ?? undefined;
34
- token = session?.accessToken;
35
- } else {
36
- const session = await getSession();
37
- token = session?.accessToken;
38
- }
39
- const decoded = token && (jwt.decode(token) as { exp?: number } | null);
40
- const currentTime = Math.floor(Date.now() / 1000);
41
- return (
42
- !!decoded && typeof decoded.exp === "number" && decoded.exp < currentTime
43
- );
44
- };
45
-
46
25
  const doFetch = async <TInput>(
47
26
  input: TInput | undefined,
48
27
  apiManagerPath: ApiPath,
@@ -60,27 +39,6 @@ const doFetch = async <TInput>(
60
39
  return result.json();
61
40
  };
62
41
 
63
- // Acquires a mutex before fetching so concurrent requests don't each trigger
64
- // a token refresh simultaneously (double-check pattern after acquiring lock).
65
- const withTokenRefresh = async <TInput>(
66
- input: TInput | undefined,
67
- apiManagerPath: ApiPath,
68
- ) => {
69
- const release = await mutex.acquire();
70
- try {
71
- const headers = new Headers();
72
- if (await checkTokenExpiry()) {
73
- headers.append("should-refresh-token", "true");
74
- }
75
- return await doFetch(input, apiManagerPath, headers);
76
- } catch (error) {
77
- console.error("Error fetching data:", error);
78
- throw new Error("Failed to fetch data");
79
- } finally {
80
- release();
81
- }
82
- };
83
-
84
42
  // --- Factory ---
85
43
 
86
44
  export const createQuery = <TInput, TOutput>(
@@ -103,10 +61,7 @@ export const createQuery = <TInput, TOutput>(
103
61
  return getApiFromPath(apiManager, apiManagerPath)(input);
104
62
  }
105
63
  : async () => {
106
- const isExpired = await checkTokenExpiry();
107
- return isExpired
108
- ? withTokenRefresh(input, apiManagerPath)
109
- : doFetch(input, apiManagerPath);
64
+ return await doFetch(input, apiManagerPath);
110
65
  },
111
66
  enabled: config?.disableProperties
112
67
  ? config.disableProperties.every((prop) => !!input?.[prop])
@@ -118,7 +73,10 @@ export const createQuery = <TInput, TOutput>(
118
73
  input?: TInput,
119
74
  options?: { enabled: boolean },
120
75
  ): UseQueryResult<TOutput> =>
121
- useQuery({ ...getQueryOptions(input), enabled: options?.enabled }),
76
+ useQuery({
77
+ ...getQueryOptions(input),
78
+ ...(options?.enabled !== undefined && { enabled: options.enabled }),
79
+ }),
122
80
  useSuspenseData: (input?: TInput): UseSuspenseQueryResult<TOutput> =>
123
81
  useSuspenseQuery(getQueryOptions(input)),
124
82
  fetchData: (queryClient: QueryClient, input?: TInput) =>
@@ -142,15 +100,6 @@ export function getApiFromPath<TInput, TOutput>(
142
100
  const [entity, method] = path;
143
101
  const result = apiManager[entity][method];
144
102
 
145
- if (process.env.NODE_ENV === "development") {
146
- // eslint-disable-next-line no-console
147
- console.log(`[CREATE QUERY] Accessing API Path:`, {
148
- entity,
149
- method,
150
- result,
151
- });
152
- }
153
-
154
103
  // Function reference loses 'this' context — re-bind for d365 classes.
155
104
  if (typeof result === "function") {
156
105
  return (result as (input?: TInput) => Promise<TOutput>).bind(