@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.
- package/package.json +9 -4
- package/src/api-manager/README.md +52 -27
- package/src/api-manager/datasources/d365/d365-cart.datasource.ts +166 -17
- package/src/api-manager/datasources/d365/d365-invoice.datasource.ts +100 -0
- package/src/api-manager/datasources/d365/d365-order.datasource.ts +75 -18
- package/src/api-manager/datasources/d365/d365-product.datasource.ts +19 -1
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +96 -22
- package/src/api-manager/datasources/d365/d365.datasource.ts +3 -0
- package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +14 -5
- package/src/api-manager/datasources/e4/e4.datasource.ts +9 -0
- package/src/api-manager/datasources/e4/middleware.ts +35 -0
- package/src/api-manager/datasources/e4/order/e4-order.datasource.ts +7 -4
- package/src/api-manager/datasources/e4/user/e4-user.datasource.ts +4 -0
- package/src/api-manager/index.ts +54 -3
- package/src/api-manager/lib/get-graphql-client.ts +13 -6
- package/src/api-manager/schemas/cart.schema.ts +10 -1
- package/src/api-manager/schemas/invoice.schema.ts +55 -0
- package/src/api-manager/schemas/order.schema.ts +13 -1
- package/src/api-manager/schemas/user.schema.ts +8 -0
- package/src/api-manager/services/create-query.ts +5 -56
- package/src/api-manager/services/invoice/queries/get-invoice-details.ts +18 -0
- package/src/api-manager/services/invoice/queries/get-invoices.ts +18 -0
- package/src/api-manager/services/order/queries/get-order-details.ts +1 -1
- package/src/api-manager/services/order/queries/get-orders.ts +6 -3
- package/src/api-manager/services/user/queries/get-customer-balance.ts +20 -0
- package/src/api-manager/types/Datasource.ts +21 -2
- package/src/auth/better-auth.ts +89 -0
- package/src/auth/signout.ts +5 -34
- package/src/cms/blocks/block-manager.tsx +18 -3
- package/src/cms/blocks/components/cta-banner.tsx +65 -0
- package/src/cms/blocks/components/hero-image.tsx +83 -0
- package/src/cms/blocks/index.tsx +2 -0
- package/src/cms/blocks/interfaces.ts +20 -0
- package/src/cms/draft-mode-badge.tsx +26 -0
- package/src/cms/queries.ts +81 -0
- package/src/components/flex-box/flex-box.tsx +2 -0
- package/src/components/header/__tests__/header.test.tsx +9 -2
- package/src/components/header/__tests__/user.test.tsx +34 -19
- package/src/components/header/components/user.tsx +70 -29
- package/src/components/mini-cart/mini-cart.tsx +3 -3
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/add-to-wishlist-dialog.tsx +3 -1
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +4 -1
- package/src/hooks/use-nextauth-session.ts +10 -14
- package/src/pages/account/account-navigation.tsx +88 -60
- package/src/pages/account/account-routes.ts +52 -0
- package/src/pages/account/orders/order-history-filter-types.tsx +26 -0
- package/src/pages/account/orders/order-history-filters-panel.tsx +375 -0
- package/src/pages/account/orders/order-history-header.tsx +54 -0
- package/src/pages/account/orders/order-history-pagination.tsx +55 -0
- package/src/pages/account/orders/order-history-root.tsx +125 -0
- package/src/pages/account/orders/order-history-row.tsx +110 -0
- package/src/pages/account/orders/order-history-search-and-filter.tsx +449 -0
- package/src/pages/account/orders/order-history-search-bar.tsx +84 -0
- package/src/pages/account/orders/order-history-search-dropdown.tsx +53 -0
- package/src/pages/account/orders/order-history-sort.tsx +90 -0
- package/src/pages/account/orders/order-history-table.tsx +54 -0
- package/src/pages/account/orders/order-row.tsx +2 -2
- package/src/pages/account/orders/orders-drop-down-handler.tsx +19 -0
- package/src/pages/account/table-header.tsx +20 -0
- package/src/pages/account/wishlist/wishlist-item.tsx +1 -1
- package/src/pages/blog/blog-card.tsx +9 -3
- package/src/pages/blog/blog-detail-view.tsx +150 -0
- package/src/pages/blog/blog-list-view.tsx +59 -0
- package/src/pages/cart/__tests__/checkout-form.test.tsx +4 -2
- package/src/pages/cart/estimate-shipping.tsx +6 -5
- package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +43 -21
- package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +144 -19
- package/src/pages/cms-page-view.tsx +53 -0
- package/src/pages/quickorder/order-upload.tsx +12 -6
- package/src/pages/quickorder/quick-order.tsx +25 -20
- package/src/api-manager/datasources/d365/utils/decode-jwt.ts +0 -10
- package/src/auth/auth-options.test.ts +0 -70
- package/src/auth/auth-options.ts +0 -101
- package/src/auth/next-auth-cookie-manager.ts +0 -87
- package/src/auth/providers/aadb2c-provider.ts +0 -32
- package/src/auth/providers/authentik-provider.ts +0 -24
- package/src/auth/providers/entra-external-id-provider.ts +0 -50
- package/src/auth/providers/index.ts +0 -16
- package/src/auth/providers/keycloak-provider.ts +0 -22
- package/src/auth/refresh-token.ts +0 -132
- 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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 {
|
|
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
|
-
|
|
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: !!
|
|
31
|
-
token:
|
|
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:
|
|
15
|
+
async getSalesOrders(input: GetSalesOrdersInput): Promise<SalesOrder[]> {
|
|
16
16
|
const e4Api = await getGraphqlClient(true);
|
|
17
17
|
|
|
18
|
-
const { data, errors } = await e4Api.e4OrderListSM({
|
|
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
|
|
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;
|
package/src/api-manager/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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({
|
|
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(
|